Update the inital array of a recursive treeview in VueJS - javascript

I was folling this tutorial for my own tree view with a recursive component in vuejs.
So the input array looks like this:
let tree = {
label: 'root',
nodes: [
{
label: 'item1',
nodes: [
{
label: 'item1.1'
},
{
label: 'item1.2',
nodes: [
{
label: 'item1.2.1'
}
]
}
]
},
{
label: 'item2'
}
]
}
<template>
<div>
...
<tree-menu
v-for="node in nodes"
:nodes="node.nodes"
:label="node.label" />
...
</div>
<template
<script>
export default {
props: [ 'label', 'nodes' ],
name: 'tree-menu'
}
</script>
So basically a label and a subarray of nodes is passed to a child node. Now I want to update or delete a node (e.g. item1.1), but reflect this change in the outmost array (here tree), because I want to send this updated structure to the server. How can I achive this? If I change the label of a node, this will be rendered in the DOM, but the tree array is not updated.

Here's how you can use the .sync modifier to update recursively:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('tree-node', {
template: `
<div style="margin-left: 5px;">
<input :value="label"
type="text"
#input="$emit('update:label', $event.target.value)" />
<tree-node v-for="(node, key) in nodes"
:key="key"
v-bind.sync="node" />
</div>
`,
props: ['label', 'nodes']
});
let tree = {
label: 'root',
nodes: [{
label: 'item 1',
nodes: [
{ label: 'item 1.1' },
{ label: 'item 1.2',
nodes: [
{ label: 'item 1.2.1' }
]
}
]
},
{ label: 'item 2' }
]
};
new Vue({
el: '#app',
data: () => ({
tree
})
})
#app {
display: flex;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<div id="app">
<div>
<tree-node v-bind.sync="tree" />
</div>
<pre v-html="tree" />
</div>
v-bind.sync="node" is shorthand for :label.sync="node.label" :nodes.sync="node.nodes". v-bind unwraps all object members as attributes of the tag, resulting in props for the component.
The other half of the solution is replacing v-model on the input with :value + an $emit('update:propName', $event.target.value) call on #input which updates the .sync-ed property in the parent. To conceptualize it, it's a DIY v-model exposed by Vue so it could be customized (you decide when to call the update and what to update with). You can replace the <input> with any other type of input, depending on what you're binding/modifying (checkboxes, textarea, select, or any fancier input wrapper your framework might feature). Depending on type of input you'll want to customize the listener: #change, #someCustomEvent, etc...
.sync makes everything reactive at each individual level. Since everything is :key-ed, no re-rendering actually happens (Vue only re-renders DOM elements which actually changed). If that wasn't the case, the input would lose focus upon re-rendering.
The update principle is: instead of making the change at child level you update the parent property which, through v-bind, sends it back to the child.
It's the same exact principle used by Vuex. Rather than changing some local prop you call a store mutation which comes back through getters and modifies the local value but it happens for any component using that store data, not just for current one.

Related

Vue reactivity, prevent re-rendering component on prop update with input v-model

