Progressive Vue

Components — Splitting HTML

The Template Is Getting Big

The Bookshelf app from the previous chapter has grown to a reasonable size.
The template includes cards, a search bar, and tag filters.

As applications grow, writing everything in a single template becomes painful.
Even with plain HTML, you'd want to split by section.

In Vue, you can extract parts of a template into components.

What Is a Component?

A component is like defining your own HTML tag.

HTML has built-in elements like <div>, <span>, <input>.
With components, you can create your own elements like <book-card> or <search-bar>.

<!-- This is the goal -->
<div id="app">
  <h1>My Bookshelf</h1>
  <search-bar></search-bar>
  <tag-filter></tag-filter>
  <book-card v-for="book in filteredBooks" :book="book"></book-card>
</div>

Similar to HTML Custom Elements (Web Components).
Just write <book-card> in the template, and its content expands.

Defining a Component

In a CDN environment, register components with app.component().
Inside setup(), receive props and return values for the template.

app.component('book-card', {
  props: ['book'],
  setup(props) {
    return { book: props.book }
  },
  template: `
    <div class="card">
      <h2>{{ book.title }}</h2>
      <p>{{ book.description }}</p>
      <span class="tag" v-for="tag in book.tags">
        {{ tag }}
      </span>
    </div>
  `
})

Notice the template content.
It's exactly the card HTML from the previous chapter, moved as-is.

props: Receiving Data from Parent

props: ['book'] declares that this component receives book data from outside.
The props received in setup(props) are reactive — when parent data changes, the component updates automatically.

<book-card :book="book"></book-card>

:book="book" — that's v-bind. Passing parent data to the child.
Written as an HTML attribute, as always.

emit: Child-to-Parent Communication

Data flow between components is one-directional.

  • Parent → Child: props (:book="book")
  • Child → Parent: emit (@select="toggleTag")

Look at the search-bar component's setup().

setup(props, { emit }) {
  const onInput = (e) => emit('update:query', e.target.value)
  return { query: props.query, onInput }
}

emit is received from setup()'s second argument.
On each input, it fires an update:query event, and the parent catches it to update searchQuery.
Same concept as HTML's event model (addEventListener / dispatchEvent).

Looking at the Root Template

With components, the root template becomes:

<h1>My Bookshelf</h1>
<search-bar :query="searchQuery" @update:query="searchQuery = $event"></search-bar>
<tag-filter :tags="allTags" :selected="selectedTag" @select="toggleTag"></tag-filter>
<book-card v-for="book in filteredBooks" :book="book"></book-card>

It reads like HTML custom elements.
The app structure is clear at a glance: search bar, tag filter, book cards.

Each component's template is also HTML.
Looking inside a component, you can immediately see what it renders.

Summary

  • Components are like HTML custom elements
  • props receive data from parent — passed as HTML attributes
  • setup(props, { emit }) receives props and emit
  • emit sends events from child to parent — same as HTML's event model
  • Even split into components, each part remains HTML
  • App structure is readable from the template
  • Still just a single index.html file (but in the next chapter...)

Related

Back to book