Vue.js prevent re-render? - vue.js

I have a template in Vue.js that looks like this:
<div class="filter">
<div class="checkbox" v-for="(value, index) in range" :key="index">
<span class="hotels">{{ count(index) }}</span>
</div><!-- /.checkbox -->
</div><<!-- /.filter -->
The count method is important here. It triggers twice and also seems to be the cause of the second trigger and re-render.
This is my method:
count(value) {
console.log('count');
// Get the filtered AJAX data.
let hotels = this.allHotels;
const filters = Object.entries(this.filters);
for (let i = 0; i < filters.length; i += 1) {
// Get the Object from the array(index 0 being the name).
const filter = filters[i][1];
// Break the v-model reference by creating a shallow copy.
const values = filter.values.slice(0);
// Merge any selected checkbox values with the one we are currently iterating.
if (!values.includes(value)) values.push(Number(value));
// Check if the current filter has selected checkboxes and filter if it does.
hotels = filter.function(hotels, values);
}
return hotels.length;
}
Whenever I everything except the console.log() it works fine. Whenever I leave in the rest of the code the entire template seems to re-trigger. This seems strange because I am leaving the data in the template and the computed properties that the template relies on alone.
Is there any way to debug what is causing the second re-render?
Entire component:
https://gist.github.com/stephan-v/a5fd0b6bae854276666536da445bbf86
Edit
My components count function has a computed property in it called allHotels. This property is being set as a prop.
On the very first console.log it seems empty and then straight away it will get set causing the re-render. I was hoping that because this prop is coming from the backend it would take it as the initial value but this is not the case.
Is there anything to overcome this and prevent a re-render? There is no need for a re-render if Vue would take the prop data from the backend as initial data straight away.
Hopefully it is clear what I mean by this.
Replication:
Somehow I can't seem to replicate this, I would expect this to also work like my code and show an empty title on the first pass with a console.log:
https://jsfiddle.net/cckLd9te/123/
Perhaps because my prop data that I am passing into my component is a pretty big json_encoded block it takes time for Vue.js to interpret the data?

Related

Changing value of outer variable used in props after emitted listener from component disables v-model listener effects in Vue.js

Considering following HTML code:
<div id="app">
<comp :is_checked="is_checked" v-on:ch="function(x){is_checked_2=x}"></comp>
<p>{{ is_checked_2 }}</p>
</div>
<script src="app.js"></script>
and app.js:
var tm = `<div>
<input type="checkbox" v-model="is_checked"
v-on:change="$emit('ch',is_checked)"
>{{ is_checked }}
</div>`
Vue.component('comp', {
template: tm,
props: ["is_checked"]
})
new Vue({
el: "#app",
data: function() {
return {
is_checked: null,
is_checked_2: null
};
}
});
If we replace is_checked_2=x by console.log(x) in v-on:ch="function(x){...}, everything works correct and v-model is changing is_checked when input checkbox value changes.
Also if we don't send the value of variable by props and define it locally in component, everything works correct.
It seems that changing the parent Vue's object variable is
regenerating the whole HTML of component where the value of variable
is sent by props and the variable is reset there immediately after
firing the event inside template. It is causing that functions
triggered by events don't change component's variables.
Is it a bug in Vue.js?
To make it more clear, the behavior is following: changing whatever Vue parent value that is not bound and however related to the component (no props, no slots) results at resetting all values in the component that are bound by props. Following only occurs if we reactivelly write to parent/main HTML using modified parent variable, we can achieve that by placing {{ .... }} there. This seems to be the bug.
Example: Vue has variables a and b. We place {{ a }} to the main code. The value of variable b is sent by props to component and matched to variable c. With the time we are changing that value of variable c inside the component. In one moment we decide to change value of a and it results by "forgetting" current state of c and it is reset to initial state by invoked props.
To summarize: the bug itself gets stuck that props should be reactivelly invoked only if corresponding parent variable is changed and not if whatever parent variable changes and at the same time they modify HTML code.
This issue doesn't have nothing to do with event listeners neither events, because it is possible to replicate it without using them.
Conclusion:
{{ is_checked_2 }} or {{ '',is_checked_2 }} or {{ '',console.log(is_checked_2) }} after changing value of is_checked_2 is causing rerendering the whole top Vue component having is_checked as variable and it is resetting child component variable, because it has the same name. The solution is never using the same name for child and parent component variables bound by props. This is issue of Vue/props architecture how it was designed.
I don't think it's a bug.
You're most likely running into a race condition where your change event gets executed before or in place of the internally attached one (hence the seemingly nullified data binding).
Because v-model is essentially just a syntactic sugar for updating data on user input events. Quoting the docs:
v-model internally uses different properties and emits different events for different input elements:
text and textarea elements use value property and input event;
checkboxes and radiobuttons use checked property and change event;
select fields use value as a prop and change as an event.
You might also want to see "Customizing Component v-model".

