Skip to content
wannacode

The first component

In short: A Vue component is a template with data: it describes how the screen looks for that data, and Vue changes only the difference on the page.

Problem

The field loses focus

The module "JavaScript in the page" ended with a cart drawn from the data by one render(). Here every change of the quantity field calls render(). The buyer wants 15 bottles of milk: they click into the field, delete 2 and type 15. Try it.

HTML, CSS, JS

After the delete the field is empty and no longer focused: 1 and 5 go nowhere. render() replaced the field with a new one on the very first key. The new field looks the same, but it is a different element, and the focus was on the old one. Touching separate places by hand forgets places. Drawing everything again loses the focus. There is a third way.

The idea

The interface as a function of the data

render() had a good idea: the screen is built only from the data. With the same data the screen is the same, whatever was on it before. The mistake was in the last step: render() removes and creates the elements itself.

So the work is split in two:

  • You write a function that says how the screen looks for these data. It does not touch the page; it returns a description: here is a <main>, in it an <h1> with the text "The cart", then a field with the value 2.
  • The library holds the real DOM. The first time it builds it from the description. On every change of the data it takes the new description, compares it with the old one and changes only the difference on the page.

Such a function is called a component. In short: the screen is a function of the data, and the component is that function. React and Vue do this in different ways. The idea is the same, and choosing between them is a matter of the team, not of a "better" library.

See it

The first component

In Vue a component is a .vue file (Single File Component) with up to three parts: <script setup> with the data and the functions, <template> with the description of the screen and, optionally, <style>.

<script setup>
const product = { name: 'Fresh milk', price: 1.6, stock: 35 };
</script>
<template>
<main>
<h1>{{ product.name }}</h1>
<p>Price: {{ money(product.price) }}</p>
</main>
</template>
  • Everything <script setup> declares (variables, functions, imported components), the template can see.
  • {{ product.name }} is a JavaScript expression in double curly braces. Its result becomes text on the page.
  • The name of the component is the name of the file: App.vue is the component App.
  • The sandbox puts App on the page by itself. In a real project one line in main.js does it: createApp(App).mount('#app').
Vue

Change the price or the stock in the object product and press "Run". You do not touch the description of the screen: the screen follows the data.

The idea

The rules of the template

The template is HTML, but Vue compiles it to JavaScript and reads its own signs in it. That is why it has rules of its own.

  • {{ expression }} in text: <h1>{{ product.name }}</h1>.
  • : before an attribute means "the value is an expression": <img :src="product.image" :alt="product.name" />. :alt is short for v-bind:alt. Without : the value is the text "product.name", letter by letter. {{ }} does not work inside an attribute: alt="{{ product.name }}" is the text {{ product.name }}.
  • @ before an event: @click="add" (short for v-on:click). A short expression works too: @click="count++" or @click="add(product)". Vue wraps it in a function that runs on click.
  • class and for stay class and for, as in HTML.
  • Several roots are allowed: the template can have a <header> and a <main> side by side.
  • Tags without content may be closed with />: <input />, <ProductCard />.

Try it

Find the bug

A click on "Add" writes nothing to the console.

Pick the line with the bug.

Try it

Fill in

The screen reader should read the product name as the description of the image.

<img :src="product.image" ="product.name" />

The idea

Components from components

The shop page has parts: the top with the menu, the product card, the cart. Each part becomes a component in its own file, and a bigger component uses them as tags. Then App is not one long function but a few lines that say what is on the page.

ShopHeader.vue
<template>
<header>
<a href="/">The shop</a>
<nav>…</nav>
</header>
</template>
App.vue
<script setup>
import ShopHeader from './ShopHeader.vue';
import ProductCard from './ProductCard.vue';
</script>
<template>
<ShopHeader />
<main>
<ProductCard />
</main>
</template>

The template sees an imported component directly. Without the import, Vue does not know what ShopHeader is: the console says "[Vue warn]: Failed to resolve component: ShopHeader", and an unknown tag stays on the page. The name starts with a capital letter by convention (PascalCase), so it differs from HTML tags. Vue also accepts <shop-header />.

