Vue + Vuex: format input data - vue.js

Inspired from several example, I'm trying to write a custom component that formats it's value with a specific method.
Here's the component:
<template>
<input
type="text"
v-model="inputValue"
/>
</template>
<script type="text/javascript">
import {formatPhoneNumber} from '~/utils/string';
export default {
computed: {
inputValue: {
get() {
return formatPhoneNumber(this.value)
},
set(value) {
this.$emit('input', formatPhoneNumber(value))
}
}
},
}
</script>
I'm using Vuex, and I call the component this way in the parent component:
<PhoneInput :value="cellPhoneNumber" class="input" #input="addCellPhoneNumber" />
computed: {
cellPhoneNumber() {
return this.$store.state.identity.cellPhoneNumber;
},
},
methods: {
addCellPhoneNumber: function(phoneNumber) {
this.$store.commit('identity/addCellPhoneNumber', phoneNumber)
},
}
The set part works, it goes to the store, but the data comes back to the component, cellPhoneNumber is called, but not inputValue#get.
Since it might be related to the fact that I use #input/:value in the parent component, I tried to use it also on it's child component:
<template>
<input
#input="formatValue"
type="text"
:value="formattedValue"
/>
</template>
<script type="text/javascript">
import {formatPhoneNumber} from '~/utils/string';
export default {
computed: {
formattedValue: function(){
return formatPhoneNumber(this.value)
},
},
methods: {
formatValue(e) {
this.$emit('input', formatPhoneNumber(e.target.value))
}
}
}
</script>
Without success, the same thing happens.
Can someone tell me what's going wrong?

As #ohgodwhy mentioned in the comments:
You're missing a prop definition in the component that expects this.value, so it's not reactive.

Related

VueJS 2.x Child-Component doesn't react to changed parent-property

I have the problem, that a component doesn't recognize the change of a property.
The component is nested about 5 levels deep. Every component above the faulty one does update with the same mechanics and flawlessly.
I invested some time to get to the problem, but I can't find it.
The flow is:
Dashboard (change value and pass as prop)
TicketPreview (Usage and
pass prop)
CommentSection (Pass prop)
CommentList (FAULTY / Usage of prop)
Everything down to the commentSection is being updated as expected, but the commentList doesn't get the update notification (beforeUpdate doesn't get triggered).
Since I tested quite a few things I will only post the essential code from commentSection (parent) and commenList (child)
DISCLAIMER: This is a prototype code without backend, therefore typical API-Requests are solved with the localStorage of the users browser.
commentSection
<template>
<div id="comment-section">
<p>{{selectedTicket.title}}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList,
},
props: {
selectedTicket: Object,
},
beforeUpdate() {
console.log("Comment Section");
console.log(this.selectedTicket);
},
updated() {
console.log("Comment Section is updated");
}
}
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem,
},
data() {
return {
comments: Array,
}
},
props: {
selectedTicket: Object,
},
methods: {
getComments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for(let i = 0; i < comments.length; i++){
if (comments[i].ticketId === this.selectedTicket.id){
filteredComments.push(comments[i]);
}
}
this.comments = filteredComments;
}
},
beforeUpdate() {
console.log("CommentList");
console.log(this.selectedTicket);
this.getComments();
},
mounted() {
this.$root.$on("updateComments", () => {
this.getComments();
});
console.log("CL Mounted");
},
}
</script>
The beforeUpdate() and updated() hooks from the commentList component are not being fired.
I guess I could work around it with an event passing the data, but for the sake of understanding, let's pretend it's not a viable option right now.
It would be better to use a watcher, this will be more simple.
Instead of method to set comments by filtering you can use computed property which is reactive and no need to watch for props updates.
CommentSection
<template>
<div id="comment-section">
<p>{{ selectedTicket.title }}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList
},
props: {
selectedTicket: Object
},
methods: {
updateTicket() {
console.log("Comment section is updated");
console.log(this.selectedTicket);
}
},
watch: {
selectedTicket: {
immediate: true,
handler: "updateTicket"
}
}
};
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem
},
props: {
selectedTicket: Object
},
computed: {
comments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for (let comment of comments) {
if (comment.ticketId == this.selectedTicket.id) {
filteredComments.push(comment);
}
}
// // using es6 Array.filter()
// let filteredComments = comments.filter(
// (comment) => comment.ticketId == this.selectedTicket.id
// );
return filteredComments;
}
}
};
</script>
I found the problem: Since commentList is only a wrapper that doesn't use any of the values from the prop, the hooks for beforeUpdate and updated are never triggered. The Vue Instance Chart is misleading in that regard. The diagram shows it like beforeUpdate would ALWAYS fire, when the data changed (then re-render, then updated), but beforeUpdate only fires if the Component and Parent has to be re-rendered.
The Object updates as expected, it just never triggered a re-render on the child component because the wrapper has not been re-rendered.