Vue - same mutation refreshes (or not!) components depending on which component it is called from?

I have problem understanding why THE SAME mutation fails to refresh data displayed in components (although it does change underlying vuex store data!) if it is called from one of the components, but it does refresh the data if called from another component?
I am updating Filter objects stored in store this way: state.report.filters[], where filters is array of Filter objects.
const state = {
report: {
filters: [], // array of Filter objects
...
}
}
My mutation looks for a filter in the array and substitutes the whole Filter object.
const mutations = {
setFilter: (state, newFilterValue) => {
let changedFilter = state.report.filters.find(filter => {
return filter.fieldName === newFilterValue.fieldName;
});
changedFilter = newFilterValue;
}
}
The mutation is called from a method of Filter class defined like this (separate module):
import { store } from './store';
export class Filter {
constructor ({
...
} = {}) {
this.operators = []; // Array of Operator objects
this.value = []; // Array of values - in this case Dates
};
updateOperator (operatorName) { // this mutation refreshes components when executed
this.operator[0] = new Operator(operatorName);
store.commit('setFilter', this); // whole object passed to the mutation
};
updateValue (newValue) { // this mutation changes store value, but fails to refresh components
this.value[0] = newValue; // newValue is a Date
store.commit('setFilter', this);
};
};
The app displays data in rows (each Filter has a separate row), each row contains cells, of which one contains components dedicated to Filter's value and Operator. These dedicated components receive as props callback functions which are methods of the Filter object. They execute the callback functions when a new value is entered passing the value to the Filter which then updates a relevant property and calls the mutation passing in both cases the whole Filter object as payload.
// TABLE CELL COMPONENT displaying filter value and operator
<template>
<td>
<operator-component
:iconName="proppedFilterObject.operator.iconName"
:callback="proppedFilterObject.updateOperator.bind(proppedFilterObject)"
></operator-component>
<value-component
:date="proppedFilterObject.value[0]"
:callback="proppedFilterObject.updateValue.bind(proppedFilterObject)"
></value-component>
</td>
</template>
<script>
export default {
props: ['proppedFilterObject'] // whole filter object
};
</script>
// OPERATOR COMPONENT
<template>
<div #click.stop="chooseOperator">
{{ iconName }} // some operator value display
</div>
</template>
<script>
export default {
methods: {
chooseOperator () {
const modal = new ChooseOperatorModal({
callback: this.callback // this displays another modal for receiving data. The modal calls the callback.
});
},
},
props: ['callback', 'iconName']
};
</script>
// VALUE COMPONENT
<template>
<date-picker v-model="computedDate"> // THIRD PARTY COMPONENT
</date-picker>
{{ date }} // additional display to verify if there's a problem within 'date-picker'
</template>
<script>
import DatePicker from 'vue2-datepicker'; // THIRD PARTY COMPONENT
export default {
components: { DatePicker },
computed: {
computedDate: {
get: function () {
return this.date;
},
set: function (newValue) {
this.callback(newValue);
}
}
},
props: ['callback', 'date']
};
</script>
So, if eg. I enter new operator value from Operator component, everything refreshes. When I enter a new value in the value component, the mutation is executed and store value changed, but displayed data are not refreshed. However, if afterwards I change an operator all the components will refresh and value will get displayed. Even if I change operator in a different Filter object(!). Ie:
a) Change in report.filters[0].value - display not refreshed, but...
b) then change report.filters[1].operator - both report.filters[1].operator AND PREVIOUSLY CHANGED report.filters[0].value get refreshed(?!).
What can be a reason of such behaviour? Where to look for the problem?
Some additional remarks:
1) I am using a third party component "vue2-date-picker" for date choice and display. However it does not seem to be responsible for the problem, as if I try to display the new value just in {{ }} notation it behaves the same way. I have used the date picker in other components and there it functions correctly as well.
2) In the code samples I left out most imports/exports and other seemingly irrelevant elements to keep the question reasonably short.
There are a lot of problems with the code and several of them are contributing to the problems you're seeing. A full, thorough answer that addresses all of these problems would be ridiculously long so instead I will skim through them without going into huge amounts of detail. You will need to do some further reading and experimentation to understand each of these topics properly.
Let's start with this line in the mutation:
changedFilter = newFilterValue;
This line assigns a new value to the local variable changedFilter. That's all. As it's the last line of the mutation the net result is that it doesn't really do anything.
Presumably your intent was to update the array state.report.filters, replacing the old entry with a new entry. However, just updating a local variable isn't going to do that.
At this point you may be wondering 'If that doesn't do anything, then why is the state in my store changing?'. I'll come to that in a moment but first let me prove to you that your existing code does nothing.
Try removing the code inside setFilter completely. Just leave an empty function. Then try clicking around in the UI just like you did before. You'll find that the store state updates just the same as it did before, even though you've removed the code to update the array.
The correct way to implement that mutation would be to use findIndex to find the relevant index and then use either Vue.set or the array's splice method to update the array accordingly. That will change the item in the array. However...
This brings us back to the earlier question. Why is the state updating if the mutation does nothing?
This is because you're using the same object in multiple places. The Filter object held in the array is the same object that your UI is editing. There are no copies being taken, there is just a single object. So when you change the properties of that object inside updateOperator or updateValue this will immediately be reflected inside the store. Calling the setFilter mutation is just asking the store to replace an object with itself.
There's nothing specific to Vue about this. This is just the standard behaviour of reference types in JavaScript. It is also common with many other programming languages that don't directly expose pointers. It can be useful to learn a little about how pointers work in other languages as it will give you a better initial mental model before attempting to understand how reference types behave in JavaScript. Understanding the difference between 'by value' and 'by reference' may also be a useful starting point.
The next topic to cover is reactivity, which very much is a Vue topic.
Specifically, there are certain changes that Vue can't detect. These are usually referred to as the reactivity caveats. You can find more about them in the official documentation:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
https://v2.vuejs.org/v2/guide/list.html#Caveats
There are at least two lines in your code that violate these rules:
this.operator[0] = new Operator(operatorName);
and
this.value[0] = newValue;
You can't set array entries directly by index. The array will update but it won't trigger any reactive dependencies within Vue. Instead you need to use either Vue.set or one of the array methods, e.g. push, pop, splice, etc.. In this example you could use splice.
e.g. Using Vue.set:
Vue.set(this.value, 0, newValue);
e.g. Using splice:
this.value.splice(0, 0, newValue);
Why does all of this matters?
Well Vue will only re-render a component if its reactive dependencies have changed. They are very similar to computed properties in that regard. Here's how it works...
Vue compiles the template down to a function. That function is referred to as the render function. When rendering a component Vue calls the render function and that function returns a description of how to render the component. Any reactive properties that are touched while that function is running will be recorded as dependencies. If, at some point in the future, the value of one of those reactive properties changes then Vue will rerun the render function to generate a new rendering of that component.
There are two key points to take out of this description:
If you fall foul of one of the reactivity caveats then Vue won't know the dependency has changed, so it won't re-render the component.
The render function runs as a whole. It doesn't just target a small chunk of the template, it always runs the whole thing.
So if you change a dependency in a non-reactive way (i.e. one of the caveats) it won't trigger a rendering update. But if you subsequently update a dependency properly, Vue will detect that and will rerun the render function. When it runs it will run the whole thing, so any new values will be picked up, even if they weren't detected when they changed.
It isn't immediately clear to me which rendering dependency is causing your component to re-render. However, it only needs one of them to change in a detectable manner. Any other changes will then get pulled in incidentally when the render function runs and reads their current values.
That covers why your code isn't working. However, I would also worry about your decision to introduce a Filter class. I understand how that may be appealing if you've come from some other OO environment but it isn't typically how Vue is used. It is possible to make it work but you will need a good understanding of both JavaScript reference types and the Vue reactivity system to avoid falling through the cracks. There is no reason why using a specific class to hold your data can't be made to work but in practice it usually ends up being less maintainable than not using such a class. A more typical Vue approach would be to use simple, anonymous objects/arrays to hold the data and then for the data owner (either a component or store module) to be responsible for making any mutations to that data. Events are used to pass changes up the component hierarchy rather than callback props.
Ultimately you will need to judge whether the Filter class is justified but it is probably not what future maintainers of your code will be expecting.

