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.
9.4 KiB
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
.vuefiles. - 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
<!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.jsso 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(notvue.esm-browser.js) for script-tag usage.
2. Creating an App
Minimal app
// js/app.js
const app = Vue.createApp({
data() {
return {
message: "Hello, Vue!",
};
},
});
app.mount("#app");
<!-- 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
<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 :)
<!-- 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
<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
<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
<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 @)
<!-- full syntax -->
<button v-on:click="increment">+1</button>
<!-- shorthand (preferred) -->
<button @click="increment">+1</button>
const app = Vue.createApp({
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++;
},
},
});
Passing arguments
<button @click="addItem('apple')">Add Apple</button>
Event modifiers
<!-- 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.
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;
},
},
});
<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).
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/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");
<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
propsarray (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
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.
const Card = {
template: `
<div class="card">
<slot></slot>
</div>
`,
};
<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.
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;
}
},
});
<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. :keyon everyv-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 havealtattributes, buttons have descriptive text.