Vue: computed vs data(), for vuex bindings? - vue.js

(Note: I not am not asking about how to use watch).
I have this form template and want to bind it to some variables, for example objectvalue3, that are tracked in a Vuex store (note: different forms may be present on one page, so props.formname tells the form where to look).
<template>
<div>
Tracking formname_:{{formname_}}:
<form method="post" autocomplete="off">
<input type="text" name="objectvalue3" :value="objectvalue3" />
<input type="submit" class="btn btn-primary btn-success" value="Track" #click.prevent="write">
</form>
</div>
</template>
....
props: {
formname: {
type: String,
default: "main"
}
},
Tracking it in data does not work - i.e. the form does not get updated - it just keeps the value the vuex was initialized to :
data: function() {
return {
formname_: this.formname
,objectvalue3: this.$store.state.tracker[this.formname].objectvalue3
},
But using computed works.
computed: {
objectvalue3: function() {
return this.$store.state.tracker[this.formname].objectvalue3
}
I know I have to use computed when I need to do calculations. But there is not real calculation going on here. Unless, could it be the hash lookup via this.formname which tells the form which tracker attribute to look at that is causing a straight data to fail? Is this specific to vuex?

Try this instead:
data: function() {
return {
formname_: this.formname,
tracker: this.$store.state.tracker[this.formname]
},
Then:
<input type="text" name="objectvalue3" :value="tracker.objectvalue3" />
This should work because the data's tracker object is pointing to the one in the store, then whenever the value changes there, it'll also change in the tracker object.
computed works because it listens to changes in the variables used within it, while data doesn't because it applies the first value when executed and doesn't track changes.

Related

Vue.js 2: How to bind to a component method?

I have a VueJS (v2) component with a private array of objects this.private.messagesReceived which I want displayed in a textarea. The array should be converted to a string by a method/function and Vue is blocking all my attempts to bind. Every attempt results in my serialization function (converting the array to a string) only being called once and never again when the data changes.
I feel there must be a way to do this without Vue.set() or some forceUpdate shenanigans.
https://jsfiddle.net/hdme34ca/
Attempt 1: Computed Methods
Here we have the problem that Vue only calls my computed method messagesReceived1 once and never again.
<script>
{
computed: {
messagesReceived1() {
console.log("This is called once and never again even when new messages arrive");
return this.private.messagesReceived.join("\n");
},
...
methods: {
addMessage(m) {
console.log("This is called multiple times, adding messages successfully");
this.private.messagesReceived.push(m);
}
}
<script>
<template>
<textarea rows="10" cols="40" v-model="messagesReceived1"></textarea>
</template
Attempt 2: Binding Methods
Here Vue decides it doesn't like moustaches inside a textarea {{ messagesReceived2() }} and balks. It also doesn't allow messagesReceived2() or messagesReceived2 in v-model.
<script>
{
methods: {
messagesReceived2() {
return this.private.messagesReceived.join("\n");
},
addMessage(m) {
console.log("This is called multiple times, adding messages successfully");
this.private.messagesReceived.push(m);
}
}
</script>
<template>
<textarea rows="10" cols="40">{{ messagesReceived2() }}</textarea><!--Nope-->
<textarea rows="10" cols="40" v-model="messagesReceived2()"></textarea><!--Nope-->
<textarea rows="10" cols="40" v-model="messagesReceived2"></textarea><!--Nope-->
</template
You can define a data variable and set its value in the function. Then bind variable with textarea, not directly with the function.

Vue.js this.$refs empty due to v-if

I have a simple Vue component that displays an address, but converts into a form to edit the address if the user clicks a button. The address field is an autocomplete using Google Maps API. Because the field is hidden (actually nonexistent) half the time, I have to re-instantiate the autocomplete each time the field is shown.
<template>
<div>
<div v-if="editing">
<div><input ref="autocomplete" v-model="address"></div>
<button #click="save">Save</button>
</div>
<div v-else>
<p>{{ address }}</p>
<button #click="edit">Edit</button>
</div>
</div>
</template>
<script>
export default {
data() {
editing: false,
address: ""
},
methods: {
edit() {
this.editing = true;
this.initAutocomplete();
},
save() {
this.editing = false;
}
initAutocomplete() {
this.autocomplete = new google.maps.places.Autocomplete(this.$refs.autocomplete, {});
}
},
mounted() {
this.initAutocomplete();
}
}
I was getting errors that the autocomplete reference was not a valid HTMLInputElement, and when I did console.log(this.$refs) it only produced {} even though the input field was clearly present on screen. I then realized it was trying to reference a nonexistent field, so I then tried to confine the autocomplete init to only when the input field should be visible via v-if. Even with this, initAutocomplete() is still giving errors trying to reference a nonexistent field.
How can I ensure that the reference exists first?
Maybe a solution would be to use $nextTick which will wait for your DOM to rerender.
So your code would look like :
edit() {
this.editing = true;
this.$nextTick(() => { this.initAutocomplete(); });
},
Moreover if you try to use your this.initAutocomplete(); during mounting it cannot work since the $refs.autocomplete is not existing yet but I'm not sure you need it since your v-model is already empty.
I think it's because your "refs" is plural
<input refs="autocomplete" v-model="address">
It should be:
<input ref="autocomplete" v-model="address">

What is the correct way to retrieve data from 2 or more identical components?

Evening. I've created a button which adds a component that has an input field inside. I might need to press that button few times so there would be 2-3 input fields that appear. Whenever I type the text I would like to send a request from the parent component but I don't know how to retrieve the data from every child component that has been created. Is this the time to start using vuex (never used it)?
ParentComponent.vue
<template>
<div>
<button class="btn btn-success" #click="addStep">Add step</button>
<div v-for="i in count">
<recipe-step v-bind:step-number="i"></recipe-step>
</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
addStep() {
this.count += 1;
}
}
}
</script>
StepComponent.vue
<template>
<div>
<div class="from-group">
<label for="step-input"></label>
<input id="step-input" v-model="text" type="text">
</div>
</div>
</template>
<script>
export default {
props: {
stepNumber: {
type: Number,
required: true
}
},
data() {
return {
step: this.stepNumber,
text: ""
}
}
}
</script>
No, you really don't need Vuex yet. As long as you are still dealing with parent-child-component communication, you should be fine. Vuex comes into play when components, spread across the hole component hierarchy, need to exchange information.
Now, you should do something like this:
Don't store the text in the child component. When the input changes, send a Custom Event right to the parent component. Note that
<input v-model="text">
is only syntax sugar for
<input :value="text" #input="text = $event">
Both have the same effect. That's way you can send the input event up to the parent, like this:
<input #input="$emit('input', $event)">
Add another prop to your child component called value which should replace text.
And now you can use v-model in the parent component:
<recipe-step v-model="text">
To store multiple values, just use an array in your data properties.
<recipe-step v-model="textArray[i]">
Vuex can help you on that, however if all you want is to get the input text value back to the parent with the minimum effort you can create a prop called value in the children and then pass it as v-model in the parent.
Since you have a v-for you could make it iterate over a list instead a counter and then pass some prop inside each item as v-model

Why does the following Vue.js code not work when switching from computed to methods?

In the following code:
https://jsfiddle.net/willywg/2g7m5qy5/
the button is disabled based on the state of the checkbox.
But when I change:
computed: {
isDisabled: function(){
return !this.terms;
}
}
to
methods: {
isDisabled: function(){
return !this.terms;
}
}
the enabling / disabling no longer works.
Why is this?
According to the documentation:
https://v2.vuejs.org/v2/guide/computed.html
shouldn't both work the same in this example?
Computed properties look for changes but methods need to be called with something like #change or #click for example. In this case, you can actually just bind your data object without either.
<div id="app">
<p>
<label for='terms'>
<input id='terms' type='checkbox' v-model='terms' /> I accept terms!!!
</label>
</p>
<button :disabled='!terms'>Send Form</button>
</div>
new Vue({
el: '#app',
data: {
terms: false
}
})
Computed and Methods are two different thing
method: call when you wan to do sth, like call an action/mutation or do something other than dealing with store/state
computed: you can use returned value to bind sth in template html, some changing value that changes according to some computations. If it wont change then just bind it to a date/state
In your current script computed locating isDisabled data change all the time .
so whenever your data is changing its function triggering automatically.
But, if you want this in methods then you should trigger the function manually.
Example:
<input id='terms' type='checkbox' v-model='terms' #change="isDisabled" /> I accept terms!!!
it will call isDisabled method when it will change.
For more details Methods vs Computed. Thanks

Trying to create generic input component

I have a lot of forms to create in a web app I'm working on, for which I'm using Vue, so I've been trying to create a generic input component I can use throughout. I'm using Bootstrap grids, so the idea is that I should be able to pass the component a number of columns to take up, a label, a name and a property to use as the v-model. I'm kind of getting there, I think, but I'm running into a problem with mutating props - [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "model"
(found in component ).
Here's the template (in simplified form):
<template id="field">
<div v-bind:class="colsClass">
<div class='form-group form-group-sm'>
<label v-bind:for="name">{{labelText}}</label>
<input v-bind:id='name' ref="input" class='form-control' v-bind:name='name' v-model='model'/>
</div>
</div>
And here's the (again simplified) JS:
Vue.component('field', {
template: '#field',
props: ['cols','label','group','name','model'],
computed:{
colsClass:function(){
return "col-xs-"+this.cols
}
,
labelText:function(){
if(this.label) {
return this.label
} else {
return _.startCase(this.name);
}
}
}
});
This is used from within another 'edit-product' component, like this:
<field :cols="8" name="name" :model="product.name"></field>
This displays OK, but throws the error (or more accurately, warning), when I edit the value of the field. So what am I doing wrong?
Actually, the solution I've gone for is rather simpler than the one suggested above, very simple in fact, taken from https://forum-archive.vuejs.org/topic/4468/trying-to-understand-v-model-on-custom-components/9.
I don't want the 'model' prop, I have a 'value' one instead, so the JS is changed to this:
Vue.component('field', {
template: '#field',
props: ['cols','label','group','name','value'],
computed:{
colsClass:function(){
return "col-xs-"+this.cols
}
,
labelText:function(){
if(this.label) {
return this.label
} else {
return _.startCase(this.name);
}
}
}
});
The template becomes this:
<div class='form-group form-group-sm'>
<label :for="name">{{labelText}}</label>
<input :id='name' :name='name' class='form-control' :value="value" #input="$emit('input', $event.target.value)"/>
</div>
</div>
And I use it like this:
<field :cols="8" name="name" v-model="product.name"></field>
The difference is that I'm not actually trying to pass a model prop down, I'm just passing a value, and listening for changes to that value. It seems to work pretty well and is clean and simple enough. My next challenge is passing an arbitary set of attributes to the input, but that's the subject of another question.
As the warning suggests you should not directly edit the prop you are passing for the value.
Instead use this as the original value and set a seperate value on the input from it - which you can pass to the v-model. If you need the parent to have the current value then also pass a prop that will allow you to update the param on the parent, i.e.
input component
# script
props: [
'origValue',
'valueChange',
],
data: {
inputValue: '',
...
},
mounted () {
this.inputValue = this.origValue
},
watch: {
inputValue () {
this.valueChange(this.inputValue)
},
...
},
...
# template
<input type="text" v-model="inputValue">
parent
# script
data () {
return {
fieldValue: 'foo',
...
},
},
methods: {
updateField (value) {
this.fieldValue = value
},
...
},
...
# template
<field :value-change="updateField" :orig-value="fieldValue"></field>