Components and state
Components: props, slots, events and lifecycle
The same card shows up in five places on the page. Copying and pasting HTML does not scale. Register it once, use it as a tag — and carry on with no build step, because the template is an ordinary HTML string.
Registering
V.component('user-card', {
props: {
name: { type: 'string', default: 'no name' },
age: { type: 'number', default: 0 }
},
state(props) {
return { likes: 0 };
},
computed: {
summary() { return `${this.name}, ${this.age} years old`; }
},
methods: {
like() { this.likes++; this.emit('liked', this.likes); }
},
mounted() { console.log('mounted', this.name); },
template: `
<article>
<h3>{ summary }</h3>
<slot></slot>
<button @click="like()">Like ({ likes })</button>
</article>
`
});
Three spellings use the same component:
<UserCard name="Ann" :age="annsAge" @liked="record($detail)">
<p>This content goes into the slot.</p>
</UserCard>
<user-card name="Bea"></user-card>
<div v-component="user-card" name="Chris"></div>
And really working, with the registration and the use in the same block:
<div>
<script>
V.component('simple-counter', {
props: { title: { type: 'string', default: 'Counter' } },
state: () => ({ n: 0 }),
computed: { double() { return this.n * 2 } },
methods: { add() { this.n++; this.emit('changed', this.n) } },
template: '<p><b>{ title }</b>: { n } (double { double }) ' +
'<button @click="add()">+1</button></p>'
});
</script>
<div v-data="{ last: 0 }">
<simple-counter title="Visits" @changed="last = $detail"></simple-counter>
<simple-counter title="Clicks" @changed="last = $detail"></simple-counter>
<p>The last value emitted was { last }.</p>
</div>
</div>
The two counters have state of their own, and the outer scope only learns about them through the event they emit. That separation is what makes a component worth it.
Props
Static props come in as attributes, dynamic props as :prop. The accepted types are
string, number, boolean, array,
object and any, and the attribute value is converted for you.
<card title="Revenue" total="1200" active></card>
<card :title="panel.name" :total="panel.revenue" :active="panel.on"></card>
All three of user-name, username and userName arrive as
userName, which heads off the argument about which spelling to use in the HTML.
When the type does not matter, there is a short form:
V.component('greeting', {
props: ['name', 'age'],
template: '<p>{ name } is { age } years old</p>'
});
Slots
V.component('panel', {
template: `
<section>
<header><slot name="header"><h3>No title</h3></slot></header>
<div><slot></slot></div>
<footer><slot name="footer"></slot></footer>
</section>
`
});
<panel>
<h3 slot="header">Report</h3>
<p>This paragraph lands in the default slot.</p>
<button slot="footer">Close</button>
</panel>
The content written inside a <slot> in the template is the default, used when
nobody fills it. And the slot content is evaluated in the scope of whoever wrote the
tag, not the component's — so it sees the variables of the place it sits in, the way any
ordinary HTML would.
Events
methods: {
confirm() { this.emit('confirmed', { id: this.id }); }
}
<dialog-box @confirmed="record($detail)"></dialog-box>
emit fires a CustomEvent that bubbles up the tree, and whatever you
pass as the second argument arrives in $detail. It is the same mechanism as
$dispatch, which means any ancestor can listen for the event without having to be
the immediate parent.
Lifecycle and instance
The hooks are beforeMount, mounted, updated,
beforeUnmount and unmounted. Inside the methods, this has
$el, $props, $refs, $scope,
$parent, $name, emit(), $watch() and
$nextTick().
<div>
<script>
V.component('a-clock', {
state: () => ({ now: new Date() }),
mounted() {
this.timer = setInterval(() => { this.now = new Date() }, 1000);
},
beforeUnmount() { clearInterval(this.timer) },
template: '<time>{ now.toLocaleTimeString() }</time>'
});
</script>
<div v-data="{ on: true }">
<button @click="on = !on">Switch on and off</button>
<p><a-clock v-if="on"></a-clock></p>
</div>
</div>
beforeUnmount exists for precisely this kind of case: without it, the
setInterval would keep running after the element left the screen.
A component does not see the scope outside it
By default a component does not see the v-data wrapping it, so that it does
not depend on where it was placed. Everything it needs comes in as a prop. When you want the
opposite — usually for internal components of a single page — declare
inheritScope: true in the definition.
provide and inject
Passing the same prop down three levels gets old. provide hands a value to the whole
tree, and inject picks that value up at any depth — the names and the behaviour are
Vue's.
<div>
<script>
V.component('user-badge', {
inject: { user: { default: 'guest' }, plan: { default: 'Free' } },
template: '<b>{ user } — { plan } plan</b>'
});
V.component('user-panel', {
provide: { user: 'Ann', plan: 'Pro' },
template: '<section><user-badge></user-badge></section>'
});
</script>
<user-panel></user-panel>
<p>Outside the panel, the default holds:</p>
<user-badge></user-badge>
</div>
inject accepts the short list of names — inject: ['user'] — or the
object with from and default, as above. The value enters the
component's state, so the template uses it like any other variable. An application created with
createApp provides too, through the options or through
app.provide(key, value).
The value goes down to the components the template itself mounts. A component arriving through
the <slot> belongs to whoever wrote the tag, and therefore sees the
provide from over there, not the one from the component that received it. This is
Vue's behaviour too, and it follows from the slot scope rule explained above.
When it is worth it
| Situation | Use |
|---|---|
| The same HTML repeats with different data | A component |
| The stretch shows up only once | v-data, with no component |
| You need the same state in distant places | A store |
| It is an ordinary UI control | A ready-made component, if one exists |
| It is a behaviour, not a structure | A directive of your own |
Where to register
The registration has to happen before the library sweeps the page, otherwise it walks past a tag
it does not know yet. With data-manual, that is simple to guarantee:
<script src="voodoo.full.min.js" data-manual defer></script>
<script src="components.js" defer></script>
<script defer>V.start();</script>
A component registered after that still works for HTML created from that moment on, because the
DOM observer stays active — but the tags that were already on the page do not go back on their
own. In that case, call V.refresh().