add beginner Vue.js skill for CDN-based development

Covers Vue 3 via script tag with no build process: app creation,
template directives, event handling, computed/watchers, components
as plain JS objects, data fetching, and a best practices checklist.
This commit is contained in:
Mark Eaton
2026-02-23 17:07:34 -05:00
parent 275425e630
commit 07c031382a
+412
View File
@@ -0,0 +1,412 @@
# Vue.js Basics
Guide Vue.js development using Vue 3 loaded via CDN. **No build process.** No Vite, no npm, no .vue single-file components. Import Vue and any other libraries via `<script>` tags.
**Keep things simple.** The user is learning Vue. Explain what each directive and option does. Use the Options API (not Composition API) unless asked otherwise. Use prettier for code formatting.
## Instructions
- **Always load Vue via CDN script tag** — never use npm, Vite, or a bundler.
- Components are plain JavaScript objects with inline templates, not `.vue` files.
- Explain directives and concepts as you introduce them.
- Format all HTML, CSS, and JavaScript with prettier conventions.
- If a more advanced pattern (Composition API, Vue Router, Pinia) would help, briefly explain what it is and why, but default to the simpler approach.
---
## 1. Project Setup
### Directory layout
```
project/
├── index.html
├── js/
│ └── app.js # Vue app and components
└── css/
└── style.css
```
### HTML boilerplate
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div id="app">
<!-- Vue templates go here -->
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="js/app.js"></script>
</body>
</html>
```
Key points:
- Vue script tag goes before `app.js` so Vue is available when your code runs.
- The `<div id="app">` is where Vue mounts — everything inside it becomes a Vue template.
- Use `vue.global.js` (not `vue.esm-browser.js`) for script-tag usage.
---
## 2. Creating an App
### Minimal app
```js
// js/app.js
const app = Vue.createApp({
data() {
return {
message: "Hello, Vue!",
};
},
});
app.mount("#app");
```
```html
<!-- in index.html, inside <div id="app"> -->
<p>{{ message }}</p>
```
### Options explained
| Option | Purpose |
|--------|---------|
| `data()` | Returns an object of reactive state. Must be a function. |
| `methods` | Object of functions the template can call. |
| `computed` | Object of derived values that update automatically. |
| `watch` | Object of functions that run when a data property changes. |
| `mounted()` | Lifecycle hook — runs after the app is inserted into the page. |
---
## 3. Template Basics
### Text interpolation
```html
<p>{{ message }}</p>
<p>{{ count + 1 }}</p>
<p>{{ items.length > 0 ? "Has items" : "Empty" }}</p>
```
Double curly braces `{{ }}` output the value of a JavaScript expression. The expression is reactive — the page updates automatically when the data changes.
### Attribute binding — `v-bind` (shorthand `:`)
```html
<!-- full syntax -->
<a v-bind:href="url">Link</a>
<!-- shorthand (preferred) -->
<a :href="url">Link</a>
<img :src="imageUrl" :alt="imageDescription" />
<button :disabled="isSubmitting">Submit</button>
```
Use `:` to bind any HTML attribute to a data property.
### Conditional rendering — `v-if` / `v-else-if` / `v-else`
```html
<p v-if="items.length === 0">No items yet.</p>
<p v-else-if="items.length === 1">One item.</p>
<p v-else>{{ items.length }} items.</p>
```
`v-if` removes or adds elements from the DOM. Use `v-show` instead if you need to toggle visibility frequently (it uses CSS `display: none`).
### List rendering — `v-for`
```html
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
```
**Always provide a `:key`** — Vue uses it to track which items changed. Use a unique identifier, not the array index.
### Two-way binding — `v-model`
```html
<input v-model="searchQuery" placeholder="Search..." />
<p>You typed: {{ searchQuery }}</p>
```
`v-model` syncs an input's value with a data property. Works on `<input>`, `<textarea>`, `<select>`, and checkboxes.
---
## 4. Event Handling
### Basic events — `v-on` (shorthand `@`)
```html
<!-- full syntax -->
<button v-on:click="increment">+1</button>
<!-- shorthand (preferred) -->
<button @click="increment">+1</button>
```
```js
const app = Vue.createApp({
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++;
},
},
});
```
### Passing arguments
```html
<button @click="addItem('apple')">Add Apple</button>
```
### Event modifiers
```html
<!-- prevent default form submission -->
<form @submit.prevent="handleSubmit">
<input v-model="name" />
<button type="submit">Save</button>
</form>
<!-- only trigger once -->
<button @click.once="initialize">Start</button>
```
Common modifiers: `.prevent`, `.stop`, `.once`, `.enter` (for keyboard events).
---
## 5. Computed Properties and Watchers
### Computed properties
Use computed for values derived from data. They cache their result and only recalculate when dependencies change.
```js
const app = Vue.createApp({
data() {
return {
items: [
{ name: "Milk", done: false },
{ name: "Bread", done: true },
],
};
},
computed: {
remainingCount() {
return this.items.filter((item) => !item.done).length;
},
},
});
```
```html
<p>{{ remainingCount }} items left</p>
```
**Computed vs methods:** Use computed when you are deriving a value from existing data. Use a method when you are performing an action (handling a click, submitting a form).
### Watchers
Use watchers when you need to perform a side effect in response to data changing (e.g. making an API call when a search query changes).
```js
const app = Vue.createApp({
data() {
return { searchQuery: "" };
},
watch: {
searchQuery(newValue, oldValue) {
console.log(`Search changed from "${oldValue}" to "${newValue}"`);
// e.g. fetch search results here
},
},
});
```
Watchers are for side effects. If you just need a derived value, use computed instead.
---
## 6. Components
Without a build process, components are plain JavaScript objects registered on the app.
### Defining a component
```js
// js/app.js
const TodoItem = {
props: ["item"],
template: `
<li>
<input type="checkbox" :checked="item.done" @change="$emit('toggle', item.id)" />
{{ item.name }}
</li>
`,
};
const app = Vue.createApp({
components: {
"todo-item": TodoItem,
},
data() {
return {
items: [
{ id: 1, name: "Milk", done: false },
{ id: 2, name: "Bread", done: true },
],
};
},
methods: {
toggleItem(id) {
const item = this.items.find((i) => i.id === id);
item.done = !item.done;
},
},
});
app.mount("#app");
```
```html
<div id="app">
<ul>
<todo-item
v-for="item in items"
:key="item.id"
:item="item"
@toggle="toggleItem"
></todo-item>
</ul>
</div>
```
### Key concepts
- **Props** flow data *down* from parent to child. Declare them in the `props` array (or object for validation).
- **Events** flow *up* from child to parent. Use `$emit('event-name', payload)` in the child, `@event-name="handler"` in the parent.
- **Templates** are inline strings in the component object (backtick template literals work well for multi-line).
### Props with validation
```js
const UserCard = {
props: {
name: {
type: String,
required: true,
},
role: {
type: String,
default: "viewer",
},
},
template: `
<div class="user-card">
<h3>{{ name }}</h3>
<span>{{ role }}</span>
</div>
`,
};
```
### Slots
Slots let a parent pass content into a child component's template.
```js
const Card = {
template: `
<div class="card">
<slot></slot>
</div>
`,
};
```
```html
<card>
<h2>Title</h2>
<p>Any content can go here.</p>
</card>
```
---
## 7. Fetching Data
Use `fetch()` in the `mounted()` lifecycle hook to load data when the page loads.
```js
const app = Vue.createApp({
data() {
return {
users: [],
loading: true,
error: null,
};
},
async mounted() {
try {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
this.users = await response.json();
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
},
});
```
```html
<div id="app">
<p v-if="loading">Loading...</p>
<p v-else-if="error">Error: {{ error }}</p>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
```
Always handle three states: **loading**, **error**, and **success**.
---
## 8. Best Practices Checklist
- [ ] **No build process** — Vue and all libraries loaded via `<script>` tags.
- [ ] **Formatted with prettier** — consistent indentation, quotes, and semicolons.
- [ ] **Semantic HTML** — use `<button>`, `<nav>`, `<main>`, `<header>`, `<form>` instead of `<div>` for everything.
- [ ] **`:key` on every `v-for`** — use a unique ID, not the array index.
- [ ] **Props down, events up** — children never modify props directly; they emit events.
- [ ] **Data is a function**`data()` must return a new object, never a shared reference.
- [ ] **Components are small** — if a component template is longer than ~50 lines, consider splitting it.
- [ ] **Loading and error states** — every data fetch handles loading, error, and success.
- [ ] **No inline styles** — use CSS classes in `style.css`.
- [ ] **Accessible** — form inputs have `<label>` elements, images have `alt` attributes, buttons have descriptive text.