How render component in v-for by button from parent - javascript

How i can render v-if component by button(button in parent) click inside v-for loop? and should render only in that item where clicked
<div v-for="item in items">
<button #click >Show child<button>
<div>{{item.name}}</div>
<child v-if="this button clicked" :item="item"><child>
<div>

You have to store info about state of every item (if it was clicked) in your data. Then, when you click on button you should update clicked property for particular item. Finally if item.clicked is set on true you will show your child component (or any other html).
<template>
<div>
<div v-for="item in items" :key="item.id">
<button #click="item.clicked = true" >Show child</button>
{{item.name}}
<div v-if="item.clicked">Item child</div>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data: function() {
return {
items: [
{
id: 1,
name: 'test1',
clicked: false
},
{
id: 2,
name: 'test2',
clicked: false
},
{
id: 3,
name: 'test3',
clicked: false
}
]
}
}
}
</script>

Plain and simple you just have to set some flag for latter v-if:
<div id="app">
<div v-for="item in items">
<button #click="$set(item, 'shown', true)">Show child</button>
<div>{{ item.name }}</div>
<div v-if="item.shown">Child component</div>
</div>
</div>
Here, $set() is used because initial item could lack shown field, so setting it directly with item.shown=true won't be reactive.
You can also hide button after click:
<button #click="$set(item, 'shown', true)" v-if="!item.shown">Show child</button>
To toggle visibility you just have to do it like this:
<button #click="$set(item, 'shown', !item.shown)">
{{ item.shown ? 'Hide' : 'Show' }} child
</button>
JSFiddle

