admin管理员组

文章数量:1355532

I'm trying to integrate a vanilla js web component in Vue. The code is super contrived and just renders a name and surname as a proof of concept.

class HelloWorld extends HTMLElement {
    
  static observedAttributes = ["name", "surname"];

    constructor() {
      super();
    }
    
    connectedCallback() {
    const 
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name } ${ this.surname }`;
  
    shadow.append(hwMsg);  
  }
  
  // attribute change
  attributeChangedCallback(property, oldValue, newValue) {
    console.log('property ', property, ' changed from ', oldValue, ' to ' newValue);
    if (oldValue === newValue) return;
    this[ property ] = newValue;
    }
}
  
  customElements.define('hello-world', HelloWorld);

I'm trying to integrate a vanilla js web component in Vue. The code is super contrived and just renders a name and surname as a proof of concept.

class HelloWorld extends HTMLElement {
    
  static observedAttributes = ["name", "surname"];

    constructor() {
      super();
    }
    
    connectedCallback() {
    const 
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name } ${ this.surname }`;
  
    shadow.append(hwMsg);  
  }
  
  // attribute change
  attributeChangedCallback(property, oldValue, newValue) {
    console.log('property ', property, ' changed from ', oldValue, ' to ' newValue);
    if (oldValue === newValue) return;
    this[ property ] = newValue;
    }
}
  
  customElements.define('hello-world', HelloWorld);

I registered the hello-world tag as custom in my vite.config file:

export default defineConfig({
  plugins: [
      vue({
        template: {
          compilerOptions: {
            isCustomElement: (tag) => tag.includes('hello-world')
          }
        }
      })
      ...]
I am including the webcomponent in the component where I want to use it:

<script setup lang="ts">
import '@/web_components/prova_web';
import { ref } from 'vue';

const name = ref('John');
const surname = ref('Smith');
</script>
<template>
  <main class="main-view">
    <form>
    <input id="model_name" v-model="name"/>
    <input  id="model_surname" v-model="surname">
   </form>
    Current values: {{ name }} {{ surname }} <br/>
    <hello-world :name="name" :surname="surname"></hello-world>
  </main>
</template>
<style scoped>
</style>
<script src="https://cdnjs.cloudflare/ajax/libs/vue/3.5.4/vue.global.min.js"></script>

The code renders just fine the first time, and the attributeChangedCallback is correctly called for both properties:

console log

Unfortunately, when I change a property using the form inputs, the attributeChangedCallback is not firing again, even though the property values have changed (and properly re-rendered above the custom tag). Screenshot

It's weird that the first time the ref value is correctly unpacked but subsequent changes to the refs' values are not recognized as such. Can I achieve reactive property binding with plain javascript or am I forced to use a framework? (Lit seems very popular).

Share Improve this question asked Mar 28 at 9:47 Fiorenza OppiciFiorenza Oppici 133 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 0

There are two different issues in your code here.

The first seems the be with template literals:

const hwMsg = `Hello ${ this.name } ${ this.surname }`;

This line will evaluate this.name and this.surname and create the string with it - once.

I.e., hwMsg is just a simple string after that and there is no reactivity involved here. So since this line is part of the connectedCallback it is only executed once when attaching the custom element. Thus, updating this.name or this.surname won't have any effect.

For attribute changes to be reflected in the text content you need to update the content of the element in the attributeChangedCallback.


Secondly, since you are setting this.name and this.surname in your attributeChangedCallback your custom element will also have the object properties name and surname which are independent from the element attributes.

With the reactive binding

<hello-world :name="name" :surname="surname"></hello-world>

vue first checks if there are properties with the same name (name/surname) - which there are in this case - and binds to them instead of the attributes of the same name.

See also What is the difference between properties and attributes in HTML?

So the reactive binding will only change the value of the properties and thus not trigger a call of the attributeChangedCallback.

A working implementation of your example could thus be

class HelloWorld extends HTMLElement {
  static observedAttributes = ['name', 'surname'];
  #shadowRoot;

  constructor() {
    super();

    this.#shadowRoot = this.attachShadow({
      mode: 'closed',
    });
  }

  attributeChangedCallback(attribute, oldValue, newValue) {
    console.log( 'attribute ', attribute, ' changed from ', oldValue, ' to ', newValue );
    if (oldValue === newValue) return;

    const hwMsg = `Hello ${this.getAttribute('name')} ${this.getAttribute('surname')}`;
    this.#shadowRoot.replaceChildren(hwMsg);
  }
}

This will

  • update the text every time an attribute is changed
  • not create the name/surname properties so vue will reactively update the attributes so the callback is triggered

It worked the first time because your connectedCallback set the whole string.

Native Web Components are not reactive like you are used to with Frameworks and Libraries;
you have to do the getter/setter part yourself.
That is why developers complain Native development is verbose.
But once you have these getters/setters it saves you from Framework bloat.

This Native Web Component will work in any setting, not just Vue

<script>
  customElements.define("hello-world", class extends HTMLElement {
      #name = "" // private properties point to <span>
      #surname = ""
      
      static observedAttributes = ["name", "surname"]
      attributeChangedCallback(attrname, oldValue, newValue) {
        this[attrname] = newValue; // matching methodname
      }
      constructor() {
        // the most helpful helper function:
        const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props)
        
        super() // sets AND returns 'this' scope
          .attachShadow({ mode: "closed" }) // returns this.shadowRoot (mode:"open" would also set this.shadowRoot)
          .append( // fet appendChild! (unless you need the single! return value)
            "Hello ",
            (this.#name = createElement("span")),
            " ",
            (this.#surname = createElement("span")),
          );
        this.onclick = (evt) => (this.name="Danny", this.surname="Engelman");
      }
      get name()       { return this.#name.innerText    }
      set name(str)    { this.#name.innerText = str     }
      get surname()    { return this.#surname.innerText }
      set surname(str) { this.#surname.innerText = str  }
    },
  )
</script>

<hello-world name="Fiorenza" surname="Oppici"></hello-world>

If you only want code for VUE, then this should be enough.
Since you are overwriting all of shadowRoot with a String, a closed shadowRoot has no meaning An "open" shadowRoot sets this.shadowRoot for free

class HelloWorld extends HTMLElement {
  static observedAttributes = ['name', 'surname'];
  attributeChangedCallback(attribute, oldValue, newValue) {
    (this.shadowRoot || this.attachShadow({mode:"open"})
    .innerHTML = `Hello ${this.getAttribute('name')} ${this.getAttribute('surname')}`;
  }
}

本文标签: javascriptintegrating native web components in Vue properties are not reactiveStack Overflow