Vue js. Recursive component ruins my life - javascript

I wanted to create a tree view from an XML file, and I did this. However, when I decided to make it more flexible I encountered some problems.
Here are my components:
Vue.component('elname', {
props: ['text'],
template: '<span>{{ text }}</span>'
})
Vue.component('recursive', {
props: ['d', 'liname', 'openclose'],
template: '#recursive',
data: function() {
return {
seen: true
}
}
}
)
and the Vue object looks like this:
var appp = new Vue({
el: '#here',
data: function(){
return {
friends: '',
}
},
beforeMount() {
parser = new DOMParser();
var response = "<scope><friend><name>Alex</name><hobbies><h>music</h><h>salsa</h></hobbies></friend><friend><name>Natasha</name><hobbies><h>hiking</h></hobbies></friend></scope>";
xml = parser.parseFromString(response, 'text/xml');
children = xml.getElementsByTagName('scope')[0];
this.friends = children;
}
})
I have this variable seen in recursive component
Vue.component('recursive', {
props: ['d', 'liname', 'openclose'],
template: '#recursive',
data: function() {
return {
seen: true // <-- here it is
}
}
}
)
It must change its value #click event to hide a nested list (please, see the JSfiddle), but when it changes it updates its value IN SEVERAL components.
How to make its value be updated only in a particular component?
Here is a template:
<div id="here">
<recursive :d="friends" openclose="[-]"></recursive>
</div>
<template id="recursive">
<div>
<ul v-if="d.children.length != 0">
<li v-for="n in d.childNodes" #click="seen = !seen">
<elname :text="n.tagName"></elname>
{{ openclose }}
{{seen}} <!-- it is just for testing purposes to illustrate how seen var changes -->
<recursive :d="n" openclose="[-]"></recursive>
</li>
</ul>
<ul v-else>
<elname :text="d.textContent"></elname>
</ul>
</div>
</template>

