define-element 0.0.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/define-element.js +264 -0
- package/package.json +24 -14
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# define-element [](https://npmjs.org/define-element) [](https://bundlephobia.com/package/define-element) [](https://github.com/dy/define-element/actions/workflows/test.yml)
|
|
2
|
+
|
|
3
|
+
A custom element to define custom elements.
|
|
4
|
+
|
|
5
|
+
* Components as content, not code — paste into any page, CMS, or markdown
|
|
6
|
+
* No class, no build, no framework — one `<script>` tag
|
|
7
|
+
* Typed props, scoped styles, shadow DOM, slots, lifecycle
|
|
8
|
+
* Pluggable template engine — native components for [sprae](https://github.com/dy/sprae), [Alpine](https://alpinejs.dev), [petite-vue](https://github.com/vuejs/petite-vue), [template-parts](https://github.com/nicegist/template-parts) and others
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<define-element>
|
|
13
|
+
<x-greeting name:string="world">
|
|
14
|
+
<template>
|
|
15
|
+
<p part="msg"></p>
|
|
16
|
+
</template>
|
|
17
|
+
<script>
|
|
18
|
+
const update = () => this.part.msg.textContent = `Hello, ${this.name}!`
|
|
19
|
+
update()
|
|
20
|
+
this.onattributechanged = update
|
|
21
|
+
</script>
|
|
22
|
+
<style>:host { font-style: italic }</style>
|
|
23
|
+
</x-greeting>
|
|
24
|
+
</define-element>
|
|
25
|
+
|
|
26
|
+
<x-greeting></x-greeting>
|
|
27
|
+
<x-greeting name="Arjuna"></x-greeting>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<script src="https://unpkg.com/define-element"></script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or `$ npm i define-element` → `import 'define-element'`
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Definition
|
|
38
|
+
|
|
39
|
+
Elements are defined by-example inside `<define-element>`. Each child becomes a custom element. A definition can contain `<template>`, `<script>`, and `<style>`.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<define-element>
|
|
43
|
+
<my-element greeting:string="hello">
|
|
44
|
+
<template>...</template>
|
|
45
|
+
<style>...</style>
|
|
46
|
+
<script>...</script>
|
|
47
|
+
</my-element>
|
|
48
|
+
</define-element>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Multiple definitions per block. After processing, `<define-element>` removes itself. Without `<template>`, instance content is preserved as-is.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Props
|
|
55
|
+
|
|
56
|
+
Declared as attributes with optional types:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<x-widget count:number="0" label:string="Click me" active:boolean>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Type | Coercion | Default |
|
|
63
|
+
|------|----------|---------|
|
|
64
|
+
| `:string` | `String(v)` | `""` |
|
|
65
|
+
| `:number` | `Number(v)` | `0` |
|
|
66
|
+
| `:boolean` | `true` unless `"false"` or `null` | `false` |
|
|
67
|
+
| `:date` | `new Date(v)` | `null` |
|
|
68
|
+
| `:array` | `JSON.parse(v)` | `[]` |
|
|
69
|
+
| `:object` | `JSON.parse(v)` | `{}` |
|
|
70
|
+
| (none) | auto-detect | as-is |
|
|
71
|
+
|
|
72
|
+
Properties reflect to attributes and vice versa. Instance attributes override definition defaults.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Template, Parts & Script
|
|
76
|
+
|
|
77
|
+
`<template>` is cloned into each instance on first connect. Elements with `part` are collected into `this.part`. `<script>` runs once per instance with `this` as the element, via script injection (no `eval`).
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<define-element>
|
|
81
|
+
<x-clock>
|
|
82
|
+
<template>
|
|
83
|
+
<time part="display"></time>
|
|
84
|
+
</template>
|
|
85
|
+
<script>
|
|
86
|
+
let id
|
|
87
|
+
const tick = () => this.part.display.textContent = new Date().toLocaleTimeString()
|
|
88
|
+
tick()
|
|
89
|
+
this.onconnected = () => id = setInterval(tick, 1000)
|
|
90
|
+
this.ondisconnected = () => clearInterval(id)
|
|
91
|
+
</script>
|
|
92
|
+
<style>:host { font-family: monospace; }</style>
|
|
93
|
+
</x-clock>
|
|
94
|
+
</define-element>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Access | Description |
|
|
98
|
+
|--------|-------------|
|
|
99
|
+
| `this` | The element instance |
|
|
100
|
+
| `this.count` | Prop value |
|
|
101
|
+
| `this.state` | Template state (from processor or plain object) |
|
|
102
|
+
| `this.part.x` | DOM ref via `part="x"` |
|
|
103
|
+
| `this.onconnected` | Connected callback |
|
|
104
|
+
| `this.ondisconnected` | Disconnected callback |
|
|
105
|
+
| `this.onadopted` | Adopted callback |
|
|
106
|
+
| `this.onattributechanged` | Attribute changed callback |
|
|
107
|
+
|
|
108
|
+
Script runs once on first connect. `onconnected` fires after script, including on first connect. On re-insertion, only `onconnected` fires. Async `await` is auto-detected.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Style
|
|
112
|
+
|
|
113
|
+
`<style>` is scoped automatically. With shadow DOM, styles use [adoptedStyleSheets](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/adoptedStyleSheets) (shared across instances). Without shadow DOM, styles are scoped via [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting) under the element tag, and `:host` is rewritten to the tag name.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## Shadow DOM & Slots
|
|
117
|
+
|
|
118
|
+
Add `shadowrootmode` to the template for encapsulation. Slots work natively:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<define-element>
|
|
122
|
+
<x-dialog open:boolean>
|
|
123
|
+
<template shadowrootmode="open">
|
|
124
|
+
<dialog part="dialog">
|
|
125
|
+
<header><slot name="title">Notice</slot></header>
|
|
126
|
+
<slot></slot>
|
|
127
|
+
<footer><button part="close">Close</button></footer>
|
|
128
|
+
</dialog>
|
|
129
|
+
</template>
|
|
130
|
+
<script>
|
|
131
|
+
const sync = () => this.open ? this.part.dialog.showModal() : this.part.dialog.close()
|
|
132
|
+
this.part.close.onclick = () => this.open = false
|
|
133
|
+
this.onattributechanged = sync
|
|
134
|
+
sync()
|
|
135
|
+
</script>
|
|
136
|
+
<style>
|
|
137
|
+
dialog::backdrop { background: rgba(0,0,0,.5); }
|
|
138
|
+
header { font-weight: bold; margin-bottom: .5em; }
|
|
139
|
+
footer { margin-top: 1em; text-align: right; }
|
|
140
|
+
</style>
|
|
141
|
+
</x-dialog>
|
|
142
|
+
</define-element>
|
|
143
|
+
|
|
144
|
+
<x-dialog open>
|
|
145
|
+
<span slot="title">Confirm</span>
|
|
146
|
+
<p>Are you sure?</p>
|
|
147
|
+
</x-dialog>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Processor
|
|
152
|
+
|
|
153
|
+
Pluggable template engine. Without a processor, templates are static HTML. Set `.processor` to a `(root, state) => state` function — called once per instance after template content is cloned into `root`.
|
|
154
|
+
|
|
155
|
+
`root` is the render target — the element itself (light DOM) or its `shadowRoot` (shadow DOM), with template content already cloned in. The original `<template>` element is available as `root.template` for processors that need it.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import sprae from 'sprae'
|
|
159
|
+
|
|
160
|
+
// sprae matches the signature directly — returns a reactive store
|
|
161
|
+
customElements.get('define-element').processor = sprae
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<define-element>
|
|
166
|
+
<x-counter count:number="0">
|
|
167
|
+
<template>
|
|
168
|
+
<button :onclick="count++">
|
|
169
|
+
Count: <span :text="count"></span>
|
|
170
|
+
</button>
|
|
171
|
+
</template>
|
|
172
|
+
</x-counter>
|
|
173
|
+
</define-element>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
No `<script>` needed — [sprae](https://github.com/dy/sprae) updates the template automatically when state changes. Other processors:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
let DE = customElements.get('define-element')
|
|
180
|
+
|
|
181
|
+
// @github/template-parts ({{x}} interpolation, W3C Template Instantiation proposal)
|
|
182
|
+
import { TemplateInstance } from '@github/template-parts'
|
|
183
|
+
DE.processor = (root, state) => {
|
|
184
|
+
root.replaceChildren(new TemplateInstance(root.template, state))
|
|
185
|
+
return state
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// petite-vue (v-text, v-bind, {{ }})
|
|
189
|
+
import { createApp, reactive } from 'petite-vue'
|
|
190
|
+
DE.processor = (root, state) => (createApp(state).mount(root), reactive(state))
|
|
191
|
+
|
|
192
|
+
// Alpine.js (x-text, x-bind, @click)
|
|
193
|
+
import Alpine from 'alpinejs'
|
|
194
|
+
DE.processor = (root, state) => {
|
|
195
|
+
let r = Alpine.reactive(state)
|
|
196
|
+
Alpine.addScopeToNode(root, r)
|
|
197
|
+
Alpine.initTree(root)
|
|
198
|
+
return r
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Frameworks with their own component models (Lit, Vue, Stencil) are better used directly.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
## Progressive Enhancement
|
|
206
|
+
|
|
207
|
+
Definitions are plain HTML — they render as inert content before JS loads. No flash of unstyled content, no blank page. The component markup is the fallback.
|
|
208
|
+
|
|
209
|
+
For shadow DOM components, server-rendered HTML can include [Declarative Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM#declaratively_with_html) (`<template shadowrootmode>`) for instant first paint. `define-element` then hydrates behavior on top.
|
|
210
|
+
|
|
211
|
+
This is not server-side rendering in the framework sense — there is no server runtime. It is HTML that works without JavaScript and gets enhanced when JavaScript arrives.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## Why
|
|
215
|
+
|
|
216
|
+
The [W3C Declarative Custom Elements proposal](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md) has stalled for years. JS-side solutions (Lit, FAST, Stencil) require build tools and class boilerplate. The declarative CE space is a [graveyard](./docs/alternatives.md).
|
|
217
|
+
|
|
218
|
+
The gap: no lightweight way to define a custom element as HTML — components as content, not as code. Paste a `<define-element>` block into any page, CMS, or markdown file and it works. No npm, no import maps, no build step. One `<script>` tag.
|
|
219
|
+
|
|
220
|
+
This ~265-line polyfill is evidence that the W3C proposal is implementable and useful. Ship it natively.
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
## Alternatives
|
|
224
|
+
|
|
225
|
+
<sup>[EPA-WG custom-element](https://github.com/EPA-WG/custom-element) · [tram-deco](https://github.com/Tram-One/tram-deco) · [tram-lite](https://github.com/Tram-One/tram-lite) · [uce-template](https://github.com/WebReflection/uce-template) · [Ponys](https://github.com/jhuddle/ponys) · [snuggsi](https://github.com/devpunks/snuggsi) · [element-modules](https://github.com/trusktr/element-modules) · [Lit](https://lit.dev) · [Stencil](https://stenciljs.com) · [FAST](https://www.fast.design) · [Catalyst](https://github.github.io/catalyst/) · [Atomico](https://atomicojs.dev) · [Minze](https://minze.dev) · [haunted](https://github.com/matthewp/haunted) · [W3C DCE Proposal](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md)</sup>
|
|
226
|
+
|
|
227
|
+
[Detailed comparison →](./docs/alternatives.md)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
### License
|
|
231
|
+
|
|
232
|
+
[Krishnized](https://github.com/krishnized/license) ISC
|
|
233
|
+
|
|
234
|
+
<p align="center"><a href="https://github.com/krishnized/license">ॐ</a></p>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <define-element> — a custom element to define custom elements.
|
|
3
|
+
* Processor signature: (root, state) => state
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* <define-element>
|
|
7
|
+
* <x-counter count:number="0">
|
|
8
|
+
* <template><output part="out">0</output><button part="inc">+</button></template>
|
|
9
|
+
* <script>this.part.inc.onclick = () => this.count++</script>
|
|
10
|
+
* </x-counter>
|
|
11
|
+
* </define-element>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Type coercions for prop values
|
|
15
|
+
const types = {
|
|
16
|
+
string: v => v == null ? '' : String(v),
|
|
17
|
+
number: v => v == null ? 0 : Number(v),
|
|
18
|
+
boolean: v => v != null && v !== 'false' && v !== false,
|
|
19
|
+
date: v => v == null ? null : new Date(v),
|
|
20
|
+
array: v => v == null ? [] : typeof v === 'string' ? JSON.parse(v) : Array.from(v),
|
|
21
|
+
object: v => v == null ? {} : typeof v === 'string' ? JSON.parse(v) : v,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Serialize value to attribute string
|
|
25
|
+
const serialize = (v, type) =>
|
|
26
|
+
v == null ? null :
|
|
27
|
+
type === 'boolean' ? (v ? '' : null) :
|
|
28
|
+
type === 'array' || type === 'object' ? JSON.stringify(v) :
|
|
29
|
+
type === 'date' ? v.toISOString?.() ?? String(v) :
|
|
30
|
+
String(v)
|
|
31
|
+
|
|
32
|
+
// Auto-detect type from string value
|
|
33
|
+
const auto = v => v == null ? v : !isNaN(v) && v !== '' ? Number(v) : v === 'true' ? true : v === 'false' ? false : v
|
|
34
|
+
|
|
35
|
+
// Track which light-DOM styles have been injected
|
|
36
|
+
let injectedStyles = new Set()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse prop declarations from element attributes.
|
|
40
|
+
* Format: name:type="default" or name="default" (auto-detect type)
|
|
41
|
+
*/
|
|
42
|
+
function parseProps(el) {
|
|
43
|
+
let props = []
|
|
44
|
+
for (let { name, value } of [...el.attributes]) {
|
|
45
|
+
if (name === 'is') continue
|
|
46
|
+
let [prop, type] = name.split(':')
|
|
47
|
+
props.push({ name: prop, type: type || null, default: value })
|
|
48
|
+
}
|
|
49
|
+
return props
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Define a custom element from a definition element.
|
|
54
|
+
* @param {Element} el - The definition child (e.g., <x-counter count:number="0">)
|
|
55
|
+
*/
|
|
56
|
+
function define(el) {
|
|
57
|
+
let tag = el.localName
|
|
58
|
+
let ext = el.getAttribute('is')
|
|
59
|
+
|
|
60
|
+
let propDefs = parseProps(el)
|
|
61
|
+
let propNames = propDefs.map(p => p.name)
|
|
62
|
+
let propMap = Object.fromEntries(propDefs.map(p => [p.name, p]))
|
|
63
|
+
|
|
64
|
+
// extract sections
|
|
65
|
+
let tpl = el.querySelector('template')
|
|
66
|
+
let shadowMode = tpl?.getAttribute('shadowrootmode') || null
|
|
67
|
+
if (shadowMode) tpl.removeAttribute('shadowrootmode')
|
|
68
|
+
|
|
69
|
+
let scriptEl = el.querySelector('script')
|
|
70
|
+
let scriptText = scriptEl?.textContent || null
|
|
71
|
+
|
|
72
|
+
let styleEl = el.querySelector('style')
|
|
73
|
+
let styleText = styleEl?.textContent || null
|
|
74
|
+
|
|
75
|
+
// shared adopted stylesheet for shadow DOM (one per definition)
|
|
76
|
+
let adoptedSheet = null
|
|
77
|
+
if (styleText && shadowMode && typeof CSSStyleSheet !== 'undefined') {
|
|
78
|
+
adoptedSheet = new CSSStyleSheet()
|
|
79
|
+
adoptedSheet.replaceSync(styleText)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// determine base class
|
|
83
|
+
let Base = ext ? document.createElement(tag).constructor : HTMLElement
|
|
84
|
+
|
|
85
|
+
let C = class extends Base {
|
|
86
|
+
static observedAttributes = propNames
|
|
87
|
+
|
|
88
|
+
constructor() {
|
|
89
|
+
super()
|
|
90
|
+
this._de_init = false
|
|
91
|
+
this._de_props = {}
|
|
92
|
+
|
|
93
|
+
for (let p of propDefs) {
|
|
94
|
+
let coerce = p.type ? types[p.type] : auto
|
|
95
|
+
this._de_props[p.name] = coerce(p.default || null)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
connectedCallback() {
|
|
100
|
+
if (!this._de_init) {
|
|
101
|
+
this._de_init = true
|
|
102
|
+
|
|
103
|
+
let root = this
|
|
104
|
+
if (shadowMode) {
|
|
105
|
+
root = this.shadowRoot || this.attachShadow({ mode: shadowMode })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (tpl && !root.firstChild) {
|
|
109
|
+
root.appendChild(tpl.content.cloneNode(true))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// inject style
|
|
113
|
+
if (styleText) {
|
|
114
|
+
if (shadowMode) {
|
|
115
|
+
if (adoptedSheet) {
|
|
116
|
+
root.adoptedStyleSheets = [adoptedSheet]
|
|
117
|
+
} else {
|
|
118
|
+
let s = document.createElement('style')
|
|
119
|
+
s.textContent = styleText
|
|
120
|
+
root.prepend(s)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// light DOM: @scope
|
|
124
|
+
let s = document.createElement('style')
|
|
125
|
+
s.textContent = scopeCSS(styleText, tag)
|
|
126
|
+
s.setAttribute('data-de', tag)
|
|
127
|
+
if (!injectedStyles.has(tag)) {
|
|
128
|
+
injectedStyles.add(tag)
|
|
129
|
+
document.head.appendChild(s)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// collect parts
|
|
135
|
+
this.part = {}
|
|
136
|
+
root.querySelectorAll('[part]').forEach(p =>
|
|
137
|
+
this.part[p.getAttribute('part')] = p
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// expose original template on root for processors that need it
|
|
141
|
+
if (tpl) root.template = tpl
|
|
142
|
+
|
|
143
|
+
// build initial state from prop defaults + current attributes
|
|
144
|
+
let state = {}
|
|
145
|
+
for (let p of propDefs) {
|
|
146
|
+
let coerce = p.type ? types[p.type] : auto
|
|
147
|
+
let attrVal = this.getAttribute(p.name)
|
|
148
|
+
state[p.name] = attrVal != null ? coerce(attrVal) : this._de_props[p.name]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// run processor (per-definition > global)
|
|
152
|
+
let p = DefineElement.processor
|
|
153
|
+
if (p) {
|
|
154
|
+
let result = p(root, state)
|
|
155
|
+
this.state = result || state
|
|
156
|
+
} else {
|
|
157
|
+
this.state = state
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// run script (errors in injected scripts don't throw — browser reports via onerror)
|
|
161
|
+
if (scriptText) runScript(scriptText, this)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.onconnected?.()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
disconnectedCallback() {
|
|
168
|
+
this.ondisconnected?.()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
adoptedCallback() {
|
|
172
|
+
this.onadopted?.()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
176
|
+
let def = propMap[name]
|
|
177
|
+
if (!def) return
|
|
178
|
+
|
|
179
|
+
let coerce = def.type ? types[def.type] : auto
|
|
180
|
+
let val = coerce(newVal)
|
|
181
|
+
this._de_props[name] = val
|
|
182
|
+
|
|
183
|
+
if (this._de_init && this.state) {
|
|
184
|
+
this.state[name] = val
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.onattributechanged?.({ attributeName: name, oldValue: oldVal, newValue: newVal })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// define props as getter/setter on prototype
|
|
192
|
+
for (let p of propDefs) {
|
|
193
|
+
let coerce = p.type ? types[p.type] : auto
|
|
194
|
+
Object.defineProperty(C.prototype, p.name, {
|
|
195
|
+
get() { return this._de_props[p.name] },
|
|
196
|
+
set(v) {
|
|
197
|
+
let val = coerce(v)
|
|
198
|
+
this._de_props[p.name] = val
|
|
199
|
+
if (this._de_init && this.state) this.state[p.name] = val
|
|
200
|
+
let s = serialize(val, p.type)
|
|
201
|
+
if (s == null) this.removeAttribute(p.name)
|
|
202
|
+
else this.setAttribute(p.name, s)
|
|
203
|
+
},
|
|
204
|
+
enumerable: true,
|
|
205
|
+
configurable: true
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// register (skip if already defined)
|
|
210
|
+
let name = ext || tag
|
|
211
|
+
if (!customElements.get(name))
|
|
212
|
+
customElements.define(name, C, ext ? { extends: tag } : undefined)
|
|
213
|
+
|
|
214
|
+
return C
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Execute script text via <script> element injection (no eval/new Function).
|
|
220
|
+
* Wraps in IIFE with `this` bound to element instance.
|
|
221
|
+
*/
|
|
222
|
+
function runScript(text, thisArg) {
|
|
223
|
+
let s = document.createElement('script')
|
|
224
|
+
let stripped = text.replace(/\/\/.*|\/\*[\s\S]*?\*\/|(['"`])(?:(?!\1)[^\\]|\\.)*\1/g, '')
|
|
225
|
+
let isAsync = /\bawait\b/.test(stripped)
|
|
226
|
+
let wrapper = isAsync
|
|
227
|
+
? `(async function(){${text}}).call(document.currentScript._de_this)`
|
|
228
|
+
: `(function(){${text}}).call(document.currentScript._de_this)`
|
|
229
|
+
s._de_this = thisArg
|
|
230
|
+
s.textContent = wrapper
|
|
231
|
+
document.head.appendChild(s)
|
|
232
|
+
s.remove()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
/** CSS scoping for light DOM via CSS nesting. :host → tag. */
|
|
237
|
+
function scopeCSS(css, tag) {
|
|
238
|
+
css = css.replace(/:host\b(?:\(([^)]*)\))?/g, (_, sel) => sel ? `${tag}${sel}` : tag)
|
|
239
|
+
return `${tag} { ${css} }`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* <define-element> — scans children for component definitions.
|
|
245
|
+
*/
|
|
246
|
+
class DefineElement extends HTMLElement {
|
|
247
|
+
connectedCallback() {
|
|
248
|
+
queueMicrotask(() => this._init())
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_init() {
|
|
252
|
+
let defs = [...this.children].filter(c => {
|
|
253
|
+
let ln = c.localName
|
|
254
|
+
return ln !== 'template' && ln !== 'script' && ln !== 'style'
|
|
255
|
+
})
|
|
256
|
+
for (let def of defs) def.remove()
|
|
257
|
+
this.remove()
|
|
258
|
+
for (let def of defs) define(def)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
DefineElement.processor = null
|
|
263
|
+
|
|
264
|
+
customElements.define('define-element', DefineElement)
|
package/package.json
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "define-element",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A custom element to define custom elements",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "define-element.js",
|
|
7
|
+
"unpkg": "define-element.js",
|
|
8
|
+
"files": ["define-element.js"],
|
|
9
|
+
"sideEffects": true,
|
|
6
10
|
"scripts": {
|
|
7
|
-
"test": "
|
|
11
|
+
"test": "node -r ./test/register.cjs test/test.js"
|
|
8
12
|
},
|
|
9
13
|
"repository": {
|
|
10
14
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
15
|
+
"url": "git+https://github.com/dy/define-element.git"
|
|
12
16
|
},
|
|
13
17
|
"keywords": [
|
|
14
|
-
"uce-template",
|
|
15
|
-
"declarative-custom-element",
|
|
16
|
-
"vue3",
|
|
17
|
-
"reactive",
|
|
18
|
-
"spect",
|
|
19
18
|
"custom-element",
|
|
20
19
|
"web-components",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"declarative",
|
|
21
|
+
"html",
|
|
22
|
+
"no-build",
|
|
23
|
+
"template",
|
|
24
|
+
"shadow-dom"
|
|
23
25
|
],
|
|
24
26
|
"author": "dmitry iv.",
|
|
25
27
|
"license": "ISC",
|
|
26
28
|
"bugs": {
|
|
27
|
-
"url": "https://github.com/
|
|
29
|
+
"url": "https://github.com/dy/define-element/issues"
|
|
28
30
|
},
|
|
29
|
-
"homepage": "https://github.com/
|
|
31
|
+
"homepage": "https://github.com/dy/define-element#readme",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@github/template-parts": "^0.7.1",
|
|
34
|
+
"alpinejs": "^3.15.8",
|
|
35
|
+
"jsdom": "^28.1.0",
|
|
36
|
+
"petite-vue": "^0.4.1",
|
|
37
|
+
"tst": "^9.2.2",
|
|
38
|
+
"wait-please": "^3.1.0"
|
|
39
|
+
}
|
|
30
40
|
}
|