@reactive-web-components/rwc 2.63.10 → 2.64.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
CHANGED
|
@@ -1,1388 +1,206 @@
|
|
|
1
|
-
# Reactive Web Components (RWC)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
count
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
count.
|
|
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
|
-
console.log(
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
name.set('Jane'); // userData doesn't update
|
|
209
|
-
age.set(30); // userData doesn't update
|
|
210
|
-
city.set('SPB'); // userData updates to ['Jane', 30, 'SPB']
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
**Application:**
|
|
214
|
-
- Synchronizing related data
|
|
215
|
-
- Creating composite objects from multiple sources
|
|
216
|
-
- Waiting for all dependencies to update before executing actions
|
|
217
|
-
|
|
218
|
-
#### combineLatest
|
|
219
|
-
|
|
220
|
-
Combines multiple signals into one that updates whenever any of the source signals receives a new value. Unlike `forkJoin`, which waits for all signals to update, `combineLatest` immediately updates when any signal changes.
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
import { combineLatest, signal } from '@shared/utils';
|
|
224
|
-
|
|
225
|
-
const name = signal('John');
|
|
226
|
-
const age = signal(25);
|
|
227
|
-
const city = signal('Moscow');
|
|
228
|
-
|
|
229
|
-
const userData = combineLatest(name, age, city);
|
|
230
|
-
// userData() returns ['John', 25, 'Moscow']
|
|
231
|
-
|
|
232
|
-
name.set('Jane'); // userData immediately updates to ['Jane', 25, 'Moscow']
|
|
233
|
-
age.set(30); // userData immediately updates to ['Jane', 30, 'Moscow']
|
|
234
|
-
city.set('SPB'); // userData immediately updates to ['Jane', 30, 'SPB']
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Application:**
|
|
238
|
-
- Real-time synchronization of multiple data sources
|
|
239
|
-
- Creating reactive computed values from multiple signals
|
|
240
|
-
- Updating UI immediately when any dependency changes
|
|
241
|
-
|
|
242
|
-
**Differences between `forkJoin` and `combineLatest`:**
|
|
243
|
-
- **`forkJoin`** — waits for all signals to update before emitting a new value. Useful when you need all values to be updated together.
|
|
244
|
-
- **`combineLatest`** — emits a new value immediately when any signal changes. Useful for real-time updates and reactive computations.
|
|
245
|
-
|
|
246
|
-
#### firstUpdate
|
|
247
|
-
|
|
248
|
-
Executes a callback after the first update of a reactive signal. Useful for performing one-time actions when a signal receives its first non-initial value.
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
import { firstUpdate, signal } from '@shared/utils';
|
|
252
|
-
|
|
253
|
-
const userSignal = signal(null);
|
|
254
|
-
|
|
255
|
-
// This callback will be called only once when userSignal is first updated
|
|
256
|
-
firstUpdate(userSignal, (user) => {
|
|
257
|
-
console.log('User loaded for the first time:', user);
|
|
258
|
-
// Perform initialization logic
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Later, when userSignal is updated:
|
|
262
|
-
userSignal.set({ name: 'John', age: 30 }); // Callback executes
|
|
263
|
-
userSignal.set({ name: 'Jane', age: 25 }); // Callback does NOT execute again
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
**Application:**
|
|
267
|
-
- Performing one-time initialization when data first arrives
|
|
268
|
-
- Triggering side effects only on the first update
|
|
269
|
-
- Handling initial data loading scenarios
|
|
270
|
-
|
|
271
|
-
### Function as Child Content (recommended style for dynamic lists and conditional render)
|
|
272
|
-
|
|
273
|
-
Functions passed as child content to `el` or `customEl` are automatically converted to reactive content. This allows convenient creation of dynamic content that will update when dependent signals change. The content function receives context (a reference to its component) as the first argument.
|
|
274
|
-
|
|
275
|
-
**Example: dynamic list with context**
|
|
276
|
-
```typescript
|
|
277
|
-
const items = signal(['Item 1', 'Item 2']);
|
|
278
|
-
div(
|
|
279
|
-
ul(
|
|
280
|
-
(self) => {
|
|
281
|
-
console.log('self!!!', self); // self - component reference
|
|
282
|
-
return items().map(item => li(item));
|
|
283
|
-
}
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
// When items changes, the entire list will be re-rendered
|
|
287
|
-
items.set(['Item 1', 'Item 2', 'Item 3']);
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Example: conditional render with context**
|
|
291
|
-
```typescript
|
|
292
|
-
div(
|
|
293
|
-
(self) => {
|
|
294
|
-
console.log('self!!!', self);
|
|
295
|
-
return when(signal(true), () => button('test-when-signal'));
|
|
296
|
-
}
|
|
297
|
-
)
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
**Best practice:**
|
|
301
|
-
- For dynamic rendering, use functions as child content instead of signalComponent
|
|
302
|
-
- For simple cases (text, attributes), use rs or other reactive primitives
|
|
303
|
-
- For complex lists with conditional logic, use functions as child content
|
|
304
|
-
- Use context (self) to access component properties and methods inside the content function
|
|
305
|
-
|
|
306
|
-
## Components
|
|
307
|
-
|
|
308
|
-
### Creating a Component
|
|
309
|
-
|
|
310
|
-
To declare a component, use classes with decorators. This provides strict typing, support for reactive props, events, providers, injections, and lifecycle hooks.
|
|
311
|
-
|
|
312
|
-
**Inside a component, it's recommended to use element factory functions** (`div`, `button`, `input`, etc.) from the factory (`@shared/utils/html-fabric/fabric`). This ensures strict typing, autocomplete, and consistent code style.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
#### Example: Class Component with props and event
|
|
316
|
-
|
|
317
|
-
```typescript
|
|
318
|
-
@component('test-decorator-component')
|
|
319
|
-
export class TestDecoratorComponent extends BaseElement {
|
|
320
|
-
@property()
|
|
321
|
-
testProp = signal<string>('Hello from Decorator!');
|
|
322
|
-
|
|
323
|
-
@event()
|
|
324
|
-
onCustomEvent = newEventEmitter<string>();
|
|
325
|
-
|
|
326
|
-
render() {
|
|
327
|
-
this.onCustomEvent('test value');
|
|
328
|
-
return div(rs`Title: ${this.testProp()}`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
export const TestDecoratorComponentComp = useCustomComponent(TestDecoratorComponent);
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
#### Brief on parameters:
|
|
335
|
-
|
|
336
|
-
- **@property** — a signal field that automatically syncs with an attribute.
|
|
337
|
-
- **@event** — an event field that emits custom events.
|
|
338
|
-
- **render** — a method that returns the component template.
|
|
339
|
-
- **@component** — registers a custom element with the given selector.
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
**Best practice:**
|
|
344
|
-
- All props/state/providers/injects — only signals (`ReactiveSignal<T>`)
|
|
345
|
-
- All events — only through `EventEmitter<T>`
|
|
346
|
-
- Use attributes to pass props
|
|
347
|
-
|
|
348
|
-
### Lifecycle
|
|
349
|
-
|
|
350
|
-
**Available hooks:**
|
|
351
|
-
- `onInit`, `onBeforeRender`, `onAfterRender`, `onConnected`, `onDisconnected`, `onAttributeChanged`
|
|
352
|
-
|
|
353
|
-
**Example:**
|
|
354
|
-
```typescript
|
|
355
|
-
@component('logger-component')
|
|
356
|
-
export class LoggerComponent extends BaseElement {
|
|
357
|
-
connectedCallback() {
|
|
358
|
-
super.connectedCallback?.();
|
|
359
|
-
console.log('connected');
|
|
360
|
-
}
|
|
361
|
-
disconnectedCallback() {
|
|
362
|
-
super.disconnectedCallback?.();
|
|
363
|
-
console.log('disconnected');
|
|
364
|
-
}
|
|
365
|
-
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
366
|
-
super.attributeChangedCallback?.(name, oldValue, newValue);
|
|
367
|
-
console.log(name, oldValue, newValue);
|
|
368
|
-
}
|
|
369
|
-
render() {
|
|
370
|
-
return div(rs`Logger`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
export const LoggerComponentComp = useCustomComponent(LoggerComponent);
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Events
|
|
377
|
-
|
|
378
|
-
**Type:**
|
|
379
|
-
```typescript
|
|
380
|
-
interface EventEmitter<T> {
|
|
381
|
-
(value: T | ReactiveSignal<T>): void; // can pass a signal — event will emit reactively
|
|
382
|
-
oldValue: null;
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
**Example:**
|
|
387
|
-
```typescript
|
|
388
|
-
@component('counter')
|
|
389
|
-
export class Counter extends BaseElement {
|
|
390
|
-
@property()
|
|
391
|
-
count = signal(0);
|
|
392
|
-
|
|
393
|
-
@event()
|
|
394
|
-
onCountChange = newEventEmitter<number>();
|
|
395
|
-
|
|
396
|
-
render() {
|
|
397
|
-
return button({
|
|
398
|
-
listeners: {
|
|
399
|
-
click: () => {
|
|
400
|
-
this.count.update(v => v + 1);
|
|
401
|
-
// one-time emit with value
|
|
402
|
-
this.onCountChange(this.count());
|
|
403
|
-
// or reactive emit: on subsequent count changes, event will emit automatically
|
|
404
|
-
// this.onCountChange(this.count);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}, rs`Count: ${this.count()}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
export const CounterComp = useCustomComponent(Counter);
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### Context (providers/injects)
|
|
414
|
-
|
|
415
|
-
**Example:**
|
|
416
|
-
```typescript
|
|
417
|
-
const ThemeContext = 'theme';
|
|
418
|
-
|
|
419
|
-
@component('theme-provider')
|
|
420
|
-
export class ThemeProvider extends BaseElement {
|
|
421
|
-
providers = { [ThemeContext]: signal('dark') };
|
|
422
|
-
render() {
|
|
423
|
-
return div(slot({ attributes: { name: 'tab-item' } }));
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
@component('theme-consumer')
|
|
428
|
-
export class ThemeConsumer extends BaseElement {
|
|
429
|
-
theme = this.inject<string>(ThemeContext); // Get context signal once outside render
|
|
430
|
-
render() {
|
|
431
|
-
return div(rs`Theme: ${this.theme}`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
@component('app-root')
|
|
436
|
-
export class AppRoot extends BaseElement {
|
|
437
|
-
render() {
|
|
438
|
-
return useCustomComponent(ThemeProvider)(
|
|
439
|
-
useCustomComponent(ThemeConsumer)
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Class Components and Decorators
|
|
446
|
-
|
|
447
|
-
RWC supports declarative component declaration using classes and TypeScript decorators. This allows using a familiar OOP approach, strict typing, and autocomplete.
|
|
448
|
-
|
|
449
|
-
#### Main Decorators
|
|
450
|
-
|
|
451
|
-
- `@component('component-name')` — registers a custom element with the given selector.
|
|
452
|
-
- `@property()` — marks a class field as a reactive property (based on signal). Automatically syncs with the eponymous attribute (kebab-case).
|
|
453
|
-
- `@event()` — marks a class field as an event (EventEmitter). Allows convenient event emission outward.
|
|
454
|
-
|
|
455
|
-
#### Class Component Example
|
|
456
|
-
|
|
457
|
-
```typescript
|
|
458
|
-
@component('test-decorator-component')
|
|
459
|
-
export class TestDecoratorComponent extends BaseElement {
|
|
460
|
-
@property()
|
|
461
|
-
testProp = signal<number>(1);
|
|
462
|
-
|
|
463
|
-
@event()
|
|
464
|
-
testEvent = newEventEmitter<number>();
|
|
465
|
-
|
|
466
|
-
private count = 0;
|
|
467
|
-
|
|
468
|
-
render() {
|
|
469
|
-
return div({ listeners: { click: () => this.testEvent(++this.count) } }, rs`test ${this.testProp()}`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
export const TestDecoratorComponentComp = useCustomComponent(TestDecoratorComponent);
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
#### How It Works
|
|
476
|
-
|
|
477
|
-
- All fields with `@property()` must be signals (`signal<T>()`). Changing the signal value automatically updates DOM and attributes.
|
|
478
|
-
- All fields with `@event()` must be created via `newEventEmitter<T>()`. Calling such a field emits a custom DOM event.
|
|
479
|
-
- The `render()` method returns the component template.
|
|
480
|
-
- The class must extend `BaseElement`.
|
|
481
|
-
|
|
482
|
-
#### Features
|
|
483
|
-
|
|
484
|
-
- Class and functional components can be used together.
|
|
485
|
-
- All reactivity and typing benefits are preserved.
|
|
486
|
-
- Decorators are implemented in `@shared/utils/html-decorators/html-property.ts` and exported through `@shared/utils/html-decorators`.
|
|
487
|
-
|
|
488
|
-
### Functional Components
|
|
489
|
-
|
|
490
|
-
RWC supports creating functional components using `createComponent`. This is an alternative approach to class components that may be more convenient for simple cases.
|
|
491
|
-
|
|
492
|
-
#### createComponent
|
|
493
|
-
|
|
494
|
-
Creates a functional component that accepts props and returns an element configuration.
|
|
495
|
-
|
|
496
|
-
```typescript
|
|
497
|
-
import { createComponent } from '@shared/utils/html-fabric/fn-component';
|
|
498
|
-
import { div, button } from '@shared/utils/html-fabric/fabric';
|
|
499
|
-
import { signal } from '@shared/utils';
|
|
500
|
-
|
|
501
|
-
interface ButtonProps {
|
|
502
|
-
text: string;
|
|
503
|
-
onClick: () => void;
|
|
504
|
-
disabled?: boolean;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const Button = createComponent<ButtonProps>((props) => {
|
|
508
|
-
return button({
|
|
509
|
-
attributes: { disabled: props.disabled },
|
|
510
|
-
listeners: { click: props.onClick }
|
|
511
|
-
}, props.text);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Usage
|
|
515
|
-
const count = signal(0);
|
|
516
|
-
const MyButton = Button({
|
|
517
|
-
text: 'Increment',
|
|
518
|
-
onClick: () => count.set(count() + 1),
|
|
519
|
-
disabled: false
|
|
520
|
-
});
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
**Advantages of functional components:**
|
|
524
|
-
- Simpler syntax for simple cases
|
|
525
|
-
- Automatic support for `classList` and `reactiveClassList` via props
|
|
526
|
-
- Better performance for stateless components
|
|
527
|
-
- Convenience for creating reusable UI elements
|
|
528
|
-
|
|
529
|
-
**When to use:**
|
|
530
|
-
- Simple components without complex logic
|
|
531
|
-
- UI elements that only accept props
|
|
532
|
-
- Reusable components (buttons, inputs, cards)
|
|
533
|
-
|
|
534
|
-
## Elements and Templates
|
|
535
|
-
|
|
536
|
-
### HTML Element Factory
|
|
537
|
-
|
|
538
|
-
To create HTML elements, use factory functions (`div`, `button`, `input`, etc.) from `@shared/utils/html-fabric/fabric`. This ensures strict typing, autocomplete, and consistent style.
|
|
539
|
-
|
|
540
|
-
```typescript
|
|
541
|
-
import { div, button, ul, li, input, slot } from '@shared/utils/html-fabric/fabric';
|
|
542
|
-
|
|
543
|
-
// Examples:
|
|
544
|
-
div('Hello, world!')
|
|
545
|
-
div({ classList: ['container'] },
|
|
546
|
-
button({ listeners: { click: onClick } }, "Click me"),
|
|
547
|
-
ul(
|
|
548
|
-
li('Item 1'),
|
|
549
|
-
li('Item 2')
|
|
550
|
-
)
|
|
551
|
-
)
|
|
552
|
-
```
|
|
553
|
-
- First argument — config object or content directly.
|
|
554
|
-
- All standard HTML tags are available through corresponding factories.
|
|
555
|
-
|
|
556
|
-
#### Element Configuration: ComponentInitConfig
|
|
557
|
-
|
|
558
|
-
To set properties, attributes, classes, events, and effects for elements and components, a special config object type is used — `ComponentInitConfig<T>`. It supports both standard and shorthand notation.
|
|
559
|
-
|
|
560
|
-
**Typing:**
|
|
561
|
-
```typescript
|
|
562
|
-
export type ComponentInitConfig<T extends ExtraHTMLElement> = Partial<{
|
|
563
|
-
classList: ConfigClassList;
|
|
564
|
-
ref: ReactiveSignal<ComponentConfig<T>>;
|
|
565
|
-
style: ConfigStyle;
|
|
566
|
-
attributes: ConfigAttribute<T>;
|
|
567
|
-
customAttributes: ConfigCustomAttribute;
|
|
568
|
-
reactiveClassList: ConfigReactiveClassList;
|
|
569
|
-
children: ConfigChildren;
|
|
570
|
-
effects: ConfigEffect<T>;
|
|
571
|
-
listeners: ConfigListeners<T>;
|
|
572
|
-
customListeners: ConfigCustomListeners<T>;
|
|
573
|
-
}> & Partial<{
|
|
574
|
-
[key in AttrSignal<T> as `.${key}`]?: AttributeValue<T, key>;
|
|
575
|
-
} & {
|
|
576
|
-
[K in keyof HTMLElementEventMap as `@${string & K}`]?: ComponentEventListener<T, HTMLElementEventMap[K]>;
|
|
577
|
-
} & {
|
|
578
|
-
[K in EventKeys<T> as `@${string & K}`]?: CustomEventListener<CustomEventValue<T[K]>, T>;
|
|
579
|
-
} & {
|
|
580
|
-
[key in `$${string}`]: EffectCallback<T>;
|
|
581
|
-
}>
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
#### Main Features
|
|
585
|
-
|
|
586
|
-
- **classList** — array of classes (strings or functions/signals)
|
|
587
|
-
- **ref** — reactive signal to get a reference to the component instance
|
|
588
|
-
- **style** — CSS styles object; supports both regular properties and CSS Custom Properties (`--var`), values can be functions/signals
|
|
589
|
-
- **attributes** — object with HTML attributes
|
|
590
|
-
- **customAttributes** — object with custom attributes
|
|
591
|
-
- **reactiveClassList** — array of reactive classes
|
|
592
|
-
- **children** — child elements/content
|
|
593
|
-
- **effects** — array of effects (functions called when element is created)
|
|
594
|
-
- **listeners** — object with DOM event handlers
|
|
595
|
-
- **customListeners** — object with custom event handlers (e.g., `route-change`)
|
|
596
|
-
|
|
597
|
-
##### Shorthand Notation
|
|
598
|
-
|
|
599
|
-
- `.attributeName` — quick attribute/property assignment
|
|
600
|
-
- `@eventName` — quick event handler assignment (DOM or custom)
|
|
601
|
-
- `$` — quick effect assignment
|
|
602
|
-
|
|
603
|
-
---
|
|
604
|
-
|
|
605
|
-
#### Usage Examples
|
|
606
|
-
|
|
607
|
-
**1. Standard config**
|
|
608
|
-
```typescript
|
|
609
|
-
div({
|
|
610
|
-
classList: ['container', () => isActive() ? 'active' : ''],
|
|
611
|
-
attributes: { id: 'main', tabIndex: 0 },
|
|
612
|
-
listeners: {
|
|
613
|
-
click: (e) => console.log('clicked', e)
|
|
614
|
-
},
|
|
615
|
-
effects: [
|
|
616
|
-
(el) => console.log('created', el)
|
|
617
|
-
]
|
|
618
|
-
}, 'Content')
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
**2. Shorthand notation**
|
|
622
|
-
```typescript
|
|
623
|
-
div({
|
|
624
|
-
'.id': 'main',
|
|
625
|
-
'.tabIndex': 0,
|
|
626
|
-
'.class': 'container',
|
|
627
|
-
'@click': (e) => console.log('clicked', e),
|
|
628
|
-
'$': (el) => console.log('created', el)
|
|
629
|
-
}, 'Content')
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
**2.1. Styles (static / reactive / custom properties)**
|
|
633
|
-
```typescript
|
|
634
|
-
const primaryColor = signal('#0d6efd');
|
|
635
|
-
div({
|
|
636
|
-
style: {
|
|
637
|
-
color: 'white',
|
|
638
|
-
backgroundColor: () => primaryColor(),
|
|
639
|
-
'--gap': '8px', // CSS Custom Property
|
|
640
|
-
marginTop: () => '12px' // reactive value
|
|
641
|
-
}
|
|
642
|
-
}, 'Styles via config.style')
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
**3. Usage with components**
|
|
646
|
-
```typescript
|
|
647
|
-
MyComponentComp({
|
|
648
|
-
'.count': countSignal, // reactive prop
|
|
649
|
-
'@onCountChange': (value) => console.log('count changed', value)
|
|
650
|
-
})
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
**3.1. Custom events via customListeners**
|
|
654
|
-
```typescript
|
|
655
|
-
div({
|
|
656
|
-
customListeners: {
|
|
657
|
-
'route-change': (e, self) => {
|
|
658
|
-
console.log('Route change:', e.detail);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
})
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
**4. Reactive classes via classList**
|
|
665
|
-
```typescript
|
|
666
|
-
div(
|
|
667
|
-
classList`static-class ${() => isActive() ? 'active' : ''}`,
|
|
668
|
-
'Content'
|
|
669
|
-
)
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
**5. Reactive classes via reactiveClassList**
|
|
673
|
-
```typescript
|
|
674
|
-
const isRed = signal(false);
|
|
675
|
-
const isBold = signal(true);
|
|
676
|
-
div({
|
|
677
|
-
reactiveClassList: {
|
|
678
|
-
'red': isRed,
|
|
679
|
-
'bold': isBold
|
|
680
|
-
}
|
|
681
|
-
}, 'Text with reactive classes');
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
**6. Child elements**
|
|
685
|
-
```typescript
|
|
686
|
-
div(
|
|
687
|
-
{ classList: ['container'] },
|
|
688
|
-
span('Text'),
|
|
689
|
-
button('Click me')
|
|
690
|
-
)
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
**7. Getting component reference with ref**
|
|
694
|
-
|
|
695
|
-
```typescript
|
|
696
|
-
const buttonRef = signal<ComponentConfig<HTMLButtonElement>>(null);
|
|
697
|
-
|
|
698
|
-
// Later, use the reference
|
|
699
|
-
button({
|
|
700
|
-
ref: buttonRef,
|
|
701
|
-
listeners: {
|
|
702
|
-
click: () => console.log('Button clicked')
|
|
703
|
-
}
|
|
704
|
-
}, 'Click me');
|
|
705
|
-
|
|
706
|
-
// Access the component later
|
|
707
|
-
effect(() => {
|
|
708
|
-
const buttonComponent = buttonRef();
|
|
709
|
-
if (buttonComponent) {
|
|
710
|
-
console.log('Button component available:', buttonComponent);
|
|
711
|
-
// You can call component methods:
|
|
712
|
-
// buttonComponent.addClass('active');
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
---
|
|
718
|
-
|
|
719
|
-
**Best practice:**
|
|
720
|
-
Use shorthand notation for brevity, and standard notation for complex cases or IDE autocomplete.
|
|
721
|
-
|
|
722
|
-
### Custom Components: useCustomComponent
|
|
723
|
-
|
|
724
|
-
To create and use custom components, use the `useCustomComponent` function from `@shared/utils/html-fabric/custom-fabric`.
|
|
725
|
-
|
|
726
|
-
**Recommended style 1:** Using the `@component` decorator
|
|
727
|
-
1. Declare the component class with the `@component` decorator.
|
|
728
|
-
2. Call `useCustomComponent` below the class, assign the result to a constant, and export it (the class itself doesn't need to be exported).
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
import { component, event, property } from '@shared/utils/html-decorators';
|
|
732
|
-
import { BaseElement } from '@shared/utils/html-elements/element';
|
|
733
|
-
import { useCustomComponent } from '@shared/utils/html-fabric/custom-fabric';
|
|
734
|
-
import { div } from '@shared/utils/html-fabric/fabric';
|
|
735
|
-
|
|
736
|
-
@component('my-component')
|
|
737
|
-
class MyComponent extends BaseElement {
|
|
738
|
-
render() {
|
|
739
|
-
return div('Hello from custom component!');
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
export const MyComponentComp = useCustomComponent(MyComponent);
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
**Recommended style 2:** Passing selector directly to `useCustomComponent`
|
|
746
|
-
1. Declare the component class **without** the `@component` decorator.
|
|
747
|
-
2. Call `useCustomComponent` with the component class and selector as the second argument.
|
|
748
|
-
|
|
749
|
-
```typescript
|
|
750
|
-
import { event, property } from '@shared/utils/html-decorators';
|
|
751
|
-
import { BaseElement } from '@shared/utils/html-elements/element';
|
|
752
|
-
import { useCustomComponent } from '@shared/utils/html-fabric/custom-fabric';
|
|
753
|
-
import { div } from '@shared/utils/html-fabric/fabric';
|
|
754
|
-
|
|
755
|
-
class MyComponent extends BaseElement {
|
|
756
|
-
render() {
|
|
757
|
-
return div('Hello from custom component!');
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
export const MyComponentComp = useCustomComponent(MyComponent, 'my-component');
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
In the second approach, the `@component` decorator is called inside `useCustomComponent` when a selector is passed. This simplifies the component code.
|
|
764
|
-
|
|
765
|
-
**Usage in other components:**
|
|
766
|
-
```typescript
|
|
767
|
-
div(
|
|
768
|
-
MyComponentComp({ attributes: { someProp: 'value' } },
|
|
769
|
-
'Nested content'
|
|
770
|
-
)
|
|
771
|
-
)
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
### Slot Templates
|
|
775
|
-
|
|
776
|
-
`slotTemplate` is a powerful mechanism for passing custom templates into a component. It's an analog of "render props" or "scoped slots" from other frameworks. It allows a child component to receive templates from a parent component and render them with slot-specific context passed.
|
|
777
|
-
|
|
778
|
-
This is useful when a component should manage logic but delegate rendering of part of its content to external code.
|
|
779
|
-
|
|
780
|
-
#### How It Works
|
|
781
|
-
|
|
782
|
-
1. **In the component (template provider):**
|
|
783
|
-
- Define a `slotTemplate` property using `defineSlotTemplate<T>()`.
|
|
784
|
-
- `T` is a type describing available templates. Keys are template names, values are functions that will render the template. Arguments of these functions are the context passed from the component.
|
|
785
|
-
- In the `render` method, the component calls these templates, passing context to them.
|
|
786
|
-
|
|
787
|
-
2. **When using the component (template consumer):**
|
|
788
|
-
- Call the `.setSlotTemplate()` method on the component instance.
|
|
789
|
-
- Pass an object with template implementations to `.setSlotTemplate()`.
|
|
790
|
-
|
|
791
|
-
#### Example
|
|
792
|
-
|
|
793
|
-
Let's say we have a list component that renders items, but we want to allow users of this component to customize how each item looks.
|
|
794
|
-
|
|
795
|
-
**1. Creating the component (`example-list.ts`)**
|
|
796
|
-
|
|
797
|
-
```typescript
|
|
798
|
-
// src/components/example-list.ts
|
|
799
|
-
import { BaseElement, component, defineSlotTemplate, div, getList, property, signal, useCustomComponent } from "@shared/utils";
|
|
800
|
-
import { ComponentConfig } from "@shared/types";
|
|
801
|
-
|
|
802
|
-
@component('example-list')
|
|
803
|
-
export class ExampleListComponent extends BaseElement {
|
|
804
|
-
// Define available templates and their context
|
|
805
|
-
public slotTemplate = defineSlotTemplate<{
|
|
806
|
-
// Template for list item, receives the item itself in context
|
|
807
|
-
item: (slotCtx: { id: number, name: string }) => ComponentConfig<any> | null,
|
|
808
|
-
// Template for index, receives the number in context
|
|
809
|
-
indexTemplate: (slotCtx: number) => ComponentConfig<any>
|
|
810
|
-
}>()
|
|
811
|
-
|
|
812
|
-
@property()
|
|
813
|
-
items = signal<{ id: number, name: string }[]>([])
|
|
814
|
-
|
|
815
|
-
render() {
|
|
816
|
-
// Use getList for efficient rendering
|
|
817
|
-
return div(getList(
|
|
818
|
-
this.items,
|
|
819
|
-
(item) => item.id,
|
|
820
|
-
(item, index) => div(
|
|
821
|
-
// Render 'item' template if provided, otherwise - standard view
|
|
822
|
-
this.slotTemplate.item?.(item) || div(item.name),
|
|
823
|
-
// Render 'indexTemplate' template if provided
|
|
824
|
-
this.slotTemplate.indexTemplate?.(index) || div()
|
|
825
|
-
)
|
|
826
|
-
));
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
export const ExampleList = useCustomComponent(ExampleListComponent);
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
**2. Using the component**
|
|
833
|
-
|
|
834
|
-
```typescript
|
|
835
|
-
// src/components/app.ts
|
|
836
|
-
import { ExampleList } from './example-list';
|
|
837
|
-
|
|
838
|
-
const allItems = [
|
|
839
|
-
{ id: 1, name: 'First' },
|
|
840
|
-
{ id: 2, name: 'Second' },
|
|
841
|
-
{ id: 3, name: 'Third' },
|
|
842
|
-
];
|
|
843
|
-
|
|
844
|
-
@component('my-app')
|
|
845
|
-
export class App extends BaseElement {
|
|
846
|
-
render() {
|
|
847
|
-
return div(
|
|
848
|
-
ExampleList({ '.items': allItems })
|
|
849
|
-
// Pass custom templates
|
|
850
|
-
.setSlotTemplate({
|
|
851
|
-
// Custom render for item
|
|
852
|
-
item: (itemCtx) => div(`Item: ${itemCtx.name} (id: ${itemCtx.id})`),
|
|
853
|
-
// Custom render for even indices
|
|
854
|
-
indexTemplate: indexCtx => indexCtx % 2 === 0
|
|
855
|
-
? div(`Even index: ${indexCtx}`)
|
|
856
|
-
: null,
|
|
857
|
-
})
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
#### Key Points:
|
|
864
|
-
|
|
865
|
-
- `defineSlotTemplate` creates a typed object for templates.
|
|
866
|
-
- The `.setSlotTemplate()` method allows passing template implementations to the component.
|
|
867
|
-
- Context (`slotCtx`) is passed from the component to the template function, providing flexibility.
|
|
868
|
-
- You can define fallback rendering if a template wasn't provided, using `||`.
|
|
869
|
-
|
|
870
|
-
### Function as Child Content (Dynamic Lists and Conditional Render)
|
|
871
|
-
|
|
872
|
-
Functions passed as child content to factories (`div`, `ul`, etc.) are automatically converted to reactive content.
|
|
873
|
-
|
|
874
|
-
**Example: dynamic list**
|
|
875
|
-
```typescript
|
|
876
|
-
const items = signal(['Item 1', 'Item 2']);
|
|
877
|
-
div(
|
|
878
|
-
ul(
|
|
879
|
-
() => items().map(item => li(item))
|
|
880
|
-
)
|
|
881
|
-
)
|
|
882
|
-
// When items changes, the entire list will be re-rendered
|
|
883
|
-
items.set(['Item 1', 'Item 2', 'Item 3']);
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
### Efficient List Rendering with getList
|
|
887
|
-
|
|
888
|
-
For performance optimization when working with lists, it's recommended to use the `getList` function. It allows efficiently updating only changed list elements instead of re-rendering the entire list.
|
|
889
|
-
|
|
890
|
-
**Signature:**
|
|
891
|
-
```typescript
|
|
892
|
-
getList<I extends Record<string, any>, K extends keyof I>(
|
|
893
|
-
items: ReactiveSignal<I[]>,
|
|
894
|
-
keyFn: (item: I) => I[K] | string,
|
|
895
|
-
cb: (item: I, index: number, items: I[]) => ComponentConfig<any>
|
|
896
|
-
): ComponentConfig<HTMLDivElement>
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
**Parameters:**
|
|
900
|
-
- `items` - reactive signal with array of elements
|
|
901
|
-
- `keyFn` - function returning a unique key for each element (supports `string` or element field `I[K]`)
|
|
902
|
-
- `cb` - element rendering function, accepting the element, its index, and the entire current `items` array
|
|
903
|
-
|
|
904
|
-
**Usage example:**
|
|
905
|
-
```typescript
|
|
906
|
-
@component('example-list')
|
|
907
|
-
class ExampleList extends BaseElement {
|
|
908
|
-
items = signal<{ id: number, name: string }[]>([
|
|
909
|
-
{ id: 1, name: 'Item 1' },
|
|
910
|
-
{ id: 2, name: 'Item 2' },
|
|
911
|
-
{ id: 3, name: 'Item 3' },
|
|
912
|
-
])
|
|
913
|
-
|
|
914
|
-
render() {
|
|
915
|
-
return div(
|
|
916
|
-
// Regular list rendering (re-renders entire list)
|
|
917
|
-
div(() => this.items().map(item => div(item.name))),
|
|
918
|
-
|
|
919
|
-
// Efficient rendering with getList (updates only changed elements)
|
|
920
|
-
div(getList(
|
|
921
|
-
this.items,
|
|
922
|
-
(item) => item.id, // key — item id
|
|
923
|
-
(item, index, items) => div(`${index + 1}. ${item.name}`) // index and entire array available
|
|
924
|
-
))
|
|
925
|
-
)
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
**Advantages of using getList:**
|
|
931
|
-
1. Optimized performance — only changed elements are updated
|
|
932
|
-
2. Preserving list element state
|
|
933
|
-
3. Efficient work with large lists
|
|
934
|
-
4. Automatic updates when data changes
|
|
935
|
-
|
|
936
|
-
**Implementation details:**
|
|
937
|
-
- Uses `data-key` to bind DOM nodes to data elements (key comes from `keyFn`).
|
|
938
|
-
- Each key has its own signal stored; changing the signal value forces update of the corresponding DOM node.
|
|
939
|
-
- Element changes are determined by comparison: `JSON.stringify(currItem) !== JSON.stringify(oldItems[index])`.
|
|
940
|
-
- Nodes whose keys are missing from the new list are removed from DOM, and their cache (signals/components/effects) is cleared.
|
|
941
|
-
- DOM node order is synchronized with key order in the current data array.
|
|
942
|
-
- Render effects are created once per key and cached in `currRegisteredEffects`.
|
|
943
|
-
- Effect initialization is deferred via `Promise.resolve().then(...)` for correct DOM insertion at the right position.
|
|
944
|
-
- Keys are normalized to string for consistent matching.
|
|
945
|
-
|
|
946
|
-
**Best practices:**
|
|
947
|
-
- Keys should be unique and stable between re-renders.
|
|
948
|
-
- Avoid deep/large objects if performance-sensitive: comparison via `JSON.stringify` can be expensive.
|
|
949
|
-
- Ensure immutable element updates so changes are detected correctly.
|
|
950
|
-
- If a specific order is needed, form it at the data level before rendering (e.g., sort the array before passing to `getList`).
|
|
951
|
-
|
|
952
|
-
**Best practice:**
|
|
953
|
-
- Always use unique keys for list elements
|
|
954
|
-
- Use `getList` for dynamic lists, especially with frequent updates
|
|
955
|
-
- For simple static lists, you can use a regular map
|
|
956
|
-
|
|
957
|
-
### Complex Component Example
|
|
958
|
-
|
|
959
|
-
```typescript
|
|
960
|
-
import { component, event, property } from '@shared/utils/html-decorators';
|
|
961
|
-
import { BaseElement } from '@shared/utils/html-elements/element';
|
|
962
|
-
import { useCustomComponent } from '@shared/utils/html-fabric/custom-fabric';
|
|
963
|
-
import { div, input } from '@shared/utils/html-fabric/fabric';
|
|
964
|
-
import { signal } from '@shared/utils/html-elements/signal';
|
|
965
|
-
|
|
966
|
-
@component('tab-bar-test-item')
|
|
967
|
-
class TabBarTestItem extends BaseElement {
|
|
968
|
-
render() {
|
|
969
|
-
return div(
|
|
970
|
-
'tab-bar-test-item 3',
|
|
971
|
-
input()
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
export const TabBarTestItemComp = useCustomComponent(TabBarTestItem);
|
|
976
|
-
|
|
977
|
-
@component('tab-bar-test')
|
|
978
|
-
class TabBarTest extends BaseElement {
|
|
979
|
-
activeTabNumber = signal(0);
|
|
980
|
-
items = signal<string[]>(['test1', 'test2', 'test3']);
|
|
981
|
-
render() {
|
|
982
|
-
const isHidden = signal(false);
|
|
983
|
-
return div(
|
|
984
|
-
this.items,
|
|
985
|
-
() => this.items().map(e => div(e)),
|
|
986
|
-
div(
|
|
987
|
-
{ classList: [() => isHidden() ? 'test1' : 'test2'] },
|
|
988
|
-
'!!!test classList!!!'
|
|
989
|
-
),
|
|
990
|
-
TabBarTestItemComp(
|
|
991
|
-
{},
|
|
992
|
-
div('test1'),
|
|
993
|
-
div('test2'),
|
|
994
|
-
div(TabBarTestItemComp()),
|
|
995
|
-
div(
|
|
996
|
-
div(
|
|
997
|
-
div()
|
|
998
|
-
)
|
|
999
|
-
),
|
|
1000
|
-
div(TabBarTestItemComp()),
|
|
1001
|
-
TabBarTestItemComp()
|
|
1002
|
-
)
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
export const TabBarTestComp = useCustomComponent(TabBarTest);
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
### Conditional Rendering with when
|
|
1010
|
-
|
|
1011
|
-
For conditional rendering, use the `when` function from the factory. It supports both static and reactive conditions.
|
|
1012
|
-
|
|
1013
|
-
```typescript
|
|
1014
|
-
import { when } from '@shared/utils/html-fabric/fabric';
|
|
1015
|
-
import { div, span } from '@shared/utils/html-fabric/fabric';
|
|
1016
|
-
import { signal } from '@shared/utils/html-elements/signal';
|
|
1017
|
-
|
|
1018
|
-
// Static condition
|
|
1019
|
-
const isVisible = true;
|
|
1020
|
-
div(
|
|
1021
|
-
when(isVisible,
|
|
1022
|
-
() => span('Shown'),
|
|
1023
|
-
() => span('Hidden')
|
|
1024
|
-
)
|
|
1025
|
-
)
|
|
1026
|
-
|
|
1027
|
-
// Reactive condition
|
|
1028
|
-
const isVisibleSignal = signal(true);
|
|
1029
|
-
div(
|
|
1030
|
-
when(isVisibleSignal,
|
|
1031
|
-
() => span('Shown'),
|
|
1032
|
-
() => span('Hidden')
|
|
1033
|
-
)
|
|
1034
|
-
)
|
|
1035
|
-
|
|
1036
|
-
// Conditional rendering with function
|
|
1037
|
-
const items = signal(['Item 1', 'Item 2']);
|
|
1038
|
-
div(
|
|
1039
|
-
when(
|
|
1040
|
-
() => items().length > 0,
|
|
1041
|
-
() => ul(
|
|
1042
|
-
...items().map(item => li(item))
|
|
1043
|
-
),
|
|
1044
|
-
() => div('No items')
|
|
1045
|
-
)
|
|
1046
|
-
)
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
- `when` automatically determines condition type (boolean, signal, or function)
|
|
1050
|
-
- Supports optional elseContent
|
|
1051
|
-
- Use for any conditional rendering instead of manual if/ternary or deprecated rxRenderIf/renderIf
|
|
1052
|
-
- Accepts functions of type `CompFuncContent` as rendering arguments (functions returning `ComponentContent` or array `ComponentContent[]`)
|
|
1053
|
-
|
|
1054
|
-
### Conditional Display with show
|
|
1055
|
-
|
|
1056
|
-
To control element visibility without removing them from DOM, use the `show` function. Unlike `when`, which completely adds/removes elements, `show` controls display via CSS `display` property.
|
|
1057
|
-
|
|
1058
|
-
```typescript
|
|
1059
|
-
// Static condition
|
|
1060
|
-
const isVisible = true;
|
|
1061
|
-
div(
|
|
1062
|
-
show(isVisible, () => span('Content'))
|
|
1063
|
-
)
|
|
1064
|
-
|
|
1065
|
-
// Reactive condition
|
|
1066
|
-
const isVisibleSignal = signal(true);
|
|
1067
|
-
div(
|
|
1068
|
-
show(isVisibleSignal, () => span('Reactive content'))
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1071
|
-
// Condition via function
|
|
1072
|
-
const itemCount = signal(5);
|
|
1073
|
-
div(
|
|
1074
|
-
show(() => itemCount() > 0, () => span('Items exist'))
|
|
1075
|
-
)
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
**Differences between `when` and `show`:**
|
|
1079
|
-
|
|
1080
|
-
- **`when`** — completely removes/adds elements from DOM. More efficient for heavy components that are rarely shown.
|
|
1081
|
-
- **`show`** — hides/shows elements via `display: none/contents`. More efficient for frequent visibility toggling, preserves element state.
|
|
1082
|
-
|
|
1083
|
-
**When to use `show`:**
|
|
1084
|
-
- For frequent visibility toggling (e.g., dropdown menus, modals)
|
|
1085
|
-
- When you need to preserve element state when hidden
|
|
1086
|
-
- For simple show/hide cases without alternative content
|
|
1087
|
-
|
|
1088
|
-
## Recommendations and Best Practices
|
|
1089
|
-
|
|
1090
|
-
### Architectural Principles
|
|
1091
|
-
|
|
1092
|
-
1. **Separation of Concerns**: Use class components for complex logic, functional — for simple UI elements
|
|
1093
|
-
2. **Reactivity**: All states should be signals for automatic UI updates
|
|
1094
|
-
3. **Typing**: Use strict typing for all props, events, and contexts
|
|
1095
|
-
4. **Performance**: Apply `getList` for large lists, `show` for frequent visibility toggles
|
|
1096
|
-
|
|
1097
|
-
### Usage Patterns
|
|
1098
|
-
|
|
1099
|
-
#### Component Composition
|
|
1100
|
-
```typescript
|
|
1101
|
-
// Good: composition of simple components
|
|
1102
|
-
const UserCard = createComponent<UserProps>((props) =>
|
|
1103
|
-
div({ classList: ['user-card'] },
|
|
1104
|
-
UserAvatar({ src: props.avatar }),
|
|
1105
|
-
UserInfo({ name: props.name, email: props.email })
|
|
1106
|
-
)
|
|
1107
|
-
);
|
|
1108
|
-
|
|
1109
|
-
// Bad: one large component with all logic
|
|
1110
|
-
const ComplexUserCard = createComponent<AllProps>((props) => {
|
|
1111
|
-
// 200+ lines of code
|
|
1112
|
-
});
|
|
1113
|
-
```
|
|
1114
|
-
|
|
1115
|
-
#### State Management
|
|
1116
|
-
```typescript
|
|
1117
|
-
// Good: local state in component
|
|
1118
|
-
class UserProfile extends BaseElement {
|
|
1119
|
-
@property()
|
|
1120
|
-
isEditing = signal(false);
|
|
1121
|
-
|
|
1122
|
-
render() {
|
|
1123
|
-
return when(this.isEditing,
|
|
1124
|
-
() => UserEditForm(),
|
|
1125
|
-
() => UserDisplay()
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Good: global state via context
|
|
1131
|
-
const ThemeContext = 'theme';
|
|
1132
|
-
class ThemeProvider extends BaseElement {
|
|
1133
|
-
providers = { [ThemeContext]: signal('dark') };
|
|
1134
|
-
}
|
|
1135
|
-
```
|
|
1136
|
-
|
|
1137
|
-
## Examples
|
|
1138
|
-
|
|
1139
|
-
#### Inserting Unsafe HTML (unsafeHtml)
|
|
1140
|
-
```typescript
|
|
1141
|
-
// Render string as HTML. Use only for trusted content!
|
|
1142
|
-
const html = signal('<b>bold</b> and <i>italic</i>');
|
|
1143
|
-
div(
|
|
1144
|
-
unsafeHtml(html)
|
|
1145
|
-
)
|
|
1146
|
-
|
|
1147
|
-
// static string
|
|
1148
|
-
div(unsafeHtml('<span style="color:red">red</span>'))
|
|
1149
|
-
```
|
|
1150
|
-
|
|
1151
|
-
### Basic Component with props and event
|
|
1152
|
-
```typescript
|
|
1153
|
-
import { component, event, property } from '@shared/utils/html-decorators';
|
|
1154
|
-
import { BaseElement } from '@shared/utils/html-elements/element';
|
|
1155
|
-
import { rs, signal } from '@shared/utils/html-elements/signal';
|
|
1156
|
-
import { newEventEmitter } from '@shared/utils';
|
|
1157
|
-
|
|
1158
|
-
@component('test-decorator-component')
|
|
1159
|
-
export class TestDecoratorComponent extends BaseElement {
|
|
1160
|
-
@property()
|
|
1161
|
-
testProp = signal<string>('Hello from Decorator!');
|
|
1162
|
-
|
|
1163
|
-
@event()
|
|
1164
|
-
onCustomEvent = newEventEmitter<string>();
|
|
1165
|
-
|
|
1166
|
-
render() {
|
|
1167
|
-
this.onCustomEvent('test value');
|
|
1168
|
-
return div(rs`Title: ${this.testProp()}`);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
export const TestDecoratorComponentComp = useCustomComponent(TestDecoratorComponent);
|
|
1172
|
-
```
|
|
1173
|
-
|
|
1174
|
-
#### Dynamic List via Function as Child Content
|
|
1175
|
-
```typescript
|
|
1176
|
-
const items = signal(['Item 1', 'Item 2']);
|
|
1177
|
-
div(
|
|
1178
|
-
() => ul(
|
|
1179
|
-
...items().map(item => li(item))
|
|
1180
|
-
)
|
|
1181
|
-
)
|
|
1182
|
-
// When items changes, the entire list will be re-rendered
|
|
1183
|
-
items.set(['Item 1', 'Item 2', 'Item 3']);
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
#### Reactive Array Display
|
|
1187
|
-
```typescript
|
|
1188
|
-
const items = signal(['A', 'B', 'C']);
|
|
1189
|
-
div(() => items().join(','));
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
#### Example: Tab Header
|
|
1193
|
-
```typescript
|
|
1194
|
-
div({ classList: ['tab-header'] }, rs`current tab: ${createSignal(() => this.activeTab() + 1)}`)
|
|
1195
|
-
```
|
|
1196
|
-
|
|
1197
|
-
#### Example: Component with props
|
|
1198
|
-
```typescript
|
|
1199
|
-
class TestDecoratorComponent extends BaseElement {
|
|
1200
|
-
@property()
|
|
1201
|
-
testProp = signal<string>('Hello from Decorator!');
|
|
1202
|
-
@event()
|
|
1203
|
-
onCustomEvent = newEventEmitter<string>();
|
|
1204
|
-
render() {
|
|
1205
|
-
this.onCustomEvent('test value');
|
|
1206
|
-
return div(rs`Title: ${this.testProp()}`);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
export const TestDecoratorComponentComp = useCustomComponent(TestDecoratorComponent);
|
|
1210
|
-
```
|
|
1211
|
-
|
|
1212
|
-
#### Example: Component with Logging
|
|
1213
|
-
```typescript
|
|
1214
|
-
class LoggerComponent extends BaseElement {
|
|
1215
|
-
connectedCallback() {
|
|
1216
|
-
super.connectedCallback?.();
|
|
1217
|
-
console.log('connected');
|
|
1218
|
-
}
|
|
1219
|
-
disconnectedCallback() {
|
|
1220
|
-
super.disconnectedCallback?.();
|
|
1221
|
-
console.log('disconnected');
|
|
1222
|
-
}
|
|
1223
|
-
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
1224
|
-
super.attributeChangedCallback?.(name, oldValue, newValue);
|
|
1225
|
-
console.log(name, oldValue, newValue);
|
|
1226
|
-
}
|
|
1227
|
-
render() {
|
|
1228
|
-
return div(rs`Logger`);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
export const LoggerComponentComp = useCustomComponent(LoggerComponent);
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
#### Example: Button with Signal
|
|
1235
|
-
```typescript
|
|
1236
|
-
class Counter extends BaseElement {
|
|
1237
|
-
@property()
|
|
1238
|
-
count = signal(0);
|
|
1239
|
-
@event()
|
|
1240
|
-
onCountChange = newEventEmitter<number>();
|
|
1241
|
-
render() {
|
|
1242
|
-
return button({
|
|
1243
|
-
listeners: {
|
|
1244
|
-
click: () => {
|
|
1245
|
-
this.count.update(v => v + 1);
|
|
1246
|
-
this.onCountChange(this.count());
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
}, rs`Count: ${this.count()}`);
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
export const CounterComp = useCustomComponent(Counter);
|
|
1253
|
-
```
|
|
1254
|
-
|
|
1255
|
-
#### Example: Slot
|
|
1256
|
-
```typescript
|
|
1257
|
-
div(slot({ attributes: { name: 'tab-item' } }))
|
|
1258
|
-
```
|
|
1259
|
-
|
|
1260
|
-
#### Example: Using Context
|
|
1261
|
-
```typescript
|
|
1262
|
-
class ThemeConsumer extends BaseElement {
|
|
1263
|
-
theme = this.inject<string>(ThemeContext); // Get context signal once outside render
|
|
1264
|
-
render() {
|
|
1265
|
-
return div(rs`Theme: ${this.theme}`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
export const ThemeConsumerComp = useCustomComponent(ThemeConsumer);
|
|
1269
|
-
```
|
|
1270
|
-
|
|
1271
|
-
#### Example: Nested Components
|
|
1272
|
-
```typescript
|
|
1273
|
-
div(
|
|
1274
|
-
ThemeProviderComp(
|
|
1275
|
-
ThemeConsumerComp()
|
|
1276
|
-
)
|
|
1277
|
-
)
|
|
1278
|
-
```
|
|
1279
|
-
|
|
1280
|
-
#### Example: Functional Component
|
|
1281
|
-
```typescript
|
|
1282
|
-
import { createComponent } from '@shared/utils/html-fabric/fn-component';
|
|
1283
|
-
import { button } from '@shared/utils/html-fabric/fabric';
|
|
1284
|
-
|
|
1285
|
-
interface CounterProps {
|
|
1286
|
-
initialValue?: number;
|
|
1287
|
-
step?: number;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
const Counter = createComponent<CounterProps>((props) => {
|
|
1291
|
-
const count = signal(props.initialValue || 0);
|
|
1292
|
-
|
|
1293
|
-
return button({
|
|
1294
|
-
listeners: {
|
|
1295
|
-
click: () => count.set(count() + (props.step || 1))
|
|
1296
|
-
}
|
|
1297
|
-
}, () => `Counter: ${count()}`);
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
// Usage
|
|
1301
|
-
const MyCounter = Counter({
|
|
1302
|
-
initialValue: 10,
|
|
1303
|
-
step: 5
|
|
1304
|
-
});
|
|
1305
|
-
```
|
|
1306
|
-
|
|
1307
|
-
#### Example: Working with Signal Utilities
|
|
1308
|
-
```typescript
|
|
1309
|
-
import { bindReactiveSignals, forkJoin, combineLatest, signal } from '@shared/utils';
|
|
1310
|
-
|
|
1311
|
-
// Two-way binding
|
|
1312
|
-
const inputValue = signal('');
|
|
1313
|
-
const displayValue = signal('');
|
|
1314
|
-
bindReactiveSignals(inputValue, displayValue);
|
|
1315
|
-
|
|
1316
|
-
// Combining signals with forkJoin (waits for all to update)
|
|
1317
|
-
const name = signal('John');
|
|
1318
|
-
const age = signal(25);
|
|
1319
|
-
const userInfo = forkJoin(name, age);
|
|
1320
|
-
// userInfo() returns ['John', 25] only when both signals update
|
|
1321
|
-
|
|
1322
|
-
// Combining signals with combineLatest (updates on any change)
|
|
1323
|
-
const firstName = signal('John');
|
|
1324
|
-
const lastName = signal('Doe');
|
|
1325
|
-
const fullName = combineLatest(firstName, lastName);
|
|
1326
|
-
// fullName() returns ['John', 'Doe'] and updates immediately when either signal changes
|
|
1327
|
-
firstName.set('Jane'); // fullName() immediately becomes ['Jane', 'Doe']
|
|
1328
|
-
```
|
|
1329
|
-
|
|
1330
|
-
#### Example: Event Handling
|
|
1331
|
-
```typescript
|
|
1332
|
-
class TestDecoratorComponent extends BaseElement {
|
|
1333
|
-
@property()
|
|
1334
|
-
testProp = signal<number>(1);
|
|
1335
|
-
@event()
|
|
1336
|
-
testEvent = newEventEmitter<number>();
|
|
1337
|
-
private count = 0;
|
|
1338
|
-
render() {
|
|
1339
|
-
return div({ listeners: { click: () => this.testEvent(++this.count) } }, rs`test ${this.testProp()}`);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
export const TestDecoratorComponentComp = useCustomComponent(TestDecoratorComponent);
|
|
1343
|
-
```
|
|
1344
|
-
|
|
1345
|
-
### Additional Utilities
|
|
1346
|
-
|
|
1347
|
-
#### Using the `classList` Function
|
|
1348
|
-
|
|
1349
|
-
For convenient assignment of dynamic and static classes in element config, you can use the `classList` function. It allows combining string values and functions (e.g., signals) that return a class string. This is especially useful for reactive class management.
|
|
1350
|
-
|
|
1351
|
-
**Signature:**
|
|
1352
|
-
```typescript
|
|
1353
|
-
classList(strings: TemplateStringsArray, ...args: (() => string)[]): { classList: (string | (() => string))[] }
|
|
1354
|
-
```
|
|
1355
|
-
|
|
1356
|
-
**Example of static and dynamic classes:**
|
|
1357
|
-
```typescript
|
|
1358
|
-
const isActive = signal(false);
|
|
1359
|
-
div(
|
|
1360
|
-
classList`my-static-class ${() => isActive() ? 'active' : ''}`,
|
|
1361
|
-
'Content'
|
|
1362
|
-
)
|
|
1363
|
-
// When isActive changes, the 'active' class will be added or removed automatically
|
|
1364
|
-
```
|
|
1365
|
-
|
|
1366
|
-
**Additionally:**
|
|
1367
|
-
- As a function inside `classList`, you can pass a **signal** that returns a class string:
|
|
1368
|
-
|
|
1369
|
-
```typescript
|
|
1370
|
-
const dynamicClass = signal('my-dynamic-class');
|
|
1371
|
-
div(
|
|
1372
|
-
classList`static-class ${dynamicClass}`,
|
|
1373
|
-
'Content'
|
|
1374
|
-
)
|
|
1375
|
-
// When dynamicClass changes, the class will automatically update
|
|
1376
|
-
```
|
|
1377
|
-
|
|
1378
|
-
- You can also pass a **function that returns a signal**:
|
|
1379
|
-
|
|
1380
|
-
```typescript
|
|
1381
|
-
const getClassSignal = () => someSignal;
|
|
1382
|
-
div(
|
|
1383
|
-
classList`test-class ${getClassSignal}`,
|
|
1384
|
-
'Content'
|
|
1385
|
-
)
|
|
1386
|
-
// The class will reactively change when the signal value returned by the function changes
|
|
1387
|
-
```
|
|
1388
|
-
|
|
1
|
+
# Reactive Web Components (RWC)
|
|
2
|
+
|
|
3
|
+
[🇷🇺 Документация на русском](./docs_readme/docs.ru.md)
|
|
4
|
+
|
|
5
|
+
**RWC** is a lightweight runtime for building reactive [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components) without depending on a specific framework. It combines fine-grained signals, effects, and a declarative TypeScript-first HTML factory to compose UI from strongly-typed primitives — no templates, no JSX, just TypeScript.
|
|
6
|
+
|
|
7
|
+
## Why RWC
|
|
8
|
+
|
|
9
|
+
### Framework-free signals & effects
|
|
10
|
+
|
|
11
|
+
Signals and effects in RWC are **independent primitives** — they work anywhere in your code with zero restrictions. This is a fundamental difference from existing frameworks:
|
|
12
|
+
|
|
13
|
+
| | Signals anywhere | Effects anywhere | Needs special context |
|
|
14
|
+
|---|:---:|:---:|:---:|
|
|
15
|
+
| **RWC** | ✅ | ✅ | No |
|
|
16
|
+
| **Angular** | ✅ | ❌ | `effect()` requires injection context (constructor, factory, `runInInjectionContext`) [1][2] |
|
|
17
|
+
| **Solid.js** | ✅ | ⚠️ | `createEffect` needs a reactive owner (`createRoot` / `render` / `runWithOwner`) — effects outside a root will never be disposed [3][4] |
|
|
18
|
+
|
|
19
|
+
In Angular, calling `effect()` in `ngOnInit`, in a regular method, or outside a component throws `NG0203`. The workaround is to inject an `Injector` and wrap code in `runInInjectionContext`. In Solid.js, `createEffect` outside a tracking scope (e.g. in `setTimeout`, async code, or global scope) either leaks memory or requires manual `getOwner` / `runWithOwner` plumbing.[1][5][2][6][4]
|
|
20
|
+
|
|
21
|
+
**RWC has none of these limitations.** Signals and effects are first-class citizens that work in any context — inside components, outside components, in utility functions, in async code, everywhere.[7]
|
|
22
|
+
|
|
23
|
+
### TypeScript all the way down
|
|
24
|
+
|
|
25
|
+
RWC takes a **no-HTML** approach: all markup is built through TypeScript factory functions (`div`, `button`, `input`, …). This means:[7]
|
|
26
|
+
|
|
27
|
+
- **Full type-checking in templates** — props, attributes, events, slots, and children are all typed.
|
|
28
|
+
- **Autocomplete everywhere** — IDE suggestions for every config option, event name, and attribute.
|
|
29
|
+
- **Compile-time errors** — typos in attribute names or wrong event handler signatures are caught before runtime.
|
|
30
|
+
|
|
31
|
+
### Typed slots
|
|
32
|
+
|
|
33
|
+
Slots in RWC are fully typed via `slotTemplate`. The parent component defines what templates it expects, and the child component consumes them with typed context — similar to scoped slots in Vue, but with compile-time guarantees.[7]
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Reactivity** — `signal`, `effect`, `createSignal`, `rs`, `computed`, `pipe`, `forkJoin`, `combineLatest`.[7]
|
|
38
|
+
- **Class components** — decorators (`@component`, `@property`, `@event`) with lifecycle hooks.[7]
|
|
39
|
+
- **Functional components** — lightweight alternative via `createComponent`.[7]
|
|
40
|
+
- **HTML factory** — declarative element creation with typed config (`ComponentInitConfig`).[7]
|
|
41
|
+
- **Shorthand config** — `.attr` for attributes, `@event` for listeners, `$name` for effects.[7]
|
|
42
|
+
- **Control flow** — `getList` (keyed efficient lists), `when` (conditional render), `show` (CSS toggle).[7]
|
|
43
|
+
- **Slots** — typed `slotTemplate` with scoped context.[7]
|
|
44
|
+
- **DI & styling** — context via providers/injects, reactive refs, reactive `classList` and `style`.[7]
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install @reactive-web-components/rwc
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> Requires TypeScript and a modern browser with [Custom Elements](https://caniuse.com/custom-elementsv1) support.
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
A minimal reactive counter using shorthand config syntax:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { component, property, event } from '@reactive-web-components/rwc';
|
|
60
|
+
import { BaseElement } from '@reactive-web-components/rwc';
|
|
61
|
+
import { div, button } from '@reactive-web-components/rwc';
|
|
62
|
+
import { signal, rs, newEventEmitter, useCustomComponent } from '@reactive-web-components/rwc';
|
|
63
|
+
|
|
64
|
+
@component('rwc-counter')
|
|
65
|
+
class Counter extends BaseElement {
|
|
66
|
+
@property()
|
|
67
|
+
count = signal(0);
|
|
68
|
+
|
|
69
|
+
@event()
|
|
70
|
+
onCountChange = newEventEmitter<number>();
|
|
71
|
+
|
|
72
|
+
render() {
|
|
73
|
+
return div(
|
|
74
|
+
button(
|
|
75
|
+
{
|
|
76
|
+
"@click": () => {
|
|
77
|
+
this.count.update((v) => v + 1);
|
|
78
|
+
this.onCountChange(this.count());
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
rs`Count: ${this.count}`, // using rs`...`
|
|
82
|
+
() => `Count: ${this.count()}`, // using function with signall call
|
|
83
|
+
);
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const CounterComp = useCustomComponent(Counter);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Usage in another component:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
CounterComp({
|
|
95
|
+
'.count': signal(10), // typed attribute via shorthand
|
|
96
|
+
'@onCountChange': (v) => console.log(v), // typed event via shorthand
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or directly in HTML:
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<rwc-counter></rwc-counter>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Config Shorthand Syntax
|
|
107
|
+
|
|
108
|
+
RWC supports a concise config notation alongside the standard one:[7]
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// Shorthand — less boilerplate, same type safety
|
|
112
|
+
div({
|
|
113
|
+
'.id': 'main',
|
|
114
|
+
'.tabIndex': 0,
|
|
115
|
+
'@click': (e) => console.log('clicked', e),
|
|
116
|
+
'$onMount': (el) => console.log('element created', el),
|
|
117
|
+
}, 'Content')
|
|
118
|
+
|
|
119
|
+
// Equivalent standard config
|
|
120
|
+
div({
|
|
121
|
+
attributes: { id: 'main', tabIndex: 0 },
|
|
122
|
+
listeners: { click: (e) => console.log('clicked', e) },
|
|
123
|
+
effects: [(el) => console.log('element created', el)],
|
|
124
|
+
}, 'Content')
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Prefix | Meaning | Example |
|
|
128
|
+
|--------|---------|---------|
|
|
129
|
+
| `.` | Attribute / property | `'.disabled': true` |
|
|
130
|
+
| `@` | Event listener (DOM or custom) | `'@click': handler` |
|
|
131
|
+
| `$` | Effect (runs on element creation) | `'$init': (el) => ...` |
|
|
132
|
+
|
|
133
|
+
## Typed Slot Templates
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
@component('item-list')
|
|
137
|
+
class ItemList extends BaseElement {
|
|
138
|
+
public slotTemplate = defineSlotTemplate<{
|
|
139
|
+
item: (ctx: { id: number; name: string }) => ComponentConfig<any>;
|
|
140
|
+
}>();
|
|
141
|
+
|
|
142
|
+
@property()
|
|
143
|
+
items = signal<{ id: number; name: string }[]>([]);
|
|
144
|
+
|
|
145
|
+
render() {
|
|
146
|
+
return div(getList(
|
|
147
|
+
this.items,
|
|
148
|
+
(item) => item.id,
|
|
149
|
+
(item) => this.slotTemplate.item?.(item) || div(item.name)
|
|
150
|
+
));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const ItemListComp = useCustomComponent(ItemList);
|
|
155
|
+
|
|
156
|
+
// Consumer — fully typed context in the slot
|
|
157
|
+
ItemListComp({ '.items': data })
|
|
158
|
+
.setSlotTemplate({
|
|
159
|
+
item: (ctx) => div(`${ctx.name} (#${ctx.id})`), // ctx is typed!
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## When to Use RWC
|
|
164
|
+
|
|
165
|
+
- Low-level but type-safe layer for Web Components without a heavy framework.
|
|
166
|
+
- Signal-based reactivity (like Solid or Angular Signals) on top of the native DOM — but without their context restrictions.[8][6]
|
|
167
|
+
- Shared runtime across projects — vanilla apps, microfrontends, or integration into Angular/React via wrappers.
|
|
168
|
+
- Compile-time safety in templates, slots, and event handlers.
|
|
169
|
+
|
|
170
|
+
## Documentation
|
|
171
|
+
|
|
172
|
+
| Resource | Description |
|
|
173
|
+
|----------|-------------|
|
|
174
|
+
| [docs.ru.md](./docs_readme/docs.ru.md) | Full documentation in Russian — complete API reference with examples |
|
|
175
|
+
| [docs.en.md](./docs_readme/docs.en.md) | Full documentation in English — complete API reference with examples |
|
|
176
|
+
| `src/` | Source code and usage examples |
|
|
177
|
+
|
|
178
|
+
## Project Status
|
|
179
|
+
|
|
180
|
+
The library is under active development. The core API is stable and used in production prototypes, but minor changes to typings and helper utilities may still occur.[7]
|
|
181
|
+
|
|
182
|
+
Contributions, issues, and pull requests are welcome!
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
[MIT](./LICENSE)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## References
|
|
191
|
+
|
|
192
|
+
1. [Signals in Angular: Building Blocks](https://www.angulararchitects.io/blog/angular-signals/) - Several building blocks for Signals such as effect can only be used in an injection context. This is...
|
|
193
|
+
|
|
194
|
+
2. [Side effects for non-reactives APIs](https://angular.dev/guide/signals/effect) - Injection context link. By default, you can only create an effect() within an injection context (whe...
|
|
195
|
+
|
|
196
|
+
3. [runWithOwner](https://docs.solidjs.com/reference/reactive-utilities/run-with-owner) - Execute code under a specific owner in SolidJS for proper cleanup and context access, especially in ...
|
|
197
|
+
|
|
198
|
+
4. [Using signal outside of component. · solidjs solid · Discussion #397](https://github.com/solidjs/solid/discussions/397) - The basic of it is while it isn't restricted to components, Solid's reactivity is built with framewo...
|
|
199
|
+
|
|
200
|
+
5. [Effects and InjectionContext in Angular(v21) - DEV Community](https://dev.to/pckalyan/effects-and-injectioncontext-in-angularv21-20ib) - Mastering the Life of an Effect: Injection Context and Beyond To understand why an...
|
|
201
|
+
|
|
202
|
+
6. [SolidJS: "computations created outside a `createRoot` or `render` will never be disposed" messages in the console log](https://stackoverflow.com/questions/70373659/solidjs-computations-created-outside-a-createroot-or-render-will-never-be) - When working on a SolidJS project you might start seeing the following warning message in your JS co...
|
|
203
|
+
|
|
204
|
+
7. [reactive-web-components/rwc 2.51.8 on npm](https://libraries.io/npm/@reactive-web-components%2Frwc) - Modern library for creating reactive web components with declarative syntax and strict typing
|
|
205
|
+
|
|
206
|
+
8. [effect() should have an option to run outside of an injection ...](https://github.com/angular/angular/issues/56357) - Which @angular/* package(s) are relevant/related to the feature request? core Description Angular's ...
|