Have you ever had a component where you needed to override it's internal state?
For example, you have an accordion that keeps track of it's own open
state internally.
But there are a few instances where you want to be able to override that internal state and force it open from the parent component.
This is a tricky but pretty common scenario.
The controlled props pattern is designed for exactly this situation.
We'll start by building a simple component that will show and hide it's content when clicked.
<template><div><divclass="title"@click="toggleHidden">{{ title }}</div><slot v-if="hidden" /></div></template>
export default {name: 'Toggle',props: {title: {type: String,required: true,},},data() {return {hidden: false,};},methods: {toggleHidden() {this.hidden = !this.hidden;},},};
We just need to supply some content for the component in order to use it:
<template><Toggle title="Toggled Content">This content can be toggled on and off.</Toggle></template>
Because the component keeps track of it's own state, the parent component doesn't have to do anything.
This content is normally hidden until the user clicks on the title.
But now our requirements have changed, and now we want to show the content when something else in the app happens. We want the parent to be able to trigger the content to be visible.
In other words, we no longer want the component to manage the open state. We want the parent to manage the hidden state of the toggle.
Refactoring hidden
to be a prop instead is fairly straightforward, so we'll do that right away:
<template><div><divclass="title"@click="$emit('click')">{{ title }}</div><slot v-if="hidden" /></div></template>
export default {name: 'Toggle',props: {title: {type: String,required: true,},hidden: {type: Boolean,default: true,},},};
Using the toggle is now a little more work, but we have full control over it:
<template><Toggletitle="Toggled Content":hidden="hidden"@click="hidden = !hidden">This content can be toggled on and off.</Toggle></template>
In this parent component we can now update hidden
whenever we want, and force the toggle component to show the content.
We've now seen two different ways of writing this component.
The first way is the easiest to use, because the toggle component keeps track of its own state. The second way gives us more control, because it's the parent that has to manage and maintain the state for the toggle.
But can we have both?
Is there a way to have one component that can be used in both ways?
Well, that's what we're going to tackle right now 😉
The big problem we have is that we want to have a hidden
prop as well as a hidden
state variable.
We'll have to use different names for these, so we'll call the state _hidden
so the prop will still have a nice name.
<template><div><divclass="title"@click="toggleHidden">{{ title }}</div><slot v-if="_hidden" /></div></template>
export default {name: 'Toggle',props: {title: {type: String,required: true,},hidden: {type: Boolean,default: undefined,}},data() {return {_hidden: false,};},methods: {toggleHidden() {this._hidden = !this._hidden;},},};
Notice we set the default value of the hidden
prop to undefined
and not false
or true
.
This is necessary because we'll be using a computed prop to "combine" the prop and internal state together:
computed: {$hidden() {return this.hidden !== undefined? this.hidden: this._hidden;},},
If the prop is set, we'll use that. Otherwise we'll use the internal state.
This is what we have so far:
<template><div><divclass="title"@click="toggleHidden">{{ title }}</div><slot v-if="$hidden" /></div></template>
export default {name: 'Toggle',props: {title: {type: String,required: true,},hidden: {type: Boolean,default: undefined,}},data() {return {_hidden: false,};},methods: {toggleHidden() {this._hidden = !this._hidden;},},computed: {$hidden() {return this.hidden !== undefined? this.hidden: this._hidden;},},};
Cool cool cool.
Now we have it set up so that our hidden
prop will override the internal state if it's set.
But now there's an issue with the toggleHidden
method 😕 It just toggles the internal state. If we're using the hidden
prop and want the parent to control the state, we need to emit an event instead.
An easy way to do that is to drop an if
statement in that method:
methods: {toggleHidden() {if (this.hidden !== undefined) {this.$emit('toggle-hidden');} else {this._hidden = !this._hidden;}},}
If you want to be fancy and take it up a level, you can also use computed setters for this too.
Bonus points if you already knew about computed setters, but don't worry if you didn't. I don't think I've ever used them.
Now we've achieved our dream: a component that can use its internal state but also be overridden by a parents state!
You can take this method and apply it to as many things in your component as you want.
There are lots of cases where it's nice to have some properties that are controlled like this while the rest of the component acts "normally".