bind click event to child component using v-bind

I created a simple Minesweeper game and when it comes to the decision, which cell to render there are three possibilities:
Unrevealed cell
Revealed mine cell
Revealed neutral cell
I created a row component that renders all the cells contained by the row.
<template>
<div>
<component
v-for="(cell, columnIndex) in row"
:key="columnIndex"
v-bind="getCellProps(cell, columnIndex)"
:is="getComponentCell(cell)"
/>
</div>
</template>
<script>
// imports here
export default {
components: {
UnrevealedCell,
RevealedNeutralCell,
RevealedMineCell
},
props: {
row: Array,
rowIndex: Number
},
methods: {
getCellProps: function(cell, columnIndex) {
if(cell.revealed) {
if (cell.isMine) {
return {};
} else {
return {
mineNeighbours: cell.mineNeighbours
};
}
} else {
return {
unrevealedCell: cell,
x: columnIndex,
y: this.rowIndex,
cellClicked: this.onCellClicked
};
}
},
getComponentCell: function(cell) {
if(cell.revealed) {
if (cell.isMine) {
return RevealedMineCell;
} else {
return RevealedNeutralCell;
}
} else {
return UnrevealedCell;
}
},
onCellClicked: function(x, y) {
debugger;
}
}
}
</script>
Unfortunately my cellClicked event is not working. The child component is able to emit the event correctly but my onCellClicked doesn't get executed. I think this is because I can't write
cellClicked: this.onCellClicked
as it would normally be
#cellClicked
Without the # the attribute might get added as a component property. How can I fix this to listen to the emitted cellClicked event?
A few thoughts occur.
Firstly, the reason this isn't working is because v-bind is used to set component props and element attributes. The # prefix is a shorthand for v-on, so it isn't a prop or attribute in this sense, it's a directive in its own right. v-on does support an object version, just like v-bind, so you can do something like v-on="getCellEvents(cell, columnIndex)" and return a suitable object for each cell type. This is probably the cleanest direct answer to your original question. Less clean and less direct answers are also available...
You could implement this by making cellClicked a prop of the child cell and then calling it as a callback function rather than emitting an event. Not saying you should, but you could. That would work with the code you posted above completely unchanged.
Another alternative is just to add the event listener for all cells. Include #cellClicked="onCellCicked" in the template without worrying about the cell type. If the other cell types don't emit that event then nothing will happen. Vue doesn't know what events a component can fire, you can listen for anything.
Further thoughts...
Your cell template is a bit anaemic. I know people generally advise keeping logic out of the template but in your case I'd say you've probably taken it too far and it just makes things harder to understand. There are two ways you could address this:
Rewrite your component to use a render function instead. Templates exist because humans find them easier to read than render functions but in your case you've got all the logic in JavaScript anyway. The template isn't really adding anything and going all-in with a render function would probably be easier to understand than what you have currently.
Move the logic into the template. I don't see any obvious reason not to do it that way from the code you've posted. I'll post an example at the end.
Either of these two approaches would remove the problem you had adding an event listener.
A final thought on the click events is that you could use event propagation to handle them instead. Add a single click listener on a suitable element of the surrounding component and don't listen for events on the cells/rows at all. The single listener could then establish which cell was clicked (potentially fiddly) and whether anything needs to be done about it. While this would increase the coupling between the components I would imagine that it wouldn't really matter as these components aren't really reusable elsewhere anyway. I'm not recommending this as an approach at this stage but it is worth keeping in mind whenever you find yourself creating large numbers of repetitive components that all need the same events. In your scenario it would probably only make sense if you start to run into performance problems, and even then there will likely be better ways to fix such problems.
So, I promised an example of the template approach:
<template>
<div>
<template v-for="(cell, columnIndex) in row">
<unrevealed-cell
v-if="!cell.revealed"
:key="columnIndex"
:unrevealed-cell="cell"
:x="columnIndex"
:y="rowIndex"
#cellClicked="onCellClicked"
/>
<revealed-mine-cell
v-else-if="cell.mine"
/>
<revealed-neutral-cell
v-else
:mineNeighbours="cell.mineNeighbours"
/>
</template>
</div>
</template>
I'm not sure why the UnrevealedCell needs the x and y but if it's just so that it can emit them as part of the event then you might want to consider registering the listener as #cellClicked="onCellClicked(columnIndex, rowIndex)" and then there's no need to emit the co-ordinates from the cell. I also wonder whether you need 3 separate components for these cells. My gut reaction is that one component would be more appropriate with the row component not needing to have any understanding of the individual cells at all.

