Files
Mark Eaton 07c031382a 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.
2026-02-23 17:07:34 -05:00

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 .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

<!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/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 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

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.
  • :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 functiondata() 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.