I am trying to workout how I can mutate a prop in a text input without causing the component to re-render.
Here is my code
//App.vue
<template>
<div class="row">
<category-component v-for="category in categories" :key="category.title" :category="category" />
</div>
</template>
export default {
name: "App",
data() {
return {
categories: [
{
title: "Cat 1",
description: "description"
},
{
title: "Cat 2",
description: "description"
},
{
title: "Cat 3",
description: "description"
}
]
};
},
components: {
CategoryComponent
}
}
// CategoryComponent.vue
<template>
<div>
<input type="text" v-model="category.title">
<br>
<textarea v-model="category.description"></textarea>
</div>
</template>
When I update the text input, the component re-renders and I lose focus. However, when I update the textarea, it does not re-render.
Can anyone shed any light on this? I am obviously missing something simple with vue reactivity.
Here is a basic codesandbox of my issue.
The issue you met is caused by :key="category.title". Check Vue Guide: key
The key special attribute is primarily used as a hint for Vue’s
virtual DOM algorithm to identify VNodes when diffing the new list of
nodes against the old list. Without keys, Vue uses an algorithm that
minimizes element movement and tries to patch/reuse elements of the
same type in-place as much as possible. With keys, it will reorder
elements based on the order change of keys, and elements with keys
that are no longer present will always be removed/destroyed.
When change the title, the key is changed, then the VNode will be destroyed then created, that is why it lost the focus and it would not lose the focus when changing the summary.
So the fix is uses second parameter=index of v-for as the key instead.
And don't mutate the props directly, so in below snippet, I used one computed setter to emit one input event to inform the parent component update the binding values (by v-model).
Also you can check this question: updating-a-prop-inside-a-child-component-so-it-updates-on-the-parent for more details on updating a prop inside the component.
Vue.component('category-component', {
template:`
<div class="hello">
<input type="text" v-model="innerTitle">
<br>
<textarea v-model="innerSummary"></textarea>
</div>
`,
props: {
value: {
default: () => {return {}},
type: Object
}
},
computed: {
innerTitle: {
get: function () {
return this.value.title
},
set: function (val) {
this.$emit('input', {summary: this.value.summary, title: val})
}
},
innerSummary: {
get: function () {
return this.value.summary
},
set: function (val) {
this.$emit('input', {title: this.value.title, summary: val})
}
}
}
})
new Vue ({
el:'#app',
data () {
return {
categories: [
{
title: "Cat 1",
summary: "description"
},
{
title: "Cat 2",
summary: "description"
},
{
title: "Cat 3",
summary: "description"
}
]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
{{ categories }}
<category-component v-for="(category, index) in categories" :key="index" v-model="categories[index]" />
</div>

Creating treeview in vue js - unknown depth of the tree - Modifying the DOM

I'm trying to create a vue js application where a treeview is displayed to the user. The elements inside the treeview can contain other elements, that can contain other elements etc ... With no limit, which means that the depth of the treeview is not known. If I'm right, it means I can't simply use the v-for directive (because it implies to know the depth right ?)
So i'm basically looping going through a json array and creating <ul> and <li> tags to append them to some other tag in the DOM, but if I do this, they don't get the styles of their class applied to them.
I suppose it's because Vue doesn't like the fact that we modify the DOM without having vue doing it for us.
Also, We don't want to use components libraries like vuetify, we want to do it with vue only and simple javascript.
Thank you !
This is actually pretty straight forward in Vue.js.
What you have to do is simply create a component that invokes itself but changing the v-for to use the current tree branch's children.
An important step for making this work in Vue is to apply the name key to the component. Otherwise, the component can not invoke itself.
I have provided a simple example below using HTML's neat details element.
// Your recursive branch component "branch.vue"
const branch = {
name: 'branch',
props: {
branch: {
type: Object,
default: () => ({}),
},
},
template: `
<details>
<summary>{{ branch.title }}</summary>
<branch
v-for="branch in branch.children"
:key="branch.title"
:branch="branch"
/>
</details>
`,
}
// Your page view(component) where you want to display the tree
new Vue({
el: '#app',
name: 'tree',
components: {
branch,
},
data() {
return {
tree: [
{
title: 'parent 1',
children: [
{
title: 'child 1',
children: [
{
title: 'child 1-1',
children: [],
},
{
title: 'child 1-2',
children: [],
},
],
},
],
},
],
};
},
})
#app > details {
margin-left: 0;
}
details {
margin-left: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<branch v-for="branch in tree" :key="branch.title" :branch="branch" />
</div>
The solution is to create a recursive component, for example see here and
here

Combine vue props with v-for

Here's sample of my child component
HTML:
<div v-for="(input, index) in form.inputs" :key="index">
<div>
<input :name"input.name" :type="input.type" />
</div>
</div>
JavaScript (Vue):
<script>
export default {
name: "child",
props: ['parentForm'],
data() {
return {
form: {
inputs: [
{
name: 'name',
type: 'text'
],
[...]
}
}
}
And sample of root component
HTML:
<child :parentsForm="form"></child>
JavaScript (Vue):
<script>
import child from "./child";
export default {
name: "root",
components: { child },
data() {
return {
form: {
data: {
name: null,
email: null,
...
}
}
}
The question is, how do I achieve combining root + v-for?
Example I want to using child component this way
<input :name"input.name" :type="input.type" v-model="parentForm.data . input.name" />
Since parentForm.data will bind form:data:{ and this will be the variable get from input.name }
Output in v-model should be bind form.data.name or form.data.email on root component
Thank you
You can use it as per follow,
<input :name="input.name" :type="input.type" v-model="parentForm.data[input.name]" />
This will bind parentForm.data.name for input.name = 'name' to v-model.
If I understood you correctly, you want to update parent data from your child component. If yes then you have two options.
In you child component use $parent.form.data to bind.
Or you can pass it down as prop assign it to a data property in child. Bind this new data property in your child and emit it whenever any changes are made. Receive this emit in your parent and update the parent property respectively (Recommended)

Why is v-model inside a component inside a v-for (using a computed list) changing weirdly when the list changes?

I'm using a computed list to display several forms for changing comments in a database. (backend Symfony / api requests via axios, but unrelated)
The form for the comments itself is in a Vue component.
The computed list is based on a list that gets loaded (and set as data property) when the page is mounted which is then filtered by an input search box in the computed property.
Now when i type different things in the input box and the comment component gets updated the v-model and labels are messing up.
I've tested in several browsers and the behaviour is the same in the major browsers.
I've also searched the docs and haven't found a solution.
Example to reproduce behaviour:
<!DOCTYPE html>
<html>
<div id="app"></app>
</html>
const ChangeCommentForm = {
name: 'ChangeCommentForm',
props: ['comment', 'id'],
data() {
return {
c: this.comment,
disabled: false
};
},
template: `
<form>
<div>{{ comment }}</div>
<input :disabled="disabled" type="text" v-model="c">
<button type="submit" #click.prevent="changeComment">
Change my comment
</button>
</form>
`,
methods: {
changeComment() {
this.disabled = true;
// do the actual api request (should be unrelated)
// await api.changeCommentOfFruit(this.id, this.c),
// replacing this with a timeout for this example
window.setTimeout(() => this.disabled = false, 1000);
}
}
};
const App = {
components: {ChangeCommentForm},
data() {
return {
fruits: [
{id: 1, text: "apple"},
{id: 2, text: "banana"},
{id: 3, text: "peach"},
{id: 4, text: "blueberry"},
{id: 5, text: "blackberry"},
{id: 6, text: "mango"},
{id: 7, text: "watermelon"},
],
search: ''
}
},
computed: {
fruitsFiltered() {
if (!this.search || this.search === "")
return this.fruits;
const r = [];
for (const v of this.fruits)
if (v.text.includes(this.search))
r.push(v);
return r;
}
},
template: `
<div>
<form><input type="search" v-model="search"></form>
<div v-for="s in fruitsFiltered">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
</div>
`
};
const vue = new Vue({
el: '#app',
components: {App},
template: '<app/>'
});
Just type some letters in the search box
Example on codepen: https://codepen.io/anon/pen/KLLYmq
Now as shown in the example the div in CommentChangeForm gets updated correctly, but the v-model is broken.
I am wondering if i miss something or this is a bug in Vue?
In order to preserve state of DOM elements between renderings, it's important that v-for elements also have a key attribute. This key should remain consistent between renderings.
Here it looks like the following might do the trick:
<div v-for="s in fruitsFiltered" :key="s.id">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
See:
https://v2.vuejs.org/v2/guide/list.html#Maintaining-State
https://v2.vuejs.org/v2/api/#key

How to render components dynamically in Vue JS?

I am making a form generator, which uses components in it for input fields, buttons etc. I want to be able to generate the form depending on what options I pass to it.
But I can't get it to render the components.
I tried to return pure HTML but that won't render the components.
I call the form generator from my Home.vue template where I want the form with an options object like this:
options: {
name: {
type: 'input',
label: 'Name'
},
submit: {
type: 'button',
label: 'Send'
}
}
In template:
<template>
<form-generator :options="options"></form-generator>
</template>
In the form generator component I have tried multiple things like:
<template>
{{ generateForm(this.options) }}
// ... or ...
<div v-html="generateForm(this.options)"></div>
</template>
I include all the components like:
import {
FormButton,
FormInput
} from './FormComponents'
Now the final part is how do I make FormInput render?
This does not work since it outputs the HTML literally:
methods: {
generateForm(options) {
// .. do stuff with options ..
var form = '<form-input />'
return form
}
}
Vue has a very simple way of generating dynamic components:
<component :is="dynamicComponentName"></component>
So I suggest you define the options as an array and set the type to be the component name:
options: [
{
type: 'FormInput',
propsData: {label: 'Name'}
},
{
type: 'FormButton',
propsData: {label: 'Send'}
}
]
Then use it in the form generator like this:
<component :is="option.type" v-for="option in options"></component>
You can also pass properties as you'd pass to ant other component, but since it's dynamic and every component has a different set of properties i would pass it as an object and each component would access the data it needs:
<component :is="option.type" v-for="option in options" :data="option.propsData"></component>
UPDATE
Since you don't have control of the components it requires a bit more manipulation:
For each component that requires text, add a text attribute in the options:
options: [
{
type: 'FormInput',
propsData: {label: 'Name'}
},
{
type: 'FormButton',
text: 'Send',
propsData: {label: 'Send'}
}
]
And then just use it in the component:
<component :is="option.type" v-for="option in options">{{option.text}}</component>
For passing attributes, I think you can pass it using v-bind and then it will automatically destructure them, so if a button accepts 2 props: rounded, color
the options would look like:
{
type: 'FormButton',
text: 'Button',
propsData: {rounded: true, color: '#bada55'}
}
and then the component:
<component :is="option.type" v-for="option in options" v-bind="option.propsData">{{option.text}}</component>
you can create an Array like this:
components_data: [
{
name: 'checkbox',
data: false
},
{
name: 'text',
data: 'Hello world!'
}
]
and then loop through this array inside of the <component>:
<component
v-for="(component,i) in components_data"
:key="i"
:is="component.name"
:data="component.data"
/>
this will create 2 component [<text>, <checkbox>] dynamically and give them data via props.
when you push new data like this components_data.push({name:'image',data: {url:'cat.jpg'}}) it will render a new component as <image :data="{url:'cat.jpg'}"/>

Categories

Resources