Skip to content

Fine Grained Inferred Observability for JSX #108

@thescientist13

Description

@thescientist13

Type of Change

Feature

Summary

Building off the outcome of #87 , would like to, and have already started playing around with, a more "fine grained" observability model; one that doesn't require an entire re-render and blowing out innerHTML, but can instead more acutely update DOM nodes (textContent, setAttribute) instead.

Details

So for example, taking our Counter component

export default class Counter extends HTMLElement {
  ...

  render() {
    const { count } = this;

    return (
      <div>
        <button onclick={this.count -= 1}> -</button>
        <span>You have clicked <span class="red">{count}</span> times</span>
        <button onclick={this.count += 1}> +</button>
      </div>
    );
  }
}

Which would produce this compiled output

export default class Counter extends HTMLElement {
  static get observedAttributes() {
      return ['count'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    function getValue(value) {
      return value.charAt(0) === '{' || value.charAt(0) === '[' ? JSON.parse(value) : !isNaN(value) ? parseInt(value, 10) : value === 'true' || value === 'false' ? value === 'true' ? true : false : value;
    }
    if (newValue !== oldValue) {
      switch (name) {
        case 'count':
          this.count = getValue(newValue);
          break;
        }
        this.render();
      }
  }
 ...
}

Instead, we would want the compiled output to look something like this instead

export default class Counter extends HTMLElement {
  static get observedAttributes() {
      return ['count'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    function getValue(value) {
      return value.charAt(0) === '{' || value.charAt(0) === '[' ? JSON.parse(value) : !isNaN(value) ? parseInt(value, 10) : value === 'true' || value === 'false' ? value === 'true' ? true : false : value;
    }
    if (newValue !== oldValue) {
      switch (name) {
        case 'count':
          this.count = getValue(newValue);
          break;
        }
       this.update(name, oldValue, newValue);
      }
  }

  update(name, oldValue, newValue) {
    const attr = \`data-wcc-\${name}\`;
    const selector = \`[\${attr}]\`;

    this.querySelectorAll(selector).forEach((el) => {
      const needle = oldValue || el.getAttribute(attr);
      switch(el.getAttribute('data-wcc-ins')) {
        case 'text':
          el.textContent = el.textContent.replace(needle, newValue);
          break;
        case 'attr':
          if (el.hasAttribute(el.getAttribute(attr))) {
            el.setAttribute(el.getAttribute(attr), newValue);
          }
          break;
      }
    })
  }
 ...
}

Additional Thoughts:

  • It might stand to reason we should map updates back to attributes, to keep things in sync? But not sure if this co-mingling is good or bad? Probably if state is meant to go "out" it should be done through custom events instead? Will need to play around with this a bit
  • Would be good to explore tagged template functions as part of this, if not for at least the underlying templating mechanics (as opposed to instead of using JSX directly)
  • Although we only scan the render function for this references, would we do ourselves a service by scanning constructor too, maybe for init values and / or something related to SSR?
  • Not sure if dataset could be useful for anything?

Metadata

Metadata

Labels

Type

No type

Projects

Status

✅ Done

Status

✅ Done

Relationships

None yet

Development

No branches or pull requests

Issue actions