vue icon indicating copy to clipboard operation
vue copied to clipboard

v-model support for web components (stenciljs)

Open jordandobrev opened this issue 6 years ago • 21 comments

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" />

jordandobrev avatar Mar 14 '18 13:03 jordandobrev

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.

multiplegeorges avatar Sep 10 '18 16:09 multiplegeorges

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.

TomCaserta avatar Oct 18 '18 11:10 TomCaserta

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.

tmorehouse avatar Nov 02 '18 23:11 tmorehouse

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.

chris-washington avatar Nov 29 '18 18:11 chris-washington

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.

calebdwilliams avatar Nov 29 '18 19:11 calebdwilliams

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.

laosb avatar Apr 07 '19 17:04 laosb

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

jacekkarczmarczyk avatar Apr 08 '19 04:04 jacekkarczmarczyk

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 avatar Apr 09 '19 14:04 ngfk

@ngfk 's solution looks better to me. No change to current api, and nothing should break in this way.

laosb avatar Apr 09 '19 17:04 laosb

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.

yyx990803 avatar Apr 10 '19 01:04 yyx990803

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 avatar Mar 11 '20 15:03 claviska

@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.

hsablonniere avatar Apr 30 '20 15:04 hsablonniere

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.

andrewwhitehead avatar Jun 20 '20 16:06 andrewwhitehead

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!

sidharthramesh avatar Mar 24 '21 18:03 sidharthramesh

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

muhimasri avatar Jul 17 '21 22:07 muhimasri

@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).

rahmanroman avatar Jan 07 '22 13:01 rahmanroman

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.

muhimasri avatar Jan 12 '22 19:01 muhimasri

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);

Ei-aaie avatar May 14 '22 09:05 Ei-aaie

this still happens in 2023,gods...

SteveWorkshop avatar Dec 14 '23 03:12 SteveWorkshop

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

WickyNilliams avatar Mar 25 '24 13:03 WickyNilliams