While writing my Vue.js UI Library, Inkline, I had to find a way to make some components work both with and without providing a model value (v-model
). While it's not a common scenario, it's something that you'll definitely come across if you're writing a library and you're serious about Developer Experience (DX).
I call them Optionally Controlled Components, because they're supposed to work out of the box without providing a v-model
, but will give you complete control over their state if you do provide a v-model
.
The Menu Example
One prime example of an Optionally Controlled Component would be a menu that can be opened (expanded) or closed (collapsed). Let's call the component simply MyMenu
.
From a Developer Experience perspective, you'll probably want your library user to be able to drop a <my-menu>
into their code and start adding collapsible content right away, without having to worry about handling its open or closed state.
Here's what the component would look like without v-model
support:
<template>
<div class="my-menu">
<button @click="toggleMenu">
Menu
</button>
<menu v-show="open">
<slot />
</menu>
</div>
</template>
<script>
export default {
name: 'MyMenu',
data() {
return {
open: false
};
},
methods: {
toggleMenu() {
this.open = !this.open;
}
}
}
</script>
~~~
## The Optional Model Value
So far so good. Let's consider the following scenario: your user wants to be able to open or close the menu from somewhere else. We know we can open and close the menu internally at this point, but how do we allow the library user to optionally control the state?
There's a future-proof solution I found, that will save you a lot of trouble. Here's what it looks like:
~~~html
<template>
<div class="my-menu">
<button @click="toggleMenu">
Menu
</button>
<menu v-show="open">
<slot />
</menu>
</div>
</template>
<script>
export default {
name: 'MyMenu',
emits: [
'update:modelValue'
],
props: {
modelValue: {
type: Boolean,
default: false
}
},
data() {
return {
open: this.modelValue
};
},
methods: {
toggleMenu() {
this.open = !this.open;
this.$emit('update:modelValue', this.open);
}
},
watch: {
modelValue(value) {
this.open = value;
}
}
}
</script>
~~~
Try a basic example out live on [CodeSandbox](https://codesandbox.io/s/optionally-controlled-components-43y0b?file=/src/components/MyMenu.vue).
You can see above that I've added the usual `modelValue` prop to provide `v-model` support in Vue 3, but mainly I've done three things:
* I'm setting the initial value of our internal `open` state property to be equal to the one provided via `v-model`. This works wonders, because when there's no `v-model` it would be equal to the specified default, `false` in our case.
* I'm emitting an `update:modelValue` event every time I change the value of `this.open` internally
* I've added a watcher that ensures I'm always keeping the internal `open` value in sync with the incoming external `modelValue` prop.
![Optionally Controlled Component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jgnucpmbptovtoai1b6s.gif)
## Conclusion
Awesome, isn't it? It's important to never forget about Developer Experience. Something as small as this can add up to precious hours of saved development time if done correctly and consistently.
I hope you learned something interesting today. I'd love to hear how the Optionally Controlled Components pattern helped you out, so feel free to reach out to me. **Happy coding!**
<small>**P.S.** Have you heard that Inkline 3 is coming with Vue 3 support? Read more about it on [GitHub](https://github.com/inkline/inkline/issues/207).</small>
Top comments (1)
Hi, that’s a neat trick. But does the watcher necessary? As your if your props changed, your data would be updated. Can I maybe use computed for lesser performance hit?