Try it

Put the page together

ShopHeader and ProductCard are ready in their files. Put App together: ShopHeader at the top, and the product card in main, under the heading "Dairy".

Vue

The idea

Only the difference

Here is the cart from the problem as a component.

ref('2') is the memory of the component: a value Vue watches. When it changes, Vue draws again the components that read it. It is the topic of the module "State". Here it is only so that something can change.

Vue

Do the same as in the problem: click into the milk field, delete 2 and type 15. The field keeps the focus and what you typed, and the total becomes €25.20. The component describes the whole screen on every key, like render(). The difference is that the page is not built again. The library sees that in the new description the field is in the same place with the same tag, and changes only its value and the text of the total.

See it

See the comparison

<script setup>
import { ref } from 'vue';
const money = (x) => '€' + x.toFixed(2);
const milk = ref('2');
const bread = ref('1');
</script>
<template>
<main>
<h1>The cart</h1>
<label>
Fresh milk
<input type="number" :value="milk" @input="milk = $event.target.value" />
</label>
<label>
White bread
<input type="number" :value="bread" @input="bread = $event.target.value" />
</label>
<p>Total: {{ money(Number(milk) * 1.6 + Number(bread) * 1.2) }}</p>
</main>
</template>
Step by step, in words
  1. The buyer has deleted 2 and typed 1: the data say milk = "1". In the middle is the last description the component returned; on the right is the DOM, with the focus in the milk field.
  2. The buyer types 5. @input writes "15" into milk: the data change, and Vue knows that the template reads milk.
  3. Vue compiled the template into a render function. Because milk changed, it runs it again, and it returns a new description: a tree of plain objects.
  4. The library compares the new description with the previous one. Same tags in the same order; two differences: the value of the field and the text of the total.
  5. Only the two differences reach the DOM. The <input> is the same element as before, so the focus and the cursor stay in it.
  6. You describe the whole screen for the given data, as with render(). The library finds what changed and touches only that. Both libraries do this, each in its own way.

The description is cheap: it is a tree of plain objects, and the component makes it again every time. The DOM is the expensive part, so the library touches only the differences there. You write as with render(): the whole screen from the data. The price of render() (lost focus, thousands of new elements) is not paid.

More precisely, if you are curious

Virtual DOM. The description is often called the virtual DOM, and comparing it with the old one is called reconciliation (React) or patching (Vue). A node of the description is an object like { type: 'h1', props: { children: 'The cart' } }.

The two ways. The template compiles to a render function that returns the nodes of the description. The compiler marks which parts never change, and the comparison skips them. Vue remembers which data each component reads (reactivity) and runs again only the components whose data changed. In both cases there is a new description at the end, a comparison and a few changes in the DOM.

Text is text. {{ product.name }} puts in text, like textContent. A name like <img src=x onerror=alert(1)> shows as characters and does not become a tag. HTML from a string goes in only with v-html, and the name tells you to be careful.

Options API. In older code a component is an object: export default { data() { … }, methods: { … } }. It still works, but new code uses <script setup> (Composition API).

v-model. The pair :value and @input has a shorthand, v-model. It belongs to the lesson on forms.

Exercise

The product card

ProductCard holds the object product (White cheese). Make the card: the name as a level-two heading, "Price: €7.90" (with money), "In stock: 12 pcs" and an "Add" button whose name for the screen reader is "Add White cheese". A click on the button writes "Added: White cheese" to the console (the function add is ready). Then put App together: ShopHeader at the top, and the card in main, under the heading "Dairy".

Vue

Test

Check yourself

1. A component is drawn a second time with the same data. What does it show?
2. Which line in the template shows the product name as the alt of the image?
3. Why does the focus stay in the field in the component, while render() from the previous module loses it?
4. App.vue uses <ShopHeader />, and the console says Failed to resolve component: ShopHeader. What is missing?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·