snice 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 +576 -0
- package/dist/snice.js +980 -0
- package/dist/snice.umd.cjs +8 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# Snice
|
|
2
|
+
|
|
3
|
+
A lightweight TypeScript framework for building web components with decorators and routing.
|
|
4
|
+
|
|
5
|
+
## Core Philosophy: Imperative, Not Reactive
|
|
6
|
+
|
|
7
|
+
Snice takes an **imperative approach** to web components. Unlike reactive frameworks that automatically re-render when data changes, Snice components:
|
|
8
|
+
|
|
9
|
+
- **Render once** when connected to the DOM
|
|
10
|
+
- **Never re-render** automatically
|
|
11
|
+
- Require **explicit method calls** to update visual state
|
|
12
|
+
- Give you **full control** over when and how updates happen
|
|
13
|
+
|
|
14
|
+
This approach eliminates the complexity of reactive systems, virtual DOM diffing, and unexpected re-renders. You write straightforward code that does exactly what you tell it to do.
|
|
15
|
+
|
|
16
|
+
## Core Concepts
|
|
17
|
+
|
|
18
|
+
Snice provides a clear separation of concerns through decorators:
|
|
19
|
+
|
|
20
|
+
### Component Decorators
|
|
21
|
+
- **`@element`** - Creates custom HTML elements with encapsulated visual behavior and styling
|
|
22
|
+
- **`@controller`** - Handles data fetching, server communication, and business logic separate from visual components
|
|
23
|
+
- **`@page`** - Sets up routing and navigation between different views
|
|
24
|
+
|
|
25
|
+
### Property & Query Decorators
|
|
26
|
+
- **`@property`** - Declares reactive properties that can reflect to attributes
|
|
27
|
+
- **`@query`** - Queries a single element from shadow DOM
|
|
28
|
+
- **`@queryAll`** - Queries multiple elements from shadow DOM
|
|
29
|
+
|
|
30
|
+
### Event Decorators
|
|
31
|
+
- **`@on`** - Listens for events on elements
|
|
32
|
+
- **`@dispatch`** - Dispatches custom events after method execution
|
|
33
|
+
- **`@channel`** - Enables bidirectional communication between elements and controllers
|
|
34
|
+
|
|
35
|
+
This separation keeps your components focused: elements handle presentation, controllers manage data, and pages define navigation.
|
|
36
|
+
|
|
37
|
+
## Basic Component
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { element } from 'snice';
|
|
41
|
+
|
|
42
|
+
@element('my-button')
|
|
43
|
+
class MyButton extends HTMLElement {
|
|
44
|
+
html() {
|
|
45
|
+
return `<button>Click me</button>`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it. Your component renders when added to the DOM:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<my-button></my-button>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The Imperative Way
|
|
57
|
+
|
|
58
|
+
In Snice, updates are explicit. Components expose methods that controllers or other components call to update state:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { element, property, query } from 'snice';
|
|
62
|
+
|
|
63
|
+
@element('counter-display')
|
|
64
|
+
class CounterDisplay extends HTMLElement {
|
|
65
|
+
@property({ type: Number })
|
|
66
|
+
count = 0;
|
|
67
|
+
|
|
68
|
+
@query('.count')
|
|
69
|
+
countElement?: HTMLElement;
|
|
70
|
+
|
|
71
|
+
@query('.status')
|
|
72
|
+
statusElement?: HTMLElement;
|
|
73
|
+
|
|
74
|
+
html() {
|
|
75
|
+
// Renders ONCE - no automatic re-rendering
|
|
76
|
+
return `
|
|
77
|
+
<div class="counter">
|
|
78
|
+
<span class="count">${this.count}</span>
|
|
79
|
+
<span class="status">Ready</span>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Imperative update methods - YOU control when updates happen
|
|
85
|
+
setCount(newCount: number) {
|
|
86
|
+
this.count = newCount;
|
|
87
|
+
if (this.countElement) {
|
|
88
|
+
this.countElement.textContent = String(newCount);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setStatus(status: string) {
|
|
93
|
+
if (this.statusElement) {
|
|
94
|
+
this.statusElement.textContent = status;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
increment() {
|
|
99
|
+
this.setCount(this.count + 1);
|
|
100
|
+
this.setStatus('Incremented!');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Key Points:**
|
|
106
|
+
- The `html()` method runs **once** when the element connects
|
|
107
|
+
- Updates happen through **explicit method calls** like `setCount()`
|
|
108
|
+
- You have **full control** over what updates and when
|
|
109
|
+
- No surprises, no magic, no hidden re-renders
|
|
110
|
+
|
|
111
|
+
## Properties
|
|
112
|
+
|
|
113
|
+
Properties can be reflected to attributes but do NOT trigger re-renders. The HTML is rendered once when the element connects to the DOM. Use properties for initial configuration.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { element, property } from 'snice';
|
|
117
|
+
|
|
118
|
+
@element('user-card')
|
|
119
|
+
class UserCard extends HTMLElement {
|
|
120
|
+
@property({ reflect: true })
|
|
121
|
+
name = 'Anonymous';
|
|
122
|
+
|
|
123
|
+
@property({ reflect: true })
|
|
124
|
+
role = 'User';
|
|
125
|
+
|
|
126
|
+
@property({ type: Boolean })
|
|
127
|
+
verified = false;
|
|
128
|
+
|
|
129
|
+
html() {
|
|
130
|
+
// This renders ONCE with the initial property values
|
|
131
|
+
return `
|
|
132
|
+
<div class="card">
|
|
133
|
+
<h3>${this.name}</h3>
|
|
134
|
+
<span class="role">${this.role}</span>
|
|
135
|
+
${this.verified ? '<span class="badge">✓ Verified</span>' : ''}
|
|
136
|
+
</div>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use it with attributes:
|
|
143
|
+
```html
|
|
144
|
+
<user-card name="Jane Doe" role="Admin" verified></user-card>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## Queries
|
|
149
|
+
|
|
150
|
+
Query single elements with `@query`:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { element, query } from 'snice';
|
|
154
|
+
|
|
155
|
+
@element('my-form')
|
|
156
|
+
class MyForm extends HTMLElement {
|
|
157
|
+
@query('input')
|
|
158
|
+
input?: HTMLInputElement;
|
|
159
|
+
|
|
160
|
+
html() {
|
|
161
|
+
return `<input type="text" />`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getValue() {
|
|
165
|
+
return this.input?.value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Query multiple elements with `@queryAll`:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { element, queryAll } from 'snice';
|
|
174
|
+
|
|
175
|
+
@element('checkbox-group')
|
|
176
|
+
class CheckboxGroup extends HTMLElement {
|
|
177
|
+
@queryAll('input[type="checkbox"]')
|
|
178
|
+
checkboxes?: NodeListOf<HTMLInputElement>;
|
|
179
|
+
|
|
180
|
+
html() {
|
|
181
|
+
return `
|
|
182
|
+
<input type="checkbox" value="option1" />
|
|
183
|
+
<input type="checkbox" value="option2" />
|
|
184
|
+
<input type="checkbox" value="option3" />
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getSelectedValues() {
|
|
189
|
+
if (!this.checkboxes) return [];
|
|
190
|
+
return Array.from(this.checkboxes)
|
|
191
|
+
.filter(cb => cb.checked)
|
|
192
|
+
.map(cb => cb.value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Events
|
|
198
|
+
|
|
199
|
+
Listen for events with `@on`:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { element, on } from 'snice';
|
|
203
|
+
|
|
204
|
+
@element('my-clicker')
|
|
205
|
+
class MyClicker extends HTMLElement {
|
|
206
|
+
html() {
|
|
207
|
+
return `<button>Click me</button>`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@on('click', 'button')
|
|
211
|
+
handleClick() {
|
|
212
|
+
console.log('Button clicked!');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Dispatching Events
|
|
218
|
+
|
|
219
|
+
Automatically dispatch custom events with `@dispatch`:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { element, dispatch, on } from 'snice';
|
|
223
|
+
|
|
224
|
+
@element('toggle-switch')
|
|
225
|
+
class ToggleSwitch extends HTMLElement {
|
|
226
|
+
private isOn = false;
|
|
227
|
+
|
|
228
|
+
@query('.toggle')
|
|
229
|
+
toggleButton?: HTMLElement;
|
|
230
|
+
|
|
231
|
+
html() {
|
|
232
|
+
return `
|
|
233
|
+
<button class="toggle">
|
|
234
|
+
<span class="slider"></span>
|
|
235
|
+
</button>
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
css() {
|
|
240
|
+
return `...`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@on('click', '.toggle')
|
|
244
|
+
@dispatch('toggled')
|
|
245
|
+
toggle() {
|
|
246
|
+
this.isOn = !this.isOn;
|
|
247
|
+
this.toggleButton?.classList.toggle('on', this.isOn);
|
|
248
|
+
|
|
249
|
+
// Return value becomes event detail
|
|
250
|
+
return { on: this.isOn };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The `@dispatch` decorator:
|
|
256
|
+
- Dispatches after the method completes
|
|
257
|
+
- Uses the return value as the event detail
|
|
258
|
+
- Works with async methods
|
|
259
|
+
- Bubbles by default
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// With options from EventInit
|
|
263
|
+
@dispatch('my-event', { bubbles: false, cancelable: true })
|
|
264
|
+
|
|
265
|
+
// Don't dispatch if method returns undefined
|
|
266
|
+
@dispatch('maybe-data', { dispatchOnUndefined: false })
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Styling
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
@element('styled-card')
|
|
273
|
+
class StyledCard extends HTMLElement {
|
|
274
|
+
html() {
|
|
275
|
+
return `<div class="card">Hello</div>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
css() {
|
|
279
|
+
return `
|
|
280
|
+
.card {
|
|
281
|
+
padding: 20px;
|
|
282
|
+
background: #f0f0f0;
|
|
283
|
+
border-radius: 8px;
|
|
284
|
+
}
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
CSS is automatically scoped to your component.
|
|
291
|
+
|
|
292
|
+
## Routing
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { Router } from 'snice';
|
|
296
|
+
|
|
297
|
+
const router = Router({
|
|
298
|
+
target: '#app',
|
|
299
|
+
routing_type: 'hash'
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const { page, navigate, initialize } = router;
|
|
303
|
+
|
|
304
|
+
@page({ tag: 'home-page', routes: ['/'] })
|
|
305
|
+
class HomePage extends HTMLElement {
|
|
306
|
+
html() {
|
|
307
|
+
return `<h1>Home</h1>`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@page({ tag: 'about-page', routes: ['/about'] })
|
|
312
|
+
class AboutPage extends HTMLElement {
|
|
313
|
+
html() {
|
|
314
|
+
return `<h1>About</h1>`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Start the router
|
|
319
|
+
initialize();
|
|
320
|
+
|
|
321
|
+
// Navigate programmatically
|
|
322
|
+
navigate('/about');
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Controllers (Data Fetching)
|
|
326
|
+
|
|
327
|
+
Controllers handle server communication separately from visual components:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { controller, element } from 'snice';
|
|
331
|
+
|
|
332
|
+
@controller('user-controller')
|
|
333
|
+
class UserController {
|
|
334
|
+
element: HTMLElement | null = null;
|
|
335
|
+
|
|
336
|
+
async attach(element: HTMLElement) {
|
|
337
|
+
const response = await fetch('/api/users');
|
|
338
|
+
const users = await response.json();
|
|
339
|
+
(element as any).setUsers(users);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async detach() {
|
|
343
|
+
// Cleanup
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@element('user-list')
|
|
348
|
+
class UserList extends HTMLElement {
|
|
349
|
+
users: any[] = [];
|
|
350
|
+
|
|
351
|
+
html() {
|
|
352
|
+
return `
|
|
353
|
+
<ul>
|
|
354
|
+
${this.users.map(u => `<li>${u.name}</li>`).join('')}
|
|
355
|
+
</ul>
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
setUsers(users: any[]) {
|
|
360
|
+
this.users = users;
|
|
361
|
+
this.innerHTML = this.html();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Use it:
|
|
367
|
+
|
|
368
|
+
```html
|
|
369
|
+
<user-list controller="user-controller"></user-list>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Channels
|
|
373
|
+
|
|
374
|
+
Request/response between elements and controllers:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// --- components/user-card.ts
|
|
378
|
+
@element('user-card')
|
|
379
|
+
class UserCard extends HTMLElement {
|
|
380
|
+
@channel('get-data')
|
|
381
|
+
async *fetchUserData() {
|
|
382
|
+
// Yield sends request, await waits for response
|
|
383
|
+
const user = await (yield { id: 123 });
|
|
384
|
+
return user;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async loadUser() {
|
|
388
|
+
const user = await this.fetchUserData();
|
|
389
|
+
console.log(user); // { name: 'Alice' }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- controllers/user-controller.ts
|
|
394
|
+
@controller('user-controller')
|
|
395
|
+
class UserController {
|
|
396
|
+
@channel('get-data')
|
|
397
|
+
handleGetData(request) {
|
|
398
|
+
console.log(request); // { id: 123 }
|
|
399
|
+
return { name: 'Alice' };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Complete Example
|
|
405
|
+
|
|
406
|
+
Here's how the pieces work together - a generic display component filled by a controller:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { element, controller, query } from 'snice';
|
|
410
|
+
|
|
411
|
+
// Generic card component - purely visual
|
|
412
|
+
@element('info-card')
|
|
413
|
+
class InfoCard extends HTMLElement {
|
|
414
|
+
@query('.title')
|
|
415
|
+
titleElement?: HTMLElement;
|
|
416
|
+
|
|
417
|
+
@query('.content')
|
|
418
|
+
contentElement?: HTMLElement;
|
|
419
|
+
|
|
420
|
+
@query('.footer')
|
|
421
|
+
footerElement?: HTMLElement;
|
|
422
|
+
|
|
423
|
+
html() {
|
|
424
|
+
return `
|
|
425
|
+
<div class="card">
|
|
426
|
+
<h2 class="title">Loading...</h2>
|
|
427
|
+
<div class="content">
|
|
428
|
+
<div class="skeleton"></div>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="footer"></div>
|
|
431
|
+
</div>
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
css() {
|
|
436
|
+
return `
|
|
437
|
+
.card {
|
|
438
|
+
border: 1px solid #ddd;
|
|
439
|
+
border-radius: 8px;
|
|
440
|
+
padding: 20px;
|
|
441
|
+
max-width: 400px;
|
|
442
|
+
}
|
|
443
|
+
.title {
|
|
444
|
+
margin: 0 0 15px 0;
|
|
445
|
+
color: #333;
|
|
446
|
+
}
|
|
447
|
+
.content {
|
|
448
|
+
min-height: 60px;
|
|
449
|
+
}
|
|
450
|
+
.skeleton {
|
|
451
|
+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
452
|
+
height: 20px;
|
|
453
|
+
border-radius: 4px;
|
|
454
|
+
animation: loading 1.5s infinite;
|
|
455
|
+
}
|
|
456
|
+
.footer {
|
|
457
|
+
margin-top: 15px;
|
|
458
|
+
font-size: 0.9em;
|
|
459
|
+
color: #666;
|
|
460
|
+
}
|
|
461
|
+
@keyframes loading {
|
|
462
|
+
0% { background-position: -200px 0; }
|
|
463
|
+
100% { background-position: 200px 0; }
|
|
464
|
+
}
|
|
465
|
+
`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Methods the controller can call to update the display
|
|
469
|
+
setTitle(title: string) {
|
|
470
|
+
if (this.titleElement) {
|
|
471
|
+
this.titleElement.textContent = title;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setContent(html: string) {
|
|
476
|
+
if (this.contentElement) {
|
|
477
|
+
this.contentElement.innerHTML = html;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
setFooter(text: string) {
|
|
482
|
+
if (this.footerElement) {
|
|
483
|
+
this.footerElement.textContent = text;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Controller that fetches and populates data
|
|
489
|
+
@controller('weather-controller')
|
|
490
|
+
class WeatherController {
|
|
491
|
+
element: HTMLElement | null = null;
|
|
492
|
+
|
|
493
|
+
async attach(element: HTMLElement) {
|
|
494
|
+
this.element = element;
|
|
495
|
+
|
|
496
|
+
// Simulate fetching weather data
|
|
497
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
498
|
+
|
|
499
|
+
const weatherData = {
|
|
500
|
+
location: 'San Francisco',
|
|
501
|
+
temp: '72°F',
|
|
502
|
+
conditions: 'Partly Cloudy',
|
|
503
|
+
humidity: '65%'
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Update the generic card with specific data
|
|
507
|
+
(element as any).setTitle(weatherData.location);
|
|
508
|
+
(element as any).setContent(`
|
|
509
|
+
<p><strong>${weatherData.temp}</strong></p>
|
|
510
|
+
<p>${weatherData.conditions}</p>
|
|
511
|
+
<p>Humidity: ${weatherData.humidity}</p>
|
|
512
|
+
`);
|
|
513
|
+
(element as any).setFooter('Updated just now');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Use the same card with different controllers:
|
|
519
|
+
```html
|
|
520
|
+
<!-- Weather widget -->
|
|
521
|
+
<info-card controller="weather-controller"></info-card>
|
|
522
|
+
|
|
523
|
+
<!-- Stock widget - same card, different controller -->
|
|
524
|
+
<info-card controller="stock-controller"></info-card>
|
|
525
|
+
|
|
526
|
+
<!-- News widget - same card, different controller -->
|
|
527
|
+
<info-card controller="news-controller"></info-card>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Decorator Reference
|
|
531
|
+
|
|
532
|
+
### Component Decorators
|
|
533
|
+
|
|
534
|
+
| Decorator | Purpose | Example |
|
|
535
|
+
|-----------|---------|---------|
|
|
536
|
+
| `@element(tagName)` | Defines a custom HTML element | `@element('my-button')` |
|
|
537
|
+
| `@controller(name)` | Creates a data controller | `@controller('user-controller')` |
|
|
538
|
+
| `@page(options)` | Defines a routable page component | `@page({ tag: 'home-page', routes: ['/'] })` |
|
|
539
|
+
|
|
540
|
+
### Property Decorators
|
|
541
|
+
|
|
542
|
+
| Decorator | Purpose | Example |
|
|
543
|
+
|-----------|---------|---------|
|
|
544
|
+
| `@property(options)` | Declares a property that can reflect to attributes | `@property({ type: Boolean, reflect: true })` |
|
|
545
|
+
| `@query(selector)` | Queries a single element from shadow DOM | `@query('.button')` |
|
|
546
|
+
| `@queryAll(selector)` | Queries multiple elements from shadow DOM | `@queryAll('input[type="checkbox"]')` |
|
|
547
|
+
|
|
548
|
+
### Event Decorators
|
|
549
|
+
|
|
550
|
+
| Decorator | Purpose | Example |
|
|
551
|
+
|-----------|---------|---------|
|
|
552
|
+
| `@on(event, selector?)` | Listens for DOM events | `@on('click', '.button')` |
|
|
553
|
+
| `@dispatch(eventName, options?)` | Dispatches custom events after method execution | `@dispatch('data-updated')` |
|
|
554
|
+
| `@channel(name, options?)` | Enables request/response communication | `@channel('fetch-data')` |
|
|
555
|
+
|
|
556
|
+
### Property Options
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
interface PropertyOptions {
|
|
560
|
+
type?: typeof String | typeof Number | typeof Boolean; // Type converter
|
|
561
|
+
reflect?: boolean; // Reflect property to attribute
|
|
562
|
+
attribute?: string; // Custom attribute name
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Dispatch Options
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
interface DispatchOptions extends EventInit {
|
|
570
|
+
dispatchOnUndefined?: boolean; // Whether to dispatch when method returns undefined
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## License
|
|
575
|
+
|
|
576
|
+
MIT
|