Get input values from child components in Vue

I would like to retrieve all input values from my child components (client and advice, seen below), but not sure how to proceed.
client.vue
<template>
<div id="client">
<input type="text" v-model="client.name" />
<input type="text" v-model="client.code" />
</div>
</template>
<script>
export default {
data() {
return {
client: {
name: '',
code: '',
}
}
}
}
</script>
advice.vue
<template>
<div id="advice">
<input type="text" v-model="advice.foo" />
<input type="text" v-model="advice.bar" />
<div v-for="index in 2" :key="index">
<input type="text" v-model="advice.amount[index]" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
advice: {
foo: '',
bar: '',
amount:[]
}
}
}
}
</script>
Each component has more fields than the above example.
My home page (parent) looks as simple as:
<template>
<form id="app" #submit="printForm">
<clientInfo />
<advice />
<input type="submit" value="Print" class="btn" />
</form>
</template>
<script>
import clientInfo from "#/components/clientInfo.vue";
import advice from "#/components/advice.vue";
export default {
components: {
clientInfo,
advice
},
methods: {
printForm() {}
}
}
</script>
My first idea was to $emit, but not sure how to do that efficiently with more than 20 fields without attaching a #emitMethod="parentEmitMethod" to every single field.
My second idea was to have a Vuex store (as seen below), but I don't know how to save all the states at once and not sure if I should.
new Vuex.Store({
state: {
client: {
name:'',
code:''
},
advice: {
foo:'',
bar:'',
amount:[]
}
}
})
You could use FormData to get the values of the form's <input>s or <textarea>s that have a name attribute (anonymous ones are ignored). This works even if the form has nested Vue components that contain <input>s.
export default {
methods: {
printForm(e) {
const form = e.target
const formData = new FormData(form) // get all named inputs in form
for (const [inputName, value] of formData) {
console.log({ inputName, value })
}
}
}
}
demo
You could use v-model with your custom components. Let's say you want to use them like this:
<advice v-model="adviceData"/>
For this, you would need to watch for value changes on your input elements inside your advice component and then emit an input event with the values. This will update the adviceData binded property. One generic way to do this could be including a watcher inside your advice component, like this:
export default {
data() {
return {
advice: {
foo: '',
bar: '',
amount:[]
}
}
},
watch: {
advice: {
handler: function(newValue, oldValue) {
this.$emit('input', newValue);
},
deep: true,
}
},
}
This way you will not have to add a handler for each input field. The deep option must be included if we need to detect changes on nested data in the advice object.
I think you can achieve what you want is when the user writes something using#change this will trigger a method when the input value is changed, you could use a button instead or anything you want, like this:
The child component
<input type="text" v-model="advice.amount[index]" #change="emitCurrStateToParent ()"/>
You gotta add #change="emitCurrStateToParent ()" in every input you have.
emitCurrStateToParent () {
this.$emit("emitCurrStateToParent", this.advice)
}
Then in you parent component
<child-component v-on:emitCurrStateToParent="reciveDataFromChild ($event)"></child-component>
reciveDataFromChild (recivedData) {
// Do something with the data
}
I would use a button instead of #change, like a "Save" button idk, the same goes for vuex, you can use the #change event
saveDataAdviceInStore () {
this.$store.commit("saveAdvice", this.advice)
}
Then in the store
mutations: {
saveAdvice (state, advice) {
state.advice = advice
}
}

vue.js wrapping components which have v-models

