Vue did not read my child component in v-for loops - javascript

What's wrong: Vue returns an empty array on when I console.log it. However when I clicked it in Developer Tools, the data was there!
Background story: I was actually trying to make a total value of child's data wheter child has this value or not, however upon trying to just opening the component data none showed up!
P.S. The child data will automatically updated every X seconds.
The result of console.log(this.$children); shows [] (empty array) in console. But when I clicked it, it opens the whole children array. But when I tried to reveal it via this.$children[0], it shows value of undefined!
So here's my link to JSFiddle https://jsfiddle.net/irfandyjip89/zmn5yhju/
Vue.component("comp", {
props: ["location"],
template: "#comp"
})
vm = new Vue({
el: "#vm",
data: {
location: [{
nation: "America",
money: "Dollar"
}, {
nation: "China",
money: "Yuan"
}, {
nation: "Thailand",
language: "Baht"
}]
},
computed: {
totalNation() {
console.log(this.$children)
return this.location.length
}
}
})
<section id="vm">
Total of Nations : {{totalNation}}
<comp v-for="loc in location" :location="loc.nation"></comp>
Child : {{$children}}
</section>
<template id="comp">
<li>{{location}}</li>
</template>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
What should happen:
I can access children's data.
Please feel free to ask for more information.

Related

Updating item in array of objects in Vue.js: is it better to replace the array or change one value

