How to pass editable data to component? - vue.js

I'm working on an app that allows you to capture and edit soccer match results.
there is a Matches component that makes an AP call to get the data of multiple matches from a server (match_list) and then renders a bunch of Match components, passing the data as props to these sub-components to fill their initial values.
<component :is="Match" v-for="match in match_list"
v-bind:key="match.id"
v-bind="match"></component>
On the Match component, I accept all the values as props.
But I get a warning that props shouldn't be edited and these should be data elements instead. So I tried passing them to the component data.
export default {
name: "Match",
props: ['local_team', 'visitor_team', 'localScore', 'visitorScore', 'date', 'time', 'location', 'matchId'],
data(){
return{
id: this.id,
local_team: this.local_team,
visitor_team: this.visitor_team,
location: this.location,
date: this.date,
time: this.time,
localScore: this.localScore,
visitorScore: this.visitorScore
}
},
Now I get a warning that editable data shouldn't be based on props.
How can I make the data from the Match component editable so it safely propagates to the parent component?

You need to accept your match object on the component's props, and make a copy of it on data (to be used as a model for your inputs). When your model changes you should emit that change to the parent so that it can change its own data appropriately (which then gets passed and reflected correctly through the child's props):
In this example I watch for any changes to the model and then emit the event directly, you can of course replace that behavior by having a submit button that fires the event upon click or something.
Vue.component('match', {
template: `
<div>
<p>{{match.name}}</p>
<input v-model="matchModel.name" />
</div>
`,
props: ['match'],
data() {
return {
matchModel: Object.assign({}, this.match)
}
},
watch: {
matchModel: {
handler(val) {
this.$emit('match-change', val)
},
deep: true,
}
}
});
new Vue({
el: "#app",
data: {
matches: [{
id: 1,
name: 'first match'
},
{
id: 2,
name: 'second match'
}
]
},
methods: {
onMatchChange(id, newMatch) {
const match = this.matches.find((m) => m.id == id);
Object.assign(match, newMatch);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<match v-for="match in matches" :match="match" :key="match.id" #match-change="onMatchChange(match.id, $event)"></match>
</div>

Related

Vue child component not rerending after parent component's data value updates

I am trying to have a child component update its props that were passed from the parents at the start of the rendering. Since the value is coming from a fetch call, it takes a bit of time to get the value, so I understand that the child component will receive a 'null' variable. But once the fetch call is completed, the value is updated but the child component still has the original null value.
During my search for a solution, I found that another way was to use Vuex Stores, so I implemented it with the count variable and had a button to call a commit and later dispatch with an action function to the store to increment it's value but when the increment happens, it doesn't show the new value on the screen even though with console logs I confirmed it did change the value when the function was called.
I guess I don't fully understand how to update the value of a variable without reassigning it within it's own component or having to call a separate function manually right after I change the value of a data variable.
App.vue
<template>
<div id="app">
<div id="banner">
<div>Title</div>
</div>
<p>count: {{count}}</p> // a small test i was doing to figure out how to update data values
<button #click="update">Click </button>
<div id="content" class="container">
<CustomDropdown title="Title Test" :valueProps="values" /> // passing the data into child component
</div>
</div>
</template>
<script>
import CustomDropdown from './components/CustomDropdown.vue'
export default {
name: 'App',
components: {
CustomDropdown,
},
data() {
return {
values: null
count: this.$store.state.count
}
},
methods: {
update() {
this.$store.dispatch('increment')
}
},
async created() {
const response = await fetch("http://localhost:3000/getIds", {
method: 'GET',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
}
});
const data = await response.json();
this.values = data // This is when I expect the child component to rerender and show the new data. data is an array of objects
console.log("data", data, this.values) // the console log shows both variables have data
}
}
</script>
CustomDropDown.vue
<template>
<div id="dropdown-container" class="">
<b-dropdown class="outline danger" variant="outline-dark" :text="title" :disabled="disabled">
<b-dropdown-item
v-for="value in values"
:key="value.DIV_ID"
href="#">
{{value.name}}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script>
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
data() {
return {
values: this.valuesProp
}
},
methods: {
},
created() {
console.log("dropdown created")
console.log(this.valuesProp) //Always undefined
}
}
</script>
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state() {
return {
count: 0,
divisionIds: []
}
},
mutations: {
increment (state) {
console.log("count", state.count)
state.count++
}
},
actions: {
increment (state) {
console.log("count action", state.count)
state.commit('increment')
}
}
})
data in your child component CustomDropdown.vue is not reactive: therefore the value of this.values is not updated when the prop changes. If you want to alias a prop, use computed instead:
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
computed: {
values() {
return this.valuesProp;
}
},
created() {
console.log("dropdown created");
}
}
If you want to console log the most updated values of this.valuesProp, you will need to watch it: the same if you want for this.values.
One thing you can do is to use a v-if in your child component to only render it after you get your result from you api.
It would be something like:
<CustomDropdown title="Title Test" :valueProps="values" v-if="values"/>
This way you would make sure that your child component gets rendered only when values are available.
It would only be a bad solution if this api call took so long and you needed to display the child component data to the user before that.
Hey you can simply watch it your child component
watch: { valuesProp: function(newVal, oldVal) { // watch it if(newVal.length > 0) do something }
it will watch for the value changes and when you get your desired value you can perform whatever hope it will help you you dont need store or conditional binding for it.

Get data to Parent component from multi child components

I need to collect data from all child components and get it in Parent component.
For example i have a form component with "Save" button.
Once i click on "Save" button i need all child component send me all data that an user put there.
<Form>
<Name />
<DatePicker />
.....
</Form>
So the main component is Form and it has several child components. Once i click on "Save" in i need to get child components data in Form.
I am thinking about giving "ref" to all child component and call their own methods in Parent once i click on "Save" inside Form. In those methods i will collect all data and fire events with this.$emit there i can send to parent the data i have collected.
Is that a good solution?
Or maybe better to use EventBus?
I prefer bind over emit.
Vue.component("InputField", {
template: `<input v-model="syncedValue" />`,
name: "InputField",
props: {
value: String
},
computed: {
syncedValue: {
get() {
return this.value;
},
set(v) {
this.$emit("input", v);
}
}
}
});
Vue.component("Form", {
template: `<div><InputField v-model="name"/><InputField v-model="surname"/><button #click="save">Save</button></div>`,
name: "Form",
data() {
return {
name: "",
surname: ""
};
},
methods: {
save() {
alert(`${this.name} ${this.surname}`);
}
}
});
new Vue({
template: `<Form></Form>`
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Data property no longer reactive after doing simple re-assignment?

I have a data property called current_room where initially it has an empty object {}.
I have a component that will receive current_room as a "prop".
In the parent component, in the mounted() hook, re-assignment takes place: this.current_room = new_room
In the child component, the current_room prop appears to be... an empty object. In the parent component, it's not an empty object, it has the data I expect to see.
What would be the proper way to make this work? It seems as though simple re-assignment doesn't work in this case, that once I define a property on the data object... and that property is an object... I have to add/remove properties to the object, rather than just wholesale re-assigning a new object to that data property.
I guess it's just a simple mistake somewhere in your code. Because following to your question - it should work, furthermore I created a simple example where I defined components and functionality as you've described - and it works. I will provide async example to make you sure for 100 percents.
Here is the working example:
App.vue
<template>
<div id="app">
<CurrentRoom :room="current_room" />
</div>
</template>
<script>
import CurrentRoom from './components/CurrentRoom.vue'
export default {
name: "App",
components: {
CurrentRoom
},
data () {
return {
current_room: {}
}
},
mounted () {
setTimeout(() => {
this.current_room = {
door: true,
windowsCount: 2,
wallColor: 'white',
members: [
{
name: 'Heisenberg',
age: 46
},
{
name: 'Pinkman',
age: 26
}
]
}
}, 2000)
}
};
</script>
CurrentRoom.vue
<template>
<div>
Current room is: <br>
<pre>{{ room }}</pre>
</div>
</template>
<script>
export default {
name: 'CurrentRoom',
props: {
room: {
type: Object,
default: () => {}
}
}
}
</script>
Codesandbox demo:
https://codesandbox.io/s/epic-banach-clyz4
And for the end, following to your question:
... What would be the proper way to make this work? ...
The answer is - 'Please, compare your code with provided example'

Child component not receiving props from parent after data is updated in parent

I have this template called Report.vue, and in that template I also have the data() property called dataSent:[]. In said template, I call the component BarChart, to which I'm trying to pass the dataSent array with updated elements like dataSent[1,2,3,4,5...] to the component (BarChart) so it can display graphs. However when I add console.log to BarChart, it never receives the updated props. I suspect somehow it has to do with events, but since I'm a newbie and still haven't fully understood how it works, was wondering if I could get some help
Since the dataSent:[] is initialized to an empty array, makes sense that the BarChart component doesn't show anything from the start, but then, I have created a method (in the methods section), called updateData(), which explicitly fills the dataSent with [12, 19, 3, 5, 2,8]. And certainly, I'm using the props bind to the component Barchart (this one is called data) which is bound to the dataSent array. Finally, in the html section, an #input event in a component called GPSelect is what it's suppose to trigger the change (updating the dataSent array), however, as mentioned, does happen in the parent (Report.vue), but not in the child component (BarChart).
Report.vue
<v-flex lg4 sm12 xs12>
<div class="selectOptions">
<GPSelect
#input="listSessions"
v-model="programSelected"
:items="programs"
label="Programs"
:disabled="programs === undefined || programs.length === 0"
/>
</div>
</v-flex>
<BarChart
:label="label"
:data="dataSent"
:labels="labels"
/>
data() {
return {
treatments: [],
programs: [],
programSelected: "",
dataSent: [],
label: "Number of sessions",
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange']
}
},
methods: {
async listSessions() {
/*...some other sentences irrelevant */
this.updateData();
},
updateData() {
this.dataSent=[12, 19, 3, 5, 2,8];
return this.dataSent;
},
BarChart.vue
<template>
<div class="chart-container">
<canvas id="barChart" ref="barChart">
</canvas>
</div>
</template>
<script>
import Chart from "chart.js";
export default {
props: {
labels: Array,
colors: Array,
data: Array,
label: ""
},
mounted() {
this.displayChart();
},
methods: {
displayChart() {
console.log(this.data);
}
}
I expect the console.log from Barchart to display the updated array (the this.data received) sent from the parent component Report.vue, but currently, it shows [__ob__: Observer]
BarChart.vue
<template>
<div class="chart-container">
<canvas id="barChart" ref="barChart" />
</div>
</template>
<script>
import Chart from "chart.js";
export default {
props: {
labels: Array,
colors: Array,
data: Array,
label: ""
},
mounted() {
this.displayChart();
},
watch: {
'data': 'displayChart'
},
methods: {
displayChart() {
console.log(this.data);
}
}
}
Try this :))

watch for the whole array as well as one of the properties of it

Let's say I have a prop such as
[{id:1, name:"first"}, {id:2, name:"second"}]
I have a watcher for this called Points. as soon as parent component changes this array, watcher function gets called in child.
Now, I also want to watch if name field in any of this array's object got changed. Workaround I know is to set deep as true in watcher, but this means this is gonna make the watcher for each property of the object. As I have a very huge array of huge objects, I don't want to make this many watcher.
So is there a way to make watcher for the whole array and also one of the properties of the array's object?
You can mark the non-reactive properties as non-configurable, and Vue will not detect changes in their values.
let arr = [{id:1, name:"first"}, {id:2, name:"second"}];
arr.forEach(item => Object.defineProperty(item, "id", {configurable: false}));
Alternatively, you could use a shallow watcher and require code that modifies the reactive properties to use Vue.set() instead of simple assignment.
Vue.set(arr[0], "name", "new name"); // don't use arr[0].name = "new name";
You can create a child component, where you bind objects to the array and place the watcher inside the child component as below:
Parent.vue
<template>
<child-component v-for="point in points" :point="point" ></child-component>
</template>
data: {
return {
points: [{id:1, name:"first"}, {id:2, name:"second"}]
}
}
Child.vue:
props: ['point']
...
watch: {
name: function(newVal){
// watch name field
}
}
One way would be to create a computed property that just touches the bits you care about and then watch that instead.
new Vue({
el: '#app',
data () {
return {
points: [{id: 1, name: 'first'}, {id: 2, name: 'second'}]
}
},
computed: {
pointStuffICareAbout () {
return this.points.map(point => point.name)
}
},
methods: {
updateId () {
this.points[0].id = Math.round(Math.random() * 1000)
},
updateName () {
this.points[0].name = Math.random().toString(36).slice(2, 7)
},
addItem () {
this.points.push({id: 3, name: 'third'})
}
},
watch: {
pointStuffICareAbout () {
console.log('watcher triggered')
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button #click="updateId">Update id</button>
<button #click="updateName">Update name</button>
<button #click="addItem">Add item</button>
<p>
{{ points }}
</p>
</div>