Vue.js Update value of nested props sync object in component - javascript

Hi i have following problem for Vue.js v1.0.28 - I have component
Vue.component('question-editor', {
template: require('./question-editor.html'),
props: ['question'],
methods: {
addChoice() {
this.question.choicesArr.push({
id: null,
body:'zzz',
icon:'',
next:null,
});
console.log(this.question.choicesArr);
},
}
});
Where ./question-editor.html :
...
<div class="box-body">
<div class="form-group" v-for="choice of question.choicesArr">
<input v-model="choice.body" type="text" class="form-control">
</div>
</div>
<div class="box-footer">
<button #pointerdown="addChoice" type="submit" class="btn btn-primary">
Add choice
</button>
</div>
I use this component in parent component in this way:
<question-editor :question.sync="currentQuestion"></question-editor>
The problem is that when I push button "Add choice" and method addChoice() is run, i see in console that property question.choicesArr have new element - but view doesnt change (I don't see this new element on screen - so the v-for not "see" this new element and not refresh itself). What to do inside addChoice() to refresh view to see new element in question.choicesArr on screen ?

I guess vue 1.x, does not detect changes in array as it does in 2.x, you can do following to let vue know that array has been changed with the help of spread operator.
addChoice() {
this.question.choicesArr= [...this.question.choicesArr, {
id: null,
body:'zzz',
icon:'',
next:null,
}];
}

I found the solution:
addChoice() {
this.question.choicesArr.push({
id: null,
body:'zzz',
icon:'',
next:null,
});
this.question = Object.assign({}, this.question, this.question);
console.log(this.question.choicesArr);
},
The statement this.question = Object.assign({}, this.question, this.question); will set __ob__:Observer and __v-for__1:Fragment to the new objects pushed to array.
I also try this.$set(this.question, 'question.choicesArr', this.question.choicesArr); but this one set only __ob__:Observer (not __v-for__1) so v-for will not updated.
I read about it here: https://v2.vuejs.org/v2/guide/reactivity.html

Related

lit-html: issue w/ using change event to propagate changes back to data model

In the below code, i am looping through a list of my data model (this.panel), and generating an input box for each. I would like to propagate any changes made by the user back to the model.
to do this i am storing the index of the item im rendering in the html element, and adding a change event handler which will update the model using that index. The problem im having is the #change event at run time is throwing an error:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'updateModel')'
What's odd is i have another event handler, updateToggleState in a check box lower down that works fine. Any thoughts on what is going on here?
Here is the code:
class Panel extends LitElement {
static properties = {
is_fetch_on_change: {},
};
constructor() {
super();
this.panel = [
{
"id": "ticker",
"type": "str",
"place_holder": "ticker (i.e. YHOO:^NDX)",
"value": "YHOO:^NDX",
"fn_param_name": "ticker"
},
{
"id": "st_dt",
"type": "datetime",
"place_holder": "st_dt (i.e. 2012-01-01)",
"value": "2012-01-01",
"fn_param_name": "st_dt"
},
{
"id": "end_dt",
"type": "datetime",
"place_holder": "end_dt (i.e. 2022-01-01)",
"value": "",
"fn_param_name": "end_dt"
}
]
this.is_fetch_on_change = false;
}
render() {
let ret_html = []
this.panel.forEach(function(c, i){
ret_html.push(html`
<div class="m-2">
<label>${c.fn_param_name}</label>
<input type="text" class="form-control" id = "${c.id}" placeholder="${c.place_holder}" value="${c.value}"
idx="${i}" #change=${this.updateModel} />
</div>
</div>
`)
})
ret_html.push(html`
<div class="m-2">
<label class="form-check-label" for="flexCheckDefault">
<input class="form-check-input" type="checkbox" #change=${this.updateToggleState}>
Refresh on change
</label>
</div>
<button type="submit" class="btn btn-primary m-2" ?hidden=${this.is_fetch_on_change}>Submit</button>
`)
return ret_html;
}
updateModel(e){
let idx = e.target.getAttribute('idx');
//add code here to update model
}
updateToggleState(e){
this.is_fetch_on_change = e.target.checked;
}
}
customElements.define('lit-panel', Panel);
Update:
I was able to solve the problem of not being able to reference 'this' from within the foreach loop. i needed to basically 'pass it in'. so with that solved, i can update the specific item of the dictionary now.
However, that does not update the element or re-render it.
Update 2: this is what i have now. i've tested this and it does add an element to this._panel_data when text input is changed. that change however is still not reflected in the UI.
class Panel extends LitElement {
static properties() {
_panel_data:{state: true}
};
constructor() {
super();
this.id = 100;
this._panel_data = [{"id":"ticker","type":"str","place_holder":"ticker (i.e. YHOO:^NDX)","value":"YHOO:^NDX","fn_param_name": "ticker"},
{"id":"st_dt","type": "datetime","place_holder": "st_dt (i.e. 2012-01-01)","value": "2012-01-01","fn_param_name": "st_dt"}]
}
render() {
return html`<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
${this._panel_data.map((c, i) =>
html`
<div class="m-2">
<label>${c.fn_param_name}</label>
<input type="text" class="form-control" id = "${c.id}" placeholder="${c.place_holder}" value="${c.value}"
idx="${i}" .ref_obj=${c} #change=${e => this.updateModel(e)} />
<!--test if changes made to above, are reflected here...-->
<input type="text" class="form-control" value="${c.value}" />
</div>
</div>
`
)}
`
}
updateModel(e){
let clone = {...e.target.ref_obj};
clone.value = e.target.value;
this._panel_data = [...this._panel_data, clone];
}
}
customElements.define('lit-panel', Panel);
Using an arrow function would solve the this problem
this.panel.forEach((c, i) => {
// `this` will be bound within here
});
Or you can use a regular for loop too.
Since panel is not specified as a reactive property, changing that will not automatically re-render the component. Perhaps you want to make that into an internal reactive state.
Keep in mind that since it is an array, just mutating it with this.panel[idx] = newValue won't trigger a re-render since the array reference will be the same. You need to create a new array reference or call this.requestUpdate(). More explained here https://lit.dev/docs/components/properties/#mutating-properties

Vue2: Use form component with input type textarea to display AND edit data (without directly manipulating props)

I am building an MVP and this is the first time I do web development. I am using Vue2 and Firebase and so far, things go well.
However, I ran into a problem I cannot solve alone. I have an idea how it SHOULD work but cannot write it into code and hope you guys can help untangle my mind. By now I am incredibly confused and increasingly frustrated :D
So lets see what I got:
Child Component
I have built a child component which is a form with three text-areas. To keep it simple, only one is included it my code snippets.
<template>
<div class="wrap">
<form class="form">
<p class="label">Headline</p>
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
// To switch between read and edit
<button
v-if="readonly"
#click.prevent="togglemode()">
edit
</button>
<button
v-else
type="submit"
#click.prevent="togglemode(), updatePost()"
>
save
</button>
</form>
</div>
</template>
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
methods: {
togglemode() {
if (this.readonly) {
this.readonly = false
} else {
this.readonly = true
}
},
updatePost() {
// updates it to the API - that works
}
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
And my parent component:
<template>
<div class="wrap">
<PostComponent
v-for="post in posts"
:key="post.id"
:knugHeadline="post.headline"
/>
</div>
</template>
<script>
import PostComponent from '#/components/PostComponent.vue'
export default {
components: { PostComponent },
data() {
return {
posts: []
}
},
created() {
// Gets all posts from DB and pushes them in array "posts"
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
Current Status
So far, everything works. I can display all posts and when clicking on "edit" I can make changes and save them. Everything gets updated to Firebase - great!
Problem / Error Message
I get the following error message:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
As the error says I should use a computed property based on the props value. But how can I achieve that?
Solution Approach
I believe I have to use a computed getter to return the prop value - how to do that?
And then I have to use the setter to emit an event to the parent to update the value so the prop passes it back down - how to do that?
I have found bits and pieces online but by now all I see is happy families passing around small packages of data...
Would be really thankful for a suggestion on how to solve this one! :)
Thanks a lot!
This error shows because of your v-model on texterea which mutate the prop, but in vue it is illegal to mutate props :
<textarea rows="2"
v-model="propHeadline"
:readonly="readonly">
</textarea>
So, what you could do is to use this created() lifecycle hook and set the propHeadline prop as data :
<script>
export default {
name: 'PostComponent'
data() {
return {
readonly: true,
headline: ""
}
},
props: {
propHeadline: {
type: String,
required: true
}
},
created() {
this.headline = this.propHeadline
}
}
</script>
An then, update the new variable on your textarea :
<textarea rows="2"
v-model="headline"
:readonly="readonly">
</textarea>

What's the proper way to update a child component in the parent's array in Vue?

I'm new to Vue and was hoping for some clarification on best practices.
I'm building an app that uses an array to keep a list of child components and I want to be able to update and remove components by emiting to the parent. To accomplish this I currently have the child check the parent array to find it's index with an "equals" method so that it can pass that index to the parent. This works fine for something simple but if my child components get more complex, there will be more and more data points I'll have to check to make sure I'm changing the correct one. Another way to do this that I can think of is to give the child component an ID prop when it's made and just pass that but then I'd have to handle making sure all the ids are different.
Am I on the right track or is there a better more widely accepted way to do this? I've also tried using indexOf(this._props) to get the index but that doesn't seem to work. Maybe I'm just doing something wrong?
Here's a simplified version of what I'm doing:
// fake localStorage for snippet sandbox
const localStorage = {}
Vue.component('child', {
template: '#template',
data() {
return {
newName: this.name
}
},
props: {
name: String
},
mounted() {
this.newName = this.name
},
methods: {
update() {
this.$emit(
"update-child",
this.$parent.children.findIndex(this.equals),
this.newName
)
},
equals(a) {
return a.name == this.name
}
}
})
var app = new Vue({
el: '#app',
data: {
children: []
},
methods: {
addNewChild() {
this.children.push({
name: 'New Child',
})
},
updateChild(index, newName) {
this.children[index].name = newName
}
},
mounted() {
if (localStorage.children) {
this.children = JSON.parse(localStorage.children)
}
},
watch: {
children(newChildren) {
localStorage.children = JSON.stringify(newChildren)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<div id="app">
<button v-on:click="addNewChild">+ New Child</button>
<hr />
<child v-for="child in children"
:key="children.indexOf(child)"
:name="child.name"
#update-child="updateChild">
</child>
</div>
<script type="text/x-template" id="template">
<div>
<p><b>Name: {{name}}</b></p>
<input placeholder="Name" type="text" v-model="newName" />
<button #click="update">Update</button>
<hr />
</div>
</script>
The great thing about v-for is that it creates its own scope. With that in mind, you can safely reference child in the event handler. For example
<child
v-for="(child, index) in children"
:key="index"
:name="child.name"
#update-child="updateChild(child, $event)"
/>
updateChild(child, newName) {
child.name = newName
}
All you need to emit from your child component is the new name which will be presented as the event payload $event
update() {
this.$emit("update-child", this.newName)
}
A quick note about :key... it would definitely be better to key on some unique property of the child object (like an id as you suggested).
Keying on array indices is fine if your array only changes in size but if you ever decide to splice or sort it, Vue won't be able to react to that change correctly since the indices never change.

Array change detection for an array of complex objects in Vue JS 2

Update
Vue JS 3 will properly handle this: https://blog.cloudboost.io/reactivity-in-vue-js-2-vs-vue-js-3-dcdd0728dcdf
Problem:
I have a vue component that looks like this:
sub-comp.vue
<template>
<div>
<input type="text" class="form-control" v-model="textA">
<input type="text" class="form-control" v-model="textB">
<input type="text" class="form-control" v-model="textC">
</div>
</template>
<script>
export default {
props: {
textA: {
type: Number,
required: false
},
textB: {
type: Number,
required: false
},
textC: {
type: Number,
required: false
}
}
}
</script>
I have a parent component that looks like this:
layout-comp.vue
<template>
<div>
<button #click="addItem">Add</button>
<ul>
<li v-for="listItem in listItems"
:key="listItem.id">
<sub-comp
:textA="listItem.item.textA"
:textB="listItem.item.textB"
:textC="listItem.item.textC"
/>
</li>
</ul>
</div>
</template>
import subComp from '../sub-comp.vue'
export default {
components: {
subComp
},
data() {
return {
listItems: []
}
},
methods: {
addItem: function () {
var item = {
textA: 5,
textB: 100,
textC: 200
}
if (!item) {
return
}
this.length += 1;
this.listItems.push({
id: length++,
item: item
});
}
}
</script>
The thing is, anything I do to edit the textboxes, the array doesn't get changed, even though the reactive data shows that it changed. For example, it will always be as
{
textA: 5,
textB: 100,
textC: 200
}
Even if I changed textB: 333, the listItems array still shows textB: 100. This is because of this:
https://v2.vuejs.org/v2/guide/list.html#Caveats
Due to limitations in JavaScript, Vue cannot detect the following changes to an array
Question:
I'm wondering how do I update the array? I also want the change to occur when leaving the textbox, using the #blur event. I'd like to see what ways this can be done.
I read these materials:
https://codingexplained.com/coding/front-end/vue-js/array-change-detection
https://v2.vuejs.org/v2/guide/list.html
But it seems my example is a bit more complex, as it has indexes associated, and the arrays have complex objects.
Update 4/12/2018
Found out that in my addItem() that I had:
item = this.conditionItems[this.conditionItems.length - 1].item);
to
item = JSON.parse(JSON.stringify(this.conditionItems[this.conditionItems.length - 1].item));
I was thinking the sync modifier in the answer below was causing problems because it duplicated all items. But that's not the case. I was copying a vue object (including the observable properties), which caused it to happen. The JSON parse and JSON stringify methods only copies the properties as a normal object, without the observable properties. This was discussed here:
https://github.com/vuejs/Discussion/issues/292
The problem is that props flow in one direction, from parent to child.
Setting the value using v-model in child won't affect parent's data.
Vue has a shortcut to update parent's data more easily. It's called .sync modifier.
Here's how.
In sub-comp.vue
<template>
<div>
<input type="text" class="form-control" :value="textA" #input="$emit('update:textA', $event.target.value)" >
<input type="text" class="form-control" :value="textB" #input="$emit('update:textB', $event.target.value)">
<input type="text" class="form-control" :value="textC" #input="$emit('update:textC', $event.target.value)">
</div>
</template>
<script>
export default {
// remains the same
}
</script>
add .sync when you add the props
<sub-comp
:textA.sync="listItem.item.textA" // this will have the same effect of v-on:update:textA="listItem.item.textA = $event"
:textB.sync="listItem.item.textB"
:textC.sync="listItem.item.textC"
/>
update:
if you have reactivity problem, don't use .sync, add a custom event and use $set
<sub-comp
:textA="listItem.item.textA" v-on:update:textA="$set('listItem.item','textA', $event)"
/>

Input-fields as components with updating data on parent

I'm trying to make a set of components for repetitive use. The components I'm looking to create are various form fields like text, checkbox and so on.
I have all the data in data on my parent vue object, and want that to be the one truth also after the user changes values in those fields.
I know how to use props to pass the data to the component, and emits to pass them back up again. However I want to avoid having to write a new "method" in my parent object for every component I add.
<div class="vue-parent">
<vuefield-checkbox :vmodel="someObject.active" label="Some object active" #value-changed="valueChanged"></vuefield-checkbox>
</div>
My component is something like:
Vue.component('vuefield-checkbox',{
props: ['vmodel', 'label'],
data(){
return {
value: this.vmodel
}
},
template:`<div class="form-field form-field-checkbox">
<div class="form-group">
<label>
<input type="checkbox" v-model="value" #change="$emit('value-changed', value)">
{{label}}
</label>
</div>
</div>`
});
I have this Vue object:
var vueObject= new Vue({
el: '.vue-parent',
data:{
someNumber:0,
someBoolean:false,
anotherBoolean: true,
someObject:{
name:'My object',
active:false
},
imageAd: {
}
},
methods: {
valueChange: function (newVal) {
this.carouselAd.autoOrder = newVal;
}
}
});
See this jsfiddle to see example: JsFiddle
The jsfiddle is a working example using a hard-coded method to set one specific value. I'd like to eighter write everything inline where i use the component, or write a generic method to update the parents data. Is this possible?
Minde
You can use v-model on your component.
When using v-model on a component, it will bind to the property value and it will update on input event.
HTML
<div class="vue-parent">
<vuefield-checkbox v-model="someObject.active" label="Some object active"></vuefield-checkbox>
<p>Parents someObject.active: {{someObject.active}}</p>
</div>
Javascript
Vue.component('vuefield-checkbox',{
props: ['value', 'label'],
data(){
return {
innerValue: this.value
}
},
template:`<div class="form-field form-field-checkbox">
<div class="form-group">
<label>
<input type="checkbox" v-model="innerValue" #change="$emit('input', innerValue)">
{{label}}
</label>
</div>
</div>`
});
var vueObject= new Vue({
el: '.vue-parent',
data:{
someNumber:0,
someBoolean:false,
anotherBoolean: true,
someObject:{
name:'My object',
active:false
},
imageAd: {
}
}
});
Example fiddle: https://jsfiddle.net/hqb6ufwr/2/
As an addition to Gudradain answer - v-model field and event can be customized:
From here: https://v2.vuejs.org/v2/guide/components.html#Customizing-Component-v-model
By default, v-model on a component uses value as the prop and input as
the event, but some input types such as checkboxes and radio buttons
may want to use the value prop for a different purpose. Using the
model option can avoid the conflict in such cases:
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
// this allows using the `value` prop for a different purpose
value: String
},
// ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>
The above will be equivalent to:
<my-checkbox
:checked="foo"
#change="val => { foo = val }"
value="some value">
</my-checkbox>

Categories

Resources