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

On purpose

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

It holds for the template tree, not for the slot

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

SituationUse
The same HTML repeats with different dataA component
The stretch shows up only oncev-data, with no component
You need the same state in distant placesA store
It is an ordinary UI controlA ready-made component, if one exists
It is a behaviour, not a structureA 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().