DrSensor / nusa

incremental runtime that bring both simplicity and power into webdev (buildless, cross-language, data-driven)
MIT License
4 stars 0 forks source link

<define-element> #60

Open DrSensor opened 1 year ago

DrSensor commented 1 year ago

Has same signature as <render-scope> but for defining a custom-element.

<define-element let=my-form extends=form>
  <link href=module.js>

  <template :=self root=closed>
    <input type=number :: on:change=set:count>
    <slot>
      <button :: value:=count>0</button>
    </slot>
  </template>
</define-element>

It only fetch then apply linked modules when the custom-element is in view.

Note that it use root=open/closed instead of shadowroot=open/closed to avoid ghosted elements

Warning it may cause layout shift

TODO

Related

Declarative Syntax for Custom Elements (strawman proposal)

Maybe partially rewriting the current implementation as module 🤔

Warning: Don't ever rewrite <render-scope> as module!

<definition name=my-form>
<link href=module.js>
<template shadowmode=closed>
<!-- ~ -->
</template>
<script type=module>
import { scope } from "nusa"
@scope
class MyElement extends HTMLFormElement {
/* ~ */
}
</script>
</definition>
DrSensor commented 1 year ago

I can't justify the amount of layout shift caused by this approach in bigger html, it just too much! Better to wait for declarative syntax being part of HTML5 engine. Otherwise, just use template engine or site generator.

DrSensor commented 1 year ago

Still cause layout shift but I think this is cool alternative (inspired from strawberry framework)

<awesome-button>singleton burton</awesome-button>

<render-scope>
  <link let=animate href=random-animation.js css.var="random duration">

  <template shadowrootmode=closed>
    <cool-button instance=false>singleton Brrr</cool-button>
    <awesome-button instance> instanced burton</awesome-button>
    <cool-button>instanced Brrr</cool-button>
  </template>

  <template instance define=cool-button use=animate>
    <button style="
        padding: 0.5rem 1rem;
        border: 2px solid black;
        border-radius: 0px;
        box-shadow: 4px 4px 0px gray;
        animation: var(--animate\.random);
        animation-duration: var(--animate\.duration);
      "
      disabled ~ !disabled
      @click=animate.shuffle
    ><slot /></button>
  </template>

  <template global define=awesome-button>
    <cool-button>
      <slot /> is awesome with <span ~ #text=animate.random /> animation
    </cool-button>
  </template>
</render-scope>

By default, each custom-element defined inside render-scope will use the same instance of all linked modules. To make each custom-element unique, instantiate attribute need to be specified so it will instantiate all linked modules every time custom-element is created. You can control which module to use (or instantiate) by using use attribute, by default it allow (and may instantiate) all modules in \<render-scope>.


I'm going to reopen this. I think there's a way to prevent layout shift by making <body hidden ~ !hidden> until <render-scope> is defined/loaded.

DrSensor commented 1 year ago

Declarative <custom-element> via <template define=custom-element> has fatal footgun when the template has <slot>. There is no way to automatically hide custom-element because the children will be rendered first before script is ready which cause glitch. At worst it show incorrect content (only show children inside of custom-element) when the JavaScript is disabled. The workaround is to use global hidden attribute on every <custom-element>.

<render-scope>
  <link let=c href=counter.js>

  <template shadowrootmode=closed>
    <div ~ !hidden>loading...</div>
    <my-counter hidden ~ !hidden count=0></my-counter>
  </template>

  <template instance define=my-counter>
    <button ~ @click=c.increment>$count</button>
    <input value=$count ~ @change=set:c.count .value=c.count>
  </template>
</render-scope>

Another alternative would be to use manual slot assignment on named slot.

Note that the initial mode of declarative shadow dom is always be slotAssignment: "named"


<render-scope>
<link let=c href=counter.js>

<div #instance slot=counter style=display:contents> <button ~ @click=c.increment>$count <input value=$count ~ @change=set:c.count .value=c.count>


⬇️ **Downside**:
* awkward custom attributes which:
  * may conflict with built-in `<slot>` and [global](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes) attributes 🤔
  * may have incorrect attribute value when JavaScript disabled 😱
* glitch in text interpolation and worst the text only show the identifier rather than the value 😢
* no `<style>` encapsulation 😢
* can't render in multiple slots 😱 (browser engine can't render same Node in multiple place)

**Solution**:
* style encapsulation:
  * handle `<style type=scoped>` (not work if JavaScript disabled 😢)
  * use `<template shadowrootmode=open>` in the light DOM (i.e `<div>`) and declare `<style>` inside that shadow DOM
* custom attributes:
  * disallow custom attributes 🤪
  * use special prefix/suffix for name/binding of that custom attribute
```html
<render-scope>
  <link let=c href=counter.js>

  <template shadowrootmode=closed>
    <slot name=counter $count=0 ~ .$suffix=c.count>Parent is</slot>
  </template>

  <div #instance slot=counter style=display:contents>
    <slot></slot> <span ~ #text=$suffix></span>
    <button ~ @click=c.increment #text=$count>loading...</button>
    <input value="loading..." ~ @change=set:c.count .value="$count~>c.count">
  </div>
</render-scope>

Note: make sure to provide a default value to handle missing custom attributes

All children inside <slot> will be moved/copied to all slot.assignedElements() 🤔

DrSensor commented 1 year ago

I guess layout shift bound to be inevitable due to engine limitation 😅

Let's keep the <custom-element> approach

<render-scope>
  <link let=c href=counter.js>

  <template shadowrootmode=closed>
    <my-counter count=0 hidden ~ !hidden .suffix=c.count>Parent is</my-counter>
  </template>

  <template -let=prop -instance -define=my-counter>
    <slot></slot> <prop.suffix/>
    <button ~ @click=c.increment><prop.count/></button>
    <input value=prop.count ~ @change=set:c.count .value=c.count>
  </template>
</render-scope>