Vue: v-model support for web components (stenciljs)

Created on 14 Mar 2018  ·  13Comments  ·  Source: vuejs/vue

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:

<ui-input v-model="mySelect" />

Works:

<ui-input :value="mySelect" @input="mySelect = $event.target.value" />

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

Vue.config.ignoredElements = [/^ui-/];

Usage

<ui-input v-model="mySelect" />
feature request

Most helpful comment

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.

All 13 comments

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.

Was this page helpful?
0 / 5 - 0 ratings