Vue vs Traditional HTML - Reusability & Components - Beginner's Guide

Published 3/9/2019

This article is part of a series:

If you want to follow along I recommend you to use codesandbox.

In the world of HTML let's say we want to create a panel that consists of a header and text. You could create something like this

<div class="panel">
    <div class="panel__header">Title</div>
    <div class="panel__body">
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusantium, sit!
    </div>
</div>

From here you can apply CSS and JavaScript to these classes. Then you can go ahead and reuse this HTML as often as you please. It became reusable thanks to the classes. This is the way how CSS frameworks like bootstrap were working for years.

Let's look at how Vue handles reusability:

The first difference is that we have to create a base class for panels, and we do so in a component.

So let's create the component Panel.vue

<template>
<div>
    <div class="header">Title</div>
    <div class="body">
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusantium, sit!
    </div>
</div>
</template>

<script>
export default {

}
</script>

Note how we can eliminate some classes since our CSS will be scoped to this component and it is clear that header refers to the panel header.

Now instead of repeating this HTML block over and over again, you can just go ahead and import the component whereever you need.

Let's add two panels to the component App.vue

<template>
<div>
    <Panel />
    <Panel />
</div>
</template>

<script>
import Panel from './Panel.vue'

export default {
    components: { Panel },
}
</script>

This seperation of concerns is great, because instead of various nested div containers we simply end up with Panel making our template very easy to follow. But wait! Like this, the title and body will always be the same. That's right, so what we need is to make these properties dynamic.

For that purpose we have to make the parent component (App.vue) pass down the title and body to the child component (Panel.vue). The child component defines what so called props it accepts.

Props

Panel.vue

<template>
<div>
    <div class="header">{{ title }}</div>
    <div class="body">{{ body }}</div>
</div>
</template>

<script>
export default {
    props: {
        title: {
            type: String,
            required: true,
        },
        body: String,
    }
}
</script>

Our component accepts two props. The title which has to be a string and is required, and the body which is also a string, but not necessarily required.

And App.vue can now pass down the props to the panel.

<template>
<div>
   <Panel title="Lorem Ipsum" body="Lorem ipsum dolor sit amet" />
   <Panel title="Something else" />
</div>
</template>

<script>
import Panel from './Panel.vue'

export default {
    components: { Panel },
}
</script>

Props are quite similar to normal HTML attributes. Take a look at the following element

<input type="submit" value="Submit" />

The input element accepts the attributes type and value, among many others. It understands and can use these because they are defined on the input element itself. If you were to write <input color="primary" /> it would simply ignore the attribute color because it does not exist on input.


Now some panels are especially important and their background needs to be highlighted. In HTML you would now add a modifier class to the panel and style it.

Let's add the class panel--primary.

<div class="panel panel--primary">
    <div class="panel__header">Title</div>
    <div class="panel__body">
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusantium, sit!
    </div>
</div>

CSS

.panel.panel--primary .panel__header {
    background-color: #369;
    color: #fff;
}

In Vue this would simply become another prop.

Panel.vue

<template>
<div :class="{primary: isPrimary}">
    <div class="header">{{ title }}</div>
    <div class="body">{{ body }}</div>
</div>
</template>

<script>
export default {
    props: {
        title: String,
        body: String,
        isPrimary: {
            type: Boolean,
            default: false,
        },
    }
}
</script>

<style scoped>
    .primary {
        background-color: #369; /* you might as well have a global CSS rule for the background color */
    }
</style>

We add the isPrimary prop to our props list. Note how we default it to false. How convenient. Now we only need to pass the isPrimary prop when we actually want a primary panel.

We also add the class primary to the classlist of the root element with :class="{primary: isPrimary}". If you are confused about that syntax be sure to check out my previous article on CSS in Vue.

Back in App.vue we can simply add the isPrimary prop the same way you would add boolean like HTML attributes like selected or checked.

<template>
<div>
   <Panel isPrimary title="Lorem Ipsum" body="Lorem ipsum dolor sit amet" />
   <Panel title="Something else" body="Lorem ipsum dolor sit amet" />
</div>
</template>

<script>
import Panel from './Panel.vue'

export default {
    components: { Panel },
}
</script>

Passing data as props

So far we have only passed strings to the child. But what happens when we have to pass any other data?

Back in App.vue let's define the title and body as actual data and try to pass it to the child.

<template>
<div>
   <Panel isPrimary title="title" body="body" />
</div>
</template>

<script>
import Panel from './Panel.vue'

export default {
    components: { Panel },
    data() {
        return {
            title: 'Lorem Ipsum',
            body: 'Lorem ipsum dolor sit amet',
        }
    }
}
</script>

The above will not work. It would literally pass the string title and body and not the contents of the variable. In order to fix that, we have to add a prefix to the prop. For that we only have to change the <template> part of App.vue.

<template>
<div>
   <Panel isPrimary v-bind:title="title" v-bind:body="body" />
</div>
</template>

