Vue vs Traditional HTML - Reusability & Components - Beginner's Guide
Published 3/9/2019
This article is part of a series:
1 Vue vs Vanilla JavaScript - Beginner's Guide
2 Vue vs Traditional CSS - Beginner's Guide
3 Vue vs Traditional HTML - Reusability & Components - Beginner's Guide
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.
Check out my e-book!
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
andrequired
. There is more here.