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
- Host: GitHub
- URL: https://github.com/dy/define-element
- Owner: dy
- Created: 2020-11-30T23:24:13.000Z (over 5 years ago)
- Default Branch: main
- Last Pushed: 2024-05-27T15:00:18.000Z (about 2 years ago)
- Last Synced: 2024-12-29T00:12:21.084Z (over 1 year ago)
- Language: JavaScript
- Size: 89.8 KB
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
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()
}
}
```
### 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
🕉