You can take advantage of an item... index available in v-for directive (e.g. v-for="(item, i) in items"), to bind it (index of the item) to the function which shows an item by changing it's property:
Update: Initial answer has been deleted after requirements refinement.
Since you prefer to keep from mutation of items, you can wrap them in Map object (as keys) and keep visibility settings separately as Map values. Unfortunately, as far as I know, for the time being Vue.js does not support reactivity for Map objects, that's why I have to trigger rerendering manually by using forceUpdate:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('child', {
template: '<p>Visible child</p>'
})
new Vue({
el: "#demo",
template: `
<div>
<div v-for="item in items">
<button #click="toggleChild(item)">Toggle child</button>
<div>{{item.name}}</div>
<child v-if="isVisible(item)" :item="item"></child>
</div>
</div>
`,
data () {
return {
itemsMap: new Map(
[
{ name: 'test1' },
{ name: 'test2' }
].map(item => [item, { visible: false }])
)
};
},
methods: {
toggleChild(item) {
this.itemsMap.set(item, { visible: !this.itemsMap.get(item).visible });
this.$forceUpdate();
},
isVisible(item) {
return this.itemsMap.get(item).visible;
}
},
computed: {
items: function() {
return Array.from(this.itemsMap.keys());
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo"></div>

Related

How to pass props to child component in vue

I have a parent component where I am doing the API call and getting the response. So what I am trying to do is pass this response as a prop to child component in Vue.
So here is my parent component and the call:
<button class="btn button col-2" #click="addToCart()">
Add to cart
</button>
addToCart: function () {
let amount = this.itemsCount !== "" ? this.itemsCount : 1;
if(this.variationId != null) {
this.warningMessage = false;
cartHelper.addToCart(this.product.id, this.variationId, amount, (response) => {
this.cartItems = response.data.attributes.items;
});
} else {
this.warningMessage = true;
}
},
So I want to pass this "this.cartItems" to the child component which is:
<template>
<div
class="dropdown-menu cart"
aria-labelledby="triggerId"
>
<div class="inner-cart">
<div v-for="item in cart" :key="item.product.id">
<div class="cart-items">
<div>
<strong>{{ item.product.name }}</strong>
<br/> {{ item.quantity }} x $45
</div>
<div>
<a class="remove" #click.prevent="removeProductFromCart(item.product)">Remove</a>
</div>
</div>
</div>
<hr/>
<div class="cart-items-total">
<span>Total: {{cartTotalPrice}}</span>
Clear Cart
</div>
<hr/>
<router-link :to="{name: 'order'}" class="btn button-secondary">Go To Cart</router-link>
</div>
</div>
</template>
<script>
export default {
computed: {
},
methods: {
}
};
</script>
So I am quite new in vue if you can help me with thi, I would be really glad.
Passing props is quite simple. If cartItems is what you wan´t to pass as a prop, you can do this:
<my-child-component :cartItems="cartItems"></my-child-component>
In this case you implemented your child as myChildComponent. You pass cartItems with :cartItems="cartItems" to it. In your child you do this:
props: {
cartItems: Object
}
Now you can use it with this.cartItems in your methods or {{cartItems}} in your themplate.
Vue.component('Child', {
template: `
<div class="">
<p>{{ childitems }}</p>
</div>
`,
props: ['childitems']
})
new Vue({
el: '#demo',
data() {
return {
items: []
}
},
methods: {
getItems() {
//your API call
setTimeout(() => {
this.items = [1, 2]
}, 2000);
}
}
})
Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<button #click="getItems">get data</button>
<Child v-if="items.length" :childitems="items" />
</div>
You can wait for response, and when you gate this.cartItems then render your child component with a v-if="this.cartItems.length" condition

Vue - how to append components?

I'm trying to create a button that, once is pushed, adds a child component to my component. My code works, except that the HTML is being parsed as a string, here is the parent component:
<template>
<div id="app">
<li v-for="item in items">
{{ item.component }}
</li>
</div>
</template>
<script>
export default {
props:{},
data(){
return {
items: []
}
},
mounted() {},
computed: {},
methods: {
addItem(){
this.items.push({component: '<testcomponent />'})
}
}
}
</script>
And the child component is very basic:
<template>
<div id="app">
<h1>Testing</h1>
</div>
</template>
So the problem with my code is that, every time i push the button, instead of loading every time a new instance of the child component, it will parse the HTML as a string: '<testcomponent />'. How can i fix this? Thanks in advance!
You can push the component name, and use <component :is="item.component"/> in the v-for as follows:
const testcomponent = Vue.component('testcomponent', {
template: '#testcomponent',
});
new Vue({
el:"#app",
components: { testcomponent },
data() { return { items: [] } },
methods: {
addItem() {
this.items.push({ component: 'testcomponent' });
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template id="testcomponent">
<div><h1>Testing</h1></div>
</template>
<div id="app">
<button #click="addItem">Add</button>
<li v-for="item in items">
<component :is="item.component"/>
</li>
</div>

Why does the todo item below a crossed one become crossed when a todo-item above is deleted?

Why does the todo item below a crossed one become crossed when a todo-item above is deleted ?
For example:
item1
item2
item3
item4
Cross item3:
item1
item2
item3 X
item 4
Remove item 2:
item1
item3
item4 X
What did I do wrong in my Vue code ? I just started learning Vue yesterday, so my mistake is most likely basic.
<div id="app">
<todo-list>
</todo-list>
</div>
<style>
.crossed {
text-decoration : line-through;
}
.todo:hover {
cursor: pointer;
font-weight: bold;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('todo-list',{
data: function () {
return {
newText: '',
todos:[],
todoId: 0
}
},
template: `
<div>
<form v-on:submit.prevent="addTodo">
<input v-model="newText">
<button>ADD TODO</button>
</form>
<ul>
<todo class="todo" v-for="(todo,index) in todos" :text="todo.text" :key="todo.todoId" #remove="removeTodo(index)"></todo>
</ul>
</div>
`,
methods: {
addTodo: function() {
this.todos.push({
text: this.newText,
done: false,
id: this.todoId++
});
this.newText = '';
},
removeTodo: function(index) {
this.todos.splice(index,1);
}
}
});
Vue.component('todo',{
props: ['text'],
data: function () {
return {
done: false
}
},
template: `
<li>
<span v-on:click="done = !done" :class="{crossed: done}">
{{text}}
</span>
<button v-on:click="$emit('remove')">Remove</button>
</li>
`
})
new Vue({
el: '#app'
})
</script>
I suspect you did not use :key="todo.id" but :key="todo.todoId thus splicing the third makes the fourth the third by replacing text property. Else it should work.
btw todo.todoId is undefined
You toggle the done value in child's todo component. It is better to make sure there is only a single source of truth. Which should be in parent data properties, todos.
What i did is pass down the todo object to child todo component as props. When done is press, emit the event like remove event, change the done value at parent like remove event as well.
Edit for further explaination:
Below is your child todo component.
Vue.component('todo',{
props: ['text'],
data: function () {
return {
done: false
}
},
template: `
<li>
<span v-on:click="done = !done" :class="{crossed: done}">
{{text}}
</span>
<button v-on:click="$emit('remove')">Remove</button>
</li>
`
})
"done" is declared in data properties, not passed down from props.
You are actually declaring a brand new "done", I will called it as "childDone", this "childDone" is not related to your parent todos array "done". This "childDone" only live in this component, so whenever you toggle the value of this "childDone", parent's todos "done" wont be affected.
Yes, UI showed the todo has been crossed, because your UI also refer to "childDone" not parent "done". Parent "done" never passed down, so child todo component not render using parent's "done"
To explain why delete item 2, item 4 will become crossed.
todos.length = 4, render 4 components
component1:todo1:childDone=false
component2:todo2:childDone=false
component3:todo3:childDone=false
component4:todo4:childDone=false
crossed 3rd component,
component1:todo1:childDone=false
component2:todo2:childDone=false
component3:todo3:childDone=true
component4:todo4:childDone=false
delete 2nd todo element, todos.length = 3, render 3 components
component1:todo1:childDone=false
component2:todo3:childDone=false
component3:todo4:childDone=true
compoenent4(not render anymore because todos.length = 3)
<div id="app">
<todo-list>
</todo-list>
</div>
<style>
.crossed {
text-decoration : line-through;
}
.todo:hover {
cursor: pointer;
font-weight: bold;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('todo-list',{
data: function () {
return {
newText: '',
todos:[],
todoId: 0
}
},
template: `
<div>
<form v-on:submit.prevent="addTodo">
<input v-model="newText">
<button>ADD TODO</button>
</form>
<ul>
<todo class="todo" v-for="(todo,index) in todos" :todo="todo" :key="todo.todoId" #remove="removeTodo(index)" #done="doneTodo(index)"></todo>
</ul>
</div>
`,
methods: {
addTodo: function() {
this.todos.push({
text: this.newText,
done: false,
id: this.todoId++
});
this.newText = '';
},
removeTodo: function(index) {
this.todos.splice(index,1);
},
doneTodo: function(index) {
this.todos[index].done = !this.todos[index].done;
}
}
});
Vue.component('todo',{
props: ['todo'],
template: `
<li>
<span v-on:click="$emit('done')" :class="{crossed: todo.done}">
{{todo.text}}
</span>
<button v-on:click="$emit('remove')">Remove</button>
</li>
`
})
new Vue({
el: '#app'
})
</script>

How to prevent multiple renders in Vue v-for lists

I've captured the current code I have here:
https://jsfiddle.net/prsauer/Lnhu2avp/71
The basic issue is that for each click on a list item every item's computeStyle is called -- I'd prefer for each click to only produce a single recompute of the style
<div id="editor">
<div v-for="item in dungeons" :key="item.isOpened">
<div v-on:click="clickedChest(item)" v-bind:style="computeChestStyle(item)">
{{ item.name }} {{ item.isOpened }}
</div>
</div>
</div>
var dgnData = [
{ name: "Lobby", isOpened: false },
{ name: "Side", isOpened: false },
];
new Vue({
el: '#editor',
data: { dungeons: dgnData },
computed: { },
methods: {
clickedChest: chest => {
chest.isOpened = !chest.isOpened;
console.log("##### Clicked chest", chest);
},
computeChestStyle:
item => {
console.log("computeStyle", item);
return item.isOpened ? "color: red" : "color: blue";
}
}
});
Function calls get re-evaluated on every update of the view. If you want results to be cached to only re-render as needed, you need a computed. That means you need to create a component for your item, and create the computed in the component.

uncheck all checkboxes from child component Vue.js

I have the following problem: I have parent component with a list of checkboxes and two inputs. So when the any of those two inputs has been changed I need to uncheck all checkboxes. I would appreciate if you can help me to solve this.
I wanted to change checkedItem to trigger watch in child and then update all children, but it doesn't work.
parent.vue
<template>
<div class="filter-item">
<div class="filter-checkbox" v-for="item in filter.items">
<checkbox :item="item" v-model="checkedItem"> {{ item }} </checkbox>
</div>
<div class="filter-range">
<input v-model.number="valueFrom">
<input v-model.number="valueTo">
</div>
</div>
</template>
<script>
import checkbox from '../checkbox.vue'
export default {
props: ['filter'],
data() {
return {
checkedItem: false,
checkedItems: [],
valueFrom: '',
valueTo: '',
}
},
watch: {
'checkedItem': function () {},
'valueFrom': function () {},
'valueTo': function () {}
},
components: {checkbox}
}
</script>
child.vue
<template>
<label>
<input :value="value" #input="updateValue($event.target.value)" v-model="checked" class="checkbox"
type="checkbox">
<span class="checkbox-faux"></span>
<slot></slot>
</label>
</template>
<script>
export default {
data() {
return {
checked: ''
}
},
methods: {
updateValue: function (value) {
let item = this.item
let checked = this.checked
this.$emit('input', {item, checked})
}
},
watch: {
'checked': function () {
this.updateValue()
},
},
created: function () {
this.checked = this.value
},
props: ['checkedItem', 'item']
}
</script>
When your v-for renders in the parent component, all the rendered filter.items are bound to the same checkedItem v-model.
To correct this, you would need to do something like this:
<div class="filter-checkbox" v-for="(item, index) in filter.items">
<checkbox :item="item" v-model="item[index]> {{ item }} </checkbox>
</div>
To address your other issue, updating the child component list is as easy as updating filter.items.
You don't even need a watcher if you dont want to use one. Here is an alternative:
<input v-model.number="valueFrom" #keypress="updateFilterItems()">
And then:
methods: {
updateFilterItems () {
// Use map() or loop through the items
// and uncheck them all.
}
}
Always ask yourself twice if watch is necessary. It can create complexity unnecessarily.

Categories

Resources