Avoid mutating a prop directly in VueJS 2 - javascript

First of all, I am just starting playing with VueJS, so this cannot be a VueJS version thing as suggested here
It might be a duplicate of :
How to solve [Vue warn]: Avoid mutating a prop directly since the value will be overwritten on vue.js 2? - the difference is that I am only trying to set the values with v-bind, once.
Avoid mutating a prop directly since the value will be overwritten - This also looks somehow similar, but the solution didn't worked for me.
What's the correct to modify VUE component via javascript? - this solution looks pretty much what I have in my case
vuejs update parent data from child component
My problem starts with my Html looking like this:
<div id="app">
<div class="row">
<div class="form-group col-md-8 col-md-offset-2">
<birthday-controls
:birthDay="birthDay"
:birthMonth="birthMonth"
:birthYear="birthYear">
</birthday-controls>
</div>
</div>
</div>
and JS:
Vue.component('birthday-controls', {
template: `<div class="birthday">
<input type="text" name="year" placeholder="yyyy" v-model="birthYear" size="4" maxlength="4"/>
<input type="text" name="month" placeholder="mm" v-show="validYear" v-model="birthMonth" size="3" maxlength="2"/>
<input type="text" v-show="validYear && validMonth" name="day" placeholder="dd" v-model="birthDay" size="2" maxlength="2"/>
</div>`,
props: ['birthDay', 'birthMonth', 'birthYear'],
computed: {
validYear: function() {
return (this.birthYear > new Date().getFullYear()-100 && this.birthYear < new Date().getFullYear()-14)
},
validMonth: function() {
return (this.birthMonth > 0 && this.birthMonth <= 12)
},
validDay: function() {
return (this.birthDay > 0 && this.birthDay <=31) //I have to add more checking here for february, leap years and ....
}
}
});
new Vue({
el: '#app',
data: function() {
return {
birthDay: "",
birthMonth: "",
birthYear: ""
}
},
});
I have prepared codepen here: http://codepen.io/AngelinCalu/pen/OpXBay
However, the second answer from here: vuejs update parent data from child component makes me realise that I'm missing something
In that example it sets an this.$emit('increment') inside one of the methods, and triggers that on specific event.
In this other example: Update a child's data component to the father component in vue.js using .vue webpack(vue2) , the answer suggest adding a watch to emit the change.
watch: {
val() {
this.$emit('title-updated', this.val);
}
}
Now I'm even more confused! What is the right (or best) way to deal with this problem?
Note:
If I remove from the initial html :
:birthDay="birthDay"
:birthMonth="birthMonth"
:birthYear="birthYear"
It still works as expected, but I'm still getting that Vue warn, however, if I'm following the method from here: https://stackoverflow.com/a/41901150/2012740, it stops working, but I'm getting no error.
My Updated code: https://jsfiddle.net/angelin8r/647m7vdf/
To conclude: I need the functionality from the beginning but without the [Vue warn]
This is what I got in my initial example:
[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: "birthYear"

The warning is the result of setting v-model to the value of your properties. The reason is because if you change birthYear, birthMonth, or birthDay outside the component, then whatever the value is currently inside the component will be immediately overwritten.
Instead, capture a copy.
Vue.component('birthday-controls', {
template: `<div class="birthday">
<input type="text" name="year" placeholder="yyyy" v-model="internalBirthYear" size="4" maxlength="4"/>
<input type="text" name="month" placeholder="mm" v-show="validYear" v-model="internalBirthMonth" size="3" maxlength="2"/>
<input type="text" v-show="validYear && validMonth" name="day" placeholder="dd" v-model="internalBirthDay" size="2" maxlength="2"/>
</div>`,
props: ['birthDay', 'birthMonth', 'birthYear'],
data(){
return {
internalBirthDay: this.birthDay,
internalBirthMonth: this.birthMonth,
internalBirthYear: this.birthYear
}
},
computed: {
validYear: function() {
return (this.internalBirthYear > new Date().getFullYear()-100 && this.internalBirthYear < new Date().getFullYear()-14)
},
validMonth: function() {
return (this.internalBirthMonth > 0 && this.internalBirthMonth <= 12)
},
validDay: function() {
return (this.internalBirthDay > 0 && this.internalBirthDay <=31) //I have to add more checking here for february, leap years and ....
}
}
});
You did this almost exactly in your fiddle, but you did not correct your computed values.
computed: {
validYear: function() {
return (this.birthYear > new Date().getFullYear()-100 && this.birthYear < new Date().getFullYear()-14)
},
validMonth: function() {
return (this.birthMonth > 0 && this.birthMonth <= 12)
},
validDay: function() {
return (this.birthDay > 0 && this.birthDay <=31) //I have to add more checking here for february, leap years and stuff
}
},
should be
computed: {
validYear: function() {
return (this.var_birthYear > new Date().getFullYear()-100 && this.var_birthYear < new Date().getFullYear()-14)
},
validMonth: function() {
return (this.var_birthMonth > 0 && this.var_birthMonth <= 12)
},
validDay: function() {
return (this.var_birthDay > 0 && this.var_birthDay <=31) //I have to add more checking here for february, leap years and stuff
}
},

Related

Vue JS call function on v-model data() change

I want to call a function on data change through v-model
HTML Part:
<input
type="date"
name="date"
id="date"
v-model="inputDate"
#change="recallMeetingDetails"
/>
VueJS Part:
data(){
return(){
inputDate: new Date().toISOString().slice(0, 10),
}
}
methods: {
recallMeetingDetails(){
console.log(this.inputData);
}
}
Now this code works fine, but in the console, I am getting the following error:
[Vue warn]: You may have an infinite update loop in a component render function.
How can I do the functionality through any other method?
You can try like following snippet :
new Vue({
el: '#demo',
data(){
return {
inputDate: new Date().toISOString().slice(0, 10)
}
},
methods: {
recallMeetingDetails(date){
this.inputDate = new Date(date).toISOString().slice(0, 10)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<input
type="date"
name="date"
id="date"
:value='inputDate'
#input="recallMeetingDetails($event.target.value)"
/>
<h3>{{ inputDate }}</h3>
</div>
Using v-model is a great idea!
Use a watcher to watch the reactive data instead of #change on the input element, and call a function when the reactive variable changes: like this
<template>
<input
type="date"
name="date"
id="date"
v-model="inputDate"
/>
</template>
<script>
export default {
data() {
return {
inputDate: new Date().toISOString().slice(0, 10)
}
},
watch: {
inputDate(value) {
console.log(value)
}
}
}
</script>
v-model watches for the value and updates it in data, try to use v-bind:value="inputDate" instead of v-model
So I managed to find a solution, the issue was in a different function.
In data(), I had 2 variables, which I was altering in a different function.
data(){
return{
inputDate: new Date().toISOString().slice(0, 10),
topValue: 0,
heightValue: 78,
}
}
fnWithIssue(x,y){
this.topValue = x + this.topValue;
this.heightValue = y + this.heightValue;
return{
top: `${topValue}px`,
height: `${heightValue}px`,
}
}
Then in a template, I was passing the aforementioned return as Inline styling, the template was in turn inside a v-for, which caused the infinite loop
Instead I was able to fix the issue by removing the data's topValue and heightValue and just decalred them in the fnWithIssue(x,y)
fnWithIssue(x,y){
let topValue = x + topValue;
let heightValue = y + heightValue;
return{
top: `${topValue}px`,
height: `${heightValue}px`
}
}

Using get and set on a data property object - VueJS

From what I know VueJS has a way to use a getter and setter for their computed properties per this documentation on Computed property.
I have here the vue component where you can see the amount is an object and we have a group of persons from the vuex store.
data() {
return {
form: {
amounts: {},
},
};
},
mounted() {
const persons = this.$store.getters.getPersons()
persons.forEach((person) => {
this.$set(this.form.amounts, person.id, '0.00');
});
},
I made it so I can associate a person to the amount he has paid on the form by linked it using the ID and the payment. This is an example of what this.form.amounts should look like.
{'HKYDUPOK': 0.00},
{'RYYJPUKA': 0.00},
{'KZMUYTAK': 0.00}
Now by default, their values should be 0.00, on the input number field where they entered the amount, by default I applied them to v-model which looks like this:
<div v-for="person in persons">
<input
class="form-control"
v-model="form.amounts[person.id]"
type="number"
step=".01"
min="0"
required>
</input>
</div>
But here is the thing, when you open your code snippet on the browser, you notice that the input number field has the default value of 0.00 which acts as somewhat a placeholder. I wanted to remove the default value of 0.00 on the number input and have it instead to an empty input yet the underlying value of the amounts per person is not null but still 0.00 or 0. This is so that the form is clear of input when the user tries to input values on the input box instead of having to erase and replace 0.00 with an actual value (Hope this is clear). Now there is a possibility that on the total amount, there are at least 1 or more persons with an amount of 0. I wanted to make sure that an empty input number field does not result in null but instead, it's 0. Is this possible?
I tried checking the computed property getter and setter for this to change the default binding yet how do you map the form.amounts to match the amount to its corresponding person? On the Get, if the value is not more than 0.00 or 0, then return an empty value to the input field. Set is the bigger problem for it only accepts one parameter which is called newValue and would be hard to say pass the personId to map the amounts to the corresponding person. Is there a way to touch upon and manipulate the binding of a data property which is an object yet also change the default behavior on the model to return empty instead of 0.00? I hope my question is clear enough.
I assume this is a follow on from your previous question...
At this stage, you're best creating a component to represent your data input element.
Something like this (using a single-file component example)
<!-- number-input.vue -->
<template>
<input class="form-control" type="number"
step=".01" min="0"
:value="amount"
#input="updated"
required />
</template>
<script>
export default {
name: 'NumberInput',
props: {
value: Number
},
computed: {
amount () {
return this.value || ''
}
},
methods: {
updated ($event) {
this.$emit('input', parseFloat($event.target.value) || 0)
}
}
}
</script>
Then you can use it in your parent template
<div v-for="person in persons" :key="person.id">
<NumberInput v-model="form.amounts[person.id]" />
</div>
Just remember to import and use the component...
<script>
import NumberInput from 'number-input'
export default {
components: { NumberInput },
// etc
}
</script>
JSFiddle Demo
Also see https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

How do I pass input text using v-on:change to my vue method?

How can I pass the input value from my html to my vue method called checkEx
ist() ? I would like to retrieve that value within my checkExist() method. Could any advice how I can do this? I am still new to vue.
HTML:
<input type="text" class="form-control" placeholder="Email*" v-model="form.email" v-validate="'required|email'" v-on:change="checkExist">
VUE ELEMENT:
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
methods: {
checkExist:function(){
}
}
})
First you need to define form:{email:"", ...} in the data as well.
Pass $event inside checkExist() .
Something like this,
function callMe() {
var vm = new Vue({
el: '#root',
data: {
form:{email:""},
email:""
},
methods: {
checkExist(event){
this.email=event.target.value;
}
}
})
}
callMe();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.11/dist/vue.js"></script>
<div id='root'>
<input type="text" #input="checkExist($event)" class="form-control" placeholder="Email*" v-model="form.email">
<p>email: {{email}}</p>
</div>
The v-model should already bind that input event.
But you can pass the event to v-on:change like this:
v-on:change="event => checkExist(event)"
Check exist would need to accept the event as a parameter.
The value of the input will now be accessible inside the vue function via event.target.value.
checkExist: function(event){
let value = event.target.value
}
Simpler even more, you don't need to pass the entire event to your handler function.
#change="checkExist($event.target.value)"
.
checkExist: function(value){
}
We could do it in ES6 syntax without using 'v-model' as below
<input v-on:change="(event) => inputValue = event.target.value" :value="inputValue" type="text"/>

React.js unwantingly reusing data-reactid when rendering subclasses

I have have a Questionnaire object that renders several QuestionnaireOption subclasses. New QuestionnaireOption subclasses are rendered when the state changes in the parent Questionnaire object.
The QuestionnaireOption class maintains state if its "selected" or not.
The Issue: When I change the state in the parent class in order to render new "Option" nodes, the new nodes are assigned the same data-reactid, I expect the Option node to reset its internal state but it isn't assigned a new id and it has contains the wrong state (in this instance, selected is still set to true on a new object despite props being set with new data).
What can I do to work around this issue?
Here's the relevant code:
QuestionnaireOption = React.createClass({
getInitialState: function() {
return {selected: false}
},
handleClick: function(e) {
e.preventDefault();
this.setState({selected: !this.state.selected});
},
render: function() {
var fullClassName = "questionnaireOption " + (this.state.selected? "selected": "unselected");
return (
<div className='questionnaireOptionWrapper large-4 small-4 columns'>
<div className={fullClassName} onClick={this.handleClick}>
<div>{this.props.name}</div>
</div>
</div>
);
}
});
Questionnaire = React.createClass({
getInitialState: function() {
return {currentStage: 0}
},
saveOptionState: function() {
// dump option state into amber.js or localstorage
},
advanceWizard: function() {
this.saveOptionState();
this.setState({currentStage: this.state.currentStage + 1});
},
rewindWizard: function() {
this.saveOptionState();
this.setState({currentStage: this.state.currentStage - 1});
},
seeResults: function() {
console.log(globalOptionState);
},
render: function() {
var currentWizardQuestion = wizardQuestions[this.state.currentStage];
var currentOptionNodes = currentWizardQuestion.options.map(function(option) {
node = (
<QuestionnaireOption
name={option.name}
value={option.value}
/>
);
return node;
});
return (
<div className="questionnaire row">
<div className="questionnaire-question large-8 small-12 columns">
<div className="questionnaire-question-text">
{currentWizardQuestion.text}
</div>
<div className="questionnaire-question-subtext">
{currentWizardQuestion.subtext}
</div>
<div className="row">
{currentOptionNodes}
</div>
<input type="button" value="Back" onClick={this.rewindWizard}
style={this.state.currentStage == 0? {display: "none"}: {}
} />
<input type="button" value="Next" onClick={this.advanceWizard}
style={this.state.currentStage == wizardQuestions.length - 1?
{display: "none"}: {}
} />
<input type="button" value="Finish" onClick={this.seeResults}
style={this.state.currentStage < wizardQuestions.length - 1?
{display: "none"}: {}
} />
</div>
</div>
);
}
});
In your console you have this warning:
Each child in an array should have a unique "key" prop. Check the render method of App. See fb.me/react-warning-keys for more information.
If you don't, you're not using the development build: you should fix that.
React uses two things to determine if something is 'the same' between renders: the component class (e.g. QuestionnaireOption), and the key prop.
If either doesn't match the previous render, react considers it different, and the instance is recreated* and the subtree dom is discarded.
Assuming option.name can be used to determine equality, change your code to this:
var currentOptionNodes = currentWizardQuestion.options.map(function(option) {
var node = (
<QuestionnaireOption
name={option.name}
value={option.value}
key={option.name}
/>
);
return node;
});
For reference, reactid is an implementation detail, and may change or be removed at any time.
* if you just change the order of items, it'll try to just change the order for performance. There's currently a few cases where this doesn't happen, so it shouldn't be relied on.

translating between cents and dollars in html input in React

I'm in a bit of a weird situation, I am dealing with currency in my we app. On the model side, I am saving currency as cents before sending to the server as I don't want to deal with decimal points on the server side.
In the view however, I want the to display normal currency and not cents.
So, I have this input field where I take the data from dollars and change it to cents:
<input name="balance" type="number" step="0.01" min="0"
placeholder="Balance in cents" onChange={this.handleUpdate}
value={this.props.user.balance / 100} />
And when there's a change in the input value, I change it back to cents before sending it upstream:
handleUpdate: function(e) {
var value = e.target.value;
// changing it back from cents to dollars
value = parseFloat(value) * 100;
// save back to the parent component managing the prop
this.props.onUserUpdate(value);
}
This puts me in kind of a deadlock, there's no way for me to enter a decimal point "." Let me demonstrate :
33 in the input box --> becomes 3300 in the parent state --> goes back as 33 in component prop - all good
33.3 in the input box --> becomes 3330 in the parent state --> goes back as 33.3 in the component prop - all good
33. in the input box --> becomes 3300 in the parent state --> goes back as 33 in the component prop - this is the problem
As you can see in case #3, when the user first enters "." this doesn't translate back to the same number with "."
Since it's a controlled input, there's basically no way of writing "."
I have tried using uncontrolled element with defaultValue, but the amount prop is not ready the time the component is rendered so it's just empty
http://jsfiddle.net/fpbhu1hs/
Controlled inputs using derived values can be tricksy - if you need to be able to display invalid or otherwise weird input then...
always hold the input's value in its component's own state
<input value={this.state.value} onChange={this.handleUpdate} // rest as above...
derive the initial value in getInitialState()
getInitialState: function() {
return {value: this.props.user.balance / 100}
}
implement componentWillReceiveProps(nextProps) to detect when the prop's value is changing from above and re-derive the state value
componentWillReceiveProps: function(nextProps) {
if (this.props.user.balance != nextProps.user.balance) {
this.setState({value: nextProps.user.balance / 100})
}
}
Now when the user enters "33.", you store their literal input using setState(), then call back to the parent.
handleUpdate: function(e) {
var value = e.target.value
this.setState({value: value})
this.props.onUserUpdate(parseFloat(value) * 100)
}
If the value the parent then passes back down to the child via props hasn't changed (3300 == 3300 in this case), then componentWillReceiveProps() won't do anything.
Working snippet:
<script src="http://fb.me/react-with-addons-0.12.2.js"></script>
<script src="http://fb.me/JSXTransformer-0.12.2.js"></script>
<div id="example"></div>
<script type="text/jsx;harmony=true">void function() { 'use strict';
var Parent = React.createClass({
getInitialState() {
return {cents: 3300}
},
_changeValue() {
this.setState({cents: Math.round(Math.random() * 2000 + Math.random() * 2000)})
},
_onCentsChange(cents) {
this.setState({cents})
},
render() {
return <div>
<p><strong>Cents:</strong> {this.state.cents.toFixed(0)} <input type="button" onClick={this._changeValue} value="Change"/></p>
<Child cents={this.state.cents} onCentsChange={this._onCentsChange}/>
</div>
}
})
var Child = React.createClass({
getInitialState() {
return {dollars: this.props.cents / 100}
},
componentWillReceiveProps(nextProps) {
if (this.props.cents != nextProps.cents) {
this.setState({dollars: nextProps.cents / 100})
}
},
_onChange(e) {
var dollars = e.target.value
this.setState({dollars})
if (!isNaN(parseFloat(dollars)) && isFinite(dollars)) {
this.props.onCentsChange(parseFloat(dollars) * 100)
}
},
render() {
return <div>
<input type="number" step="0.01" min="0" value={this.state.dollars} onChange={this._onChange}/>
</div>
}
})
React.render(<Parent/>, document.querySelector('#example'))
}()</script>
I'm using this simple solution to handle controlled inputs and decimal values.
Create two props in your state, one to hold actual value and another to hold string.
constructor(props) {
....
this.state = {
myProperty: 1.42,
myPropertyString: '1.42'
}
}
Set your input value to String one
<input type="text"
onChange={this.handleUpdate}
value={this.state.myPropertyString}/>
In handleUpdate method update both state variables.
handleUpdate(e) {
this.setState({
myProperty: parseFloat(e.target.value),
myPropertyString: e.target.value
});
}

Categories

Resources