{"id":16125682,"url":"https://github.com/dy/define-element","last_synced_at":"2025-08-03T14:36:53.585Z","repository":{"id":224776160,"uuid":"317371243","full_name":"dy/define-element","owner":"dy","description":"Custom element definitions for HTML","archived":false,"fork":false,"pushed_at":"2024-05-27T15:00:18.000Z","size":92,"stargazers_count":4,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-12-29T00:12:21.084Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-30T23:24:13.000Z","updated_at":"2024-08-30T07:04:21.000Z","dependencies_parsed_at":"2024-10-09T21:30:48.476Z","dependency_job_id":"ba6a5453-bc26-4080-ad3e-325e524a8b6d","html_url":"https://github.com/dy/define-element","commit_stats":null,"previous_names":["dy/define-element","spectjs/define-element"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dy%2Fdefine-element","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dy%2Fdefine-element/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dy%2Fdefine-element/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dy%2Fdefine-element/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dy","download_url":"https://codeload.github.com/dy/define-element/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232792150,"owners_count":18577262,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-09T21:30:38.671Z","updated_at":"2025-01-06T21:54:04.742Z","avatar_url":"https://github.com/dy.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# define-element (proposal)\n\n`\u003cdefine-element\u003e` - custom element to declare custom elements. (Similar to `\u003cdefs\u003e` in SVG).\nCompilation of existing proposals / prototypes.\n\n```html\n\u003cdefine-element\u003e\n  \u003cx-time\u003e\n    \u003ctemplate\u003e{{ time.toLocaleTimeString() }}\u003c/template\u003e\n\n    \u003cscript scoped\u003e\n      let id\n      this.onconnected = () =\u003e id = setInterval(() =\u003e this.field.time = new Date(), 1000)\n      this.ondisconnected = () =\u003e clearInterval(id)\n    \u003c/script\u003e\n\n    \u003cstyle scoped\u003e\n      :host { font-family: monospace; }\n    \u003c/style\u003e\n  \u003c/x-time\u003e\n\u003c/define-element\u003e\n\n\u003cx-time\u003e\u003c/x-time\u003e\n```\n\n### Contents\n\n* [Element Definition](#element-definition)\n* [Property Types](#property-types)\n* [Template](#template)\n* [Expressions](#expressions)\n* [Shadowmode](#shadowmode)\n* [Slots](#slots)\n* [Script](#script)\n* [Style](#style)\n* [Lifecycle events](#lifecycle-events)\n* [Examples](#examples)\n\n## Element Definition\n\nElement is defined by-example (similar to `\u003cdefs\u003e` in SVG) and may contain `\u003ctemplate\u003e`, `\u003cstyle\u003e` and `\u003cscript\u003e` sections.\n\n```html\n\u003cdefine-element\u003e\n  \u003cmy-element prop:type=\"default\"\u003e\n    \u003ctemplate\u003e\n      {{ content }}\n    \u003c/template\u003e\n    \u003cstyle\u003e\u003c/style\u003e\n    \u003cscript\u003e\u003c/script\u003e\n  \u003c/my-element\u003e\n\n  \u003canother-element\u003e...\u003c/another-element\u003e\n\u003c/define-element\u003e\n\n\u003cmy-element\u003e\u003c/my-element\u003e\n```\n\nInstances of `\u003celement-name\u003e` automatically receive defined attributes and content.\n\nIf `\u003ctemplate\u003e` section isn't defined, the instance content preserved as is.\n\n#### Why? \n\nTemplate-instantiation proposal naturally accomodates for template fields/parts, making it work outside of `\u003ctemplate\u003e` 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.\n\nSingle `\u003cdefine-element\u003e` can define multiple custom elements.\n\n\n## Property types\n\nProps with optional types are defined declaratively as custom element attributes:\n\n```html\n\u003cdefine-element\u003e\n  \u003cx-x count:number=\"0\" flag:boolean text:string time:date value=\"default\"\u003e\n    \u003ctemplate\u003e{{ count }}\u003c/template\u003e\n    \u003cscript scoped\u003e\n      console.log(this.props.count) // 0\n      this.props.count++\n      console.log(this.props.count) // 1\n    \u003c/script\u003e\n  \u003c/x-x\u003e\n\u003c/define-element\u003e\n```\n\nAvailable types are any primitives (attributes are case-agnostic):\n\n* `:string` for _String_\n* `:boolean` for _Boolean_\n* `:number` for _Number_\n* `:date` for _Date_\n* `:array` for _Array_\n* `:object` for _Object_\n* no type for automatic detection\n\nProps values are available under `element.props`.\nChanging any of `element.props.*` is reflected in attributes.\n\nSee [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).\n\n\n## Template\n\n`\u003ctemplate\u003e` supports [template parts](https://github.com/w3c/webcomponents/blob/159b1600bab02fe9cd794825440a98537d53b389/proposals/Template-Instantiation.md#2-use-cases) with expressions:\n\n```html\n\u003cdefine-element\u003e\n  \u003cmy-element\u003e\n    \u003ctemplate\u003e\n      \u003ch1\u003e{{ user.name }}\u003c/h1\u003eEmail: \u003ca href=\"mailto:{{ user.email }}\"\u003e{{ user.email }}\u003c/a\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      this.field.user = { name: 'George Harisson', email: 'george@harisson.om' }\n    \u003c/script\u003e\n  \u003c/my-element\u003e\n\u003c/define-element\u003e\n```\n\nTemplate part values are available as `element.field` object. Changing any of the `field.*` automatically rerenders the template.\n\nA field can potentially support reactive types as well: _Promise_/_Thenable_, _Observable_/_Subject_, _AsyncIterable_ etc. In that case update happens by changing the reactive state:\n\n```html\n\u003ctemplate\u003e{{ count }}\u003c/template\u003e\n\u003cscript scoped\u003e\n  this.field.count = asyncIterator\n\u003c/script\u003e\n```\n\nSee [template-parts](https://github.com/dy/template-parts), [template-expressions](https://github.com/luwes/template-extensions) – polyfills for _Template-Parts_ proposal.\n\n## Expressions\n\nSyntax is [JS subset](https://github.com/dy/subscript?tab=readme-ov-file#justin):\n\nPart | Expression | Accessible as\n---|---|---\nValue | `{{ foo }}` | `field.foo` \nProperty | `{{ foo.bar?.baz }}`, `{{ foo[\"bar\"] }}` | `field.foo.bar` \nFunction call | `{{ foo(bar) }}` | `field.foo`, `field.bar` \nMethod call | `{{ foo.bar() }}` | `field.foo.bar` \nBoolean operators | `{{ !foo \u0026\u0026 bar \\|\\| baz }}` | `field.foo`, `field.bar`, `field.baz` \nTernary | `{{ foo ? bar : baz }}` | `field.foo`, `field.bar`, `field.baz` \nPrimitives | `{{ \"foo\" }}`, `{{ true }}`, `{{ 0.1 }}` | \nComparison | `{{ foo == 1 }}`, `{{ bar \u003e foo }}` | `field.foo`, `field.bar` \nMath | `{{ a * 2 + b / 3 }}` | `field.a`, `field.b` \nLoop | `{{ item, idx in list }}` | `field.list` \nSpread | `{{ ...foo }}` | `field.foo` \n\n### Loops\n\nOrganized via `foreach` directive:\n\n```html\n\u003cdefine-element\u003e\n  \u003cul is=\"my-list\"\u003e\n    \u003ctemplate\u003e\n      \u003ctemplate directive=\"foreach\" expression=\"item, index in items\"\u003e\u003cli id=\"item-{{ index }}\"\u003e{{ item.text }}\u003c/li\u003e\u003c/template\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      this.field.items = [1,2,3]\n    \u003c/script\u003e\n  \u003c/ul\u003e\n\u003c/define-element\u003e\n\n\u003cul is=\"my-list\"\u003e\u003c/ul\u003e\n```\n\n### Conditions\n\nOrganized via `if` directive or ternary operator.\n\nFor text variants ternary operator is shorter:\n\n```html\n\u003cspan\u003eStatus: {{ status === 0 ? 'Active' : 'Inactive' }}\u003c/span\u003e\n```\n\nTo optionally display an element, use `if`-`else if`-`else` directives:\n\n```html\n\u003ctemplate directive=\"if\" expression=\"status === 0\"\u003eInactive\u003c/template\u003e\n\u003ctemplate directive=\"else if\" expression=\"status === 1\"\u003eActive\u003c/template\u003e\n\u003ctemplate directive=\"else\"\u003eFinished\u003c/template\u003e\n```\n\n## Shadowmode\n\nCan be defined via `shadowrootmode` property:\n\n```html\n\u003cmy-element\u003e\n  \u003ctemplate shadowrootmode=\"closed\"\u003e\u003ctemplate\u003e\n\u003c/my-element\u003e\n\u003cmy-element\u003e\n  \u003ctemplate shadowrootmode=\"open\"\u003e\u003ctemplate\u003e\n\u003c/my-element\u003e\n```\n\nSee [declarative-shadow-dom](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom).\n\n## Slots\n\nSlots allow injecting content into instances aside from attributes.\n\n```html\n\u003cdefine-element\u003e\n  \u003cmy-element\u003e\n    \u003ctemplate\u003e\n      \u003ch1\u003e\u003cslot name=\"title\"\u003e\u003c/slot\u003e\u003c/h1\u003e\n      \u003cp\u003e\u003cslot name=\"content\"\u003e{{ children }}\u003c/slot\u003e\u003c/p\u003e\n    \u003c/template\u003e\n  \u003c/my-element\u003e\n\u003c/define-element\u003e\n\n\u003cmy-element\u003e\n  \u003cspan slot=\"title\"\u003eHello World\u003c/span\u003e\n  \u003cspan slot=\"content\"\u003eOur adventure has begun\u003c/span\u003e\n\u003c/my-element\u003e\n```\n\n## Script\n\nThere are two possible ways to attach scripts to the defined element.\n\n_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.\n\nScript runs in `connectedCallback` with children and properties parsed and present on the element.\n\n```html\n\u003cdefine-element\u003e\n  \u003cmain-header text:string\u003e\n    \u003ctemplate\u003e\n      \u003ch1 part=\"header\"\u003e{{ content }}\u003c/h1\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      this // my-element\n      this.part.header // h1\n      this.field.content = this.prop.text\n    \u003c/script\u003e\n  \u003c/main-header\u003e\n\u003c/define-element\u003e\n```\n\nSee `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 [`\u003cscript scoped\u003e` polyfill](https://gist.github.com/dy/2124c2dfcbdd071f38e866b85436c6c5) implementation.\n\n\n_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.\nAt the same time, it would require manual control over children, props and reactivity.\n\n```html\n\u003cdefine-element\u003e\n  \u003cmy-element\u003e\n    \u003ctemplate\u003e\u003c/template\u003e\n    \u003cscript type=\"module\"\u003e\n      export default class MyCustomElement extends HTMLElement {\n        constructor() {\n          super()\n        }\n        connectedCallback() {}\n        disconnectedCallback() {}\n      }\n    \u003c/script\u003e\n  \u003c/my-element\u003e\n\u003c/define-element\u003e\n```\n\n\n## Style\n\nStyles can be defined either globally or with `scoped` attribute, limiting CSS to only component instances.\n\n```html\n\u003cdefine-element name=\"percentage-bar\" percentage:number=\"0\"\u003e\n  \u003ctemplate shadowrootmode=\"closed\"\u003e\n    \u003cdiv id=\"progressbar\" role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"{{percentage}}\"\u003e\n      \u003cdiv part=\"bar\" style=\"width: {{percentage}}%\"\u003e\u003c/div\u003e\n      \u003cdiv part=\"label\"\u003e\u003cslot\u003e\u003c/slot\u003e\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/template\u003e\n  \u003cstyle scoped\u003e\n    :host { display: inline-block; }\n    #progressbar { position: relative; display: block; width: 100%; height: 100%; }\n    #bar { background-color: #36f; height: 100%; }\n    #label { position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; text-align: center; }\n  \u003c/style\u003e\n\u003c/define-element\u003e\n```\n\nSee [`\u003cstyle scoped\u003e`](https://github.com/samthor/scoped).\n\n\n\n## Lifecycle events\n\nThere are `connected`, `disconnected` and `attributechanged` events generated to simplify instance lifecycle management. They're available as `onconnected`, `ondisconnected` and `onattributechanged` event handlers as well.\n\n```html\n\u003cdefine-element\u003e\n  \u003cx-element\u003e\n    \u003cscript scoped\u003e\n      // by default the script is run once when instance is `connected`, to have children and attributes available\n\n      this.onconnected = () =\u003e console.log('connected')\n      this.ondisconnected = () =\u003e console.log('disconnected')\n      this.onattributechanged = (e) =\u003e console.log('attributechanged', e.attributeChanged, e.newValue, e.oldValue)\n    \u003c/script\u003e\n  \u003c/x-element\u003e\n\u003c/define-element\u003e\n```\n\nSee [disconnected](https://github.com/WebReflection/disconnected), [attributechanged](https://github.com/WebReflection/attributechanged).\n\n\n## Examples\n\n### Hello World\n\n```html\n\u003cdefine-element\u003e\n  \u003cwelcome-user\u003e\n    \u003ctemplate\u003eHello, {{ name || '...' }}\u003c/template\u003e\n    \u003cscript scoped\u003e\n      this.field.name = await fetch('/user').json()\n    \u003c/script\u003e\n  \u003c/welcome-user\u003e\n\u003c/define-element\u003e\n\n\u003cwelcome-user/\u003e\n```\n\n### Timer\n\n```html\n\u003cdefine-element\u003e\n  \u003cx-timer start:number=\"0\"\u003e\n    \u003ctemplate\u003e\n      \u003ctime part=\"timer\"\u003e{{ count }}\u003c/time\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      this.field.count = this.prop.start\n      let id\n      this.onconnected = () =\u003e id = setInterval(() =\u003e this.field.count++, 1000)\n      this.ondisconnected = () =\u003e clearInterval(id)\n    \u003c/script\u003e\n  \u003c/x-timer\u003e\n\u003c/define-element\u003e\n\n\u003cx-timer start=\"0\"/\u003e\n```\n\n### Clock\n\n```html\n\u003cdefine-element\u003e\n  \u003cx-clock start:date\u003e\n    \u003ctemplate\u003e\n      \u003ctime datetime=\"{{ time }}\"\u003e{{ time.toLocaleTimeString() }}\u003c/time\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      this.field.time = this.prop.start || new Date();\n      let id\n      this.onconnected = () =\u003e id = setInterval(() =\u003e this.field.time = new Date(), 1000)\n      this.ondisconnected = () =\u003e clearInterval(id)\n    \u003c/script\u003e\n    \u003cstyle scoped\u003e\n      :host {}\n    \u003c/style\u003e\n  \u003c/x-clock\u003e\n\u003c/define-element\u003e\n...\n\u003cx-clock start=\"17:28\"/\u003e\n```\n\n### Counter\n\n```html\n\u003cdefine-element\u003e\n  \u003cx-counter count:number=\"0\"\u003e\n    \u003ctemplate\u003e\n      \u003coutput\u003e{{ count }}\u003c/output\u003e\n      \u003cbutton part=\"inc\"\u003e+\u003c/button\u003e\n      \u003cbutton part=\"dec\"\u003e‐\u003c/button\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      this.part.inc.onclick = e =\u003e this.props.count++\n      this.part.dec.onclick = e =\u003e this.props.count--\n    \u003c/script\u003e\n  \u003c/x-counter\u003e\n\u003c/define-element\u003e\n\n```\n\n### Todo list\n\n```html\n\u003cdefine-element\u003e\n  \u003ctodo-list\u003e\n    \u003ctemplate\u003e\n      \u003cinput part=\"text\" placeholder=\"Add Item...\" required\u003e\n      \u003cbutton type=\"submit\"\u003eAdd\u003c/button\u003e\n      \u003cul class=\"todo-list\"\u003e\n        \u003ctemplate directive=\"foreach\" expression=\"items in todos\"\u003e\u003cli class=\"todo-item\"\u003e{{ item.text }}\u003c/li\u003e\u003c/template\u003e\n      \u003c/ul\u003e\n    \u003c/template\u003e\n    \u003cscript scoped\u003e\n      // initialize from child nodes\n      this.field.todos = this.children.map(child =\u003e {text: child.textContent})\n      this.part.text.onsubmit = e =\u003e {\n        e.preventDefault()\n        if (form.checkValidity()) {\n          this.field.todos.push({ text: this.part.text.value })\n          form.reset()\n        }\n      }\n    \u003c/script\u003e\n  \u003c/todo-list\u003e\n\u003c/define-element\u003e\n\n\u003ctodo-list\u003e\n  \u003cli\u003eA\u003c/li\u003e\n  \u003cli\u003eB\u003c/li\u003e\n\u003c/todo-list\u003e\n```\n\n### Form validator\n\n```html\n\u003cdefine-element\u003e\n  \u003cform is=\"validator-form\"\u003e\n    \u003ctemplate shadowrootmode=\"closed\"\u003e\n      \u003clabel for=email\u003ePlease enter an email address:\u003c/label\u003e\n      \u003cinput id=\"email\"\u003e\n      \u003ctemplate expression=\"!valid\" directive=\"if\"\u003eThe address is invalid\u003c/span\u003e\u003c/template\u003e\n    \u003c/template\u003e\n\n    \u003cscript scoped type=\"module\"\u003e\n      const isValidEmail = s =\u003e /.+@.+\\..+/i.test(s);\n      export default class ValidatorForm extends HTMLFormElement {\n        constructor () {\n          this.email.onchange= e =\u003e this.field.valid = isValidEmail(e.target.value)\n        }\n      }\n    \u003c/script\u003e\n  \u003c/form\u003e\n\u003c/define-element\u003e\n\n\u003cform is=\"validator-form\"\u003e\u003c/form\u003e\n```\n\n## Refs\n\n* [element-modules](https://github.com/trusktr/element-modules)\n* [EPA-WG custom-element](https://github.com/EPA-WG/custom-element)\n* [vue3 single piece](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md)\n* [uce-template](https://github.com/WebReflection/uce-template#readme)\n* [snuggsi](https://github.com/devpunks/snuggsi)\n* [tram-lite](https://github.com/Tram-One/tram-lite)\n* [tram-deco](https://github.com/Tram-One/tram-deco)\n* [Declarative Custom Elements Proposal](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md)\n* [Template Instantiation Proposal](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md)\n\n### License\n\nISC\n\n\u003cp align=\"center\"\u003e🕉\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdy%2Fdefine-element","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdy%2Fdefine-element","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdy%2Fdefine-element/lists"}