Combining v-for with v-show on same element in template

I want to display a list of entries, and I have it working up through retrieving JSON from a server, parsing it, storing it in a Vuex.Store and iterating through it with v-for-"entry in this.$store.state.entries".
When a user first visits the page all entries will be visible. The next step is to filter the entries so that only matching entries remain visible. Since this filtering will be changing a lot, I want to use v-show. I have a separate component that lets users enter search terms, the server is queried, and an array of numbers—matching IDs—is returned. I want to only show entries with IDs that match the numbers in the array, queriedEntries. My template is below:
<template>
<div id="entries">
<div v-for="entry in this.$store.state.entries"
v-html="entry.content"
v-show="this.$store.state.queriedEntries.includes(entry.id)">
</div>
</div>
</template>
I get an error that I don't understand, and searching for answers hasn't yielded anything because it doesn't match the problem others have had.
[Vue warn]: Error in render: "TypeError: this is undefined"
It's the this in the v-show, but every other this works. What's up?
Your problem is occurring because you are referencing this inside your template. This is not necessary.
The first thing I recommend you do is have a read into Vuex' Getters. Further down on the same page, you'll find information about mapGetters. This will help to prevent you from directly targeting/modifying data within your state. Modification of data should be left only to Mutations and Actions.
For example, your code may look like the below:
// in your component script
...
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
allEntries: 'entries', // map state.entries to the name 'allEntries'
queriedEntries, // your other state value. You may want to convert this to a getter
// other state values if necessary
})
}
}
...
// in your component template
<template>
<div id="entries">
<div v-for="entry in allEntries"
v-html="entry.content"
v-show="queriedEntries.includes(entry.id)">
</div>
</div>
</template>
...
Here you can see that we have used mapState which helpfully generates computed getter functions from our data in the store. We can then use the property name we have assigned it to within our template.
I ended up removing this from everything but the v-for, as suggested, and the code worked. Why this causes an error in v-show and v-html is still a mystery.
Final, working code:
<div v-for="(entry, entryindex) in this.$store.state.entries"
v-bind="{id:entryindex}"
v-bind:key="entryindex"
v-show="$store.state.queryMatchedEntries[0] == -1 || $store.state.queryMatchedEntries.indexOf(parseInt(entryindex)) != -1">

VueJS - Adjusting property from method does not update the view

I'm using the Vue Multiselect component, along with a list that displays suggested items.
I've basically got
selectedItems: []
allItems: []
onSelectOfItem(item) {
item.selected = true;
}
My HTML has the #select="onSelectOfItem" event which fires when multiselect picks an item, and it passes in the item.
I've also got a list of suggested items thats like
<li v-for="item in allItems" v-if="!item.selected"></li>
However when the onSelectOfItem method fires and I assign selected = true the v-for I have does not hide the item, however if I use the vue debug tools and inspect the data selected is set to false
If i add a #click="onSelectOfItem(item)" to the repeater, it works fine.
Id expect that changing the value on the object would update throughout, I'm from an Angular background which passes everything by reference, so no matter where i update item.selected from the whole UI will be in sync.
Why is the UI not reacting to the change within the object?? and how can I fix.
Thanks