I'm new with Vue, so I trying to create a Input Date Picker using Vuetify.
This is my code:
Codepen
The problem is, when I select a date in the picker, I get an error in console:
[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: '$value'
Is that the correct way to create that component?
As the error states, you cannot change a prop from a child. This is because Vue uses this parent - child flow:
With this structure, you assure that the parent's data is the only source of truth.
In order to change data, you need to emit it up to the parent. You almost had it in your example. Instead of emitting a change event, you need to emit input for v-model to sync the changes.
This is more clear when you see the example from the docs:
<input v-model="searchText">
Is indeed the same as:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
However, since vue 2.3.0, you can now do changes to props via the .sync modifier
Vue.component('date-picker', {
props: ['label'],
data: () => ({
date: null,
dateFormatted: null,
open: false
}),
created() {
this.date = this.$value;
},
methods: {
formatDate (date) {
if (!date) return null
const [year, month, day] = date.split('-')
return `${day}/${month}/${year}`
},
parseDate (date) {
if (!date) return null
const [day, month, year] = date.split('/')
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
},
update() {
this.$emit('input', this.dateFormatted);
}
},
watch: {
date (val) {
this.dateFormatted = this.formatDate(this.date)
},
dateFormatted(val) {
this.update();
}
},
template:`
<v-menu
ref="open"
:close-on-content-click="false"
v-model="open"
:nudge-right="40"
lazy
transition="scale-transition"
offset-y
full-width
>
<v-text-field
slot="activator"
v-model="dateFormatted"
:label="label"
placeholder="dia/mês/ano"
#blur="date = parseDate(dateFormatted)"
></v-text-field>
<v-date-picker v-model="date" no-title #input="open = false"></v-date-picker>
</v-menu>
`
});
Vue.config.productionTip = false;
Vue.config.devtools=false
new Vue({
el: '#app',
data: () => ({
myDate: '2018-09-01'
})
});
<link href="https://cdn.jsdelivr.net/npm/vuetify#1.1.4/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuetify/1.2.0/vuetify.min.js"></script>
<div id="app">
<v-app>
<v-flex xs24>
<date-picker label="Select a date" v-model="myDate"></date-picker>
<br>
Current date: {{myDate}}
</v-flex>
</v-app>
</div>
Related
I have a component called customer-type.
I do have the full year and I do have a lot of components as well. So I want to reduce rendering.
How can I load a component after click renderComponent?
<template v-for="(date, index) in daysOfYear">
<b-tr :key="date" :id="`row-${getDay(date)}`">
<customer-type :id="`customer-${index}`" #test="setCustomer" v-bind:listTypes="listTypes" />
<button #click="renderComponent(index)"> </button>
</b-tr>
</template>
methods: {
renderComponent(index) {
}
}
I don't want to render the component before I explicitly click on it.
You can modify the daysOfYear to be a list of objects, each having a boolean to show/hide its customer-type component using v-if.
Here is a simple demo:
const customertype = Vue.component('customertype', { template: '#customertype', props: ['id'] });
new Vue({
el:"#app",
components: { customertype },
data: () => ({
daysOfYear: ['01-01-2021','01-02-2021','01-03-2021']
}),
created() {
this.daysOfYear = this.daysOfYear.map(date => ({ date, showCustomerType:false }));
},
methods: {
renderComponent(index) {
this.daysOfYear[index].showCustomerType = true;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template id="customertype"><p>{{id}}</p></template>
<div id="app">
<div v-for="({ date, showCustomerType }, index) in daysOfYear" :key="index">
<button #click="renderComponent(index)">Show</button>
<customertype
v-if="showCustomerType"
:id="`customer-${index}`"
/>
</div>
</div>
#Majed is right. Basically, v-if will only render the element in the DOM when the condition is met. Another alternative is v-show basically it works the same way as v-if, the difference is that v-show will always render the element in the DOM and v-if won't
I have a CodePen demonstrating the issue at Checking validation of VTextField. The CodePen code is below as well.
Inside of the updateTitle method, I would like to be able to check to see whether or not the value is valid, because I need to run some custom logic when the value changes and what is executed will depend upon whether or not the value is valid.
I can see that v-text-field has a valid and validationState property. Is it possible to access either property inside of updateTitle?
HTML
<div id="app">
<v-app id="inspire">
<v-form>
<v-container>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
:value = "title"
:rules="[rules.required, rules.counter]"
label="Title"
counter
maxlength="20"
#input="updateTitle"
></v-text-field>
</v-col>
</v-row>
</v-container>
</v-form>
</v-app>
</div>
JS:
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
title: 'Preliminary report',
rules: {
required: value => !!value || 'Required.',
counter: value => value.length <= 10 || 'Max 10 characters'
}
}
},
methods: {
updateTitle( value ) {
console.log( "update", value );
}
}
})
The answer is to provide a ref to the v-text-field. This allows one to access the field via the special $refs property and call the validate function. The changes to the CodePen are:
<v-text-field
ref = "theField"
:value = "title"
:rules="[rules.required, rules.counter]"
label="Title"
counter
maxlength="20"
#input="updateTitle"
></v-text-field>
updateTitle( value ) {
const isValid = this.$refs.theField.validate();
console.log( "update", value, isValid );
}
I want to validate two textfields which are related to each other. The first one must be smaller than the second one (e.g. min/max, start date/end date).
So for the coding part I created this
HTML:
<div id="app">
<v-app id="inspire">
<v-text-field
v-model="values[0]"
:rules="firstValidation"
></v-text-field>
<v-text-field
v-model="values[1]"
:rules="secondValidation"
></v-text-field>
</v-app>
</div>
JS:
new Vue({
el: '#app',
data () {
return {
values: [1, 2]
}
},
computed: {
firstValidation: function () {
return [value => parseFloat(value) < this.values[1] || "Must be less than second value"]
},
secondValidation: function () {
return [value => parseFloat(value) > this.values[0] || "Must be greater than first value"]
}
}
})
I will also provide a snippet here
https://codepen.io/anon/pen/NZoaew?editors=1010
When I change the value of one field the other one will not revalidate. Steps to reproduce:
change the value of the first field to 12
the second field has a value of 2 so you will get an error
change the value of the second field to 22
now the form is valid but the first field still throws the error because it didn't revalidate.
remove one character from the first field
now this field revalidates and you can submit it. Is there a mechanism to revalidate the other field on changes and vice versa?
I think a possible solution would be to listen to the #input event of a field but how would you force the other field to revalidate then?
This will validate them both but only show an error in the field once the user has typed in that field:
new Vue({
el: '#app',
data () {
return {
values: [1, 2],
firstValidation: [true],
secondValidation: [true]
}
},
methods: {
validate: function (index) {
const valid = (this.values[0] - this.values[1]) < 0
if (valid) {
this.firstValidation = [true];
this.secondValidation = [true];
return;
}
if (index > 0)
this.secondValidation = ["Must be greater than first value"];
else
this.firstValidation = ["Must be less than second value"];
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#1.5.16/dist/vuetify.min.js"></script>
<div id="app">
<v-app id="inspire">
<v-text-field
v-model="values[0]"
#input="validate(0)"
:rules="firstValidation"
></v-text-field>
<v-text-field
v-model="values[1]"
#input="validate(1)"
:rules="secondValidation"
></v-text-field>
</v-app>
</div>
(I'm not sure why the styles aren't rendering but it uses the same scripts as your codepen)
Any reason why your data model is an array? Be mindful of this. From the docs
Due to limitations in JavaScript, Vue cannot detect the following
changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
i.e. when values[1] = parseFloat($event) is called, it is NOT reactive. I would avoid this if possible, otherwise use $set. Then to force validation of the field when another field changes, you will have to watch for the change and then manually call validation.
Template
<div id="app">
<v-form ref="form"> <!-- Need a reference to the form to validate -->
<v-app id="inspire">
<v-text-field
:value="values[0]"
:rules="firstValidation"
#input="$set(values, 0, parseFloat($event))"
></v-text-field>
<v-text-field
:value="values[1]"
:rules="secondValidation"
#input="$set(values, 1, parseFloat($event))"
></v-text-field>
<v-btn #click="submit">Submit</v-btn>
</v-app>
</form>
</div>
Code
Add this to your component
methods: {
submit: function(){
console.log(this.values);
},
validate: function() {
// manually call validation
this.$refs.form.validate();
}
},
watch: {
// watch for change and then validate
values: 'validate'
}
See updated working codepen
I'm trying to create a layout component which dynamically creates given components from a parent in either one or the other column. In the layout component I'm using the directive to create the children components. That works, however I'm having a hard time to get the data into the components themselves (they have different sets of properties).
Therefore:
How to inject property data into the < component :is="componentFromParent" / > directive?
I've got something to work, but it's pretty messy. I could do it via a "genericProps" property which contains all the data. Then forcing the children to update and they, via updated(), use Object.assign(this, 'props', this.genericProps) to unpack the genericProps and overwrite it's $props.
It feels pretty much as if I'm working against some princiles of vue.js here. When debugging the this.$options.components of the layout component are filled, however all these components don't have any propsData assigned. But directly using Object.assign() on the this.$options.components doesn't work, as it seems the layout component as the actually instances of these components as it's this.$childrend.
Also I've tried looping through the this.$childrend and assigning the propsData of the this.$options.components but there would have to be a key to match the components with it's correct child and at that point the childrend don't have any properties filled.
Code snippets to illustrate the (ugly) example which kinda works:
Parent Template
<template>
<two-column-layout :firstColumn="$vuetify.breakpoint.mdAndUp ? firstColumn : singleColumn"
:secondColumn="$vuetify.breakpoint.mdAndUp ? secondColumn : []"
/>
</template>
Parent Code
This code is called via mounted() or via watch when the async call from the API is finished.
createMetadataContent() {
let currentContent = this.currentMetadataContent;
const components = this.$options.components;
currentContent = this.mixinMethods_enhanceMetadataEntry(currentContent, this.cardBGImages);
if (currentContent && currentContent.title !== undefined) {
this.body = metaDataFactory.createBody(currentContent);
this.$set(components.MetadataBody, 'genericProps', this.body);
this.citation = metaDataFactory.createCitation(currentContent);
this.$set(components.MetadataCitation, 'genericProps', this.citation);
// a few more components and data here
this.firstColumn = [
components.MetadataBody,
// a few more components here
];
this.secondColumn = [
components.MetadataCitation,
// a few more components here
];
this.singleColumn = [
components.MetadataBody,
components.MetadataCitation,
// a few more components here
];
this.$forceUpdate();
}
}
TwoColumnLayout Template
<template>
<v-layout row wrap>
<v-flex>
<v-layout column>
<v-flex mb-2
v-for="(entry, index) in firstColumn"
:key="`left_${index}`"
>
<component :is="entry"
:genericProps="entry.genericProps"
/>
</v-flex>
</v-layout>
</v-flex>
<v-flex v-if="secondColumn" >
<v-layout column>
<v-flex mb-2
v-for="(entry, index) in secondColumn"
:key="`right_${index}`"
>
<component :is="entry"
:genericProps="entry.genericProps"
/>
</v-flex>
</v-layout>
</v-flex>
</v-layout>
</template>
TwoColumnLayout Code
updated() {
this.$children.forEach((child) => {
child.$forceUpdate();
});
},
Child Code
props: {
genericProps: Object,
id: String,
citationText: String,
citationXmlLink: String,
ciationIsoXmlLink: String,
ciationGCMDXmlLink: String,
fixedHeight: Boolean,
showPlaceholder: Boolean,
},
updated: function updated() {
if (this.genericProps) {
Object.assign(this, 'props', this.genericProps);
}
},
I once had a similar situation where I had a dynamic <compnonent :is="currentComponent"> and solved it by storing a 'childPayload'-attribute to my data and passed it as prop to the children. Whenever my payload changed, it got refreshed in the children.
<template>
<div>
<div class="btn btn-primary" #click="changePayload">Change Payload</div>
<component :is="isWhat" :payload="payload"></component>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
isWhat: 'div',
payload: { any: 'data', you: 'want', to: 'pass'}
}
},
methods: {
changePayload() { // any method or api call can change the data
this.payload = { some: 'changed', data: 'here' }
}
}
}
</script>
I could solve it via slot but I've still needed to use forceUpdate() in the LayoutComponent...
However with the genericProps it's good enough for my case.
Parent Template
<template v-slot:leftColumn >
<div v-for="(entry, index) in firstColumn" :key="`left_${index}`" >
<component :is="entry" :genericProps="entry.genericProps" />
</div>
</template>
<template v-slot:rightColumn>
<div v-for="(entry, index) in secondColumn" :key="`right_${index}`" >
<component :is="entry" :genericProps="entry.genericProps" />
</div>
</template>
Parent Code
This code is called via mounted() or via watch when the async call from the API is finished.
createMetadataContent() {
let currentContent = this.currentMetadataContent;
const components = this.$options.components;
currentContent = this.mixinMethods_enhanceMetadataEntry(currentContent, this.cardBGImages);
if (currentContent && currentContent.title !== undefined) {
this.body = metaDataFactory.createBody(currentContent);
this.$set(components.MetadataBody, 'genericProps', this.body);
this.citation = metaDataFactory.createCitation(currentContent);
this.$set(components.MetadataCitation, 'genericProps', this.citation);
// a few more components and data here
this.firstColumn = [
components.MetadataBody,
// a few more components here
];
this.secondColumn = [
components.MetadataCitation,
// a few more components here
];
this.singleColumn = [
components.MetadataBody,
components.MetadataCitation,
// a few more components here
];
this.$forceUpdate();
}
}
TwoColumnLayout Template
<v-flex v-bind="firstColWidth" >
<v-layout column>
<slot name="leftColumn" />
</v-layout>
</v-flex>
<v-flex v-if="secondColumn" v-bind="secondColWidth" >
<v-layout column>
<slot name="rightColumn" />
</v-layout>
</v-flex>
TwoColumnLayout Code
updated() {
this.$children.forEach((child) => {
child.$forceUpdate();
});
},
Child Code
All child components have the genericProps object which is accessed via mixin method
props: {
genericProps: Object,
},
computed: {
title() {
return this.mixinMethods_getGenericProp('title');
},
id() {
return this.mixinMethods_getGenericProp('id');
},
description() {
return this.mixinMethods_getGenericProp('description');
},
}
Mixin method
mixinMethods_getGenericProp(propName) {
return this.genericProps[propName] ? this.genericProps[propName] : null;
},
I am trying to set v-select value programmatically, however the associated #change event does not fire. If I manually select the option, the event fires as expected.
Template:
<div id="app">
<v-app dark>
<v-select
v-model="currentItem"
:items="items"
#change="itemChanged"
/>
</v-app>
</div>
JS:
Vue.use(Vuetify);
var vm = new Vue({
el: "#app",
methods: {
itemChanged(item) {
console.log('item changed',item);
}
},
data: {
currentItem: '',
items: [
{ text: "Name", value: 'name' },
{ text: "Number", value: 'number' },
{ text: "Inputs", value: 'inputs' }
]
},
mounted() {
setTimeout(() => {
this.currentItem = 'inputs';
}, 2000);
}
});
The jsfiddle is here
Does anybody have any solutions aside from manually calling the function when setting the value programmatically?
The change event is defined in such a way so it is emitted when the input is changed by user interaction (see the event documentation). If you want to monitor all the changes, you can either use the input event:
<v-select
v-model="currentItem"
:items="items"
#input="itemChanged"/>
https://jsfiddle.net/0qbomzta/
or add a watcher to watch the currentItem:
<div id="app">
<v-app dark>
<v-select
v-model="currentItem"
:items="items"/>
</v-app>
</div>
In Vue instance, add:
watch: {
currentItem (newVal) {
console.log('item changed', newVal);
}
},
https://jsfiddle.net/w5voub3h/