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:
+412
@@ -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.
|
||||||
Reference in New Issue
Block a user