I'm using Vue/Vuex to generate components from an array with this structure (retrieved from SQLite using better-sqlite3).
let table=[
{
id:1,
column_1:'data',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
},
{
id:2,
column_1:'data',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
},
...
]
There are cases where the data of multiple rows need to be updated at once. Since I need to pass data back and forth between SQLite and Vue, I'm wondering whether it'll be simpler and harmless to update the data with sql and then replace the entire javascript array with the updated data, including the unmodified rows. Otherwise I imagine I'll have to use .find() to go through and change the specific items. So my question is whether replacing the whole array is a Bad Idea as far as reactivity goes, or whether Vue can smart update accordingly.
Vue has a different way of tracking when it comes to update the UI
Vue uses getters/setters on the data object for mutation tracking. When you execute this.table = [{}, {}, {}, ...];, It will go through the setter of table. In the setter, there's a function to notify the watcher and add this data changes to a queue.
Limitation/Behaviour while updating the Arrays :
Vue cannot detect the following changes to an array :
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
Demo as per above two statements :
const app = new Vue({
el: '#app',
data() {
return {
table: [{
id:1,
column_1:'data1',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}, {
id:2,
column_1:'data2',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}]
}
},
mounted() {
// We are updating the array item and It's not reactive (You can see it's not updating UI)
this.table[1] = {
id: 3,
column_1: 'data3',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="item in table" :key="item.id">
{{ item.column_1 }}
</li>
</ul>
</div>
Answer of your Question : In consideration of above two statements, You have to update whole array including the unmodified rows.
Demo as per the answer of your question :
const app = new Vue({
el: '#app',
data() {
return {
table: [{
id:1,
column_1:'data1',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}, {
id:2,
column_1:'data2',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}]
}
},
mounted() {
// We are updating the whole array and It's reactive (You can see it's updating UI)
this.table = [{
id:1,
column_1:'data1',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}, {
id: 3,
column_1: 'data3',
column_2:'data',
column_3:{'1':'data','2':'data','3':'data'}
}]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="item in table" :key="item.id">
{{ item.column_1 }}
</li>
</ul>
</div>

For sibling communication between many identical components, how should I store the data in the lowest-common ancestor?

Background:
I'm a Python/Vue developer; I've been using Vue since 2016.
I have a client who runs a weight loss / meal planning business: clients pay her to prepare weekly single-page PDF menus that tell them (the clients) exactly what to eat for breakfast, lunch, and dinner of every day of the week. (image of an example menu)
Each meal is shown as a list of ingredients.
Right now she's preparing these menus in Excel, and she hired me to reproduce and extend the functionality of what she has in Excel, but in a Python/Vue app.
The app I'm building for her has many "pages" ("top-level" components) to allow her to add/modify/delete objects like clients, ingredients, and recipes (image), but the most complicated part of the UI is the component in which she can define the meals for every meal of every day of the week (image). That component is named WeeklyMenu.vue.
WeeklyMenu.vue itself contains seven DailyMenu.vue children, one for each day of the week (Monday, Tuesday, etc.). (image)
Each DailyMenu.vue component itself contains four Meal.vue components, one for each of four meal types: Breakfast, Lunch, Dinner, and Snacks. (image)
Important: At the moment, the DailyMenu.vue and Meal.vue components themselves contain their data rather than accessing it from the Vuex store.
For example, the list of ingredients for each meal is contained within the Meal.vue component as a mealIngredients variable within the component's data attribute. (image)
Side-note: This means that there are lots of HTTP requests being sent to the back-end when the page loads as all of the meals are requesting their own data, rather than a single request being sent via a Vuex action (for example). This seems like it can't be best practice.
The problem:
The problem is that she is now asking me to add features in which a change to the data in one subcomponent should update the data in a different subcomponent.
For example, she wants the app to work so that when she has the same recipe in several different Meals of the week, then a change to an ingredient in one of the meals will propagate to the other meals that have the same recipe. (image explanation)
My question:
What is the best practice for handling a situation like this? Should I move the ingredient data into the Vuex store or (in the same vein) the lowest-common-ancestor WeeklyMenu.vue component? If so, how exactly should it work? Should there be a separate variable for each meal? Or should I have an object that contains data for all of the different meals? If I use a single object, do I need to worry that a watcher on that object in the Meal.vue component would be triggering even when a change was made to a different meal's data?
If I store all the meal ingredients in separate variables, I would need to pass all of those to every meal (so every meal would need to receive every other meal's ingredients as separate props). So that doesn't seem like the right way to go.
If a user is making a particular change to a particular meal, how would I only have the other meals with the same name react?
Related links:
Communication between sibling components in VueJs 2.0
I'm looking into whether it would make sense to move the ingredient data up to the level of the WeeklyMenu.vue component as described in the "Lowest Common Ancestor" approach (here and here).
Simplified example of the situation I'm trying to handle:
Without Vuex: https://codepen.io/NathanWailes/pen/zYBGjME
Using Vuex: https://codepen.io/NathanWailes/pen/WNxWxWe
With everything working (including the state being kept in Vuex) except the propagation: https://codepen.io/NathanWailes/pen/KKMYNVZ
Yes, problem domain seems complex enough to more than justify use of Vuex. I would not go with keeping data in components and sharing by props - that doesn't scale well
Keep each Recipe as an object in single object recipes - you don't need to worry about watchers. If one particular Recipe object will change, Vue will re-render only components using same Recipe object (and if done properly you don't even need watchers for that)
Create a "weekly menu" object inside the store
In leaf nodes (Meals) of that object just use some kind of reference (by name or unique ID if you have one) into recipes. As a result multiple Meal.vue components on a menu will use same object in the store and update automatically
I ended up getting it working in a simple example in CodePen, which I'm going to use as a guide when trying to get it working on the actual site.
The summary of my findings with this solution is, "Vue will actually update when the nested entries of a Vuex state object are updated; you don't need to worry about it not detecting those changes. So it's OK to just keep all the data in a single big Vuex store object when you have many duplicate sibling components that need to react to each other."
Here's the CodePen: https://codepen.io/NathanWailes/pen/NWRNgNz
Screenshot
Summary of what the CodePen example does
The data used to populate the menu all lives in the Vuex store in a single weeklyMenu object, which has child objects to break up the data into the different days / meals.
The individual meals have computed properties with get and set functions so that it can both get changes from the store and also update the store.
The DailyMenu and WeeklyMenu components get their aggregate data by simply having computed properties that iterate over the Vuex weeklyMenu object, and it "just works".
I have same-named meals update to match each other by iterating over the meals in the Vuex mutation and looking for meals with the same "Ingredient Name".
The code
HTML
<html>
<body>
<div id='weekly-menu'></div>
<h3>Requirements:</h3>
<ul>
<li>Each row should have all the numbers in it summed and displayed ('total daily calories').</li>
<li>The week as a whole should have all the numbers summed and displayed ('total weekly calories').</li>
<li>If two or more input boxes have the same text, a change in one numerical input should propagate to the other same-named numerical inputs.</li>
<li>Ideally the data (ingredient names and calories) should be stored in one place (the top-level component or a Vuex store) to make it more straightforward to populate it from the database with a single HTTP call (which is not simulated in this example).</li>
</ul>
</body>
</html>
JavaScript
const store = new Vuex.Store(
{
state: {
weeklyMenu: {
Sunday: {
Breakfast: {
name: 'aaa',
calories: 1
},
Lunch: {
name: 'bbb',
calories: 2
},
},
Monday: {
Breakfast: {
name: 'ccc',
calories: 3
},
Lunch: {
name: 'ddd',
calories: 4
},
}
}
},
mutations: {
updateIngredientCalories (state, {dayOfTheWeekName, mealName, newCalorieValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['calories'] = newCalorieValue
const ingredientNameBeingUpdated = state.weeklyMenu[dayOfTheWeekName][mealName]['name']
for (const dayOfTheWeekName of Object.keys(state.weeklyMenu)) {
for (const mealName of Object.keys(state.weeklyMenu[dayOfTheWeekName])) {
const mealToCheck = state.weeklyMenu[dayOfTheWeekName][mealName]
const ingredientNameToCheck = mealToCheck['name']
if (ingredientNameToCheck === ingredientNameBeingUpdated) {
mealToCheck['calories'] = newCalorieValue
}
}
}
},
updateIngredientName (state, {dayOfTheWeekName, mealName, newValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['name'] = newValue
}
}
}
)
var Meal = {
template: `
<td>
<h4>{{ mealName }}</h4>
Ingredient Name: <input v-model="ingredientName" /><br/>
Calories: <input v-model.number="ingredientCalories" />
</td>
`,
props: [
'dayOfTheWeekName',
'mealName'
],
computed: {
ingredientCalories: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['calories']
},
set (value) {
if (value === '' || value === undefined || value === null) {
value = 0
}
this.$store.commit('updateIngredientCalories', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newCalorieValue: value
})
}
},
ingredientName: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['name']
},
set (value) {
this.$store.commit('updateIngredientName', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newValue: value
})
}
}
}
};
var DailyMenu = {
template: `
<tr>
<td>
<h4>{{ dayOfTheWeekName }}</h4>
Total Daily Calories: {{ totalDailyCalories }}
</td>
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Breakfast" />
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Lunch" />
</tr>
`,
props: [
'dayOfTheWeekName'
],
data: function () {
return {
}
},
components: {
meal: Meal
},
computed: {
totalDailyCalories () {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[this.dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[this.dayOfTheWeekName][mealName]['calories']
}
return totalDailyCalories
}
}
};
var app = new Vue({
el: '#weekly-menu',
template: `<div id="weekly-menu" class="container">
<div class="jumbotron">
<h2>Weekly Menu</h2>
Total Weekly Calories: {{ totalWeeklyCalories }}
<table class="table">
<tbody>
<daily_menu day-of-the-week-name="Sunday" />
<daily_menu day-of-the-week-name="Monday" />
</tbody>
</table>
</div>
</div>
`,
data: function () {
return {
}
},
computed: {
totalWeeklyCalories () {
let totalWeeklyCalories = 0
for (const dayOfTheWeekName of Object.keys(this.$store.state.weeklyMenu)) {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[dayOfTheWeekName][mealName]['calories']
}
totalWeeklyCalories += totalDailyCalories
}
return totalWeeklyCalories
}
},
components: {
daily_menu: DailyMenu
},
store: store
});

error on removing an object in list in vue

I'm deploying a client app with vue.js.
In this app I have some tabs that are being rendered using v-for. this tab array is formated like this in vuex store:
tabs: [
{
id: 1,
title: 'first tab',
number: '09389826331',
accountName: 'DelirCo',
contactName: 'PourTabarestani',
startTime: '2019-02-25 15:11:30',
endTime: '2019-02-25 18:04:10',
duration: null,
subject: '',
description: ''
},
{
id: 2,
title: 'second tab',
number: '09124578986',
accountName: 'Cisco',
accountGUID: '',
contactName: 'Arcada',
contactGUID: '',
startTime: '2019-02-25 15:11:45',
endTime: '2019-02-25 15:13:55',
duration: null,
subject: '',
description: ''
}
]
I'm using getters to load the tabs in my vuex store which renders the tabs using following template:
<template>
<div id="Tabs">
<vs-tabs color="#17a2b8" v-model="selectedTab">
<vs-tab v-for="tab in tabs" :key="tab.id" :vs-
label="tab.title">
<Tab :tab="tab"></Tab>
</vs-tab>
</vs-tabs>
</div>
</template>
I'm using vuesax components for creating the tabs displays.
each object in this list is a tab in my front end which shows the related data when I click each tab.
it's doing perfectly fine when I try to show the tabs or even adding another object in the array.
the problem is when I try to remove a certain item from this array the content goes away but the tab title (the button where I can select the tab with it) remains on the page.
I'm using
state.tabs.splice(objectIndex, 1)
state.selectedTab -= 1
for removing the tab and changing the selected tab to the previous one.
but as I said the title of the tab is not being removed like the picture below:
and when I click on that tab I'm getting this error:
webpack-internal:///./node_modules/vuesax/dist/vuesax.common.js:4408 Uncaught TypeError: Cannot set property 'invert' of undefined
at VueComponent.activeChild (webpack-internal:///./node_modules/vuesax/dist/vuesax.common.js:4408)
at click (webpack-internal:///./node_modules/vuesax/dist/vuesax.common.js:4127)
at invoker (webpack-internal:///./node_modules/vue/dist/vue.esm.js:2140)
at HTMLButtonElement.fn._withTask.fn._withTask (webpack-internal:///./node_modules/vue/dist/vue.esm.js:1925)
anyone have any suggestion around this matter ?
You can just add a key to the parent element to force the rerender.
<div :key="forceRender">
<vs-tabs :value="activeTab">
<template v-for="item in items">
<vs-tab :label="item.name" #click="onTabChange(item)">
<div class="con-tab-ejemplo">
<slot name="content"></slot>
</div>
</vs-tab>
</template>
</vs-tabs>
</div>
After that add a watcher on the items:
watch: {
items (val) {
this.forceRender += 1
}
},
It seems to be an issue with the vuesax library. The <vs-tabs> component doesn't support <vs-tab> components being added/removed as child components dynamically.
Here's an excerpt from the vsTab.vue file in the repo:
mounted(){
this.id = this.$parent.children.length
this.$parent.children.push({
label: this.vsLabel,
icon: this.vsIcon,
id: this.$parent.children.length,
listeners: this.$listeners,
attrs: this.$attrs
})
}
When the <vs-tab> component is mounted, it adds itself to the parent (<vs-tabs>) as a child, but it does not remove itself from this array when it is destroyed.
You can open an issue on their GitHub page to see if they could support what you want, or you can submit a pull request.

Working with Vuex with Vue and displaying data

This is kind of a long explanation of an issue that I'm having on a personal project. Basically, I want to set a data property before my page loads when I read in data from a CSV file using D3.JS. I almost have it done but running into a small issue. Please read on to get more detail.
Basically, when the user comes to a page in my application, I want to display weather graphs. Like I said, I'm using D3.js to read in the data and created an action to do that. It works perfectly fine-I can console.log the data and I know its been read. However, in my vue instance I have a data property, which would hold the data set like this:
data() {
return {
name: this.$store.state.name
weatherData: this.$store.state.yearData
}
}
I then want to ensure that the weatherData is filled, with data from the csv file so I display it on the page like this:
<p>{{ weatherData }}</p>
Nothing special here. When the page loads, weatherData is blank. But I have a beforeMount life cycle hook and if I comment out the only line in it then it will display the data. If I then refresh the page, fire the action to get the data and then uncomment out the line in the beforeMount hook then the data appears! So before I continue this is my full code for the store:
export const store = new Vuex.Store({
state: {
name: 'Weather Data'
yearData: []
},
getters: {
},
mutations: {
setYearData(state, data) {
state.yearData = data
}
},
actions: {
getYearData: ({commit}) => {
d3.csv("../src/components/data/alaska.csv")
.then(function(data){
let yearData = []
for (let i = 0; i < data.length; i++){
let day = data[i].AKST
yearData.push(day)
}
//console.log(yearData)
commit('setYearData', yearData)
})
}
})
Here are parts of the vue file: The template:
<p>{{ weatherData }}</p>
The Vue Intance:
export default {
name: 'Weather',
data() {
return {
name: this.$store.state.name,
weatherData: this.$store.state.yearData
}
},
methods: {
...mapActions([
'getYearData'
])
},
beforeMount(){
this.$store.dispatch('getYearData') //(un)Commenting out this line will make my data appear
}
}
Page when it loads: Notice empty array:
Then either comment out or comment the one line in the beforeMount hook and get this: THE DATA!!!
Again, my end goal is to have the action called and the data set before the page finishes loading. Finally, I know that I don't need VUEX but this project is further helping me understand it. Any guidance on why this is happening would be great.
use mapState instead of putting your data in the data object, which sometimes being late on updating the template.
just make your Vue instance to look like:
import {mapState} from 'vuex'
export default {
name: 'Weather',
data() {
return { }
},
computed:{
...mapState({
name: state=>state.name,
weatherData: state=>state.yearData
})
},
methods: {
...mapActions([
'getYearData'
])
},
beforeMount(){
this.$store.dispatch('getYearData') //(un)Commenting out this line will make my data appear
}
thats way, you work directly with one source of truth-the store, and your name and weatherData will be reactive as well.
more about mapState here: https://vuex.vuejs.org/guide/state.html#the-mapstate-helper

Vue dynamic child and sub-child component

I have Address.vue component that contain child component Contact.vue.
One address container many component
What I have done:
I have created the Address.vue component can increase dynamically so user can press Add new address and he will have as many addresses as he wants.
Also in the Address.vue component user can add multiple Contacts as much as he want.
So lets say the relation here is 1 address can have multiple contacts and also the user can add multiple addresses.
I have done this UI and everything works perfectly.
What I am trying to do:
Save the contact of each address in a JSON Array for example:
values: [
{
address: {
location: "Any location",
contacts: [{
name: "Contact One",
phone_number: "12345"
}]
}
},
{
address: {
location: "another location",
contacts: [
{
name: "Contact 3",
phone_number: "6789"
},
{
name: "Contact 4",
phone_number: "101010"
},
]
}
},
]
What I have done:
I have stored the contacts array in the Contact.vue component in an array and made an event to send data from the child component (Contact.vue) to the parent component (Address.vue).
Then I got the contacts array from the event created in the Address.vue component
Problem:
I can't refer contacts to the parent address as in the JSON structure mentioned above.
I hope I am understanding properly your problem:
Note: If you create the Contacts vue component within the Address vue component. Vue will assigned each contact to its parent address.
Ex:
Address.vue
<contact :id="this.count"></contact>
If Contacts.vue are created within Address.vue then in Address.vue:
Then in Andress.vue you can create an object or array element:
data(){
return{
location: "Any location"
contacts:[]
}
}
Then create a function saveContacts:
methods: {
saveContacts(event, contact){
this.contacts.push(contact);
}
}
And finally when you check the event call the function :
#addedContact="saveContact($event,contact)"
This way you will have all addresses with its own contacts. More or less what you showed in your comment.
Hope this can be helpful to you.

Categories

Resources