Instantiating a deep object and referencing different levels in other instances - vue.js

I would like to know how to render two html elements that reference different levels in the same object.
Let's say I have a global deep object:
var sports = [{
sportName: 'soccer',
teams: [{
teamName: 'TeamA'
},
{
teamName: 'TeamB'
}
]
},
{
sportName: 'basketball',
teams: [{
teamName: 'TeamC'
},
{
teamName: 'TeamD'
}
]
}
]
Now, I want to two unordered lists that represent the different levels in the hierarchy of that object.
<ul id="sports">
<li v-for:"sport in sports">
<span>{{ sport.sportName }}</span>
</li>
</ul>
<script>
var sportsList = new Vue({
el:'#sports',
data: {
sports: sports
}
})
</script>
Here is the other list, in a completely different part of the app:
<ul id="teams">
<li v-for:"team in teams">
<span>{{ team.teamName }}</span>
</li>
</ul>
<script>
var sportsList = new Vue({
el:'#teams',
data: {
teams: sports[sportName].teams
}
})
</script>
My questions are these:
1) Will rendering two seperate instances of the different levels in the sports data object still result in data reactivity in each of those instances?
2) I've noticed that as soon as I instantiate the first list (sports), the get and set for the nested items (teams) are stored in the prototype... which leads me on to the next question. Does it make sense to instantiate a second Vue instance for teams, when it has already been instantiated in the first? I'm finding it difficult to navigate deeper objects within Vue :(

I think the best approach is to use a computed, rather than trying to copy parts of an array to data, which can cause you to end up in a bit of a mess. So in your case I can see you're trying to get teams for a sport, so I would set up a computed that returns teams for a sport stored in data:
computed: {
teams() {
return this.sports.filter(sport => {
return this.sportName === sport.sportName
})[0].teams;
}
},
Now all you need to do is set a data property called sportName which that filter can react to, so the full view model looks like this:
var app = new Vue({
el: '#app',
computed: {
teams() {
return this.sports.filter(sport => {
return this.sportName === sport.sportName
})[0].teams;
}
},
methods: {
setSport(name) {
this.sportName = name;
}
},
data: {
sportName: 'basketball',
sports: [{
sportName: 'soccer',
teams: [{
teamName: 'TeamA'
}, {
teamName: 'TeamB'
}]
}, {
sportName: 'basketball',
teams: [{
teamName: 'TeamC'
}, {
teamName: 'TeamD'
}]
}]
}
});
And here's the JSFiddle: https://jsfiddle.net/hgw2upzf/

Related

Vue.js Function called many times

Run the code and look in the console in your browser. You will see function "formatName()" is called many times. Why? I dont update data of race.
But if i change function "amIStarted()" to "return start < 5", then it will be executed 2 times, which is correct.
(sorry my english)
https://jsfiddle.net/a496smx2/48/
var stopwatch = new Vue({
el: "#stopwatch",
data: {
time: 1
},
created: function() {
setInterval(function(){
stopwatch.time++;
}, 1000);
}
})
var race = new Vue({
el: "#race",
data: {
startList: [
{name: "John", start: 3},
{name: "Mike", start: 7},
{name: "Gabe", start: 333},
],
},
methods: {
amIStarted: function(start) {
return start < stopwatch.time;
},
formatName: function(name) {
console.log("I was called.")
return 'Mr. '+name;
}
}
});
<div id="stopwatch" ><h4>Time: <span class="gold" >{{time}}</span></h4></div>
<small>Yellow color means the person started</small>
<div id="race" >
<div v-for="oneStart in startList" :class="{gold : amIStarted(oneStart.start)}" >
{{formatName(oneStart.name)}} will start when time is more then {{oneStart.start}}
</div>
<br><br>
</div>
You're looking for computed properties instead of methods. They will be optimized so they are run only when a prop it depends on changes. Methods are run every time an update happens, which could be as often as the mouse moves, depending on your application structure and complexity.
function "formatName()" is called many times. Why?
Because you have added a variable which is continuously changing and checked to add a class :class="{gold : amIStarted(oneStart.start)}" and in each chanve vue reload that part in which function comes and it is calling it again.
var stopwatch = new Vue({
el: "#stopwatch",
data: {
time: 1
},
created: function() {
setInterval(function(){
stopwatch.time++;
}, 1000);
}
})
var race = new Vue({
el: "#race",
data: {
startList: [
{name: "John", start: 3},
{name: "Mike", start: 7},
{name: "Gabe", start: 333},
],
},
methods: {
amIStarted: function(start) {
return start < stopwatch.time;
},
formatName: function(name) {
console.log("I was called.")
return 'Mr. '+name;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="stopwatch" ><h4>Time: <span class="gold" >{{time}}</span></h4></div>
<small>Yellow color means the person started</small>
<div id="race" >
<div>
<div v-for="oneStart in startList" >
{{formatName(oneStart.name)}} will start when time is more then {{oneStart.start}}
</div>
<br><br>
</div>
</div>
Here is the flow
https://vuejs.org/images/data.png
There are many other ways to achieve what you want to do. Please check for those.
Functions or filters embedded in a template will be called every time the template is rendered, so every time you update the time, that function will be run (once per name) as part of the rerender.
So long as your methods have no unwanted side effects, this is generally fine! But in cases where you may have a lot of these going at once and start running into performance issues, you can switch to a computed function.
Computed functions will be called only when the data they depend on changes. You can't pass parameters to a computed function, so rather than handling each individual name separately you'd need to have it modify the full list of names in one go:
var stopwatch = new Vue({
el: "#stopwatch",
data: {
time: 1
},
created: function() {
setInterval(function() {
stopwatch.time++;
}, 1000);
}
})
var race = new Vue({
el: "#race",
data: {
startList: [{
name: "John", start: 3
}, {
name: "Mike", start: 7
}, {
name: "Gabe", start: 333
}
]
},
computed: {
formattedList() {
console.log("Computed function ran");
let arr = [];
for (let oneStart of this.startList) {
arr.push({
formattedName: 'Mr. ' + oneStart.name,
start: oneStart.start
})
}
return arr
}
},
methods: {
amIStarted: function(start) {
return start < stopwatch.time;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="stopwatch">
<h4>Time: <span class="gold">{{time}}</span></h4>
</div>
<small>Yellow color means the person started</small>
<div id="race">
<div v-for="oneStart in formattedList" :class="{gold : amIStarted(oneStart.start)}">
{{oneStart.formattedName}} will start when time is more then {{oneStart.start}}
</div>
<br><br>
</div>
An alternative approach would be to modify the data in a beforeMount() or created() block, but this would only be appropriate if you're certain the input data will not change during the lifespan of the component.

determine if a set of related values have changed in VueJS

I am very new to VueJS.
From what I've seen there is probably an elegant answer to this. I have a table of records. Clicking on one of them opens a modal and loads that row/record. My code looks like this (made easier to read):
Javascript
app = new Vue({
el: '#app',
data: {
records: [], //keys have no significance
focusRecord: { //this object will in the modal to edit, initialize it
id: '',
firstname: '',
lastname: ''
},
focusRecordInitial: {}
},
created: function(){
//load values in app.records via ajax; this is working fine!
app.records = ajax.response.data; //this is pseudo code :)
},
methods: {
loadRecord: function(record){
app.focusRecord = record; // and this works
app.focusRecordInitial = record;
}
}
});
Html
<tr v-for="record in records">
<td v-on:click="loadRecord(record)">{{ record.id }}</td>
<td>{{ record.firstname }} {{ record.lastname }}</td>
</tr>
What I'm trying to do is really simple: detect if focusRecord has changed after it has been loaded into the modal from a row/record. Ideally another attribute like app.focusRecord.changed that I can reference. I'm thinking this might be a computed field which I'm learning about, but again with Vue there may be a more elegant way. How would I do this?
What you need is to use VueJS watchers : https://v2.vuejs.org/v2/guide/computed.html#Watchers
...
watch : {
focusRecord(newValue, oldValue) {
// Hey my value just changed
}
}
...
Here is another way to do it, however I didn't know what's refers "focusRecordInitial"
new Vue({
el: '#app',
data() {
return {
records: [],
focusRecordIndex: null
}
},
computed : {
focusRecord() {
if (this.focusRecordIndex == null) return null
if (typeof this.records[this.focusRecordIndex] === 'undefined') return null
return this.records[this.focusRecordIndex]
}
},
watch : {
focusRecord(newValue, oldValue) {
alert('Changed !')
}
},
created() {
this.records = [{id: 1, firstname: 'John', lastname: 'Doe'}, {id: 2, firstname: 'Jane', lastname: 'Doe'}, {id: 3, firstname: 'Frank', lastname: 'Doe'}]
},
methods : {
loadRecord(index) {
this.focusRecordIndex = index
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<table>
<tr v-for="(record, i) in records">
<td><button #click="loadRecord(i)">{{ record.id }}</button></td>
<td>{{ record.firstname }} {{ record.lastname }}</td>
</tr>
</table>
{{focusRecord}}
</div>
Vue Watchers
Vue provides a more generic way to react to data changes through the watch option. This is most useful when you want to perform asynchronous or expensive operations in response to changing data.
Vue JS Watchers
You can do something like this:
app = new Vue({
el: '#app',
data: {
records: [], //keys have no significance
focusRecord: { //this object will in the modal to edit, initialize it
id: '',
firstname: '',
lastname: ''
},
focusRecordInitial: {}
},
created: function(){
//load values in app.records via ajax; this is working fine!
app.records = ajax.response.data; //this is pseudo code :)
},
methods: {
loadRecord: function(record){
app.focusRecord = record; // and this works
app.focusRecordInitial = record;
}
},
watch: {
loadRecord: function () {
alert('Record changed');
}
}
});
You can also check out: Vue JS Computed Properties
You can set up a watcher to react to data changes as:
watch: {
'focusRecord': function(newValue, oldValue) {
/* called whenever there is change in the focusRecord
property in the data option */
console.log(newValue); // this is the updated value
console.log(oldValue); // this is the value before changes
}
}
The key in the watch object is the expression you want to watch for the changes.
The expression is nothing but the dot-delimited paths of the property you want to watch.
Example:
watch: {
'focusRecord': function(newValue, oldValue) {
//do something
},
'focusRecord.firstname': function(newValue, oldValue){
//watch 'firstname' property of focusRecord object
}
}

add our own function to when something is added to a Vuejs data element

I have a fairly simple Vuejs app and am just learning Vuejs. When I add or delete from a data property, I'd like some other function to happen. The code is like this:
data: {
pricings: null,
},
mounted(){
var somePrice = {"name":"service price", "price":"2.45"}
this.pricings.push(somePrice);
},
methods:{
callMe: function(){
alert("call me");
}
}
I'd like when I add or delete from pricings for some other method (callMe in this case) to be called. I am sure this is possible but am not having luck finding how to do it.
You could use either a computed or a watch property. It really depends on what your use case is.
Take the following example:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
data: {
pricings: [],
},
mounted() {
const somePrices = [{
"name": "Service",
"price": "2.45"
}, {
"name": "Another Service",
"price": "5.25"
}, {
"name": "Service Three",
"price": "1.52"
}];
this.pricings.push(...somePrices);
},
methods: {
callMe: function(newVal) {
// console.log(newVal);
// async or expensive operation ...
console.log("call me");
}
},
computed: {
pricingsSum: function() {
return this.pricings.reduce((sum, item) => sum + Number.parseFloat(item.price), 0);
}
},
watch: {
pricings: function(newVal) {
this.callMe(newVal);
}
}
});
<script src="https://unpkg.com/vue#2.4.3/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="item in pricings" :key="item.name">
{{ item.name }} ${{ item.price }}</li>
</ul>
<p>Total: ${{ pricingsSum }}</p>
</div>
We used a computed property for complex logic that would prevent the template from being simple and declarative by doing something like:
<p>Total: ${{ this.pricings.reduce((sum, item) => sum + Number.parseFloat(item.price), 0) }}</p>
That would look even worse if you needed to repeat this operation in several parts of your template.
On the other hand, we used a watch property for pricings, which reacts to data changes for pricings.
Quoting the docs:
This is most useful when you want to perform asynchronous or expensive
operations in response to changing data.
Meaning that here you would probably make an asynchronous request to your server or some other complex/expensive operation instead of just manipulating the data like we did with our computed property.
Hope this helps, I recommend reading the full documentation here.
At the end of the day a computed property is just a watcher, you can see this here:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
The important distinction is that computed properties are synchronous and must return a value.

In vue2 v-for nested component props aren't updated after element is removed in parent

For my app I'm using two Vue components. One that renders a list of "days" and one that renders for each "day" the list of "locations". So for example "day 1" can have the locations "Berlin", "London", "New York".
Everything gets rendered ok but after removing the "Day 1" from the list of days the view isn't rendered corrected. This is what happens:
The title of the day that was removed is replaced -> Correct
The content of the day that was removed isn't replaced -> Not correct
Vue.component('day-list', {
props: ['days'],
template: '<div><div v-for="(day, index) in dayItems">{{ day.name }} Remove day<location-list :locations="day.locations"></location-list><br/></div></div>',
data: function() {
return {
dayItems: this.days
}
},
methods: {
remove(index) {
this.dayItems.splice(index, 1);
}
}
});
Vue.component('location-list', {
props: ['locations', 'services'],
template: '<div><div v-for="(location, index) in locationItems">{{ location.name }} <a href="#" #click.prevent="remove(index)"</div></div>',
data: function() {
return {
locationItems: this.locations
}
},
methods: {
remove(index) {
this.locationItems.splice(index, 1);
}
}
});
const app = window.app = new Vue({
el: '#app',
data: function() {
return {
days: [
{
name: 'Day 1',
locations: [
{name: 'Berlin'},
{name: 'London'},
{name: 'New York'}
]
},
{
name: 'Day 2',
locations: [
{name: 'Moscow'},
{name: 'Seul'},
{name: 'Paris'}
]
}
]
}
},
methods: {}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>
<div id="app">
<day-list :days="days"></day-list>
</div>
Please use Vue-devtools if you are not already using it. It shows the problem clearly, as seen in the image below:
As you can see above, your day-list component comprises of all the days you have in the original list, with locations listed out directly. You need one more component in between, call it day-details, which will render the info for a particular day. You may have the location-list inside the day-details.
Here is the updated code which works:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.js"></script>
<div id="app">
<day-list :days="days"></day-list>
</div>
Vue.component('day-list', {
props: ['days'],
template: `
<div>
<day-details :day="day" v-for="(day, index) in days">
Remove day
</day-details>
</div>`,
methods: {
remove(index) {
this.days.splice(index, 1);
}
}
});
Vue.component('day-details', {
props: ['day'],
template: `
<div>
{{ day.name }}
<slot></slot>
<location-list :locations="day.locations"></location-list>
<br/>
</div>`
});
Vue.component('location-list', {
props: ['locations', 'services'],
template: `
<div>
<div v-for="(location, index) in locations">
{{ location.name }}
[x]
</div>
</div>
`,
methods: {
remove(index) {
this.locations.splice(index, 1);
}
}
});
const app = window.app = new Vue({
el: '#app',
data: function() {
return {
days: [{
name: 'Day 1',
locations: [{
name: 'Berlin'
}, {
name: 'London'
}, {
name: 'New York'
}]
}, {
name: 'Day 2',
locations: [{
name: 'Moscow'
}, {
name: 'Seul'
}, {
name: 'Paris'
}]
}]
}
},
methods: {}
});
One other thing - your template for location-list has an error - you are not closing the <a> element. You may use backtick operator to have multi-line templates as seen in the example above, to avoid template errors.
Also you are not supposed to change objects that are passed via props. It works here because you are passing objects which are passed by reference. But a string object getting modified in child component will result in this error:
[Vue warn]: Avoid mutating a prop directly...
If you ever get this error, you may use event mechanism as explained in the answer for this question: Delete a Vue child component

Passing data into a Vue template

I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent