Consider the <input>
element. It maintains its own internal state for its value (by the browser) irrespective of whether or not it is being driven by v-model
.
After looking at some Vue component libraries, most of the components externalize their state through props, meaning that the parent component must define the value for that component in its data and use v-model
to wire it up -- the component cannot be used without v-model
because it doesn't maintain the state internally to the component.
Ideally I'd like to author input components that satisfy the following requirements:
v-model
(the component maintains the state itself)v-model
(the state is driven entirely by the prop emitting input
events)This results in somewhat messy code like this:
Vue.component('checkbox', {
template: '<div class="checkbox" @click="toggle">{{ internalValue ? "Checked" : "Unchecked" }}</div>',
props: {
value: {
type: Boolean,
default: false,
},
},
data() {
return {
internalValue: false,
};
},
watch: {
value: {
immediate: true,
handler(value) {
this.internalValue = value;
},
},
internalValue(internalValue) {
this.$emit('input', internalValue);
},
},
methods: {
toggle() {
this.internalValue = !this.internalValue;
},
},
});
In order to distinguish between the prop and the data, I've used value
and internalValue
respectively. The problem is that value
is rarely used in the code (it's only used in the watcher) and I have to remember to use internalValue
throughout the component's code and template instead of value
(since internalValue
is the mutable source of truth).
To avoid this, I could instead name the prop externalValue
and the data value
, but now I would have to use <checkbox :external-value="x">
which is not ideal (the external prop should be called simply "value").
I'm proposing a feature in which you can specify what internal name a prop should be exposed as within the component without changing the external name of that prop in templates (kind of like how method arguments work in objective-c).
Vue.component('foo', {
props: {
value: {
internalName: 'valueProp',
},
},
template: '<div>{{ valueProp }}</div>',
});
<foo value="bar">
@decademoon Just to me, it looks like you don't need to maintain internalValue
yourself:
Vue.component('checkbox', {
template: '<div class="checkbox" @click="toggle">{{ value ? "Checked" : "Unchecked" }}</div>',
props: {
value: {
type: Boolean,
default: false,
},
},
methods: {
toggle() {
this.$emit('input', !this.value);
},
},
});
That's it! This gives some additional control on Checkbox
e.g.
<!-- standard usage: -->
<Checkbox v-model="isChecked" />
<!-- advanced usage: -->
<Checkbox :value="isChecked" @input="inCaseAllIsOkSetChecked($event)" />
This allows to maintain model changes from the outside of Checkbox
component. I mean emitting an input
event doesn't mean that Checkbox
state will be changed. It only indicates that it should change but we have full control on that. I believe that is why v-model
exists.
@OEvgeny Thank you for your response, but your solution won't allow the component to be used without v-model
. If you check out this fiddle, you can see how the checkbox component can still be toggled without requiring its state to be specified by v-model
, hence why internalValue
is necessary.
The checkbox is probably a bad example, because I can't imagine a use case where you'd want to have a checkbox without v-model
.
A better example would be a tabs component. Sometimes you don't care what the selected tab is and the component itself should maintain that state. In other situations you need to control the selected tab by the parent component, so a prop binding is needed.
your solution won't allow the component to be used withou
Exactly. I think it is ok. We want our components to be data/state driven. So, we want to control them from the outside.
You can create uncontrolled one utilizing controlled Checkbox:
Vue.component('uncontrolled-checkbox', {
template: '<Checkbox :value="value" @input="toggle" />',
props: {
initial: { type: Boolean, default: false }
},
data() {
return {
value: this.initial
}
},
methods: {
toggle() {
this.value = !this.value;
this.$emit('input', this.value);
},
},
});
Note that it hasn't value
prop which means we only pass initial value into it (and can't use it with v-model
). On the other hand it also emits input event, so we can listen for its changes.
This gives us cleaner separation where checkbox state is. When we mix controlled and uncontrolled (from the outside) state in one components (your Checbox
example) it becomes unclear that state it has when we use it like it was shown above:
<Checkbox :value="isChecked" @input="inCaseAllIsOkSetChecked($event)" />
Does that make sense to you?
A better example would be a tabs component.
The same approach can be used for all v-model
driven components I guess.
But your uncontrolled-checkbox
component isn't a checkbox
, it's a new component that wraps a checkbox
with the necessary state. I'd have to duplicate all the props from checkbox
to uncontrolled-checkbox
, and forward all emitted events too in order for uncontrolled-checkbox
to fully mimic a checkbox
(again, a checkbox is a bad example, a lot of my components are more complex than that). And if I have <uncontrolled-checkbox ref="cb">
, then this.$refs.cb
isn't a checkbox and won't have all the methods that the checkbox has.
I believe controlled/uncontrolled input is something that shouldn't be mixed. That about usage example I provided?
But your uncontrolled-checkbox component isn't a checkbox
Yes, it isn't. On the other hand if you need a generic component for checkbox you could wrap it one more time and set which to use explicitly:
<Checkbox controlled :value="myVal" :input="onInput" />
<Checkbox :initial="true" :input="onInput" />
Now when you have a ref
you know it is a Checkbox. Although this doesn't solve a problem with props routing.
Vue.component('foo', { props: { value: { internalName: 'valueProp', }, }, template: '<div>{{ valueProp }}</div>', });
I think this is achievable in userland via global mixin. All that you need is to manually set up watchers and provide additional keys in data
.
Here's a mixin I created which will automatically update a prop's value in response to an event emitted from that component (demo):
Vue.mixin({
created() {
if (this.$options.props) {
for (const prop of Object.keys(this.$options.props)) {
const config = this.$options.props[prop];
if (config.syncOn) {
this.$on(config.syncOn, value => this[prop] = value);
}
}
}
},
});
const Checkbox = {
template: '<div class="checkbox" @click="toggle">{{ value ? "Checked" : "Unchecked" }}</div>',
props: {
value: {
type: Boolean,
default: false,
syncOn: 'input',
},
},
methods: {
toggle() {
this.$emit('input', !this.value);
},
},
};
But Vue will output a warning about the prop being mutated...
I can disable console error like this :sweat:
const error = console.error;
console.error = () => {};
this[prop] = value;
console.error = error;
@decademoon I updated the implementation with support for mapping prop to another field and added case when internal state could be out of sync with the state provided by parent component see here.
This is solved more elegantly by using a computed setter:
Vue.component('checkbox', {
template: '<div class="checkbox" @click="toggle">{{ internalValue ? "Checked" : "Unchecked" }}</div>',
props: {
value: {
type: Boolean,
default: false,
},
},
computed: {
internalValue: {
get () { return this.value },
set (v) { this.$emit('input', v) }
}
}
methods: {
toggle() {
this.internalValue = !this.internalValue;
},
},
});
@posva That component won't work without v-model
, all you've done is hide emitting the input event through a setter (which does make writing controlled components easier since you don't have to litter $emit('input', ...)
throughout your code, but then you still have the original problem of having value
and internalValue
and needing to use internalValue
everywhere which was what I wanted to avoid in the first place).
@OEvgeny
I believe controlled/uncontrolled input is something that shouldn't be mixed.
I agree. I think I'll just settle for making all of my components controlled, even if that means I need to use v-model
on them just to make it work even if I don't plan on using that data in the parent component.
Thank you for your help everyone π
It does work with v-model
though. Maybe you're talking about the checkbox appearing checked (#7506)
@posva The main idea was to create a component which state could be driven with or without v-model
. If the value
provided it uses that. If not - it falls back to the internal state providing a way to react to state changes π
@posva I second this proposal. This will be useful in building "smart dumb components" which when used with a external prop behaves like a dumb component but when used without a prop can fallback to it's own internal state behaving like a smart component. If this sort of renaming can be done using the option.model
API, I do not see why cannot be extended to any props and events in general.
Take for example this "smart dumb component":
export default {
props: ['externalValue'],
data () {
return {
internalValue: 1
}
},
computed: {
value: {
get () {
return this.externalValue != null ? this.externalValue : this.internalValue
},
set (value) {
if (this.externalValue != null) this.$emit('update:externalValue', value)
else this.internalValue = value
}
}
}
}
As you can see, even if using the computed property approach, it will require 3 names externalValue
, internalValue
and value
. Either my component user get used to writing v-bind:external-value.sync
or I will have to rename my computed as internalValue
and the underlying state as internalInternalValue
which is not exactly the most clean way to express my code.
A proposed API could be:
export default {
model: {
'prop.value': externalValue,
'event.update:value': 'update:externalValue'
}
}
Here's a simple real-world example I've come across. You have a password field with a toggle icon to show/hide the password. Normally the parent component doesn't care about the internal state.
But then you have a new or change-password form which has two (or even three) password fields, and it's useful to the user if they all toggle the show/hide at the same time. You end up having to write this:
// parent:
<Password v-model="oldPassword" :show.sync="show" label="Old Password" />
<Password v-model="newPassword1" :show.sync="show" label="New Password" />
<Password v-model="newPassword2" :show.sync="show" label="New Password (again)" />
// Password.vue
<template>
<v-text-field
v-bind="$attrs" v-on="$listeners" @click:append="click"
:type="iShow?'text':'password'" :append-icon="iShow?'mdi-eye':'mdi-eye-off'"
/>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false,
},
},
data() { return {
iShow: this.show,
} },
watch: {
show(val) {
this.iShow = val;
},
},
methods: {
click() {
this.iShow = !this.iShow;
this.$emit('update:show', this.iShow);
},
},
}
</script>
I agree, it's relatively easy enough to do in user-land, but it could be made simpler.
Also, I think it could be made possible without any extra boilerplate... If the prop definition provides a default
value then Vue could do one of two things:
v-model
or v-bind:prop.sync
: allow the prop to be mutated without complaining and automatically emit the update event. (probably too much of a breaking change and anti-pattern to be seriously considered).What's the status on this one?
There is an rfc on the rfcs repo
Most helpful comment
Here's a simple real-world example I've come across. You have a password field with a toggle icon to show/hide the password. Normally the parent component doesn't care about the internal state.
But then you have a new or change-password form which has two (or even three) password fields, and it's useful to the user if they all toggle the show/hide at the same time. You end up having to write this:
I agree, it's relatively easy enough to do in user-land, but it could be made simpler.
Also, I think it could be made possible without any extra boilerplate... If the prop definition provides a
default
value then Vue could do one of two things:v-model
orv-bind:prop.sync
: allow the prop to be mutated without complaining and automatically emit the update event. (probably too much of a breaking change and anti-pattern to be seriously considered).