custom-elements-ts 0.0.16 → 0.1.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/.eslintrc.json +46 -0
- package/.github/workflows/ci.yml +49 -0
- package/.prettierrc +7 -0
- package/LICENSE +20 -0
- package/README.md +426 -168
- package/demos/counter/counter.element.html +1 -0
- package/demos/counter/counter.element.scss +234 -0
- package/demos/counter/counter.element.ts +68 -0
- package/demos/counter/index.html +205 -0
- package/demos/counter/index.ts +1 -0
- package/demos/site/code-example/code-example.element.scss +168 -0
- package/demos/site/code-example/code-example.element.ts +88 -0
- package/demos/site/event-log/event-log.element.scss +179 -0
- package/demos/site/event-log/event-log.element.ts +134 -0
- package/demos/site/favicon.svg +14 -0
- package/demos/site/index.html +346 -0
- package/demos/site/index.ts +13 -0
- package/demos/site/message/message.element.scss +75 -0
- package/demos/site/message/message.element.ts +76 -0
- package/demos/site/og-image.png +0 -0
- package/demos/site/styles/site.css +1023 -0
- package/demos/site/styles/tokens.css +56 -0
- package/demos/site/toast/toast.element.scss +110 -0
- package/demos/site/toast/toast.element.ts +63 -0
- package/demos/todo-dashboard/index.html +141 -0
- package/demos/todo-dashboard/index.ts +4 -0
- package/demos/todo-dashboard/todo-dashboard.element.scss +1145 -0
- package/demos/todo-dashboard/todo-dashboard.element.ts +332 -0
- package/demos/todo-dashboard/todo-filters.element.ts +54 -0
- package/demos/todo-dashboard/todo-item.element.ts +126 -0
- package/demos/todo-dashboard/todo-stats.element.ts +189 -0
- package/package.json +73 -29
- package/src/custom-element.ts +206 -0
- package/{index.d.ts → src/index.ts} +2 -0
- package/src/listen.ts +70 -0
- package/src/prop.ts +92 -0
- package/src/state.ts +129 -0
- package/src/template-runtime.ts +435 -0
- package/src/toggle.ts +66 -0
- package/src/tsconfig.json +24 -0
- package/src/util.ts +33 -0
- package/src/watch.ts +14 -0
- package/tests/basic.spec.ts +70 -0
- package/tests/custom-element.spec.ts +77 -0
- package/tests/dispatch.spec.ts +52 -0
- package/tests/init.spec.ts +94 -0
- package/tests/listen.spec.ts +118 -0
- package/tests/prop.spec.ts +118 -0
- package/tests/templating-runtime.spec.ts +575 -0
- package/tests/toggle.spec.ts +92 -0
- package/tests/watch.spec.ts +183 -0
- package/tools/build.js +119 -0
- package/tools/bundle.js +167 -0
- package/tools/rollup-config.js +70 -0
- package/tools/start.js +188 -0
- package/tsconfig.json +38 -0
- package/vite.config.mts +30 -0
- package/bundles/custom-elements-ts.umd.js +0 -315
- package/bundles/custom-elements-ts.umd.js.map +0 -1
- package/custom-element.d.ts +0 -12
- package/esm2015/custom-elements-ts.js +0 -260
- package/esm2015/custom-elements-ts.js.map +0 -1
- package/esm5/custom-elements-ts.js +0 -298
- package/esm5/custom-elements-ts.js.map +0 -1
- package/listen.d.ts +0 -17
- package/prop.d.ts +0 -2
- package/toggle.d.ts +0 -1
- package/util.d.ts +0 -4
- package/watch.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,168 +1,426 @@
|
|
|
1
|
-
# custom-elements-ts
|
|
2
|
-
|
|
3
|
-
[](https://coveralls.io/github/geocine/custom-elements-ts?branch=master)
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/custom-elements-ts)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1
|
+
# custom-elements-ts
|
|
2
|
+
|
|
3
|
+
[](https://coveralls.io/github/geocine/custom-elements-ts?branch=master)
|
|
4
|
+

|
|
5
|
+
[](https://www.npmjs.com/package/custom-elements-ts)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
Author native Web Components in TypeScript with a small set of decorators
|
|
9
|
+
(`@CustomElement`, `@Prop`, `@State`, `@Watch`, `@Listen`, `@Dispatch`,
|
|
10
|
+
`@Toggle`) plus a tiny `html` / `render()` runtime. **Zero dependencies.
|
|
11
|
+
Framework-free.**
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm install custom-elements-ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Live demos:** [geocine.github.io/custom-elements-ts](https://geocine.github.io/custom-elements-ts/) — counter, sprint board, install pill, and a live event log, all built with the library.
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [Plain HTML — no `render()` required](#plain-html--no-render-required)
|
|
23
|
+
- [Reactive components with `render()`](#reactive-components-with-render)
|
|
24
|
+
- [Template bindings](#template-bindings)
|
|
25
|
+
- [Decorators](#decorators)
|
|
26
|
+
- [@Prop()](#prop)
|
|
27
|
+
- [@State()](#state)
|
|
28
|
+
- [@Toggle()](#toggle)
|
|
29
|
+
- [@Dispatch()](#dispatch)
|
|
30
|
+
- [@Watch()](#watch)
|
|
31
|
+
- [@Listen()](#listen)
|
|
32
|
+
- [Project layout](#project-layout)
|
|
33
|
+
- [Running the demos](#running-the-demos)
|
|
34
|
+
- [Building](#building)
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
There are **two ways** to author a component, and they compose freely:
|
|
39
|
+
|
|
40
|
+
1. **Plain HTML, imperative updates.** Declare a `template` (or
|
|
41
|
+
`templateUrl`) and update the DOM yourself in `connectedCallback`,
|
|
42
|
+
`@Watch()` handlers, or `@Listen()` handlers. No `render()`. No
|
|
43
|
+
reactive runtime. **Use this when the DOM is mostly static** —
|
|
44
|
+
buttons, badges, panels, copy-to-clipboard pills, and so on.
|
|
45
|
+
2. **Reactive `render()` with the `html` helper.** Define `render()` and
|
|
46
|
+
the runtime re-renders for you on `@Prop()` / `@State()` / `@Toggle()`
|
|
47
|
+
changes. Use this for stateful components like dashboards, counters,
|
|
48
|
+
forms, and lists.
|
|
49
|
+
|
|
50
|
+
`render()` is **optional** — components without it pay zero runtime
|
|
51
|
+
cost beyond the decorators themselves.
|
|
52
|
+
|
|
53
|
+
### Plain HTML — no `render()` required
|
|
54
|
+
|
|
55
|
+
A small toast-firing "click to copy" pill, written entirely with a
|
|
56
|
+
static template and imperative DOM. This is exactly the pattern used by
|
|
57
|
+
`<cts-message>` on the [showcase page](https://geocine.github.io/custom-elements-ts/):
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import {
|
|
61
|
+
CustomElement,
|
|
62
|
+
Prop,
|
|
63
|
+
Listen,
|
|
64
|
+
Dispatch,
|
|
65
|
+
DispatchEmitter,
|
|
66
|
+
} from 'custom-elements-ts';
|
|
67
|
+
|
|
68
|
+
@CustomElement({
|
|
69
|
+
tag: 'cts-message',
|
|
70
|
+
template: `
|
|
71
|
+
<div class="row" role="button" tabindex="0">
|
|
72
|
+
<span class="prompt">$</span>
|
|
73
|
+
<code class="cmd"></code>
|
|
74
|
+
</div>
|
|
75
|
+
`,
|
|
76
|
+
styleUrl: './message.element.scss',
|
|
77
|
+
})
|
|
78
|
+
export class MessageElement extends HTMLElement {
|
|
79
|
+
@Prop() message!: string;
|
|
80
|
+
|
|
81
|
+
// Bubbling, composed CustomEvent — any ancestor can listen for it
|
|
82
|
+
// (e.g. a <cts-toast> at the document root).
|
|
83
|
+
@Dispatch('cts:toast') toast!: DispatchEmitter;
|
|
84
|
+
|
|
85
|
+
connectedCallback() {
|
|
86
|
+
// Imperative DOM update — no render() needed.
|
|
87
|
+
this.shadowRoot!.querySelector('.cmd')!.textContent = this.message;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Listen('click')
|
|
91
|
+
async handleClick() {
|
|
92
|
+
await navigator.clipboard.writeText(this.message);
|
|
93
|
+
this.toast.emit({
|
|
94
|
+
bubbles: true,
|
|
95
|
+
composed: true,
|
|
96
|
+
detail: { title: 'Copied to clipboard', message: this.message },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<!-- Drop it anywhere — React, Vue, Svelte, plain HTML — it just works -->
|
|
104
|
+
<cts-message message="npm install custom-elements-ts"></cts-message>
|
|
105
|
+
<script src="message.umd.js"></script>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
You can also keep markup in its own file with `templateUrl` and
|
|
109
|
+
`styleUrl`, exactly as you would with any other framework:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
@CustomElement({
|
|
113
|
+
tag: 'counter-element',
|
|
114
|
+
templateUrl: './counter-element.html',
|
|
115
|
+
styleUrl: './counter-element.scss',
|
|
116
|
+
})
|
|
117
|
+
export class CounterElement extends HTMLElement {
|
|
118
|
+
// Wire up DOM manually in connectedCallback / @Watch / @Listen.
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Reactive components with `render()`
|
|
123
|
+
|
|
124
|
+
Add a `render()` method that returns an `html` template literal and the
|
|
125
|
+
runtime takes care of efficient DOM updates whenever any
|
|
126
|
+
`@Prop()` / `@State()` / `@Toggle()` value changes:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { CustomElement, State, html } from 'custom-elements-ts';
|
|
130
|
+
|
|
131
|
+
@CustomElement({ tag: 'cts-counter' })
|
|
132
|
+
export class CounterElement extends HTMLElement {
|
|
133
|
+
@State() count = 0;
|
|
134
|
+
|
|
135
|
+
render() {
|
|
136
|
+
return html`<button @click=${this.increment}>Count: ${this.count}</button>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private increment() {
|
|
140
|
+
this.count++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`render()` output is mounted into the shadow root by default. Pass
|
|
146
|
+
`shadow: false` to render into the host element instead.
|
|
147
|
+
|
|
148
|
+
### Template bindings
|
|
149
|
+
|
|
150
|
+
When you do opt into `render()`, the `html` helper supports the common
|
|
151
|
+
binding forms used by render-based components:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
html`<p>${this.label}</p>`;
|
|
155
|
+
html`<p>${() => this.label}</p>`;
|
|
156
|
+
html`<button @click=${this.handleClick}></button>`;
|
|
157
|
+
html`<input .value=${this.value} />`;
|
|
158
|
+
html`<div title=${this.title}></div>`;
|
|
159
|
+
html`<ul>
|
|
160
|
+
${this.items.map((item) => html`<li>${item.label}</li>`)}
|
|
161
|
+
</ul>`;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Attribute bindings remove the attribute when the value is `false`,
|
|
165
|
+
`null`, or `undefined`. Event bindings replace old listeners when a
|
|
166
|
+
render supplies a new handler and are cleaned up automatically when the
|
|
167
|
+
rendered template is disposed.
|
|
168
|
+
|
|
169
|
+
## Decorators
|
|
170
|
+
|
|
171
|
+
| Decorator | Target | Parameters | Description |
|
|
172
|
+
| ----------- | -------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| @Prop() | property | - | custom attribute/properties; reflects primitive values (string, number, boolean) to attributes |
|
|
174
|
+
| @State() | property | - | private reactive state for render-based components; not reflected to attributes |
|
|
175
|
+
| @Toggle() | property | - | boolean attribute/properties based on the presence of the attribute; also accepts `"true"` and `"false"` |
|
|
176
|
+
| @Dispatch() | property | (event?) | declares a `CustomEvent` you can fire via the `.emit` method of its `DispatchEmitter` type. The `event` parameter sets the `CustomEvent` name |
|
|
177
|
+
| @Watch() | method | (property) | runs the method when `property` changes |
|
|
178
|
+
| @Listen() | method | (event, selector?) | listens for `event` on the host (or on `selector` inside the shadow tree) |
|
|
179
|
+
|
|
180
|
+
### @Prop()
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { CustomElement, Prop } from 'custom-elements-ts';
|
|
184
|
+
|
|
185
|
+
@CustomElement({
|
|
186
|
+
tag: 'todo-list',
|
|
187
|
+
...
|
|
188
|
+
})
|
|
189
|
+
export class TodoList extends HTMLElement {
|
|
190
|
+
@Prop() color: string;
|
|
191
|
+
@Prop() list: TodoItem[];
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Since `color` is a primitive type of `string` it can be accessed via
|
|
196
|
+
attributes and properties:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const element = document.querySelector('todo-list');
|
|
200
|
+
// accessing value via attribute
|
|
201
|
+
const attrValue = element.getAttribute('color');
|
|
202
|
+
// setting value via attribute
|
|
203
|
+
element.setAttribute('color', 'red');
|
|
204
|
+
|
|
205
|
+
// accessing value via property
|
|
206
|
+
const propertyValue = element.color;
|
|
207
|
+
// setting via property
|
|
208
|
+
element.color = 'red';
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`list` is a rich data type (objects or arrays) and functions/classes can
|
|
212
|
+
only be accessed/set via property — they are not reflected as
|
|
213
|
+
attributes:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// Functions and classes are not reflected to attributes
|
|
217
|
+
@Prop() onChange: (detail: any) => void;
|
|
218
|
+
@Prop() itemConstructor: { new(...args: any[]): any };
|
|
219
|
+
|
|
220
|
+
element.onChange = () => {};
|
|
221
|
+
// not reflected as attribute
|
|
222
|
+
console.log(element.getAttribute('on-change')); // null
|
|
223
|
+
|
|
224
|
+
class Foo {}
|
|
225
|
+
element.itemConstructor = Foo;
|
|
226
|
+
// not reflected as attribute
|
|
227
|
+
console.log(element.getAttribute('item-ctor')); // null
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Render-based components update after a real `@Prop()` value change.
|
|
231
|
+
Multiple prop and state changes inside the same synchronous turn are
|
|
232
|
+
batched into one render.
|
|
233
|
+
|
|
234
|
+
### @State()
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { CustomElement, State, Watch, html } from 'custom-elements-ts';
|
|
238
|
+
|
|
239
|
+
@CustomElement({ tag: 'profile-card' })
|
|
240
|
+
export class ProfileCard extends HTMLElement {
|
|
241
|
+
@State() user = { name: 'Ada' };
|
|
242
|
+
@State() items = [{ label: 'One' }];
|
|
243
|
+
|
|
244
|
+
@Watch('user')
|
|
245
|
+
userChanged(value: { old: unknown; new: unknown }) {
|
|
246
|
+
console.log(value.new);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
render() {
|
|
250
|
+
return html`
|
|
251
|
+
<strong>${this.user.name}</strong>
|
|
252
|
+
<ul>
|
|
253
|
+
${this.items.map((item) => html`<li>${item.label}</li>`)}
|
|
254
|
+
</ul>
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
State is internal to the element: it is not reflected to attributes and
|
|
261
|
+
is not included in `observedAttributes`. Plain objects and arrays
|
|
262
|
+
assigned to state are deeply proxied, so nested mutations such as
|
|
263
|
+
`this.user.name = 'Grace'`, `this.items.push(...)`, and
|
|
264
|
+
`this.items[0].label = 'Updated'` schedule a render.
|
|
265
|
+
|
|
266
|
+
Only plain objects and arrays are proxied. Functions, class
|
|
267
|
+
constructors, DOM nodes, `Date`, `Map`, `Set`, `WeakMap`, and `WeakSet`
|
|
268
|
+
are left as-is — reassign those values to trigger a render.
|
|
269
|
+
|
|
270
|
+
### @Toggle()
|
|
271
|
+
|
|
272
|
+
Toggle attributes work the same way as HTML boolean attributes as
|
|
273
|
+
defined by [W3C](http://www.w3.org/TR/2008/WD-html5-20080610/semantics.html#boolean)
|
|
274
|
+
for the most part. We changed a few things to overcome confusion. Check
|
|
275
|
+
the table below for reference:
|
|
276
|
+
|
|
277
|
+
| Markup | `disabled` | Description |
|
|
278
|
+
| ----------------------------- | ---------- | -------------------------------------------------------------------- |
|
|
279
|
+
| `<c-input />` | false | Follows W3C standard |
|
|
280
|
+
| `<c-input disabled/>` | true | Follows W3C standard |
|
|
281
|
+
| `<c-input disabled="true"/>` | true | Follows W3C standard |
|
|
282
|
+
| `<c-input disabled="asd"/>` | false | `false` since `asd` does not evaluate to a valid boolean |
|
|
283
|
+
| `<c-input disabled="false"/>` | false | `false` since the boolean `false` converted to a string is `"false"` |
|
|
284
|
+
| `<c-input disabled="true"/>` | true | `true` since the boolean `true` converted to a string is `"true"` |
|
|
285
|
+
|
|
286
|
+
### @Dispatch()
|
|
287
|
+
|
|
288
|
+
**Creating a custom event**
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { CustomElement, Dispatch, DispatchEmitter } from 'custom-elements-ts';
|
|
292
|
+
|
|
293
|
+
...
|
|
294
|
+
export class TodoList extends HTMLElement {
|
|
295
|
+
// Creating a CustomEvent
|
|
296
|
+
// custom event name will be `on.change`
|
|
297
|
+
@Dispatch() onChange: DispatchEmitter;
|
|
298
|
+
|
|
299
|
+
// Creating a CustomEvent with custom name `ce.select`
|
|
300
|
+
@Dispatch('ce.select') onSelect: DispatchEmitter;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Triggering the custom event** from the example above:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
triggerOnChange() {
|
|
308
|
+
// adding more data to the event object
|
|
309
|
+
this.onChange.emit({ detail: 'event changed' });
|
|
310
|
+
this.onSelect.emit({ detail: 'select triggered' });
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
For events that need to cross the shadow boundary (e.g. so a parent or
|
|
315
|
+
the document can listen) opt into bubbling and composed delivery on the
|
|
316
|
+
`emit()` call:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
this.onChange.emit({
|
|
320
|
+
bubbles: true,
|
|
321
|
+
composed: true,
|
|
322
|
+
detail: { count: this.count },
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### @Watch()
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { CustomElement, Prop, Watch } from 'custom-elements-ts';
|
|
330
|
+
|
|
331
|
+
...
|
|
332
|
+
export class TodoList extends HTMLElement {
|
|
333
|
+
@Prop() color: string;
|
|
334
|
+
|
|
335
|
+
@Watch('color')
|
|
336
|
+
colorChanged() {
|
|
337
|
+
// trigger when color property color changes
|
|
338
|
+
// either via property or attribute
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### @Listen()
|
|
344
|
+
|
|
345
|
+
`@Listen()` takes an `event` and an optional `selector`. `event` is any
|
|
346
|
+
valid JavaScript event. `selector` is anything that works with
|
|
347
|
+
[`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { CustomElement, Listen } from 'custom-elements-ts';
|
|
351
|
+
|
|
352
|
+
...
|
|
353
|
+
export class TodoList extends HTMLElement {
|
|
354
|
+
@Listen('click')
|
|
355
|
+
elementClicked() {
|
|
356
|
+
// triggers when the element is clicked
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@Listen('click', 'a')
|
|
360
|
+
anchorClicked() {
|
|
361
|
+
// triggers when an `a` inside the element is clicked
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Project layout
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
src/ # the library — decorators + html/render runtime
|
|
370
|
+
demos/
|
|
371
|
+
counter/ # @State() + render() — single counter card
|
|
372
|
+
todo-dashboard/ # composed elements: stats, filters, items, parent
|
|
373
|
+
site/ # the showcase landing page that hosts every live demo
|
|
374
|
+
tests/ # vitest specs for the runtime + decorators
|
|
375
|
+
tools/ # build / start / bundle scripts
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The site demo (`demos/site`) imports the counter and the todo-dashboard
|
|
379
|
+
elements from sibling demo folders, so the showcase page on
|
|
380
|
+
`localhost:3000` runs the **real** components — not screenshots — and
|
|
381
|
+
includes a `<cts-event-log>` panel that subscribes to their bubbling
|
|
382
|
+
`CustomEvent`s in real time.
|
|
383
|
+
|
|
384
|
+
## Running the demos
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
npm start <element-name>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Element | Highlights |
|
|
391
|
+
| ---------------- | ---------------------------------------------------------------- |
|
|
392
|
+
| `site` | Showcase landing page (hero, code preview, live demos, OG graph) |
|
|
393
|
+
| `counter` | `@State()` + `@Watch()` + `@Dispatch()` on a single card |
|
|
394
|
+
| `todo-dashboard` | Parent / child composition with deeply proxied state |
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
npm start site
|
|
398
|
+
npm start counter
|
|
399
|
+
npm start todo-dashboard
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The dev server runs on `http://localhost:3000` and live-reloads on
|
|
403
|
+
TypeScript / SCSS / HTML changes.
|
|
404
|
+
|
|
405
|
+
## Building
|
|
406
|
+
|
|
407
|
+
### Building a demo
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
npm run build <element-name>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
For a minified bundle:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
npm run build -- <element-name> --prod
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Building the library (publish artifacts)
|
|
420
|
+
|
|
421
|
+
Builds the library from `src/index.ts` into `dist/` (UMD + ESM builds
|
|
422
|
+
with typings):
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
npm run bundle
|
|
426
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<button id="count"></button>
|