Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<define-element> #60

Open
4 tasks
DrSensor opened this issue Feb 1, 2023 · 4 comments
Open
4 tasks

<define-element> #60

DrSensor opened this issue Feb 1, 2023 · 4 comments
Labels
enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now.

Comments

@DrSensor
Copy link
Owner

DrSensor commented Feb 1, 2023

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

  • define custom-element
  • extends from existing HTML<*>Element
  • delete custom-element from registry when disconnected from document
  • immediate query and lazy bind on every slotchange

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 DrSensor added enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now. labels Feb 1, 2023
@DrSensor
Copy link
Owner Author

DrSensor commented May 15, 2023

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 DrSensor closed this as not planned Won't fix, can't repro, duplicate, stale May 15, 2023
@DrSensor
Copy link
Owner Author

DrSensor commented Jun 6, 2023

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 DrSensor reopened this Jun 6, 2023
@DrSensor
Copy link
Owner Author

DrSensor commented Jul 4, 2023

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>

  <template shadowrootmode=closed>
    <slot name=counter count=0></slot>
  </template>

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

⬇️ Downside:

  • awkward custom attributes which:
    • may conflict with built-in <slot> and global 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
<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
Copy link
Owner Author

DrSensor commented Jul 4, 2023

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now.
Development

No branches or pull requests

1 participant