Is there any way to speed up rendering time for one small change in a long list of elements? - vue.js

My example linked below shows a very large list of a grid of divs (1,000 x 20)
and when clicking on one div, it will highlight only that one element. However,
it seems there is significant overhead by VueJS in the rerendering which introduces a lag when clicking.
<div class="row" v-for="row in rows">
<div v-for="column in row.columns" :class="{red: isHighlighted(row,column)}" #click.prevent="setHighlighted({row: row.id, column: column.id})">
<div>Value: {{column['value']}}</div>
</div>
</div>
Code Pen Example

Sure, you can speed it up by not doing something that requires an evaluation on every update. In your case, the class setting has to call a function for every box every time row or column changes.
I added the style as a member of the column, so that on highlighting, it can be found directly, rather than requiring each cell to notice the change to highlighted. However, this still didn't remove the lag.
After working on this for a while, I surmised that the :class setting was being re-evaluated on every update, even if it was not a function call. My earlier solution handled class-setting explicitly, and that avoided the :class issue. This solution uses a component for each cell, which avoids re-calculation because components only re-evaluate when their props change.
const store = new Vuex.Store({
state: {
rows: [],
rowCount: 2000,
highlighted: {
row: null,
column: null
}
},
getters: {
rows(state) {
return state.rows;
},
rowCount(state) {
return state.rowCount;
},
highlighted(state) {
return state.highlighted;
}
},
mutations: {
setRows(state, rows) {
state.rows = rows;
},
setHighlighted(state, highlighted) {
state.highlighted = highlighted;
}
}
});
new Vue({
el: "#app",
store,
data() {
return {
highlightedEntry: null,
highlightedEl: null
};
},
created() {
this.setRows(
Array.from(Array(this.rowCount).keys()).map(i => {
return {
id: i,
columns: Array.from(Array(parseInt(20)).keys()).map(j => {
return {
id: j,
value: Math.random().toPrecision(4),
isHighlighted: false
};
})
};
})
);
},
computed: {
...Vuex.mapGetters(["rows", "rowCount", "highlighted"])
},
components: {
aCell: {
props: ['rowId', 'column'],
template: '<div :class="{red: column.isHighlighted}" #click="highlight">Value: {{column.value}}</div>',
computed: {
style() {
return this.column.style;
}
},
methods: {
highlight() {
this.$emit('highlight', this.rowId, this.column);
}
}
}
},
methods: {
...Vuex.mapMutations(["setRows", "setHighlighted"]),
highlight(rowId, column) {
if (this.highlightedEntry) {
this.highlightedEntry.isHighlighted = false;
}
this.highlightedEntry = column;
column.isHighlighted = true;
this.setHighlighted({
row: rowId,
column: column.id
});
}
}
});
.row {
display: flex;
justify-content: space-between;
}
.row>* {
border: 1px solid black;
}
.red {
background-color: red;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.js"></script>
<div id="app">
<div>
Cells: {{rowCount * 20}}
</div>
<div class="row" v-for="row in rows" :key="row.id">
<div v-for="column in row.columns">
<a-cell :row-id="row.id" :column="column" #highlight="highlight"></a-cell>
</div>
</div>
</div>

If each of your items has a unique property like an id, pass that to :key="item.id":
<div v-for="column in row.columns" :key="column.id" ...
When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values (using shorthand here):
<div v-for="item in items" :key="item.id">
<!-- content -->
</div>
It is recommended to provide a key with v-for whenever possible, unless the iterated DOM content is simple, or you are intentionally relying on the default behavior for performance gains.
Since it’s a generic mechanism for Vue to identify nodes, the key also has other uses that are not specifically tied to v-for, as we will see later in the guide.
https://v2.vuejs.org/v2/guide/list.html#key

Related

Vue v-model does not select value on checkbox

I'm fairly new to Vue and I've researched as much as I could, but cannot find a solution to this strange issue. I'm building a filter function for an online shop, and one section allows filtering based on values with a checkbox.
My vue template is as following:
<template>
<div>
<h3>{{data.filterLabel}}</h3>
<ul>
<li v-for="(item, index) in data.options" :key="index">
<input v-model="values" type="checkbox" :id="item" :value="item" :index="index" />
<label class="products__label products__capitalize" :for="item">{{ item }}</label>
</li>
</ul>
</div>
</template>
I am getting the options from a database, and loop through the data.options array with v-for. I have created a new empty array in
data() {
return {
values: []
};
},
as in the form-bindings example on the vue.js website here: https://v2.vuejs.org/v2/guide/forms.html#Checkbox
My script is as following:
<script>
export default {
name: "CheckBoxFilter",
data() {
return {
values: []
};
},
props: {
data: Object,
filterCheckBox: Function
},
watch: {
values: function(value) {
const optionRange = JSON.parse(JSON.stringify(this.values));
this.$emit("filterCheckBox", this.data.filterValue, optionRange);
}
}
};
</script>
For some strange reason, the $emit function works perfectly fine, and the array of products is filtered correctly in the UI. But when I check a value in the checkbox, the checkbox is not ticked. How is it possible that the checkbox is not ticked, while at the same time it is clearly correctly filtering the values?
I even looked at the :checked value with $event.target.checked which also correctly returns true or false, but the checkbox is still not ticked in the UI.
I have the same issue with radio buttons.
There are no issues with the <input type="text"> and also no issues with a <select>.
Has anyone experienced this before and if so what is the solution?
Thanks!
I tested and the UI displays the checked/unchecked checkboxes properly. Which version of Vue do you use? I'm not sure of what you want to do, but I think it would be cleaner to expose your values through a computed property:
export default {
name: "CheckBoxFilter",
props: {
data: Object,
},
data() {
return {
internalValues: [],
};
},
computed: {
values: {
get() {
return this.internalValues;
},
set(newVal) {
this.internalValues = newVal;
this.$emit("filterCheckBox", this.data.filterValue, [...newVal]);
},
},
},
};
</script>
With your current implementation, the values change are not observable and the filterCheckBox event is never emitted.
EDIT: I also don't understand why you set a filterCheckBox prop, it is not React ;)

Vue component communication

I'm looking for a concise example of two Vue components. The first component should contain a text input or textarea. The second component displays a character counter. I would like the first component to emit change events, and the second component should listen for those events and display its computed values (character count). I'm new to Vue and trying to wrap my head around the best way to implement this functionality. It seems rather straightforward in pure JavaScript but doing it the Vue way is not as clear to me. Thanks.
Here is how I'd do it in JavaScript:
Here's the textarea:
<textarea id="pagetext" name="pagetext"
onChange="characterCount();"
onKeyup="characterCount();">Type here</textarea>
Here's the JavaScript:
function characterCount()
{
var characters=document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML=characters+"";
}
My concern with Vue is passing the entire value around... for performance reasons this seems less than ideal. I may want my text editing Vue component to self-contain the value and emit the stats, ie the value for character count which would then be observed by a text stats component.
You can create a "Model" for value of textarea and provide this model to second component by using following way https://v2.vuejs.org/v2/guide/components-props.html
I've written up a snippet with four examples: your original, a simple Vue app (no components) that does the same thing, and two apps with two components that are coordinated by the parent.
The simple Vue app is actually more concise than the pure JavaScript app, and I think it shows off the reason for having a framework: your view doesn't act as a store for your program data, from which you have to pull it out.
In the final example, the parent still owns pageText, but passes it down to the my-textarea component. I like to hide the emitting behind the abstraction of a settable computed, so that the element can use v-model. Any changes are emitted up to the parent, which changes pageText, which propagates back down to the component.
I think your performance concerns fall into the realm of premature optimization, but it is possible not to use the text content as data at all, and only be concerned with the length. The fourth example does that. emitLength could have used event.target.value.length, but I wanted to use it in the mounted to initialize the length properly, so I used a ref.
function characterCount() {
var characters = document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML = characters + "";
}
new Vue({
el: '#app',
data: {
pageText: 'Type here'
}
});
new Vue({
el: '#app2',
data: {
pageText: 'Type here'
},
components: {
myTextarea: {
props: ['value'],
template: '<textarea name="pagetext" v-model="proxyValue"></textarea>',
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
new Vue({
el: '#app3',
data: {
textLength: null
},
components: {
myTextarea: {
template: '<textarea ref="ta" name="pagetext" #input="emitLength">Type here</textarea>',
methods: {
emitLength() {
this.$emit('change', this.$refs.ta.value.length);
}
},
mounted() {
this.emitLength();
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<form name="myForm">
<textarea id="pagetext" name="pagetext" onChange="characterCount();" onKeyup="characterCount();">Type here</textarea>
</form>
<div id="charcounter"></div>
<div id="app">
<h1>Vue (simple)</h1>
<form>
<textarea name="pagetext" v-model="pageText"></textarea>
</form>
<div>{{pageText.length}}</div>
</div>
<div id="app2">
<h1>Vue (with components)</h1>
<form>
<my-textarea v-model="pageText"></my-textarea>
</form>
<text-length :value="pageText.length"></text-length>
</div>
<div id="app3">
<h1>Vue emitting stats</h1>
<form>
<my-textarea #change="(v) => textLength=v"></my-textarea>
</form>
<text-length :value="textLength"></text-length>
</div>

How to keep carel onInput event in vue.js

I want to use contenteditable attribute of HTML for my component to make a area editable.
For this, I use #input directive but it does not work as I expect.
I expect the caret keeps the end of the entire input value but it moves to the head position.
Animation Gif Image
Demo
https://codesandbox.io/s/oj9p82kvp6
Code
<template>
<div>
<p contenteditable="true" #input="update">{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'ReadWrite',
data () {
return {
text: 'edit here!'
}
},
methods: {
update (e) {
this.text = e.target.textContent
}
}
}
</script>
<style scoped>
:read-write {
border: solid 1px #cccccc;
padding: 5px;
}
</style>
So upon checking your sandbox code.
Whenever you edit the text inside the contenteditable p tag. The cursor moves to the first position. Assuming you don't really need to always show the cursor at the bottom and just want to fix this quirk. It's 2018 yet there is not yet existing a neat way of handling this. My two cents to solve this is to use the focusout event.
You may use the focusout directive instead.
<template>
<div>
<p contenteditable="true" #focusout ="update">{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'ReadWrite',
data () {
return {
text: 'edit here!'
}
},
methods: {
update (e) {
this.text = e.target.textContent
console.log(this.text);
}
}
}
</script>
See it working here
https://codesandbox.io/s/0o9qow6zvp
Unless you need a two way binding here, this should do the work without a lot of nitty gritty codes just for something simple
Now the value will be bind to the text variable and the cursor will not move at the first position. Hence the jankiness will be gone.

2-way binding in Vue 2.3 component

I understand the .sync modifier returned in Vue 2.3, and am using it for a simple child component which implements a 'multiple-choice' question and answer. The parent component calls the child like this:
<question
:stem="What is your favourite colour?"
:options="['Blue', 'No, wait, aaaaargh!']
:answer.sync="userChoice"
>
The parent has a string data element userChoice to store the result from the child component. The child presents the question and radio buttons for the options. The essential bits of the child look like this (I'm using Quasar, hence q-radio):
<template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<label >
<q-radio v-model="option" :val="opt.val" #input="handleInput"></q-radio>
{{opt.text}}
</label>
</div>
</div>
</template>
export default {
props: {
stem: String,
options: Array,
answer: String
},
data: () => ({
option: null
}),
methods: {
handleInput () {
this.$emit('update:answer', this.option)
}
}
}
This is all working fine, apart from the fact that if the parent then changes the value of userChoice due to something else happening in the app, the child doesn't update the radio buttons. I had to include this watch in the child:
watch: {
answer () {
this.option = this.answer
}
}
But it feels a little redundant, and I was worried that emitting the event to update the parent's data would in fact cause the child 'watch' event to also fire. In this case it would have no effect other than wasting a few cycles, but if it was logging or counting anything, that would be a false positive...
Maybe that is the correct solution for true 2-way binding (i.e. dynamic Parent → Child, as well as Child → Parent). Did I miss something about how to connect the 'in' and 'out' data on both sides?
In case you're wondering, the most common case of the parent wanting to change 'userChoice' would be in response to a 'Clear Answers' button which would set userChoice back to an empty string. That should have the effect of 'unsetting' all the radio buttons.
Your construction had some oddities that didn't work, but basically answer.sync works if you propagate it down to the q-radio component where the changing happens. Changing the answer in the parent is handled properly, but to clear values, it seems you need to set it to an object rather than null (I think this is because it needs to be assignable).
Update
Your setup of options is a notable thing that didn't work.
I use answer in the q-radio to control its checked state (v-model has special behavior in a radio, which is why I use value in conjunction with v-model). From your comment, it looks like q-radio wants to have a value it can set. You ought to be able to do that with a computed based on answer, which you would use instead of your option data item: the get returns answer, and the set does the emit. I have updated my snippet to use the val prop for q-radio plus the computed I describe. The proxyAnswer emits an update event, which is what the .sync modifier wants. I also implemented q-radio using a proxy computed, but that's just to get the behavior that should already be baked-into your q-radio.
(What I describe is effectively what you're doing with a data item and a watcher, but a computed is a nicer way to encapsulate that).
new Vue({
el: '#app',
data: {
userChoice: null,
options: ['Blue', 'No, wait, aaaaargh!'].map(v => ({
value: v,
text: v
}))
},
components: {
question: {
props: {
stem: String,
options: Array,
answer: String
},
computed: {
proxyAnswer: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('update:answer', newValue);
}
}
},
components: {
qRadio: {
props: ['value', 'val'],
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
}
}
}
},
methods: {
clearSelection() {
this.userChoice = {};
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<question stem="What is your favourite colour?" :options="options" :answer.sync="userChoice" inline-template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<div>Answer={{answer && answer.text}}, option={{opt.text}}</div>
<label>
<q-radio :val="opt" v-model="proxyAnswer" inline-template>
<input type="radio" :value="val" v-model="proxyValue">
</q-radio>
{{opt.text}}
</label>
</div>
</div>
</question>
<button #click="clearSelection">Clear</button>
</div>

how to expose property on component in vue.js 2.0

I am thinking how to implement validation on vue.js component.
The initial idea is that component validates and return an error code, like: "require", "min", "max" etc. Another component will display full text message according to this error code.
Because error message might not always display inside component's template. I need two separated components.
pseudo code is like this.
<div>
<mycomponent ref="salary" errcode="ErrorCode"></mycomponent>
</div>
.....
<div>
<errormessage watchcomponent="salary" message="salaryErrorMessages"></errormessage>
</div>
salaryErrorMessages is a hash of codes and messages. for example:
{"require":"please enter salary",
"min": "minimum salary value is 10000"}
Vue has ref attribute on component, . I don't know if I can use ref to reference a component in attribute.
Other solutions I considered:
add an error object in model of the page, and pass into using :sync binding. can monitor same object.
This requires to declare error messages in model.
If I consider the requirement that page also needs to know if there is an error before post back. a global error object might be necessary.
use event bus or Vuex.
This seems official solution, but I don't know .
When a page has multiple instances of , they will trigger same event. all instances will monitor same event.
You should definitely use Vuex. Not only it solves your problem, but it gives you huge scalability in your project.
You're working literally backwards. Vue is about passing data from parent to child, not exposing child properties to parent components.
add an error object in model of the page, and pass into using :sync binding. can monitor same object. This requires to declare error messages in model.
sync is gone from Vue v2, but the idea is fundamentally sort of correct: a parent component holds one object, child components get passed chunks of it as props, parent object automagically gets updated in the parent when it is changed in a child. It doesn't have to be the root component.
use event bus or Vuex. This seems official solution, but I don't know .
Vuex is pretty much always the Correct solution if you have an application that needs to manage a lot of state-related shenanigans.
When a page has multiple instances of , they will trigger same event. all instances will monitor same event.
Vue will very frequently pollute your console with warnings that data must be a function. This is why!
Here is an abridged version really shameful hackjob I inflicted on innocen made for an acquaintance recently:
In my defense, putting things in __proto__ is a very quick way to make them non-enumerable.
Vue.component('error', {
template: '#error',
props: ['condition', 'errorMessage']
})
Vue.component('comp', {
template: '#comp',
props: ['errorMessage', 'model'],
})
Vue.component('app', {
template: '#app',
data() {
return {
models: {
nameForm: {
firstName: '',
lastName: '',
__proto__: {
validator: function(value) {
return _(value).every(x => x.length > 2)
}
}
},
otherForm: {
notVeryInterestingField: 'There are words here',
veryImportantField: 0,
__proto__: {
validator: function(value) {
return value.veryImportantField > 20
}
}
},
__proto__: {
validator: function(value) {
return _(value).every(x => x.validator(x))
}
}
}
}
}
})
const vm = new Vue({
el: '#root'
})
.error {
background: orange
}
.alright {
background: mediumaquamarine
}
section > div, pre {
padding: 6px;
}
section {
flex: 1;
}
#root {
display: flex;
flex-direction: column;
}
<script src="https://unpkg.com/vue#2.1.6/dist/vue"></script>
<script src="https://unpkg.com/lodash"></script>
<template id="comp">
<div :class="[model.validator(model) ? 'alright' : 'error']" style="display: flex; border-bottom: 2px solid rgba(0,0,0,0.4)">
<section>
<div v-for="(field, fieldName) in model">
{{_.startCase(fieldName)}}:
<input v-model="model[fieldName]">
<br>
</div>
<error :condition="!model.validator(model)" :error-message="errorMessage"></error>
</section>
<section>
<pre>props: {{$options.propsData}}</pre>
</section>
</div>
</template>
<template id="error">
<div v-if="condition">
{{ errorMessage || 'Oh no! An error!' }}
</div>
</template>
<template id="app">
<div :class="[models.validator(models) ? 'alright' : 'error']">
<comp :model="model" error-message="Mistakes were made." v-for="model in models"></comp>
<pre>data: {{$data}}</pre>
</div>
</template>
<div id="root">
<app></app>
</div>