Vue.js dynamic two way data binding between parent and child components - javascript

I'm trying to use a combination of v-for and v-model to get a two-way data bind for some input forms. I want to dynamically create child components. Currently, I don't see the child component update the parent's data object.
My template looks like this
<div class="container" id="app">
<div class="row">
Parent Val
{{ ranges }}
</div>
<div class="row">
<button
v-on:click="addRange"
type="button"
class="btn btn-outline-secondary">Add time-range
</button>
</div>
<time-range
v-for="range in ranges"
:box-index="$index"
v-bind:data.sync="range">
</time-range>
</div>
<template id="time-range">
<div class="row">
<input v-model="data" type="text">
</div>
</template>
and the js this
Vue.component('time-range', {
template: '#time-range',
props: ['data'],
data: {}
})
new Vue({
el: '#app',
data: {
ranges: [],
},
methods: {
addRange: function () {
this.ranges.push('')
},
}
})
I've also made a js fiddle as well https://jsfiddle.net/8mdso9fj/96/

Note: the use of an array complicates things: you cannot modify an alias (the v-for variable).
One approach that isn't mentioned often is to catch the native input event as it bubbles up to the component. This can be a little simpler than having to propagate Vue events up the chain, as long as you know there's an element issuing a native input or change event somewhere in your component. I'm using change for this example, so you won't see it happen until you leave the field. Due to the array issue, I have to use splice to have Vue notice the change to an element.
Vue.component('time-range', {
template: '#time-range',
props: ['data']
})
new Vue({
el: '#app',
data: {
ranges: [],
},
methods: {
addRange: function () {
this.ranges.push('')
},
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div class="container" id="app">
<div class="row">
Parent Val
{{ ranges }}
</div>
<div class="row">
<button
v-on:click="addRange"
type="button"
class="btn btn-outline-secondary">Add time-range
</button>
</div>
<time-range
v-for="range, index in ranges"
:data="range"
:key="index"
#change.native="(event) => ranges.splice(index, 1, event.target.value)">
</time-range>
</div>
<template id="time-range">
<div class="row">
<input :value="data" type="text">
</div>
</template>
To use the .sync modifier, the child component has to emit an update:variablename event that the parent will catch and do its magic. In this case, variablename is data. You still have to use the array-subscripting notation, because you still can't modify a v-for alias variable, but Vue is smart about .sync on the array element, so there's no messy splice.
Vue.component('time-range', {
template: '#time-range',
props: ['data'],
methods: {
emitUpdate(event) {
this.$emit('update:data', event.target.value);
}
}
})
new Vue({
el: '#app',
data: {
ranges: [],
},
methods: {
addRange: function () {
this.ranges.push('')
},
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div class="container" id="app">
<div class="row">
Parent Val
{{ ranges }}
</div>
<div class="row">
<button
v-on:click="addRange"
type="button"
class="btn btn-outline-secondary">Add time-range
</button>
</div>
<time-range
v-for="range, index in ranges"
:data.sync="ranges[index]"
:key="index">
</time-range>
</div>
<template id="time-range">
<div class="row">
<input :value="data" type="text" #change="emitUpdate">
</div>
</template>

Related

medium-zoom pagination issues

In my project I use medium-zoom with pagination. On first page it works well but on second, third, fourth... I have to click several times for close image...on second page...two times, on third 3 times...
it seems to be a problem with maybe any index number?
Here is my code:
<template>
<div>
<div v-if="loading" class="text-center">
<i class="fas fa-spinner fa-pulse fa-5x"></i>
</div>
<div v-else>
<div v-for="(item, imageIndex) in pageOfItems" :key="item.id" class="m-3">
<div class="row mt-3">
<div class="col-lg-9 my-auto" v-html="item.mytext"></div>
<div class="col-lg-3 my-auto text-center">
<article class="container">
<img
class="img-thumbnail"
:src="'http://localhost:4000/api/galeria/' + item.galeriaId + '_f.jpg'"
/>
</article>
</div>
</div>
<hr class="hr1" />
</div>
</div>
<div class="pb-0 pt-3 text-center">
<jw-pagination :items="info" :page-size="10" #changePage="onChangePage"></jw-pagination>
</div>
</div>
</template>
<script>
import mediumZoom from 'medium-zoom'
import axios from 'axios'
export default {
data() {
return {
info: [],
customLabels,
pageOfItems: [],
loading: true,
}
},
mounted() {
axios
.get('http://localhost:4000/api/fetch_galeria.php/')
.then((response) => (this.info = response.data))
.finally(() => (this.loading = false))
},
updated() {
mediumZoom('article img', {
background: 'transparent',
})
},
methods: {
onChangePage(pageOfItems) {
// update page of items
this.pageOfItems = pageOfItems
},
},
}
</script>
You will pretty much need to detach the event listeners here. Because it should be a lot of them added everytime due to the updated hook.
A naive implementation would be to add mediumZoom on the mounted hook. And when you do have a new changePage event, to detach it from all the images, then to apply it to the new ones with the same mediumZoom call.
Below is a way to see which event listeners (and probably how many) you have linked to a specific VueJS component. Select it in the Vue devtools and then, you will have access to the element's properties via $vm0.

VueJS Components inside a loop act as one

UPDATE
Was able to make it work, but got one last problem. Updated code is here:
VueJs not working on first click or first event
-----------------------------------------------------------
I've been trying to find out a way for the components inside a loop to not act as one.
I have a loop (3 divs), and inside the loop, I have 2 textboxes. But whenever I enter a value in any of them, the value is populated to everyone.
Can anyone help me separate those components?
I'm trying to make the parent div (1st loop) dynamic. So the children components (2nd loop) should be acting separately with their own grandparent components (textbox).
Here's my code:
<div id="app">
<div v-for="(ctr, c) in 3" :key="c">
<button #click="input_add">1st</button>
<div>
<div v-for="(input, act) in inputs" :key="act.id">
<input type="text" v-model="input.name">
<input type="text" v-model="input.time">
<button #click="input_remove(act)">Delete</button>
<button #click="input_add">Add row</button>
</div>
</div>
{{ inputs }}
</div>
</div>
const app = new Vue({
el: "#app",
data: {
inputs: [],
counter: 0,
},
methods: {
input_add() {
this.inputs.push({
id: this.counter + 1,
day: null,
name: null,
time: null,
})
this.counter += 1
},
input_remove(index) {
this.inputs.splice(index,1)
this.counter -= 1
}
}
});
Result:
as I mentioned in the comment, you should create a component for the iterated item.
parent component:
<div v-for="(item, index) in array" :key="index">
<child :item="item" />
</div>
Now you sent the item as prop. Let's catch it in child.
child components:
<div>
<input type="text" v-model="input.name">
<input type="text" v-model="input.time">
<button #click="input_remove(act)">Delete</button>
<button #click="input_add">Add row</button>
</div>
{{ inputs }}
props: [item], // I am not sure you need it or not, BUT just showing how to do it.
data() {return { // your datas };},
methods: {
// your methods...
},
//and else...
Now each iterated item can control self only. I am hope it make sense now.
then build the buttons an input in child component. After that you can apply the events for just clicked item.
You should use Array of Objects. Here's a codesandbox. This way everytime you add a new object to the array, a new index is created with a new name and time ready to be filled in.
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<div v-for="item in basic" :key="item.id">
<button #click="addRow">Add row</button>
<input type="text" v-model="item.name">
<input type="text" v-model="item.time">
{{ item.name }} - {{ item.time }}
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
id: 1,
basic: [{ name: "", time: "" }]
};
},
methods: {
addRow() {
console.log("added");
this.id += 1;
this.basic.push({
name: "",
time: ""
});
}
}
};
</script>

How to pass vuejs for-loop index value as the parameter of HTML button onclick javascript function

<div v-for="(grp,idx) in vm">
<button onclick="addPlant(idx)">
.......
</button>
</div>
addPlant() is a javascript function and not a VueJS method.
How can I pass the idx value to the javascript method now?
You can't reference the Vue template variables from a vanilla javascript onclick handler like you're trying to do.
You should pass the index value to a Vue #click handler and call the vanilla javascript method from there:
function addPlant(idx) {
alert(idx)
}
new Vue({
el: '#app',
data() {
return {
groups: ['a', 'b', 'c']
}
},
methods: {
onButtonClick(idx) {
addPlant(idx)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(grp, idx) in groups">
<button #click="onButtonClick(idx)">
{{ idx }}
</button>
</div>
</div>
If we go to be limited to your specific use case we could assign index to data-label attribute (which is bound to index) and pass this.getAttribute('data-label') as parameter, this refers to the Html element not to the Vue instance or component:
new Vue({
el: '#app',
data(){
return{
bars:['a','b','c']
}
}
})
function addPlant(index){
console.log("# "+index)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(bar,idx) in bars">
<button :data-label="idx" onclick="addPlant(this.getAttribute('data-label'))">
{{bar}}
</button>
</div>
</div>
Create a Vue method calling the javascript function
methods: {
callAddPlant: function(idx) {
addPlant(idx)
}
}
...
<div v-for="(grp,idx) in vm">
<button v-on:click="callAddPlant(idx)">
.......
</button>
</div>

V-model with two text inputs in a child component

I have been wrestling with an issue with the way v-model works when you have two inputs in the same child component. I am passing in an array of objects from a parent component and need to update different parts of an object in an array of objects with the two input fields. I can get one input field to work but have no idea how to have both inputs effect different items in the object (which is in an array). I am new to Vue and don't totally understand the functionality of passing an array of objects and affecting specific items in those objects.
Parent Component
<template>
<div class="budgetGroup">
<header><input title="CardTitle" type="text" v-bind:placeholder="budgetItemHeading"></header>
<div class="budgetItemContainer">
<div class="budgetItemRow">
<!--creates new component when click event happens and places below-->
<div v-for="(input, index) in budgetRows" :key="index">
<budgetItemRowContent v-model="budgetRows"></budgetItemRowContent>
<progress data-min="0" data-max="100" data-value="20"></progress>
</div>
</div>
</div>
<footer class="budgetGroupFooter">
<div class="budgetGroupFooter-Content budgetGroupFooter-Content--Narrow">
<button class="addBudgetItem" id="addBudgetItem" v-on:click="createNewContent()">
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<path fill="#FD0EBF" d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"></path>
</svg>
Add Item
</button>
</div>
</footer>
</div>
export default {
name: 'budgetGroup',
components: {
budgetItemRowContent,
BudgetItemButton,
},
data: () => {
return {
budgetItemHeading: 'Housing',
// creates array containing object for budget row information
budgetRows: [
{inputbudget: '', amountbudgeted: 0, remaining: 0, id: uniqId()},
],
};
},
methods: {
// creates new budgetRow when button is clicked
createNewContent() {
this.budgetRows.push({inputbudget: '', amountbudgeted: 0, remaining: 0, id: uniqId() });
},
},
};
Child Component- needing both inputs to affect different items in object
<template>
<div class="budgetItemRowContent">
<div class="budgetItemRow-Column">
<div class="budgetItemLabel">
//Input that is suppose to update InputBudget in object
<input type="text" maxlength="32" placeholder="Label" class="input-Budget-Inline-Small budgetItemRow-Input">
</div>
</div>
<!--input that will update amoundbudgeted -->
<div class="budgetItemRow-Column">
<div class="amountBudgetedInputContainer">
//Input that will be updating amountBudgeted in object
<input v-model.number="amount" class="amountBudgetedNumber budgetItemRow-Input input-Budget-Inline-Small" type="number" placeholder="$">
</div>
</div>
<div class="budgetItemRow-Column">
<span class="budgetItemSecondColumnMoney-Spent">
<span class="money-symbol">$</span>
<span class="money-integer"></span>
<!--<span class="money-decimal">.</span>-->
<!--<span class="money-fractional">04</span>-->
</span>
</div>
</div>
<script>
export default {
//only one prop that is being updated(dont know how to have two seperate items in object to get updated
props: ['value'],
computed: {
amount: {
set(newVal) {
this.$emit('input', newVal);
},
get() {
return this.value;
},
},
},
};

Declare reactive data properties in vue.js?

I have a very basic vue.js app:
var app = new Vue({
el: '#app',
delimiters: ['${', '}'],
data: {
messages: [
'Hello from Vue!',
'Other line...'
]
}
})
The following html works fine:
<div class="container" id="app">
<div class="row" style="">
<div class="col-md-8 offset-md-2">
<span v-for="msg in messages">${msg}</span>
</div>
</div>
</div>
However very similar html block does not:
<div class="container" id="app">
<div class="row" style="">
<div class="col-md-8 offset-md-2">
<textarea id="chat_area" readonly="" rows="20">
<span v-for="msg in messages">${msg}</span>
</textarea>
</div>
</div>
</div>
[Vue warn]: Property or method "msg" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm using Vue v2.3.3. What could be the problem?
As documentation says, interpolation in textareas won't work, so you need to use v-model.
If the only thing you want to do is to display html inside textarea, you could in theory use an ugly workaround by wrapping your array fields inside a function and set that function as textarea v-model:
var app = new Vue({
el: '#app',
delimiters: ['${', '}'],
data: {
messages: [
'Hello from Vue!',
'Other line...'
]
},
computed:{
multiLineMessages: function(){
var result = "";
for(var message of this.messages){
result += '<span>' + message + '</span>'
}
return result;
}
}
});
template part:
<div class="container" id="app">
<div class="row" style="">
<div class="col-md-8 offset-md-2">
<textarea v-model="multiLineMessages" placeholder="add multiple lines">
</textarea>
</div>
</div>
</div>
It's more like a proof that it's doable but I highly don't recommend using it anywhere, as html shouldn't be generated this way (especially larger chunks of it).
jsFiddle preview

Categories