Is there a way to tell Vue to call a method only once when used as an expression?
Here's my code:
<div v-for="i in a.b.c.items">
<div :id="foo(i.value)"></div>
</div>
The way it is now, the foo() method will be executed any time anything on the model changes, not only items. Is there something in Vue that I can tell to evaluate this only once?
like this: <div :id.only-once="foo(i.value)"
Unfortunately that's only possible for certain events, e.g. in this question here. What you may want to consider instead is a computed property where you compute all of these values and return the array. This resulting array will be cached by Vue and will not be reevaluated until your items array is modified (and modified in such a way that Vue will detect the change).
An example:
Vue Setup
<script>
new Vue({
el: '. . .',
data: {
a: {b: {c: {items: [. . .]}}}
},
methods: {
foo: function(val) {
. . .
}
},
computed: {
itemsAfterFoo: function() {
var this_vue_instance = this;
var computed_items = [];
this_vue_instance.items.forEach(function(item) {
computed_items.push(this_vue_instance.foo(item.value));
});
return computed_items;
}
}
});
</script>
Template
<div v-for="(i, index) in a.b.c.items">
<div :id="itemsAfterFoo[index]"></div>
</div>
Or something to that effect.
More information on computed properties here: https://v2.vuejs.org/v2/guide/computed.html
Related
This question already has answers here:
What is the most efficient way to deep clone an object in JavaScript?
(67 answers)
Closed 12 days ago.
I have some form data which I share with children components through props. Now I want to clone the prop object and make it non-reactive. In my case I want the user to be able to modify the props value without actually changing the cloned value. The cloned value should only be there to show the user what the form was when editing. Below code shows this:
<template>
<div>
<div v-if="computedFormData">
original prop title: {{orgData.title}}
new title:
<input type="text" v-model="formData.title"/>
//changing data here will also change orgData.title
</div>
</div>
</template>
<script>
export default {
props: ['formData'],
data() {
return {
orgData: [],
}
},
computed: {
computedFormData: function () {
this.orgData = this.formData;
return this.orgData;
},
},
methods: {
},
}
</script>
I have tried with Object.freeze(testData); but it doesnt work, both testData and orgData are reactive. Note also that using mounted or created property does not render orgData so I'm forced to use the computed property.
Try copying the prop values with Object.assign. No more issue with reactivity since the new, assigned values are just the copy instead of the reference to the source.
If your data object is a lot more complex, I'd recommend deepmerge in place of Object.assign.
Vue.component('FormData', {
template: `
<div>
<div v-if="testData">
<p>Original prop title: <strong>{{orgData.title}}</strong></p>
<p>Cloned prop title:</p>
<input type="text" v-model="testData.title" />
</div>
</div>
`,
props: ['orgData'],
data() {
return {
testData: Object.assign({}, this.orgData)
}
}
});
const vm = new Vue({
el: '#app',
data() {
return {
dummyForm: {
title: 'Some title'
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form-data :org-data="dummyForm"></form-data>
</div>
Not entirely sure why but using Object.assign on a computed property did not work for me. I solved it by using a watch property for the props value:
watch:{
formData(){
this.orgData = Object.assign({}, this.formData)
}
},
Object.assign is merely a shallow copy. If you have a copy consists that of only primitive data types (string, number, bigint, boolean, undefined, symbol, and null) it's ok. It to remove its reactivity. But, if you have a copy that has reference types you can’t shallow clone it to remove its reactivity.
For depping clone you can use the JSON.parse(JSON.stringify()) pattern. But keep in mind that is going to work if your data consists of supported JSON data types.
props: ['orgData'],
data() {
return {
cloneOrgData: JSON.parse(JSON.stringify(this.orgData))
}
}
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>
<div id="app">
<input v-model="msg"/>
<p>{{ msg }}</p>
</div>
<script>
class A{
}
A.a = 1
new Vue({
el: '#app',
data: {
},
computed: {
msg: {
cache: false,
set: function(val){
A.a = val
},
get: function(){
return A.a
}
}
}
})
</script>
run on jsfiddle
How vue use getter setter on v-model? I tried use getter and setter on v-model, but it didn't work.
Your getters and setters are fine as is. (They're not strictly necessary in this example, since they're not doing anything to modify the user input, but I assume that's a simplification for the purposes of your question.)
There are two separate issues with your code:
the input field is outside the Vue root node, so the framework can't see it. [You corrected this in a late edit to the question.]
You're defining your data (A.a) outside of Vue, so the framework doesn't know to watch it for changes.
For the framework to be reactive to changes you must put the variable A in the data block of the component (and, if you really need an external variable, copy the updated value into it using the setter function).
new Vue({
el: '#app',
data: {
A: { a: 1 } // <-- your external variable, moved to where Vue can see it
},
computed: {
msg: {
set: function(val) {
this.A.a = val;
// If necessary, also copy val into an external variable here
},
get: function() {
return this.A.a
}
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<input v-model="msg" />
<p>{{ msg }}</p>
</div>
First of all, your input has to be inside the #app element. Yours is currently not even being watched by Vue instance.
<div id="app">
<input v-model="msg"/>
<p>{{ msg }}</p>
</div>
Also, your A.a = 1 doesn't do anything. If you console.log A's value you won't see a anywhere. Instantiate A and add a variable in it's constructor:
class A {
constructor(a) { this.a = a}
}
let myA = new A(0)
with Vue instance like this it will work:
new Vue({
el: '#app',
data: {
a: myA.a = 1
},
computed: {
msg: {
set: function(val) {
this.a = val
},
get: function() {
return this.a
}
}
}
})
However, I'd move class instantiation to data:
data() {
return {
a: new A(1).a
}
},
If you keep a outside of data your setter will work and update the value, but your getter will not since variables outside of Vue instance aren't being observed.
The code to implement a model in vue is simple as:
var v1 = new Vue({
el:'#vue1',
data:{
msg:'demo'
}
});
And the html as:
<div id='vue1'>
<input type='text' v-model='msg' />
<p>
{{msg}}
</p>
</div>
The first problem is the scope. Since in your Vue instance you are providing the element id as #app, all the vue related markup should be inside an element with id app, in your case the div.
Second, the way you save the data, once you use v-model directive, it directly observes the changes in your model and make changes to the dom accordingly. You do not need the getter and setter methods.
Lastly, what was the code about the class A??
Please look into the the javascript manuals because it is well outside the scope of this question to explain all of that part in detail.
Here is the updated fiddle
I also want to access add string in the template but it seems like not work.
please help me solve my problem here...
Vue.component('comp',{
template:`
<div id="test">
<div v-for="(item,index) in this.$root.items">
<p>{{this.$root.string}}</p>
<p>{{item}}</p>
</div>
</div>
`
})
var vm = new Vue({
el:'#test',
data:function(){
return {
'string':'xxx',
'items':['a','b','c']
}
}
})
http://jsfiddle.net/89kgnrvf/5/
change this.$root.items to $root.items
You don't have to prefix with this. when you're inside the template of your component.
So, you just have to replace this.$root.items by $root.items and this.$root.string by $root.string.
However, you need to prefix with this. if you're referencing the value inside your Vue object.
Example :
computed: {
exampleComputed: function () {
return this.string;
}
}
And the corrected JSFiddle : http://jsfiddle.net/89kgnrvf/19/
How can I get reference to the element that fired the method in Vue.js?
I have HTML like this:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" />
And in my Vue.js viewmodel I have a method:
dataFieldClass: function () {
// Here I need the element and get its ID
// Pseudo code
var elementId = $element.id;
}
I know that it's possible to get the element from event (v-on:click), but this is not an event, it's a simple method returning CSS class for the element according to few conditions of the viewmodel. It should be computable as well, but the problem is the same.
You can get the reference to your element in three ways
1. with Method Event Handlers (doc)
template:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" />
script:
dataFieldClass: function (e) {
const element = e.target;
}
2. with Inline Handlers (doc)
template:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass($event, otherArgument)" />
script:
dataFieldClass: function (e, otherArgument) {
const element = e.target;
}
3. with Refs (doc)
template:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" ref="el"/>
script:
dataFieldClass: function () {
const element = this.$refs.el;
}
Maybe you could use ref?
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" ref="el" />
And use it like this:
dataFieldClass: function () {
var elementId = this.$refs.el;
}
See documentation here: https://v2.vuejs.org/v2/api/#ref
What about using the ref pattern. Put ref="someName" in your DOM element, and access it in your method with this.$refs["someName"] (you can pass 'someName' as parameter to your method).
Note that's not a very good pattern except if for some reason you really need the DOM element. Otherwise just pass a relevant parameter to your method.
It's not a good method mainly because it has a major drawback: there is no $refs the first time the vue is rendered (because the element is not present yet). So you should force the vue to render twice.
If you have multiple elements inside a v-for loop, then this.$refs["someName"] becomes an array. You can get it to work with some adaptation, here is an example:
new Vue({
el: '#app',
data() {
return {
fields: [{
name: 'field1',
value: 'value1'
},
{
name: 'field2',
value: 'value2'
}
]
};
},
methods: {
dataFieldClass(index) {
if (!this.$refs.fields) {
// First render, the element is not there yet
return '';
} else {
// Here is the element
console.log(this.$refs.fields[index]);
}
}
},
mounted() {
// Force the instance to render a second time
this.$forceUpdate();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.js"></script>
<div id="app">
<label v-for="(field, index) in fields">
{{ field.name }}:
<input ref="fields" :value="field.value" v-bind:class="dataFieldClass(index)">
</label>
</div>
You can get the reference from DOM event object. "event.currentTarget" is the property that references the element where the event listener(vuejs method) assigned.
This is standard DOM specification, but you can also use this property in Vuejs.
dataFieldClass: function (event) {
var elementId = event.currentTarget.id;
}
A straightforward solution is to pass a reference to the element in the method to be called.
Here's what worked for me (a pretty basic example to help understand):
new Vue({
el: '#app',
data: {
msg: '',
},
methods: {
// in order to access the HTML element,
// add an argument (namely 'event') in the method definition,
// and access the element's current value by `event.target.value`
updateValue: function(event) {
this.msg = event.target.value;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input :value="msg" #input="updateValue" autofocus>
<br/>
<h2>
>>> {{ msg }}
</h2>
</div>
This seem to work for me, using ref (if element is nested another element)
<div ref="element">
vm.$refs.element
or $el if targeted element is the outermost
<template><div class="targeted-element">
this.$el
You can use refs as mentioned in other answers here.
Remember, refs cannot apply to computed objects. So be careful when using refs