Open jordandobrev opened 6 years ago
I tracked this down a bit and seems to come from how Vue treats custom components differently than a regular element like an <input />
.
For regular inputs, Vue looks for the value in $event.target.value
and finds it because the browser is emitting a regular InputEvent object.
For custom components like ion-input, it seems that Vue is expecting the value to be emitted directly and not as a subclass of Event. So, Vue looks for the value in $event
rather than $event.target.value
.
This behaviour is documented here: https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components
This was probably done to make the coding of custom components simpler: the developer wouldn't need to instantiate a new Event and emit it.
A simple fix (and uneducated guess on my part) would be to get rid of the special case for custom components and check if what is being $emit
'ed is actually an Event object and wrap it in an Event if it isn't.
Leaving a comment here as it may be helpful to others, whilst this definitely should be handled inside the vue project itself in the meantime if you're looking for a solution right now I've created a compile directive that is configurable and allows you to use the same syntax on web-components until a proper solution is in place:
https://www.npmjs.com/package/vue-wc-model
I wouldn't mind creating a PR and getting something implemented in vue itself but there is some unknowns regarding how this should be handled. For example, not all web components use the input
and change
handlers nor even expose value
as a property on the event target (eg. components created via vue-web-component-wrapper
).
I think this needs to be thought out some more in terms of what to do.
What if v-model
accepted an event name as a directive argument? i.e.:
v-model:blur="fooBar"
Could even be used for custom event names (as long as they didn't contain any colons or periods)
Any event specified this way would set the model value to event.target.value
, and do no underlying "magic" that happens with regular v-model
or .lazy
modifier.
Has anyone began to investigate this. This has had a huge impact on us in moving forward with using Vue. It also appears that Vue is the only framework where we are seeing binding issues with our web components.
I really like the idea of v-model:eventName
, that would definitely help us out. We've created a series of form web components that have their own APIs that work fine in Angular and React, but this issue is hampering out adoption in Vue. Would love to see some input from maintainers on this and would be interested in helping implement if needed.
We've also trying to use WebComponents with Vue. The problem here is, Vue must know this WebComponent implemented input
event and have value
in target, as what <input>
do. I believe Vue can just assume this when v-model
tis applied to any tag which is not a Vue component, nor a known non-text form component.
Vue.config.ignoredElements = [/^ui-/];
That imho is not a good option name, should be more self-explanatory.
How about .native
modifier? v-model="foo"
would use $event
as a value, v-model.native="foo"
would use $event.target.value
Why should there be an extra modifier/directive? The v-model:<event>
construct is useful but isn't that a separate feature request?
For regular inputs Vue uses $event.target.value
and for Vue components it uses $event
. We already tell Vue that our web-components aren't Vue components by adding them to Vue.config.ignoredElements
. Why can't v-model check this config, notice that an element is not a Vue component, and not use the Vue specific syntax in that case?
@ngfk 's solution looks better to me. No change to current api, and nothing should break in this way.
Note there are currently two 3.0 RFCs that uses the v-model
directive argument for a different purpose: https://github.com/vuejs/rfcs/pull/8 https://github.com/vuejs/rfcs/pull/31
In RFC#31 there is a section that talks about v-model
usage on custom elements.
The problem with Vue.config.ignoredElements
is that it is runtime only, so the compiler does not have that information and ends up outputting code that intended to be used for a Vue component. A solution for 2.x would be adding an option to the template compiler (configured via vue-loader
options) which serves as the compile-time counterpart of Vue.config.ignoredElements
.
I created a custom directive that makes this less painful. Suggestions or improvements are welcome!
// model-custom-element.js
import Vue from 'vue';
const wm = new WeakMap();
export default {
bind(el, binding, vnode) {
const inputHandler = event => Vue.set(vnode.context, binding.expression, event.target.value);
wm.set(el, inputHandler);
el.value = binding.value;
el.addEventListener('input', inputHandler);
},
componentUpdated(el, binding) {
el.value = binding.value;
},
unbind(el) {
const inputHandler = wm.get(el);
el.removeEventListener(el, inputHandler);
}
};
// main.js
import modelCustomElement from './model-custom-element.js';
// ... your vue init here
Vue.directive('model-custom-element', modelCustomElement);
<!-- Usage example -->
<flux-textfield v-model-custom-element="name"></flux-textfield>
@claviska I just used your code and adapted it to my custom WC input ;-) Thanks a lot!!! :tada:
I had to do add naïve support for "dot notation" and since I already had lodash in the project, I used its get
function.
The current situation with v-model
is causing some difficulties in creating a simple API for our custom components. To support the unadorned v-model
parameter for a two-way prop binding, the component needs to introduce a new modelValue
prop. To support a one-way prop binding, either users are asked to assign :model-value="x"
or the component can introduce a second prop value, such as value
or checked
or whatever's appropriate.
In the second case, the component seemingly needs to watch (f.ex) both of modelValue
and value
and use whichever was updated most recently, which makes the code more confusing to write and to document. In the first case, it's simply not intuitive coming from a vue-2 background where a static initial binding to value
or checked
is often used.
My preference if it's possible would be to allow components to override the default model value binding with a new top-level property on the definition, maybe something like this:
export const MyComp = defineComponent({
props: { value: String },
modelValue: "value",
setup(props, ctx) { .. }
})
With this definition writing <my-comp v-model="x" />
would effectively bind value: x
and onUpdate:value: val => (x = val)
. Specific named properties could still be bound using the extended v-model:value=
syntax.
This change would also allow the default model value binding for that component to be changed later on without updating all invocations of the component, for example to bind by default to a live input
property instead of the committed value
.
I created a custom directive that makes this less painful. Suggestions or improvements are welcome!
// model-custom-element.js import Vue from 'vue'; const wm = new WeakMap(); export default { bind(el, binding, vnode) { const inputHandler = event => Vue.set(vnode.context, binding.expression, event.target.value); wm.set(el, inputHandler); el.value = binding.value; el.addEventListener('input', inputHandler); }, componentUpdated(el, binding) { el.value = binding.value; }, unbind(el) { const inputHandler = wm.get(el); el.removeEventListener(el, inputHandler); } };
// main.js import modelCustomElement from './model-custom-element.js'; // ... your vue init here Vue.directive('model-custom-element', modelCustomElement);
<!-- Usage example --> <flux-textfield v-model-custom-element="name"></flux-textfield>
This is great! Thank you!
Here is a detailed article on how to support v-model with web components using a custom directive.
https://muhimasri.com/blogs/how-to-create-custom-v-model-for-web-components/
Cheers
@claviska @muhimasri Thank you for awesome solutions. Do you have any idea how to do the same in Vue3?
This code doesn't work because of breaking changes: the expression
string is no longer passed as part of the binding
object (https://v3.vuejs.org/guide/migration/custom-directives.html#overview).
You are welcome @rahmanroman I will look into making it work in Vue3. As you mentioned, the expression
string is no longer being passed making it challenging to figure which data to update on input change.
I created a custom directive that makes this less painful. Suggestions or improvements are welcome!
// model-custom-element.js import Vue from 'vue'; const wm = new WeakMap(); export default { bind(el, binding, vnode) { const inputHandler = event => Vue.set(vnode.context, binding.expression, event.target.value); wm.set(el, inputHandler); el.value = binding.value; el.addEventListener('input', inputHandler); }, componentUpdated(el, binding) { el.value = binding.value; }, unbind(el) { const inputHandler = wm.get(el); el.removeEventListener(el, inputHandler); } };
// main.js import modelCustomElement from './model-custom-element.js'; // ... your vue init here Vue.directive('model-custom-element', modelCustomElement);
<!-- Usage example --> <flux-textfield v-model-custom-element="name"></flux-textfield>
Thanks for this, works pretty well for primitive based models :) For people wondering how to make it work as well for Object based models, you can replace this code :
const inputHandler = event => Vue.set(vnode.context, binding.expression, event.target.value);
By this (using lodash) :
import {set} from 'lodash';
// ....
const inputHandler = event => set(vnode.context, binding.expression, event.target.value);
this still happens in 2023,gods...
Extremely late to the party here, but thought i'd add some learned knowledge for anyone arriving via google (and also future me!).
As long as your component raises an input
event, and has a value
prop then v-model
will "just" work. No need for custom directives or any other shenanigans.
<script setup>
const value = ref(0)
</script>
<template>
<p>value from v-model: {{ value }}</p>
<my-counter v-model="value"></my-counter>
</template>
If your component uses a change
event rather than input
, then you can use v-model.lazy
instead.
Here's a working example https://stackblitz.com/edit/vue-web-component-v-model?file=src%2FApp.vue
Extremely late to the party here, but thought i'd add some learned knowledge for anyone arriving via google (and also future me!).
As long as your component raises an
input
event, and has avalue
prop thenv-model
will "just" work. No need for custom directives or any other shenanigans.<script setup> const value = ref(0) </script> <template> <p>value from v-model: {{ value }}</p> <my-counter v-model="value"></my-counter> </template>
If your component uses a
change
event rather thaninput
, then you can usev-model.lazy
instead.Here's a working example https://stackblitz.com/edit/vue-web-component-v-model?file=src%2FApp.vue
yeah i don't know how but it started working for me as well with shoelace. Can't find any specific v-model updates in the latest releases but I'm super glad it works
What problem does this feature solve?
V-model support for web components(tested with web component implemented with ionic's stenciljs compiler).
Does not work:
Works:
Can this be enabled to support ignored elements as well that have been declared with:
Vue.config.ignoredElements = [/^ui-/];
What does the proposed API look like?
Declaration
Usage