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

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.

Related

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">

VueJS: Is it really bad to mutate a prop directly even if I want it to ovewrite its value everytime it re-renders?

The question says it all. As an example, think of a component that can send messages, but depending on where you call this component, you can send a predefined message or edit it. So you would end with something like this:
export default {
props: {
message: {
type: String,
default: ''
}
},
methods: {
send() { insert some nice sending logic here }
}
}
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
If I do this and try to edit the predefined message then Vue warns me to "Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders", but that's exactly the behaviour I'm searching for as the predefined message should return to being unedited if the user closes the component and opens it again.
I'm also not passing the prop to the father component, so the sending logic itself can be included in this same component.
It would still be considered bad practice? Why? How can I make it better? Thanks in advance!
A solution would be to assign the message you are passing as a prop to a variable in data and set this variable to the v-model instead.
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
<script>
export default {
data(){
return{ message:this.msg
}
},
props: {
msg: {
type: String,
default: ''
}
},
methods: {
send() { use a bus to send yout message to other component }
}
}
</script>
If you are not passing the data to another component or from a component, you shouldn't be using props, you should use Vue's data object and data binding. This is for any component data that stays within itself, the component's local state. This can be mutated by you as well so for our example I would do something like:
export default {
data: function () {
return {
message: '',
}
},
methods: {
send() {
// insert some nice sending logic here
// when done reset the data field
this.data.message = '';
}
}
}
<template>
<div>
<input>{{ message }}</input>
<button #click="send">Send</button>
</div>
</template>
More info on props vs data with Vue

Vue/Vuex Accessing Objects elements

Hi I am getting confused as to how I can access some data within my Object. I am using Vuex and I have a standard page. Here, I use a getter to obtain the Payment
Object and pass it to a component.
<template>
<div>
<div class="container">
<div class="row">
<div class="col-12 flex">
<payment-details :payment="payment"></payment-details>
</div>
</div>
</div>
</div>
</template>
<script>
import {
PaymentDetails
} from "#/components/Payment";
export default {
components: {
'payment-details': PaymentDetails
},
created () {
if (!this.payment.paymentID) {
this.$store.dispatch('paymentById', this.$route.params['id'])
}
},
computed: {
payment () {
return this.$store.getters.paymentById(this.$route.params['id'])
}
}
}
</script>
Now within the component, within the template, I can do something like this
<template>
<div v-if="payment">
<div class="row">
<div class="col-12">
<h3 class="h-100">{{ payment.details }}</h3>
</div>
</div>
</div>
</template>
This all works fine. However, within this component, I need to access some elements of the payment object. This is where I get confused, if I create a mounted or created hook and do this
created() {
console.log(this.payment)
}
I always get an Observer object. If I try accessing an element from this Object e.g.
created() {
console.log(this.payment.details)
}
I get undefined. I basically have a slideshow I am creating in a mounted hook. I need to push some items contained within this Object onto the slideshow array.
So how can I actually get access to the elements of this Object?
Thanks
You should use watcher on your vuex object.
Here is link to documentation https://v2.vuejs.org/v2/guide/computed.html#Computed-vs-Watched-Property
Most probably your this.payment.details is instantiated after your created method was called.
Move your code from created method to:
export default {
watch: {
payment: function (val) {
console.log('-------- this is this.payment.details:');
console.log(val.details);
},
...
Yes it will gave you of undefined because in your props you declare only a payment object alone not like this one below.
payment : {
details: '',
etc: ''
}
But it will still works when you use this payment data in your component, it's like it only gives you an error something like 'calling details on null' like that. I prefer to put condition first if payment has already data before you call in your component. Like below
<div v-if="payment.details">{{payment.details}}</div>

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

How am I suppose to get the value of a checkbox?

So I have the following element:
<input v-on:click="$emit('addPartId', $event)" v-bind:value="13209" name="add_13209" type="checkbox">
Which then calls the following method:
methods: {
....
addPartId(evnt) {
console.log(evnt);
}
},
In the parent container and is passed to the child:
<table-body
v-bind:items="items"
v-bind:columns="columns"
v-bind:sort-column="sortColumn"
v-bind:direction="direction"
#sort="sort"
#addPartId="addPartId"
>
</table-body>
The question I have, that I can't find on stack, is how do I register a click event so that when the checkbox is clicked I get the event object (I want the value, from v-bind:value, of the checkbox.
You should use event name which is the kebab-cased version, check Vue Guide: Custom Event,
As the guide says:
Unlike components and props, event names will never be used as
variable or property names in JavaScript, so there’s no reason to use
camelCase or PascalCase. Additionally, v-on event listeners inside DOM
templates will be automatically transformed to lowercase (due to
HTML’s case-insensitivity), so v-on:myEvent would become v-on:myevent
– making myEvent impossible to listen to.
For these reasons, we recommend you always use kebab-case for event
names.
Vue.component('my-checkbox', {
template: `
<input v-on:click="$emit('add-part-id', {'whole': $event, 'value':13209})" v-bind:value="13209" name="add_13209" type="checkbox">
`
})
Vue.component('my-another-checkbox', {
template: `
<input v-on:click="$emit('add-part-id', $event)" v-bind:value="13209" name="add_13209" type="checkbox">
`
})
new Vue({
el: '#emit-example-simple',
methods: {
getChecked1: function (ev) {
console.log('checkbox1', ev.value)
console.log('checkbox1', ev.whole.target.value)
},
getChecked2: function (ev) {
console.log('checkbox2', ev.target.value)
}
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="emit-example-simple">
First Example: <my-checkbox #add-part-id="getChecked1"></my-checkbox>
Second Example: <my-another-checkbox #add-part-id="getChecked2"></my-another-checkbox>
</div>