I have a 3rd party input component (a vuetify v-text-field).
For reasons of validation i prefer to wrap this component in my own.
my TextField.vue
<template>
<v-text-field
:label="label"
v-model="text"
#input="onInput"
#blur="onBlur"
:error-messages="this.getErrors(this.validation, this.errors)"
></v-text-field>
</template>
<script>
import VTextField from "vuetify/es5/components/VTextField";
import {vuelidateErrorsMixin} from '~/plugins/common.js';
export default {
name: "TextField",
props: ['label', 'value', 'validation', 'errors'],
mixins: [vuelidateErrorsMixin], //add vuelidate
data: function() {
return {
'text': this.value
}
},
components: {
VTextField
},
methods : {
onInput: function(value) {
this.$emit('input', value);
this.validation.$touch();
},
onBlur: function() {
this.validation.$touch();
}
},
watch: {
value: {
immediate: true,
handler: function (newValue) {
this.text = newValue
}
}
}
}
</script>
which is used in another component
<template>
...
<TextField v-model="personal.email" label="Email"
:validation="$v.personal.email" :errors="[]"/>
...
</template>
<script>
...imports etc.
export default { ...
data: function() {
return {
personal: {
email: '',
name: ''
}
}
},
components: [ TextField ]
}
</script>
This works fine but i wonder if there is a much more cleaner approach than to replicate the whole v-model approach again. As now my data is duplicated in 2 places + all the extra (non needed) event handling...
I just want to pass the reactive data directly through to the v-text-field from the original temlate. My TextField doesn't actually need access to that data at all - ONLY notified that the text has changed (done via the #input, #blur handlers). I do not wish to use VUEX as this has it's own problems dealing with input / forms...
Something more close to this...
<template>
<v-text-field
:label="label"
v-model="value" //?? SAME AS 'Mine'
#input="onNotify"
#blur="onNotify"
:error-messages="this.getErrors(this.validation, this.errors)"
></v-text-field>
</template>
<script>
import VTextField from "vuetify/es5/components/VTextField";
import {vuelidateErrorsMixin} from '~/plugins/common.js';
export default {
name: "TextField",
props: ['label', 'validation', 'errors'], //NO VALUE HERE as cannot use props...
mixins: [vuelidateErrorsMixin], //add vuelidate
components: {
VTextField
},
methods : {
onNotify: function() {
this.validation.$touch();
}
},
}
</script>
I cannot find anything that would do this.
Using props + v-model wrapping is what i do.
You need to forward the value prop down to the wrapped component, and forward the update event back up (see https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components for more details):
<template>
<wrapped-component
:value='value'
#input="update"
/>
</template>
<script>
import wrappedComponent from 'wrapped-component'
export default {
components: { 'wrapped-component': wrappedComponent },
props: ['value'],
methods: {
update(newValue) { this.$emit('input', newValue); }
}
}
</script>
Somewhere else:
<my-wrapping-component v-model='whatever'/>
I've create a mixin to simplify wrapping of a component.
You can see a sample here.
The mixin reuse the same pattern as you with "data" to pass the value and "watch" to update the value during a external change.
export default {
data: function() {
return {
dataValue: this.value
}
},
props: {
value: String
},
watch: {
value: {
immediate: true,
handler: function(newValue) {
this.dataValue = newValue
}
}
}
}
But on the wraping component, you can use "attrs" and "listeners" to passthrough all attributes and listener to your child component and override what you want.
<template>
<div>
<v-text-field
v-bind="$attrs"
solo
#blur="onBlur"
v-model="dataValue"
v-on="$listeners" />
</div>
</template>
<script>
import mixin from '../mixins/ComponentWrapper.js'
export default {
name: 'my-v-text-field',
mixins: [mixin],
methods: {
onBlur() {
console.log('onBlur')
}
}
}
</script>

Vue model not updating

When I try to update my custom text-area component's model data this.message='<span id="foo">bar</span> the text and html does not display in the htmltextarea tag like it should, but I can see the update applied in the Vue dev tool's console. I've also tried switching to an object instead of a string and using Vue.set, but this does not work either.
Any suggestions on how to fix this?
The goal with the htmlTextArea component is to get the users text from the htmlTextArea tag (this works), manipulate this text and bind it back to the textarea, but with HTML in it.
Custom text-area component:
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
// Custom textarea
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
}
</script>
Other component:
<template>
...
<htmlTextArea id="textarea" v-model="message"></htmlTextArea>
...
</template>
<script>
data: {
return {
message: 'something'//this works
}
}
...
methods: {
changeText() {
this.message='<span id="foo">bar</span>'//this does not
}
},
components: {
htmlTextArea
}
</script>
You need to set the value explicitly after the value props change. you can watch for value change.
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
// Custom textarea
name: "htmlTextArea",
props: ["value"],
mounted: function() {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v;
}
},
methods: {
updateHTML: function(e) {
this.$emit("input", e.target.innerHTML);
}
}
};
</script>
Change the data property into a function, as you have it defined it is not reactive.
data () {
return {
message: 'something'//this works
}
}
Now when you update the message property in your method, the component will update accordingly.
Reactivity in depth

