-
Notifications
You must be signed in to change notification settings - Fork 14
Description
Motivation
Dependency / reactivity tracking in JSX compilation can be tricky, and admittedly the current implementation merged in #108 is rather naive, at least in how it targets DOM elements to update.
So taking this basic counter component render function
export default class Counter extends HTMLElement {
constructor() {
super();
this.count = 0;
}
connectedCallback() {
this.count = parseInt(this.getAttribute('count'), 10) || this.count;
this.render();
}
render() {
const { count } = this;
return (
<div style="width: 50%; margin: 0 auto; text-align:center;">
<button onclick={(this.count -= 1)}> -</button>
<span>
You have clicked <span class="red">{count}</span> times
</span>
<button onclick={(this.count += 1)}> +</button>
</div>
);
}
}
customElements.define('app-counter', Counter);It leaves a bunch of attributes inlined into elements with instruction sets for what to update (an attribute or text, see <span class="red" data-wcc-count="${this.count}" data-wcc-ins="text">${count}</span>) and uses DOM updates are not very efficient (see the update function)
export const inferredObservability = true;
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?.shadowRoot || this).querySelectorAll(selector).forEach(el => {
const needle = oldValue === '' ? '' : 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;
}
});
if (['count'].includes(name)) {}
}
constructor() {
super();
this.count = 0;
}
connectedCallback() {
this.count = parseInt(this.getAttribute('count'), 10) || this.count;
this.render();
}
render() {
const {count} = this;
this.innerHTML = `<div style="width: 50%; margin: 0 auto; text-align:center;">
<button onclick="this.parentElement.parentElement.count-=1; this.parentElement.parentElement.setAttribute(\'count\', this.parentElement.parentElement.count);"> -</button>
<span>
You have clicked <span class="red" data-wcc-count="${this.count}" data-wcc-ins="text">${count}</span> times
</span>
<button onclick="this.parentElement.parentElement.count+=1; this.parentElement.parentElement.setAttribute(\'count\', this.parentElement.parentElement.count);"> +</button>
</div>`;
}
}
customElements.define('app-counter', Counter);Technical Design
With Signals, a few things would get simpler, so here is what a Signals based example would look like
export const inferredObservability = true;
export default class Counter extends HTMLElement {
constructor() {
super();
this.count = new Signal.State(0);
this.parity = new Signal.Computed(() => (this.count.get() % 2 === 0 ? "even" : "odd"));
}
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({
mode: 'open'
});
this.render();
}
}
increment() {
this.count.set(this.count.get() + 1);
}
decrement() {
this.count.set(this.count.get() - 1);
}
render() {
const { count, parity } = this;
return (
<div>
<button onclick={this.increment}>Increment (+)</button>
<button onclick={this.decrement}>Decrement (-)</button>
<button onclick={() => this.count.set(this.count.get() * 2)}>Double (++)</button>
<span>The count is ${count.get()} (${parity.get()})</span>
</div>
)
}
}
customElements.define("app-counter", Counter);The compiled output would now look something like this
export const inferredObservability = true;
export default class Counter extends HTMLElement {
static $$tmpl = (count, parity) => _wcc`The count is ${count} (${parity})`;
static parseAttribute = (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;
static get observedAttributes() {
return ['count'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (newValue !== oldValue) {
switch (name) {
case 'count':
this.count.set(Counter.parseAttribute(newValue));
break;
}
}
}
constructor() {
super();
this.count = new Signal.State(0);
this.parity = new Signal.Computed(() => (this.count.get() % 2 === 0 ? "even" : "odd"));
}
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({
mode: 'open'
});
this.render();
}
// register effects
effect(() => {
this.shadowRoot.querySelector('span').textContent = SignalCounterComponentRaw.$$tmpl(this.count.get(), this.parity.get());
})
}
increment() {
this.count.set(this.count.get() + 1);
}
decrement() {
this.count.set(this.count.get() - 1);
}
render() {
const { count, parity } = this;
const template = document.createElement('template');
template.innerHTML = `
<button onclick="this.getRootNode().host.increment()">Increment (+)</button>
<button onclick="this.getRootNode().host.decrement()">Decrement (-)</button>
<button onclick="this.getRootNode().host.count.set(this.getRootNode().host.count.get() * 2)">Double (++)</button>
<span>The count is ${count.get()} (${parity.get()})</span>
`;
if (!this.shadowRoot) {
this.shadowRoot.appendChild(template.content.cloneNode(true));
} else {
this.shadowRoot.innerHTML = template.innerHTML;
}
}
}
customElements.define("app-counter", Counter);Some observations
- With
effects, we have built in reactivity, there is no need for anupdatefunction - No "sprouting" attributes
- No need for "wrapper" elements from userland to "narrow down" holes in the template
- With Signals, updating attributes on the host to the same value already set doesn't trigger an
effect(this is just how Signals work!) - Overall less boilerplate per component
Some considerations though
- The tricky part will still be mapping signals usage (the "holes" in the template) to effects, but we can probably iterate on this over time
- How to handle the runtime (e.g.
effect) and polyfill - We should make sure computeds used in the template are NOT mapped to attributes
Additional Context
Have been doing some prototyping here
https://github.com/thescientist13/wcc-jsx-to-signals/pull/1/
Metadata
Metadata
Assignees
Labels
Type
Projects
Status