DEV Community

Bryan Ollendyke
Bryan Ollendyke

Posted on

haxHooks: webcomponents that ship their own editing UX

This time I had an idea. What if instead of a new tag bringing in a totally different experience, I leveraged haxHooks to allow for modification of the element in context via contenteditable.

How this works in the video in task-list:

  • listen for activeElement state change in hax
  • set internal editMode state to match
  • on enable, set a mutation observer and apply contenteditable to internal shadowRoot elements
  • make changes by typing
  • on activeElement disable state, turn off content editable, remove mutationObserver, and look at the shadowRoot and update internal state values to match.

Here's the relevant pieces of code from the video:

render() {
    return html`
      <div id="wrapper">
        <h3 property="oer:name" ?contenteditable="${this.editMode}">${this.name}</h3>
        <ol ?contenteditable="${this.editMode}">
          ${this.tasks.map(
            (task) => html`
              <li>
                ${task.link
                  ? html`
                      <a href="${task.link}" property="oer:task">${task.name}</a>
                    `
                  : html` <span property="oer:task">${task.name}</span> `}
              </li>
            `
          )}
        </ol>
      </div>
    `;
  }
  /**
   * Implements haxHooks to tie into life-cycle if hax exists.
   */
  haxHooks() {
    return {
      activeElementChanged: "haxactiveElementChanged",
      inlineContextMenu: "haxinlineContextMenu",
    };
  }
  /**
   * double-check that we are set to inactivate click handlers
   * this is for when activated in a duplicate / adding new content state
   */
  haxactiveElementChanged(el, val) {
    if (this.__thereAreChanges) {
      this.alignState();
    }
    this.editMode = val;
  }
  haxinlineContextMenu(ceMenu) {
    ceMenu.ceButtons = [
      {
        icon: "icons:add",
        callback: "haxClickInlineAdd",
        label: "Add task",
      },
      {
        icon: "icons:remove",
        callback: "haxClickInlineRemove",
        label: "Remove task",
      },
    ];
  }
  haxClickInlineAdd(e) {
    let d = this.tasks;
    d.push({ name: "Do this" });
    this.tasks = [...d];
    return true;
  }
  haxClickInlineRemove(e) {
    if (this.tasks.length > 0) {
      let d = this.tasks;
      d.pop();
      this.tasks = [...d];
      return true;
    }
  }
  static get tag() {
    return "task-list";
  }
  alignState() {
    this.name = this.shadowRoot.querySelector('h3').innerText;
    this.__thereAreChanges = false;
  }
  updated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => {
      if (propName === 'editMode') {
        if (!this[propName]) {
          if (this._observer) {
            this._observer.disconnect();
          }
        }
        else {
          this._observer = new MutationObserver((mutations) => {
            this.__thereAreChanges = true;
          });
          this._observer.observe(this.shadowRoot.querySelector('#wrapper'), {
            childList: true,
            subtree: true,
            characterData: true,
          });
        }
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

Next I'm going to use this state change to update the array of values in the task list, a more complex testing and iteration activity than I wanted to do via live coding :)

code repo: https://github.com/elmsln/lrnwebcomponents/blob/master/elements/task-list/src/task-list.js
will be in the next release of task-list on npm later this month

Discussion (0)