javascriptcallbackweb-componentobject-properties

Web Components: Is there an equivalent to attributeChangedCallback for properties?


You're not supposed to put rich data (objects, arrays, functions) in HTML element attributes. Instead, it's suggested to only put rich data in properties (according to the Google custom elements best practices article). I need to run actions when these properties are updated. We have observedAttributes and attributeChangedCallback, but there's nothing similar for properties.

Let's say I have a user prop with things like name, DoB, and address on it. I thought I might be able to trick observedAttributes by putting a bunk setter a la

set user(val) {
  return;
}

Didn't work. return this.user = val gives an infinite loop.

My only idea at this point is to have a property called _user that simply gets set to [Object object] on every change, which triggers the change I actually want. Don't really like that though.

UPDATE: This is what I'm currently doing

In user-info.js:

class UserInfo extends HTMLElement {
  connectedCallback() {
    subscribers.push({ element: this, props: ['user'] });
    this._user = state.user;
    this.render();
  }
  static get observedAttributes() {
    return ['user'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    this.render();
  }
  get user() {
    return this._user;
  }
  set user(val) {
    if (JSON.stringify(val) !== JSON.stringify(this._user)) {
      this._user = val;
      return this.setAttribute('user', val);
    }
  }
  render() {
    this.innerHTML = `<span>${this._user.name}</span> was born on <span>${this._user.dob}</span>`;
  }
}

In main.js:

document.querySelector('.actions--user').addEventListener('input', e => {
  state.user = {...state.user, [e.target.dataset.action]: e.target.value};
})

Solution

  • You can use a Proxy to detect updated properties of an object.

    customElements.define( 'user-info', class extends HTMLElement {
      connectedCallback() {
        this._user = {
            name: 'Bruno',
            dob: '1/1/2000'
        }
        this.render();
        this._proxy = new Proxy( this._user, {
            set: ( obj, prop, val ) => {
                if ( prop === 'name' ) 
                    if ( this._user.name !== val ) {
                        console.log( 'username updated to ' + val )
                        this._user.name = val
                        this.render()
                    }
            }
        } )
      }
      get user() {
        return this._proxy
      }
      set user(val) {
        if (JSON.stringify(val) !== JSON.stringify(this._user)) {
          this._user = val
          this.render()
        }
      }
      render() {
        this.innerHTML = `<span>${this._user.name}</span> was born on <span>${this._user.dob}</span>`
      }
    } )
    <user-info id=ui></user-info><br>
    <label>Name: <input oninput="ui.user.name=this.value"></label>


    Alternately you could define a User object / class with setters that would interact with the custom element.

    class User {
      constructor( elem ) {
        this._elem = elem
        this._name = 'Bruno'
        this._dob = '1/1/2000'
      }
      set name( val ) {
        if ( val !== this._name ) {
          this._name = val
          this._elem.render()
        }
        return false
      }
      get name() {
        return this._name
      }
      get dob() {
        return this._dob
      }
      update( obj ) {
        this._name = obj.name
        this._dob = obj.dob
      }
    }
    
    class UserInfo extends HTMLElement {
      connectedCallback() {
        this._user = new User( this )
        this.render()
      }
      get user() {
        return this._user
      }
      set user(val) {
        this._user.update( val )
        this.render()
      }
      render() {
        this.innerHTML = `<span>${this._user.name}</span> was born on <span>${this._user.dob}</span>`
      }
    }
    
    customElements.define( 'user-info', UserInfo )
    <user-info id=ui></user-info><br>
    <label>Name: <input oninput="ui.user.name=this.value"></label>