VUE v-model, two-way data binding and editing in multi nested components, dynamic components
Roland Csibrei
Posted on January 5, 2020
Hello guys!
Check out my first public VUE sandbox. Check it on desktop. It shows how to deal with multi nested components in VUE and how to pass data with v-model from the topmost component down to the children components.
I am quite new to VUE, so I really just want to share my experiences and not to tell you, how do you must do it. Maybe some more experienced VUEers will highlight the weak spots in the comment section. They are welcome!
So let's move on! At first, this is not a complete beginner article, you need to be familiar with the basics of VUE. The sandbox shows, how to pass data between components just using v-model
and without emit
, or get
, set
. There is a value-peeker
object, which real time shows the current value
for the given component. On the right you can see the root object, App.inputList
.
Here is the sandbox:
Some intro to this sandbox
Every article I read about VUE's two-way binding in nested components was showing examples only with two level deep nesting and used one of these techniques to ensure two-way binding:
The first one is creating computed getters and setters for the property:
<template>
// use the value
</template>
props: {
value: Object
},
computed: {
value: {
get () {
return this.value
},
set (val) {
this.$emit('input', val)
}
}
}
or this, the second technique relies on a watcher:
<template>
// use the localValue
</template>
props: {
value: Object
},
data () {
localValue: {...this.value}
},
watch: {
localValue(val) {
this.$emit('input', val)
}
}
IMHO, nobody ever mentioned, that in multi nested components we do not have to use any of those techniques to pass the object. The only thing we need is v-model
.
App.vue
I created the root or base object inputList
, which will be edited by multiple nested components:
data() {
return {
inputList: [
{
name: "titles",
label: "Titles",
type: "my-input-list",
items: [
{ label: "PhDr", value: "phdr" },
{ label: "Dr", value: "dr" }
]
},
{
name: "firstname",
label: "First name",
type: "my-input-text"
},
...
The root object is passed via v-model
to a custom component called my-form
:
<template>
<div id="app">
<div class="col1">
<my-form v-model="inputList"/>
</div>
...
Components/Form.vue
Iterates the object value
, which is a vue property and was passed by App.vue using the v-model
directive. For each entry it creates a custom component. The component is dynamically created using the vue component
tag. You have to set it's property called is
to the name of the component, you want to create.
<template>
<div>
<div v-for="(item, idx) in value" :key="idx">
<component :is="item.type" v-model="value[idx]"></component>
<div class="hr"></div>
</div>
<!-- Save button -->
<input type="button" value="SAVE" @click="save" class="save">
</div>
</template>
You need to register (or load dynamically, it is another story) your components, before you can create them with the component
tag.
Javascript imports
import InputText from "./dynamic/InputText";
import InputBoolean from "./dynamic/InputBoolean";
import InputList from "./dynamic/InputList";
...
Registering VUE components:
export default {
name: "Form",
...
components: {
"my-input-text": InputText,
"my-input-boolean": InputBoolean,
"my-input-list": InputList
}
};
Component with dynamic v-model
Maybe you are surprised why I use v-model="value[idx]"
<div v-for="(item, idx) in value" :key="idx">
<component :is="item.type" v-model="value[idx]"></component>
<div class="hr"></div>
</div>
instead of v-model="item"
<div v-for="(item, idx) in value" :key="idx">
<component :is="item.type" v-model="item"></component>
<div class="hr"></div>
</div>
You can't use the iteration variable item
as v-model
because of variable scope, so just stick to the first valid solution.
components/dynamic/InputList.vue
<template>
<div>
<h1>{{ value.label }}</h1>
<my-value-peeker v-model="getPeekValue" :label="value.type + '.value:'"/>
<div class="component">
<select v-model="value.value" multiple>
<option v-for="(item, idx) in value.items" :key="idx" :value="item.value">{{ item.label }}</option>
</select>
<p>
<input type="button" value="CREATE NEW ITEM" @click="createNewItem" v-if="!isCreateNewItem">
<my-input-list-item-editor
v-model="newItem"
v-if="isCreateNewItem"
@confirm="confirmAdd"
@cancel="cancelAdd"
/>
</p>
</div>
</div>
</template>
Here we have our peeker component with a dynamic :label
. Sometimes it makes sence to do it this way, however it would be cleaner to have a computed label
property in <my-value-peeker>
which alters the label.
<template>
<div>
<h1>{{ value.label }}</h1>
<my-value-peeker v-model="getPeekValue" :label="value.type + '.value:'"/>
...
Set the v-model
attribute for the html select
tag
<select v-model="value.value" multiple>
<option
v-for="(item, idx) in value.items"
:key="idx"
:value="item.value"
>{{ item.label }}</option>
</select>
...
We have a my-input-list-item-editor
, which allows us to add new records to the collection contained in items
. This component emits
two custom events called confirm
and cancel
. The names are self explaining.
<my-input-list-item-editor
v-model="newItem"
v-if="isAdd"
@confirm="confirmAdd"
@cancel="cancelAdd"
/>
confirmAdd() {
// hide add UI
this.isAdd = false;
// push a shallow copy
this.value.items.push({ ...this.newItem });
},
The method confirmAdd
just adds a shallow copy of newItem
to the collection of items
on the components value
object. Since it is an Array
, we can use push. Create a shallow copy (deep clone is not needed) of the object prior to adding it to the collection. If your newItem
contains objects references, you would end up with creating a deep clone of your object instead of the shallow copy.
Use
const arrayDeepClone = JSON.parse(JSON.stringify(originalArray));
or
const arrayDeepClone = Vue._.cloneDeep(originalArray);
For the second one you need to install vue-lodash and set it up. I just ended up adding the initialization directly to main.js. If you are using a framework which hides main.js from you, you have to initialize it other way. For example in Quasar you end up with adding a new boot component.
import VueLodash from "vue-lodash";
Vue.use(VueLodash);
And the event 'cancel' will be handled by method cancelAdd
. It just hides the add new item UI.
cancelAdd() {
// hide add UI
this.isAdd = false;
}
components/dynamic/InputText.vue
This is pretty basic, just pass the v-model
to the input
.
<template>
<div>
<my-value-peeker v-model="getPeekValue" :label="value.type"/>
<div class="component">
<label>{{ value.label }}:</label>
<input type="text" v-model="value.value">
</div>
</div>
</template>
Just the basic stuff.
It is always a good idea to use this style of property definition and set the property type, if it is required, and if not, you should return a default value.
export default {
props: {
// v-model injects the object to be edited into props.value
value: {
type: Object,
required: true
},
},
methods: {
// just the value for the peeker
getPeekValue() {
return this.value;
}
}
};
As you can see no emit
here, no local copies, no setters, no getters, just the v-model
directive. We are actually ended up editing a string
property called value
on object value
. This way we are editing directly the objects properties, but we do not touch the object reference itself.
components/dynamic/InputBoolean.vue
The same basic thing, as in InputText
, just the input was changed to a checkbox.
<template>
<div>
<my-value-peeker v-model="getPeekValue" label="InputBoolean"/>
<div class="component">
<label>{{ value.label }}</label>
<input v-model="value.value" type="checkbox" label="YES">
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
required: true
}
},
methods: {
getPeekValue() {
return this.value;
}
}
};
</script>
components/InputListItemEditor.vue
We have one my-value-peeker
object here and two html input
tags, bound to value.label
and value.value
using v-model
.
<template>
<div>
<my-value-peeker v-model="getPeekValue" label="InputText"/>
<div class="component">
<p>
<label>Label:</label>
<input type="text" v-model="value.label">
</p>
<p>
<label>Name:</label>
<input type="text" v-model="value.value">
</p>
<input type="button" value="Confirm" @click="confirm">
<input type="button" value="Cancel" @click="cancel">
</div>
</div>
</template>
As far as we want to have control over when the value
, which gets written to the parent component, we use two buttons. Both of them has click
event listeners set.
confirm() {
this.$emit("confirm", this.value);
},
This sends or emits a message to the parent component, in our case to components/dynamic/InputList.vue
. Our parent component handles these messages as written above. In confirm
method we send a reference to the object to the parent component.
You can send more parameters with
$emit
. If two or more parameters are used, send the payload asobject
, like this: this.$emit("confirm", { value: this.value, entity: "users", counter: 0, immediate: false})This way you are building your object by setting property names in
v-model
. Beware of typos, you can easily end up with objects with mispelled property names.
Bonus
For demonstration purposes I added a button to App.vue which modifies the inputList[0]
and adds a new item to it. You can see how the change is immediatelly propagated to all children components.
addTitle() {
this.inputList[0].items.push({
label: "Kokki-" + new Date().getTime(),
value: "kokki-" + new Date().getTime()
});
}
And one more: If you press the CREATE NEW ITEM in a ListItem
component a counters starts to count presses. It is just for demonstration, how can we modify the base object from the child component.
Conclusion
v-model
is cool!
Hopefully someone will find this article helpfull! Let me know in the comments!
Feel free to play on my sandbox, comments/critics are appreciated.
Keep coding!
Posted on January 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.