vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
208.05k stars 33.69k forks source link

v-model support for web components (stenciljs) #7830

Open jordandobrev opened 6 years ago

jordandobrev commented 6 years ago

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" />
multiplegeorges commented 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.

TomCaserta commented 6 years ago

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.

tmorehouse commented 6 years ago

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.

chris-washington commented 5 years ago

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.

calebdwilliams commented 5 years ago

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.

laosb commented 5 years ago

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.

jacekkarczmarczyk commented 5 years ago

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

ngfk commented 5 years ago

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?

laosb commented 5 years ago

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

yyx990803 commented 5 years ago

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.

claviska commented 4 years ago

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>
hsablonniere commented 4 years ago

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

andrewwhitehead commented 4 years ago

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.

sidharthramesh commented 3 years ago

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!

muhimasri commented 3 years ago

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

rahmanroman commented 2 years ago

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

muhimasri commented 2 years ago

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.

Ei-aaie commented 2 years ago

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);
SteveWorkshop commented 11 months ago

this still happens in 2023,gods...

WickyNilliams commented 8 months ago

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

amish1188 commented 7 months ago

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

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