Why `#change` trigger don't work for v-data-picker? - javascript

I use v-calendar package in my Vue.js application.
I want to send selected data range values to parent component. Why #change trigger don't work?
Parent.vue:
<template>
<div>
<Child #setRange="setRange" :range="range"/>
</div>
</template>
<script>
data() {
return {
range: this.range,
}
},
mounted() {
firstCallToPage();
},
methods: {
firstCallToPage(){
axios.get('URL').then(response => {
let self = this;
this.range = {
start: response.startDate,
end: response.endDate,
};
}
},
setRange(range_value) {
this.range = range_value;
}
}
</script>
Child.vue:
<v-date-picker class='v-date-picker'
mode='range'
v-model='rangeValue'
:show-day-popover=false
:max-date='new Date()'
show-caps
:input-props='{placeholder: "", readonly: true}'
#change="sendRange">
</v-date-picker>
props: {
range: {
type: Object,
},
},
data() {
return {
rangeValue: this.range
}
},
sendRange: function () {
this.$emit('setRange', this.rangeValue);
}
ERROR in console:
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: "range"

Try #input instead of #change. In v-datetime-picker works only with #input.

The error message is pretty explicit. The problem is that you give a prop to your child component (the one that contains v-date-picker) and you are overriding this prop with v-model (v-model is just syntactic sugar for :value and #change).
Derive your prop's value with a data value and use it for your operations:
<v-date-picker class='v-date-picker'
mode='range'
v-model='rangeValue'
:show-day-popover=false
:max-date='new Date()'
show-caps
:input-props='{placeholder: "", readonly: true}'
>
</v-date-picker>
props: {
range: {
type: Object,
},
},
data() {
return {
rangeValue: this.range
}
},
sendRange: function () {
this.$emit('setRange', this.rangeValue);
}

Instead of using a method, you can make use of watch...
Consider you have the following attributes in the range Object
range: {
start:value,
end: value
}
<v-date-picker class='v-date-picker'
mode='range'
v-model='rangeValue'
:show-day-popover=false
:max-date='new Date()'
show-caps
:input-props='{placeholder: "", readonly: true}'
>
</v-date-picker>
props: {
range: {
type: Object,
},
},
watch: {
'rangeValue.start': function(newVal){
this.$emit('setRange', newVal);
}
},
data() {
return {
rangeValue: this.range
}
}

Related

Make a reactive component with vuejs

I need a Vue component to show some HTML content in v-data-table from Vuetify. I have seen this post Vue 2 contentEditable with v-model, and I created a similar code shown below.
My problem is the component is not reactive. When I click the "Test button", no content is updated in HtmlTextArea.
<template>
<div>
<v-btn #click="doTest()">Test Button</v-btn>
<HtmlTextArea
v-model="content"
style="max-height:50px;overflow-y: scroll;"
></HtmlTextArea>
</div>
<template>
export default {
name: "ModelosAtestados",
components: { HtmlTextArea },
data: () => ({
content: "",
}),
methods: {
doTest() {
this.content = "kjsadlkjkasfdkjdsjkl";
},
},
};
//component
<template>
<div ref="editable" contenteditable="false" v-on="listeners"></div>
</template>
<script>
export default {
name: "HtmlTextArea",
props: {
value: {
type: String,
default: "",
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs.editable.innerHTML = this.value;
},
methods: {
onInput(e) {
this.$emit("input", e.target.innerHTML);
},
},
};
</script>
This occurs because HtmlTextArea sets the div contents to its value prop only in the mounted lifecycle hook, which is not reactive.
The fix is to setup a watcher on value, so that the div contents are updated to match whenever a change occurs:
// HtmlTextArea.vue
export default {
watch: {
value: {
handler(value) {
this.$refs.editable.innerHTML = value;
}
}
}
}
demo
In the #click event binder, you have to pass a function. You passed the result of an executed function.
To make it work: #click="doTest" or #click="() => doTest()".
How to debug such problems:
Display the value you want to update on your template to check if its updated: {{content}}
Use the vue devtool extension to check the current state of your components

Computed property was assigned to but it has no setter - a toggle component

I am creating a simple switch toggle component in Vue where it has a v-model and #updated. But I can't seem to change the model when the user toggles the switch. First I was getting the error to avoid mutating a prop directly. But now I am getting another error.
[Vue warn]: Computed property "isSwitchOn" was assigned to but it has
no setter.
The component is meant to be used like this
<iswitch v-model="switchGender" #updated="handleUpdatedGender" />
Here is the component itself
export default {
template: `
<span
#click="toggleSwitch"
:class="{ active: isSwitchOn }">
<span class="toggle-knob"></span>
</span>
`,
props: ['value'],
methods:
{
toggleSwitch()
{
this.isSwitchOn = !this.isSwitchOn
this.$emit('input', this.isSwitchOn)
this.$emit('updated')
}
},
computed:
{
isSwitchOn()
{
return this.value
}
},
};
The error is triggered by this statement: this.isSwitchOn = !this.isSwitchOn. You are trying to assign a value to a computed property but you didn't provide a setter.
You need to define your computed property as follow for it to work as a getter and a setter:
computed:
{
isSwitchOn:
{
get()
{
return this.value
},
set(value)
{
this.value = value
}
}
}
Also, it is not advised to mutate a prop directly. What you could do is to add a new data property and sync it with the value prop using a watcher.
I think something like this will work:
props: ['value'],
data()
{
return {
val: null
}
},
computed:
{
isSwitchOn:
{
get()
{
return this.val
},
set(value)
{
this.val = value
}
}
},
watch: {
value(newVal) {
this.val = newVal
}
}
Computed properties are by default getter-only, but you can also provide a setter when you need it. Check official documentation
computed:
{
isSwitchOn() {
get() { return this.value }
set(val) { this.value = val }
}
}
Alternative way:
In your parent component:
<iswitch ref="switcher" #input="methodForInput" v-model="switchGender" #updated="handleUpdatedGender" />
methods: {
methodForInput(event){
this.$refs.switcher.isSwitchOn = event;
}
}
In your child component:
export default {
template: `
<span
#click="toggleSwitch"
:class="{ active: isSwitchOn }">
<span class="toggle-knob"></span>
</span>
`,
data() {
return {
isSwitchOn: false
};
},
methods:
{
toggleSwitch()
{
this.isSwitchOn = !this.isSwitchOn
this.$emit('input', this.isSwitchOn)
this.$emit('updated')
}
}
};
Updates 3: Sorry, didn't include parent component at first.

Dynamic `v-model` for computed properties - based on route param

I am building a component that can be used to set various vuex properties, depending on the name passed in the route. Here is the naive gist of it:
<template>
<div>
<input v-model="this[$route.params.name]"/>
</div>
</template>
<script>
export default {
computed: {
foo: {
get(){ return this.$store.state.foo; },
set(value){ this.$store.commit('updateValue', {name:'foo', value}); }
},
bar: {
get(){ return this.$store.state.bar; },
set(value){ this.$store.commit('updateValue', {name:'bar', value}); }
},
}
}
</script>
Note that I pass this[$route.params.name] to the v-model, to make it dynamic. This works for setting (component loads fine), but when trying to set a value, I get this error:
Cannot set reactive property on undefined, null, or primitive value: null
I assume this is because this inside v-model becomes undefined (?)
How can I make this work?
UPDATE
I would also be curious to know why this does not work (compilation error):
<template>
<div>
<input v-model="getComputed()"/>
</div>
</template>
<script>
export default {
computed: {
foo: {
get(){ return this.$store.state.foo; },
set(value){ this.$store.commit('updateValue', {name:'foo', value}); }
},
bar: {
get(){ return this.$store.state.bar; },
set(value){ this.$store.commit('updateValue', {name:'bar', value}); }
},
},
methods: {
getComputed(){
return this[this.$route.params.name]
}
}
}
</script>
Yeah everything inside your <template> is in the this scope, so this is undefined.
v-model is just a syntactic sugar for :value and #input, so you can handle this with a custom event and a computed property for :value.
You can also use a computed property with a getter and setter; Something like
computed: {
model: {
get: function () {
return this.$store.state[this.$route.params.name]
},
set: function (value) {
this.$store.commit('updateValue', { name: this.$route.params.name, value})
}
}
}
Edit
If you have more logic to do in your setter, i'd separate it like so, keep the getter simple, and stick to one computed property;
computed: {
model: {
get: function () {
return this.$store.state[this.$route.params.name]
},
set: function (value) {
switch(this.$route.params.name) {
case 'foo':
return this.foo(value)
default:
return this.bar(value)
}
}
}
},
methods: {
foo(val) {
this.$store.commit(...)
},
bar(val) {
this.$store.commit(...)
}
}

Vue2: handling multi-child prop synchronization with models

I'm new to Vue (about a, and while reading the docs is very helpful it is often the case where I can not derive how I am supposed to achieve the desired behavior.
I have made several small components to get the feel for passing props and handling events, so now I am trying to makes something a bit larger but am facing some difficulty.
This difficulty stems from the following:
I would like to have a custom select component that are initialized via a v-for loop. All the while I would like to have access to these components selected option. I can bind the select data with v-model in the select component, but I am struggling to get that information out to the wrapper, yet alone the container looping over the wrappers.
Note: I am using Rollup and single file components
top-level
<template >
<div>
<select-container
v-for="(select, index) in selects"
:index="index"
:key="select.id"
:select.sync="select"
/>
</div>
</template>
<script>
import selectContainer from './select-container.vue';
export default {
components: { selectContainer },
props: {
records: {
default: function(){return{}},
type: Object
}
},
data: function() {
return {
selects: [{}, {}]
}
},
computed: {
},
methods: {
}
}
</script>
<style scoped>
</style>
select-container
<template>
<div>
<my-select
v-model.sync="select"
/>
</div>
</template>
<script>
import mySelect from './my-select.vue';
export default {
components: { mySelect },
props: {
index: { type: Number },
select: {
type: Object,
default: function(){return{}}
}
},
data : function(){
return {
}
},
methods: {
},
computed: {
}
}
</script>
<style scoped>
</style>
my-select
<template>
<select v-model="selected">
<option
v-for="(attr, index) in attributes"
:value="attr"
:selected="attr == selected"
>
{{attr}}
</option>
</select>
</template>
<script>
export default {
props: {
attributes: {
type: Array,
default: function() {
return []
}
}
},
data: function() {
return {
selected: ""
}
}
}
</script>
<style scoped>
</style>
There's still a lot of code in the post that I'm not following, but I think the gist of your question is how to reflect the selected property from the <my-select> component through to the <select-container> component. If that's the case, then the most straightforward approach is probably just to add a value and emit input events.
In the template, add an event handler for the native <select>
<select v-model="selected" #input="onInput">
Then, in the code, reflect that event up to the parent. Also be sure to accept a value property from that parent.
export default {
props: {
value: null,
},
data: function() {
return {
selected: this.value
}
},
methods: {
onInput() {
this.$emit("input", this.selected)
}
},
watch: {
value(newValue) {
this.selected = newValue;
}
}
}
And then the parent can simply use the conventional v-model binding.
<my-select v-model="select"/>

VueJS pass default prop without reference to child component

I've stumbled upon this situation where I want to pass a prop to a child component that will be the default value of the component, but it will only be showed when the initial value is empty.
Parent Component:
<multi-line-input v-model="data.something" placeholder="Enter Something" :default="data.something"/>
Child Component
props: {
value: {
type: String,
default: ''
},
default: {
type: String,
default: ''
},
},
methods: {
emitBlur (e) {
if (!this.value && this.default) {
this.value = this.default
}
this.$emit('blur')
},
emitInput () {
this.$emit('input', this.$el.value)
}
}
So what I am trying to achieve basically, is when the component loads will get the value from v-model it will also receive a default value that shouldn't change, and only used as a value when the actual value is empty on blur
The default will have the initial value of data.something and it should not change!
I tried to get rid of the reference using JSON.parse(JSON.stringify(this.value)) but it doesn't seem to work either!
So if I understand your question correctly, you want this behavior: upon the blur event on your <multi-line-input> component, if the value of the input is empty, then set the value to a default value which is specified by the parent (through a prop).
First of all, it is an error to do this.value = ... in your component. You must not modify props, props pass data from parent to child only, the data passed through props is not yours to modify directly from within the component.
Try something like this:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
default: {
type: String,
default: '',
},
},
methods: {
onBlur() {
if (!this.value && this.default) {
this.$emit('input', this.default);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
initialUser: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
// Make a copy of the data for the purpose of assigning the
// default prop of each input
this.initialUser = _.cloneDeep(this.user);
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name" :default="initialUser.name"></multi-line-input>
<multi-line-input v-model="user.email" :default="initialUser.email"></multi-line-input>
<multi-line-input v-model="user.address" :default="initialUser.address"></multi-line-input>
</template>
</div>
Or, if you want the default value to be determined by the component instead of the parent (through a prop), you can do something like this instead:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
},
created() {
this.def = this.value;
},
methods: {
onBlur() {
if (!this.value && this.def) {
this.$emit('input', this.def);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name"></multi-line-input>
<multi-line-input v-model="user.email"></multi-line-input>
<multi-line-input v-model="user.address"></multi-line-input>
</template>
</div>
However I do not recommend the second approach because the child component instance will only every have one default value for its entire lifetime. Vue reuses component instances whenever possible, so it wouldn't work if Vue were to bind it to a different parent component (how/when would it update its own default state?).

Categories

Resources