Search code examples
vue.jsbindingweb-component

How to create bidirectional binding from vue.js instance to custom native web component?


Below is an example for a custom web component my-input. I would like to bind the value attribute of my custom input component to the email attribute of a vue instance. (The example might require Chrome to support custom web components.)

=>How do I have to adapt my web component example to get the binding working?

If I replace my-input with a plain input tag, the binding works. Therefore, my syntax for the vue.js part seems to be just fine.

https://jsfiddle.net/j5f9edjt/

 new Vue({
  el: '#app',
  template: '#app-template',
  data: {
  //email data is blank initially
    email: ''
  }
})
<script type="text/javascript" src="https://unpkg.com/[email protected]"></script>

<script>
  class MyInput extends HTMLElement {

		static get observedAttributes() {
        return ['value'];
    }
    
    constructor(){
       super();
       this.wrappedInput=undefined;
    }

    connectedCallback(){
        var self=this;
        if(!self.wrappedInput){
            var wrappedInput = document.createElement('input');
            wrappedInput.type='text';
            wrappedInput.onchange = ()=>this.wrappedInputChanged();
            self.appendChild(wrappedInput);
            self.wrappedInput = wrappedInput;
        }
    }
    
    attributeChangedCallback(attr, oldValue, newValue) {
      if(attr==='value'){
          console.log('attribute changed ' + newValue);
          if(this.wrappedInput){
      	      this.wrappedInput.value= newValue; 
          }   
                                                 
      } 
    }
    
    wrappedInputChanged(){
      console.log('wrapepd input changed')
    	var newValue = this.wrappedInput.value;
      this.value = newValue;
    }
    
     get value() {
        console.log('get value')
				return this.getAttribute('value');
		}

	 set value(newValue) {	
    	this.setAttribute('value',newValue);     
      console.log('set value ' + newValue);
		}

  }
  window.customElements.define('my-input', MyInput);

</script>

<div id="app"></div>

<template id="app-template">
  <div>
  <my-input v-model="email"></my-input>
    <h1>
       You entered {{email}}
    </h1>
  </div>
  
</template>

I tried to dispatch an extra input event but that did not help:

     var myInput = new CustomEvent("input", 
        {
          detail: {
            message: "Hello World!",
            type: 'text',
          },
          bubbles: true,
          cancelable: true
        }
      );
      this.dispatchEvent(myInput);

Where can I find the source code for the v-model directive to understand what it does?

Related question:

How to target custom element (native web component) in vue.js?


Solution

  • To make v-model work you need to make a wrapper component for your webcomponent. The wrapper will conform to the requirements for using v-model with a component.

    Alternatively, you can decompose the v-model into its two parts: set the value prop and handle input events. Vue doesn't seem to recognize a webcomponent as a native element as far as v-model goes.

    class MyInput extends HTMLElement {
    
      static get observedAttributes() {
        return ['value'];
      }
    
      constructor() {
        super();
        this.wrappedInput = undefined;
      }
    
      connectedCallback() {
        var self = this;
        if (!self.wrappedInput) {
          var wrappedInput = document.createElement('input');
          wrappedInput.type = 'text';
          wrappedInput.onchange = () => this.wrappedInputChanged();
          self.appendChild(wrappedInput);
          self.wrappedInput = wrappedInput;
        }
      }
    
      attributeChangedCallback(attr, oldValue, newValue) {
        if (attr === 'value') {
          console.log('attribute changed ' + newValue);
          if (this.wrappedInput) {
            this.wrappedInput.value = newValue;
          }
        }
      }
    
      wrappedInputChanged() {
        var newValue = this.wrappedInput.value;
        this.value = newValue;
      }
    
      get value() {
        console.log('get value')
        return this.getAttribute('value');
      }
    
      set value(newValue) {
        this.setAttribute('value', newValue);
        console.log('set value ' + newValue);
      }
    
    }
    
    window.customElements.define('my-input', MyInput);
    
    new Vue({
      el: '#app',
      template: '#app-template',
      data: {
        //email data is blank initially
        email: ''
      },
      methods: {
        handleInput(event) {
          this.email = event.target.value;
        }
      },
      components: {
        wrappedMyInput: {
          template: '#wmi-template',
          props: ['value'],
          methods: {
            emitInput(event) {
              this.$emit('input', event.target.value);
            }
          }
        }
      }
    })
    <script type="text/javascript" src="https://unpkg.com/[email protected]"></script>
    
    <div id="app"></div>
    
    <template id="app-template">
      <div>
        <my-input :value="email" @input="handleInput"></my-input>
        <h1>
           You entered {{email}}
        </h1>
        <wrapped-my-input v-model="email"></wrapped-my-input>
        <h1>
           You entered {{email}}
        </h1>
      </div>
    </template>
    
    <template id="wmi-template">
      <my-input :value="value" @input="emitInput"></my-input>
    </template>