You have two issues:
You need to use click.stop so that the click event doesn't propagate to parents
You need a component inside your recursive to handle the toggling
Vue.component('elname', {
props: ['text'],
template: '<span>{{ text }}</span>'
});
Vue.component('recursive', {
props: ['d', 'openclose'],
template: '#recursive',
components: {
toggler: {
data() {
return {
seen: true
}
},
methods: {
toggle() {
this.seen = !this.seen;
}
}
}
}
});
var appp = new Vue({
el: '#here',
data: function() {
return {
friends: '',
}
},
beforeMount() {
parser = new DOMParser();
var response = "<scope><friend><name>Alex</name><hobbies><h>music</h><h>salsa</h></hobbies></friend><friend><name>Natasha</name><hobbies><h>hiking</h></hobbies></friend></scope>";
xml = parser.parseFromString(response, 'text/xml');
children = xml.getElementsByTagName('scope')[0];
this.friends = children;
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js" integrity="sha256-Ab5a6BPGk8Sg3mpdlsHzH6khPkniIWsvEuz8Fv/s9X8=" crossorigin="anonymous"></script>
<div id="here">
<recursive :d="friends" openclose="[-]"></recursive>
</div>
<template id="recursive">
<div>
<ul v-if="d.children.length != 0">
<li is="toggler" v-for="n in d.childNodes" inline-template>
<div #click.stop="toggle">
<elname :text="n.tagName"></elname>
{{ openclose }}
<recursive v-if="seen" :d="n" openclose="[-]"></recursive>
</div>
</li>
</ul>
<ul v-else>
<elname :text="d.textContent"></elname>
</ul>
</div>
</template>

Currently you have 1 seen variable on an element, which controls the state for all child-elements. So a click on any child will change the seen value in the parent and show/hide all children of this parent.
Solution 1
Change the type of your seen variable to an array - with the same length as the children array. And change your handler to #click="seen[i] = !seen[i]"
Solution 2
Move the click listener to the children. So put #click="seen = !seen" on your outermost div in the template and render the whole list only on v-if="d.children.length && seen"
Vue.component( 'recursive-list', {
props: ["d"],
data: () => ({ expand: true }),
template: `<div style="margin: 5px">
<div v-if="Array.isArray(d)"
style="border: 1px solid black">
<button #click="expand = !expand">Show/Hide</button>
<template v-show="expand">
<recursive-list v-for="e in d" :d="e" />
</template>
<p v-show="!expand">...</p>
</div>
<p v-else>{{d}}</p>
</div>`
} )
new Vue({
el: '#main',
data: { d: ["Text", ["a","b","c"],[[1,2,3],[4,5,6],[7,8]]]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.js"></script>
<div id='main'>
<h3>List:</h3>
<recursive-list :d="d"></recursive-list>
</div>

I've some modifications on your structure, maybe it's not exactly what you need but I think will became more clear.
<template id="tree">
<div>
<ul v-for="(tree, k, idx) in tree.childNodes">
<node :tree="tree" :idx="idx"></node>
</ul>
</div>
</template>
<template id="node">
<li>
<div v-if="tree.childNodes.length">
<span #click="seen = !seen">{{ tree.tagName }}</span>
<span>{{ seen }}</span>
<ul v-for="(node, k, id) in tree.childNodes">
<node :tree="node" :idx="id"></node>
</ul>
</div>
<div v-else>{{ tree.textContent }}</div>
</li>
</template>
https://jsfiddle.net/jonataswalker/Lw52t2dv/

Related

Count the occurrences of a child component

I have a single file component like this:
<template>
<div>
<template v-if="offers.length > 3">
View all offers here
</template>
<template v-else-if="offers.length > 1">
<offer v-for="offer in offers" :data="offer"></offer>
</template>
<template v-else-if="offers.length == 1">
<offer :title="The offer" :data="offers[0]"></offer>
</template>
</div>
</template>
Based on the number of offers, I choose how many to render.
Question: How do I efficiently get/count the number of <offer> components? I also need that number to be reactive.
There's no clean way how.
You could count the children of the current instance that are of a specific type. But you would have to call the "recount" logic on update hook (as well as mounted).
Example:
Vue.component('offer', {
name: 'Offer',
template: '<span> offer </span>'
})
new Vue({
el: '#app',
data: {
offers: [1, 2],
offerCount: 0
},
methods: {
updateOfferCount() {
this.offerCount = this.$children.filter(child => child.constructor.options.name === 'Offer').length;
}
},
updated() {
this.updateOfferCount()
},
mounted() {
this.updateOfferCount()
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<div>
<template v-if="offers.length > 3">
View all offers here
</template>
<template v-else-if="offers.length > 1">
<offer v-for="offer in offers" :data="offer"></offer>
</template>
<template v-else-if="offers.length == 1">
<offer :data="offers[0]"></offer>
</template>
</div>
<br>
<button #click="offers.push(123)">Add Offer</button> offerCount: {{ offerCount }}
</div>
I'm answering this based solely on the idea that you want to count instantiations and destructions of Offer components. I'm not sure why you don't just count offers.length. Maybe other things can trigger instantiations.
Have the component emit events on creation and destruction and have the parent track accordingly.
Alternatively (and maybe overkill) you could use Vuex and create a store that the Offer commits to on creation and destruction. This means that you don't have to manually attach #offer-created/destroyed directives every time you put an <offer> in your markup.
Both methods are included in the following example:
const store = new Vuex.Store({
strict: true,
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
}
});
const Offer = {
props: ["data"],
template: "<div>{{data.name}}</div>",
created() {
console.log("Created");
this.$emit("offer-created");
this.$store.commit("increment");
},
destroyed() {
console.log("Destroyed");
this.$emit("offer-destroyed");
this.$store.commit("decrement");
}
};
const app = new Vue({
el: "#app",
store,
components: {
offer: Offer
},
data() {
return {
offers: [],
offerCount: 0
};
},
computed: {
offerCountFromStore() {
return this.$store.state.count;
}
},
methods: {
offerCreated() {
this.offerCount++;
},
offerDestroyed() {
this.offerCount--;
},
addOffer() {
this.offers.push({
name: `Item: ${this.offers.length}`
});
},
removeOffer() {
this.offers.pop();
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.min.js"></script>
<div id="app">
<div>Offer instances: {{offerCount}}</div>
<div>Offer instances (from store): {{offerCountFromStore}}</div>
<div>
<div v-if="offers.length > 3">
View all offers here
</div>
<div v-else-if="offers.length > 1">
<offer #offer-created="offerCreated" #offer-destroyed="offerDestroyed" v-for="offer in offers" :data="offer"></offer>
</div>
<div v-else-if="offers.length == 1">
<offer #offer-created="offerCreated" #offer-destroyed="offerDestroyed" :data="offers[0]"></offer>
</div>
</div>
<div>
<button #click.prevent="addOffer">Add</button>
<button #click.prevent="removeOffer">Remove</button>
</div>
</div>
The problem with trying to use $children is that it is, inherently, not reactive:
The direct child components of the current instance. Note there’s no
order guarantee for $children, and it is not reactive. If you find
yourself trying to use $children for data binding, consider using an
Array and v-for to generate child components, and use the Array as
the source of truth.

Getting index of a data in an array in VUE js

I want to change the status of Tasks when a particular method is called. But The problem is I cannot get the index of the particular item of the array to change its status.
This is my HTML:
<div class="main" id="my-vue-app">
<ul>
<li v-for="task in completeTask">
{{ task.description }} <button #click="markIncomplete">Mark as Incomplete</button>
</li>
</ul>
<ul>
<li v-for="task in incompleteTask">
{{ task.description }} <button #click="markComplete">Mark as Complete</button>
</li>
</ul>
</div>
And this is my Vue:
<script>
new Vue(
{
el: '#my-vue-app',
data:
{
tasks: [
{description:'go to market', status: true},
{description:'buy book', status: true},
{description:'eat biriani', status: true},
{description:'walk half kilo', status: false},
{description:'eat icecream', status: false},
{description:'return to home', status: false}
]
},
computed:
{
incompleteTask()
{
return this.tasks.filter(task => ! task.status);
},
completeTask()
{
return this.tasks.filter(task => task.status);
}
},
methods:
{
markComplete()
{
return this.task.status = true;
},
markIncomplete()
{
return this.task.status = false;
}
}
}
)
</script>
I need make use of markComplete() and markIncomplete() but the problem is I couldn't find the way to get the index of current element to change its status.
You could get the index by declaring a second argument at the v-for:
<li v-for="(task, index) in incompleteTask">
{{ task.description }} <button #click="markComplete(index)">Mark as Complete</button>
</li>
methods:
{
markComplete(index)
{
return this.tasks[index].status = true;
},
But a, maybe simpler, alternative is to simply **pass the `task` as argument**:
<li v-for="task in incompleteTask">
{{ task.description }} <button #click="markComplete(task)">Mark as Complete</button>
</li>
methods:
{
markComplete(task)
{
return task.status = true;
},
RTFM:
You can use the v-repeat directive to repeat a template element
based on an Array of objects on the ViewModel. For every object in the
Array, the directive will create a child Vue instance using that
object as its $data object. These child instances inherit all data
on the parent, so in the repeated element you have access to
properties on both the repeated instance and the parent instance. In
addition, you get access to the $index property, which will be the
corresponding Array index of the rendered instance.
var demo = new Vue({
el: '#demo',
data: {
parentMsg: 'Hello',
items: [
{ childMsg: 'Foo' },
{ childMsg: 'Bar' }
]
}
})
<script src="https://unpkg.com/vue#0.12.16/dist/vue.min.js"></script>
<ul id="demo">
<li v-repeat="items" class="item-{{$index}}">
{{$index}} - {{parentMsg}} {{childMsg}}
</li>
</ul>
Source:
https://012.vuejs.org/guide/list.html
Note: The directive v-repeat is available in old versions of Vue.js :-)

Updating a value when v-model changes

I have a text input with v-model binding it's value to data. I would also like to be able to call my parse() function when this v-model value changes in order to update an array that is also on data.
<div id="app">
<input
id="user-input"
type="text"
v-model="userInput">
<ul id="parsed-list">
<li v-for="item in parsedInput">
{{ item }}
</li>
</ul>
</div>
new Vue({
el: '#app',
data: {
userInput: '',
parsedInput: []
}
})
let parse = input => {
return input.split(',')
}
How should I go about updating data.parsedInput with the parse() function using the v-model change? What is the proper Vue way of doing this?
A proper Vue way of a data property that depends on another is with a computed property, that way parsedInput is automatically updated whenever userInput changes:
let parse = input => {
return input.split(',')
}
new Vue({
el: '#app',
data: {
userInput: '',
},
computed: {
parsedInput() {
return parse(this.userInput)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<body>
<div id="app">
<input id="user-input" type="text" v-model="userInput">
<ul id="parsed-list">
<li v-for="item in parsedInput">
{{ item }}
</li>
</ul>
</div>
</body>
As a sidenote: declare the parse function before using it, to prevent is not defined errors.

Vue `$refs` issues

I've an issue in this code
let bus = new Vue();
Vue.component('building-inner', {
props: ['floors', 'queue'],
template: `<div class="building-inner">
<div v-for="(floor, index) in floors" class="building-floor" :ref="'floor' + (floors - index)">
<h3>Floor #{{floors - index }}</h3>
<button type="button" class="up" v-if="index !== floors - floors">up</button>
<button type="button" class="down" v-if="index !== floors - 1">down</button>
</div>
</div>`,
beforeMount(){
bus.$emit('floors', this.$refs);
}
})
Vue.component('elevator', {
data: {
floorRefs: null
},
props: ['floors', 'queue'],
template: `<div class="elevator" ref="elevator">
<button type="button" v-for="(btn, index) in floors" class="elevator-btn" #click="go(index + 1)">{{index + 1}}</button>
</div>`,
beforeMount() {
bus.$on('floors', function(val){
this.floorRefs = val;
console.log(this.floorRefs)
})
},
methods: {
go(index) {
this.$refs.elevator.style.top = this.floorRefs['floor' + index][0].offsetTop + 'px'
}
}
})
new Vue({
el: '#building',
data: {
queue: [],
floors: 5,
current: 0
}
})
<div class="building" id="building">
<elevator v-bind:floors="floors" v-bind:queue="queue"></elevator>
<building-inner v-bind:floors="floors" v-bind:queue="queue"></building-inner>
</div>
I tried to access props inside $refs gets me undefined, why?
You should use a mounted hook to get access to the refs, because on "created" event is just instance created not dom.
https://v2.vuejs.org/v2/guide/instance.html
You should always first consider to use computed property and use style binding instead of using refs.
<template>
<div :style="calculatedStyle" > ... </div>
</template>
<script>
{
//...
computed: {
calculatedStyle (){
top: someCalculation(this.someProp),
left: someCalculation2(this.someProp2),
....
}
}
}
</script>
It's bad practice to pass ref to another component, especially if it's no parent-child relationship.
Refs doc
Computed

Vue js 2 : this.$emit dont Trigger

I has a simple code to test comunicate between child and parent component follow example from vuejs doc : http://vuejs.org/guide/components.html#Using-v-on-with-Custom-Events. but apparently it does not work at the parent component
My jsfiddle: Jsfiddle
html:
Vue.component('tasks-item', {
template: '<div>{{item.title}} <button v-on:click="deleteItem(item)">x</button></div>',
props: ['item'],
methods: {
deleteItem: function(item){
console.log('child click')
document.getElementById('output').innerHTML='child click : '+item.title
this.$emit('deleteItem')
}
}
})
Vue.component('tasks-list', {
template: '#tasks-list',
props: ['tasks'],
methods: {
deleteTask: function(){
document.getElementById('output').innerHTML='parent click'
}
}
})
new Vue({
el: '#app',
data: function(){
return {
data:[{"id":51,"title":"rr4434","content":"rtrtrrtrtr"},{"id":50,"title":"rrrr","content":"rrr"},{"id":49,"title":"rrrr","content":"rrr"},{"id":48,"title":"rrr","content":"rrr"},{"id":47,"title":"rrr","content":"rrr"},
{"id":46,"title":"c\u00f4 d\u00e2\u0300n","content":"pha\u0309i khong em"},
{"id":45,"title":"we are you","content":"content"},
{"id":44,"title":"cai min nek","content":"co gi kh\u00f4ng"},{"id":43,"title":"abc","content":"dghjj"},{"id":42,"title":"dddd","content":"ddd"},{"id":38,"title":"444","content":"4444"},{"id":36,"title":"rrr","content":"rr"},{"id":35,"title":"rr","content":"rr"},{"id":34,"title":"rrrr","content":"rrr"},{"id":33,"title":"rrr","content":"rr"}]
}
},
methods: {
}
})
<script src="https://unpkg.com/vue#next/dist/vue.js"></script>
<div id="app">
<div id="output">click output</div>
<hr/>
<tasks-list :tasks="data"></tasks-list>
</div>
<template id="tasks-list">
<div>
<div v-for="item in tasks">
<tasks-item :item="item" v-on:deleteItem="deleteTask()"></tasks-item>
</div>
</div>
</template>
Change
this.$emit('deleteItem')
to
this.$emit('delete-item')
and inside template fix component's v-on from
v-on:deleteItem
to
v-on:delete-item
You can read more at https://v2.vuejs.org/v2/guide/components-custom-events.html

Categories