You can and I recommend you to abbreviate the above to

<template>
<div>
   <Panel :title="title" :body="body" />
</div>
</template>

In fact v-bind allows any JavaScript expression.

<template>
<div>
   <Panel :title="title.toUpperCase() + ', ' + body.substr(0, 20)" />
</div>
</template>

Also, if you want to pass a number, boolean, array or object, you also have to do that through an expression.

<template>
<div>
   <Panel 
       :someNumber="1"
       :someBoolean="false"
       booleanThatEvaluatesToTrue
       :array="[1, 2, 3]"
       :object="{ key: 'value' }"
   />
</div>
</template>

Be aware that you not only create new components for reusability reasons. Whenever a component becomes too complex or you realize it is doing more than one thing, consider splitting it up into multiple components. It helps making the code organized.

Imagine our panel header becomes more complex and we want to split it up into its own component.

Panel.vue

<template>
<div>
    <div class="header">
        <PanelHeader :title="title" :isPrimary="isPrimary"/>
    </div>
    <div class="body">{{ body }}</div>
</div>
</template>

<script>
import PanelHeader from './PanelHeader'

export default {
    components: { PanelHeader },
    props: {
        title: String,
        body: String,
        isPrimary: {
            type: Boolean,
            default: false,
        },
    }
}
</script>

PanelHeader.vue

<template>
<div :class="{ primary: isPrimary }">
    {{ title }}
</div>
</template>

<script>
export default {
    props: {
        title: String,
        isPrimary: {
            type: Boolean,
            default: false,
        },
    }
}
</script>

<style scoped>
.primary {
    background-color: #369;
}
</style>

Panel.vue still receives the props title and isPrimary from App.vue. However, it is not really doing anything with them. It simply passes the props further down to PanelHeader.vue.

You can shorten <PanelHeader :title="title" :isPrimary="isPrimary"/> to <PanelHeader v-bind="{ title, isPrimary }" />.

Please note that App.vue has no idea and doesn't care that the panel header became its own component.

Slots

Props are great but what if we want more than just basic text in our panel body. What if we want some HTML with specific styling and functionality. For this case we have slots.

We no longer need the body prop, so let's remove that. For the slot, let's add <slot name="body" />.

Panel.vue

<template>
<div>
    <div class="header">{{ title }}</div>
    <div class="body">
        <slot name="body" />
    </div>
</div>
</template>

<script>
export default {
    props: {
        title: String,
    }
}
</script>

Basically in the template all we do is replace {{ body }} with <slot name="body" />.

And in App.vue we can now add the button inside an element that has the attribute slot="body".

<template>
<div>
    <Panel title="Lorem Ipsum" body="Lorem ipsum dolor sit amet">
        <div slot="body">
            <button @click="scream">Scream</button>
        </div>
    </Panel>
    <Panel title="Something else" body="Lorem ipsum dolor sit amet" />
</div>
</template>

<script>
import Panel from './Panel.vue'

export default {
    components: { Panel },
    methods: {
        scream() {
            alert('AAAAH')
        },
    },
}
</script>

What will happen is that <div slot="body"> from App.vue will be placed in <slot name="body" /> from Panel.vue.

Events

Since we pass down the title as a prop we can not update it inside panel. If we want to update the title we have to fire an event from the child to parent.

Props down, events up

For this let's choose a different example that makes a little more sense.

AwesomeCounter.vue

<template>
<div>
    <button @click="increment">{{ awesomeCount }}</button>
</div>
</template>

<script>
export default {
    props: {
        awesomeCount: Number
    },
    methods: {
        increment() {
            this.$emit('update:awesomeCount', this.awesomeCount + 1)
        },
    },
}
</script>

This is our counter, when we press the button it will send the event update:awesomeCount to the parent and passes the incremented value.

In the following code snippet you can see that we can listen to the event update:awesomeCount the same way we listen to normal events like click using @click. In the function we can access the new count using $event.

App.vue

<template>
<div>
    <AwesomeCounter :awesomeCount="count" @update:awesomeCount="count = $event"/>
</div>
</template>

<script>
import AwesomeCounter from './AwesomeCounter'

export default {
    components: { AwesomeCounter },
    data() {
        return {
            count: 10,
        }
    }
}
</script>

In fact, this is such a common scenario that you can abbreviate the above template to simply be

<template>
<div>
    <Counter :awesomeCount.sync="count"/>
</div>
</template>

For this to work the event has to have the name update:{name of prop}.

Events are not only used to update data from the child. You can do anything you want like making ajax calls for example.

devtools

If you have problems understanding some of the concepts explained here, need help debugging or simply want to step up your vue game, check out vue devtools. You will be able to see your components and how they are connected in a DOM like tree view, which data and props they own, what events have been fired etc. You can even manipulate data!

Notes

  • You don't necessarily have to define props in an object. You can also define them in an array props: ['title', 'body']
  • We looked at some requirements to define props, like type, default and required. There is more here.