Using v-model with a prop on VUE.JS

I'm trying to use a data coming from a prop with v-model, the following code works, but with a warning.
<template>
<div>
<b-form-input v-model="value" #change="postPost()"></b-form-input>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: {
value: String
},
methods: {
postPost() {
axios.put('/trajectory/inclination', {
body: this.value
})
.then(response => {
})
.catch(e => {
this.errors.push(e)
})
}
}
}
</script>
The warning says:
"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: "value"
So I changed and now I'm using a data as the warning says.
<template>
<div>
<b-form-input v-model="_value" #change="postPost()"></b-form-input>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: {
value: String
},
data() {
return {
_value: this.value
}
},
methods: {
postPost() {
axios.put('/trajectory/inclination', {
body: this._value
})
.then(response => {
})
.catch(e => {
this.errors.push(e)
})
}
}
}
So now the code it's not working and the warning says:
"Property or method "_value" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option"
Any idea how to fix the first code to suppress the warning? (or some idea on how to fix the second code?)
Obs.: b-form-input it's not my componente, this is the Textual Input from Boostrap-Vue (Doc for b-form-input)
Answer is from https://github.com/vuejs/vue/issues/7434
Props are read-only, but you are trying to change its value with v-model. In this case, if you change the input value, the prop is not modified and the value is restored on the next update.
Use a data property or a computed setter instead:
computed: {
propModel: {
get () { return this.prop },
set (value) { this.$emit('update:prop', value) },
},
},
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter
Bert addresses your direct issue, but I think you should also know that your approach is a bit off. Since ultimately you are sending the new value to postPost, you don't really need to modify your local copy. Use the event object that is sent to the change handler to get the current value from the input.
Instead of v-model, just use :value, and don't include the invocation parentheses when specifying the change handler.
<template>
<div>
<b-form-input :value="value" #change="postPost"></b-form-input>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: {
value: String
},
methods: {
postPost(event) {
axios.put('/trajectory/inclination', {
body: event.target.value
})
.then(response => {
})
.catch(e => {
this.errors.push(e)
})
}
}
}
</script>
_ prefixed properties are reserved for Vue's internal properties.
Properties that start with _ or $ will not be proxied on the Vue
instance because they may conflict with Vue’s internal properties and
API methods.
Try changing _value to something that doesn't start with an underscore.
One general workaround is to introduce a data-variable and watch the props to update-variable. This is quite subtle and so easy to get wrong so here's an example with a Vuetify modal using v-model (the same technique, in theory, should work with <input> and others):
<template>
<v-dialog v-model="isVisible" max-width="500px" persistent>
</v-dialog>
</template>
<script>
export default {
name: 'Blablabla',
props: {
visible: { type: Boolean, required: true }
},
data() {
isVisible: false
},
watch: {
// `visible(value) => this.isVisible = value` could work too
visible() {
this.isVisible = this.$props.visible
}
}
}
</script>
The official Vue docs shows how to use v-model on a custom component: https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
TL;DR:
You simply need to have a specifically named value prop, and emit an input event which the v-model when you instantiate the component maps for you.
More info on how this works on the link above.
<template>
<input
type="text"
:value="value"
#input="$emit('input', $event.target.value)"
/>
</template>
<script>
export default {
name: "Input",
props: {
value: String,
},
};
</script>
<Input v-model="searchText"></Input>
Point your input v-model directive to a data property named value_ (or any other name not starting with prefixes _ or $ which are reserved by Vue). Set the data property's default value to null. Then, add a method getValue() which will set property value_ based on your value prop's value. Finally, call getValue() in Vue's created() lifecycle hook. Like so:
<template>
<div>
<b-form-input v-model="value_" #change="postPost()">
</b-form-input>
</div>
</template>
<script>
import axios from 'axios';
export default {
data: () => ({
value_: null
}),
props: {
value: String
},
methods: {
postPost() {
axios.put('/trajectory/inclination', {
body: this.value_
})
.then(response => {
})
.catch(e => {
this.errors.push(e)
})
},
getValue() {
this.value_ = this.value;
}
},
created() {
this.getValue()
}
}
</script>
You can use a data like below.
<template>
<input type="text" v-bind:value="value" v-on:input="dValue= $event.target.value" />
</template>
<script>
export default {
props: ["value"],
data: function () {
return {
dValue: this.value,
};
},
methods: {
alertValue() {
alert("Current Value" + this.dValue);
},
},
};
</script>