An open API service indexing awesome lists of open source software.

https://github.com/dy/define-element

Custom element definitions for HTML
https://github.com/dy/define-element

Last synced: 11 months ago
JSON representation

Custom element definitions for HTML

Awesome Lists containing this project

README

          

# define-element (proposal)

`` - custom element to declare custom elements. (Similar to `` in SVG).
Compilation of existing proposals / prototypes.

```html


{{ time.toLocaleTimeString() }}


let id
this.onconnected = () => id = setInterval(() => this.field.time = new Date(), 1000)
this.ondisconnected = () => clearInterval(id)


:host { font-family: monospace; }

```

### Contents

* [Element Definition](#element-definition)
* [Property Types](#property-types)
* [Template](#template)
* [Expressions](#expressions)
* [Shadowmode](#shadowmode)
* [Slots](#slots)
* [Script](#script)
* [Style](#style)
* [Lifecycle events](#lifecycle-events)
* [Examples](#examples)

## Element Definition

Element is defined by-example (similar to `` in SVG) and may contain ``, `` and `<script>` sections.

```html
<define-element>
<my-element prop:type="default">
<template>
{{ content }}
</template>
<style>

...

```

Instances of `` automatically receive defined attributes and content.

If `` section isn't defined, the instance content preserved as is.

#### Why?

Template-instantiation proposal naturally accomodates for template fields/parts, making it work outside of `` tag would encounter certain issues: [parsing table](https://github.com/github/template-parts/issues/24), [SVG attributes](https://github.com/github/template-parts/issues/25), [liquid syntax](https://shopify.github.io/liquid/tags/template/#raw) conflict etc.

Single `` can define multiple custom elements.

## Property types

Props with optional types are defined declaratively as custom element attributes:

```html


{{ count }}

console.log(this.props.count) // 0
this.props.count++
console.log(this.props.count) // 1

```

Available types are any primitives (attributes are case-agnostic):

* `:string` for _String_
* `:boolean` for _Boolean_
* `:number` for _Number_
* `:date` for _Date_
* `:array` for _Array_
* `:object` for _Object_
* no type for automatic detection

Props values are available under `element.props`.
Changing any of `element.props.*` is reflected in attributes.

See [Element Properties proposal](https://github.com/developit/unified-element-properties-proposal), [attr-types](https://github.com/qwtel/attr-types), [element-props](https://github.com/spectjs/element-props).

## Template

`` supports [template parts](https://github.com/w3c/webcomponents/blob/159b1600bab02fe9cd794825440a98537d53b389/proposals/Template-Instantiation.md#2-use-cases) with expressions:

```html



{{ user.name }}

Email: {{ user.email }}


this.field.user = { name: 'George Harisson', email: 'george@harisson.om' }

```

Template part values are available as `element.field` object. Changing any of the `field.*` automatically rerenders the template.

A field can potentially support reactive types as well: _Promise_/_Thenable_, _Observable_/_Subject_, _AsyncIterable_ etc. In that case update happens by changing the reactive state:

```html
{{ count }}

this.field.count = asyncIterator

```

See [template-parts](https://github.com/dy/template-parts), [template-expressions](https://github.com/luwes/template-extensions) – polyfills for _Template-Parts_ proposal.

## Expressions

Syntax is [JS subset](https://github.com/dy/subscript?tab=readme-ov-file#justin):

Part | Expression | Accessible as
---|---|---
Value | `{{ foo }}` | `field.foo`
Property | `{{ foo.bar?.baz }}`, `{{ foo["bar"] }}` | `field.foo.bar`
Function call | `{{ foo(bar) }}` | `field.foo`, `field.bar`
Method call | `{{ foo.bar() }}` | `field.foo.bar`
Boolean operators | `{{ !foo && bar \|\| baz }}` | `field.foo`, `field.bar`, `field.baz`
Ternary | `{{ foo ? bar : baz }}` | `field.foo`, `field.bar`, `field.baz`
Primitives | `{{ "foo" }}`, `{{ true }}`, `{{ 0.1 }}` |
Comparison | `{{ foo == 1 }}`, `{{ bar > foo }}` | `field.foo`, `field.bar`
Math | `{{ a * 2 + b / 3 }}` | `field.a`, `field.b`
Loop | `{{ item, idx in list }}` | `field.list`
Spread | `{{ ...foo }}` | `field.foo`

### Loops

Organized via `foreach` directive:

```html



  • {{ item.text }}



  • this.field.items = [1,2,3]


    ```

    ### Conditions

    Organized via `if` directive or ternary operator.

    For text variants ternary operator is shorter:

    ```html
    Status: {{ status === 0 ? 'Active' : 'Inactive' }}
    ```

    To optionally display an element, use `if`-`else if`-`else` directives:

    ```html
    Inactive
    Active
    Finished
    ```

    ## Shadowmode

    Can be defined via `shadowrootmode` property:

    ```html

    ```

    See [declarative-shadow-dom](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom).

    ## Slots

    Slots allow injecting content into instances aside from attributes.

    ```html




    {{ children }}



    Hello World
    Our adventure has begun

    ```

    ## Script

    There are two possible ways to attach scripts to the defined element.

    _First_ is via `scoped` script attribute. That enables script to run with `this` defined as _element_ instance, instead of _window_. Also, it automatically exposes internal element references as parts.

    Script runs in `connectedCallback` with children and properties parsed and present on the element.

    ```html



    {{ content }}




    this // my-element
    this.part.header // h1
    this.field.content = this.prop.text

    ```

    See `scoped` proposal discussions: [1](https://discourse.wicg.io/t/script-tags-scoped-to-shadow-root-script-scoped-src/4726/2), [2](https://discourse.wicg.io/t/proposal-chtml/4716/9) and [`` polyfill](https://gist.github.com/dy/2124c2dfcbdd071f38e866b85436c6c5) implementation.

    _Second_ method is via custom element constructor, as proposed in [declarative custom elements](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md). It provides more granular control over constructor, callbacks and attributes.
    At the same time, it would require manual control over children, props and reactivity.

    ```html
    <define-element>
    <my-element>
    <template></template>
    <script type="module">
    export default class MyCustomElement extends HTMLElement {
    constructor() {
    super()
    }
    connectedCallback() {}
    disconnectedCallback() {}
    }

    ```

    ## Style

    Styles can be defined either globally or with `scoped` attribute, limiting CSS to only component instances.

    ```html








    :host { display: inline-block; }
    #progressbar { position: relative; display: block; width: 100%; height: 100%; }
    #bar { background-color: #36f; height: 100%; }
    #label { position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; text-align: center; }

    ```

    See [``](https://github.com/samthor/scoped).

    ## Lifecycle events

    There are `connected`, `disconnected` and `attributechanged` events generated to simplify instance lifecycle management. They're available as `onconnected`, `ondisconnected` and `onattributechanged` event handlers as well.

    ```html
    <define-element>
    <x-element>
    <script scoped>
    // by default the script is run once when instance is `connected`, to have children and attributes available

    this.onconnected = () => console.log('connected')
    this.ondisconnected = () => console.log('disconnected')
    this.onattributechanged = (e) => console.log('attributechanged', e.attributeChanged, e.newValue, e.oldValue)
    </script>
    </x-element>
    </define-element>
    ```

    See [disconnected](https://github.com/WebReflection/disconnected), [attributechanged](https://github.com/WebReflection/attributechanged).

    ## Examples

    ### Hello World

    ```html
    <define-element>
    <welcome-user>
    <template>Hello, {{ name || '...' }}</template>
    <script scoped>
    this.field.name = await fetch('/user').json()
    </script>
    </welcome-user>
    </define-element>

    <welcome-user/>
    ```

    ### Timer

    ```html
    <define-element>
    <x-timer start:number="0">
    <template>
    <time part="timer">{{ count }}</time>
    </template>
    <script scoped>
    this.field.count = this.prop.start
    let id
    this.onconnected = () => id = setInterval(() => this.field.count++, 1000)
    this.ondisconnected = () => clearInterval(id)
    </script>
    </x-timer>
    </define-element>

    <x-timer start="0"/>
    ```

    ### Clock

    ```html
    <define-element>
    <x-clock start:date>
    <template>
    <time datetime="{{ time }}">{{ time.toLocaleTimeString() }}</time>
    </template>
    <script scoped>
    this.field.time = this.prop.start || new Date();
    let id
    this.onconnected = () => id = setInterval(() => this.field.time = new Date(), 1000)
    this.ondisconnected = () => clearInterval(id)
    </script>
    <style scoped>
    :host {}

    ...

    ```

    ### Counter

    ```html



    {{ count }}
    +



    this.part.inc.onclick = e => this.props.count++
    this.part.dec.onclick = e => this.props.count--

    ```

    ### Todo list

    ```html




    Add


    • {{ item.text }}




    // initialize from child nodes
    this.field.todos = this.children.map(child => {text: child.textContent})
    this.part.text.onsubmit = e => {
    e.preventDefault()
    if (form.checkValidity()) {
    this.field.todos.push({ text: this.part.text.value })
    form.reset()
    }
    }

  • A

  • B
  • ```

    ### Form validator

    ```html



    Please enter an email address:

    The address is invalid


    const isValidEmail = s => /.+@.+\..+/i.test(s);
    export default class ValidatorForm extends HTMLFormElement {
    constructor () {
    this.email.onchange= e => this.field.valid = isValidEmail(e.target.value)
    }
    }

    ```

    ## Refs

    * [element-modules](https://github.com/trusktr/element-modules)
    * [EPA-WG custom-element](https://github.com/EPA-WG/custom-element)
    * [vue3 single piece](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md)
    * [uce-template](https://github.com/WebReflection/uce-template#readme)
    * [snuggsi](https://github.com/devpunks/snuggsi)
    * [tram-lite](https://github.com/Tram-One/tram-lite)
    * [tram-deco](https://github.com/Tram-One/tram-deco)
    * [Declarative Custom Elements Proposal](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md)
    * [Template Instantiation Proposal](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md)

    ### License

    ISC

    🕉