@helfy/helfy 0.0.7 → 0.0.8
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 +265 -266
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
# Helfy
|
|
2
2
|
|
|
3
|
-
Helfy
|
|
3
|
+
Helfy — a TypeScript UI framework with a decorator-oriented API, custom JSX, and fine-grained reactivity.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Components are defined as classes with the `@View` decorator, state is managed via `@state` and `@observable`, and templates use extended JSX with `@if`, `@for`, and `@else` directives. Reactivity is built on signals (similar to SolidJS): `render()` runs once, and subsequent DOM updates are granular — only nodes that read the changed signal are updated.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Table of contents
|
|
8
8
|
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Components](#components)
|
|
11
|
+
- [Basic component](#basic-component)
|
|
12
12
|
- [Props](#props)
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
13
|
+
- [Local state (@state)](#local-state-state)
|
|
14
|
+
- [Nested components](#nested-components)
|
|
15
|
+
- [DOM mounting](#dom-mounting)
|
|
16
|
+
- [DOM and component refs (@ref)](#dom-and-component-refs-ref)
|
|
17
|
+
- [Two-way binding (@bind)](#two-way-binding-bind)
|
|
18
|
+
- [Forms (@Form, @field, @field in JSX)](#forms-form-field-field-in-jsx)
|
|
19
|
+
- [Template directives](#template-directives)
|
|
20
20
|
- [@if / @elseif / @else](#if--elseif--else)
|
|
21
21
|
- [@for](#for)
|
|
22
22
|
- [@empty](#empty)
|
|
23
23
|
- [State Management](#state-management)
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
24
|
+
- [Creating a Store](#creating-a-store)
|
|
25
|
+
- [Subscribing with @observe](#subscribing-with-observe)
|
|
26
|
+
- [Store context](#store-context)
|
|
27
|
+
- [Logging (@logger)](#logging-logger)
|
|
28
28
|
- [Context & DI](#context--di)
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [DI Container](#di-container
|
|
29
|
+
- [Provider (@Context, @provide)](#provider-context-provide)
|
|
30
|
+
- [Consumer (@ctx)](#consumer-ctx)
|
|
31
|
+
- [Reactive fields](#reactive-fields)
|
|
32
|
+
- [Optional injection](#optional-injection)
|
|
33
|
+
- [DI Container](#di-container-constructor-injection)
|
|
34
34
|
- [JSX](#jsx)
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [CSS
|
|
35
|
+
- [Attributes](#attributes)
|
|
36
|
+
- [Events](#events)
|
|
37
|
+
- [Styles](#styles)
|
|
38
|
+
- [CSS classes](#css-classes)
|
|
39
39
|
- [API](#api)
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Installation
|
|
44
44
|
|
|
45
|
-
###
|
|
45
|
+
### Creating a new project (helfy-create)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Quick start — create a project with a single command:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
npx helfy-create my-app
|
|
@@ -52,21 +52,21 @@ cd my-app
|
|
|
52
52
|
npm run dev
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Without a project name (creates `helfy-app`):
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
npx helfy-create
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
The `--skip-install` option — create without running `npm install` (e.g. for offline use or custom registry).
|
|
62
62
|
|
|
63
|
-
###
|
|
63
|
+
### Adding to an existing project
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
66
|
npm install @helfy/helfy
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Or in `package.json`:
|
|
70
70
|
|
|
71
71
|
```json
|
|
72
72
|
{
|
|
@@ -78,11 +78,11 @@ npm install @helfy/helfy
|
|
|
78
78
|
|
|
79
79
|
### Build setup
|
|
80
80
|
|
|
81
|
-
Babel
|
|
81
|
+
The Babel plugin runs the DI scanner before transformation (no pre-scripts in `package.json` needed). The scanner generates `.helfy/di-tokens.ts` and `.helfy/di-registry.ts`. Add `.helfy` to `.gitignore`.
|
|
82
82
|
|
|
83
83
|
### Babel
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
A single preset in `.babelrc` is enough:
|
|
86
86
|
|
|
87
87
|
```json
|
|
88
88
|
{
|
|
@@ -90,11 +90,11 @@ Babel-плагин автоматически запускает DI-сканер
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
The preset includes: JSX runtime, TypeScript, legacy decorators, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI for `@Injectable<IX>()`, auto-registration at `createApp`, `@logger()` → `@logger("<ClassName>")` transformation).
|
|
94
94
|
|
|
95
95
|
### Webpack
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
To process directives (`@if`, `@for`, `@ref`, `@bind`, `@field`) in `.tsx` files, add the loader:
|
|
98
98
|
|
|
99
99
|
```javascript
|
|
100
100
|
{
|
|
@@ -108,7 +108,7 @@ Babel-плагин автоматически запускает DI-сканер
|
|
|
108
108
|
|
|
109
109
|
### TypeScript
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
In `tsconfig.json` set:
|
|
112
112
|
|
|
113
113
|
```json
|
|
114
114
|
{
|
|
@@ -121,15 +121,15 @@ Babel-плагин автоматически запускает DI-сканер
|
|
|
121
121
|
}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
The `@helfy/helfy-ts-plugin` plugin provides IDE support for `@if`, `@for`, `@ref`, `@bind`, `@field` directives (autocomplete, typing, go-to-definition). Install separately: `npm install @helfy/helfy-ts-plugin`.
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
128
|
-
##
|
|
128
|
+
## Components
|
|
129
129
|
|
|
130
|
-
###
|
|
130
|
+
### Basic component
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
A component is a class with the `@View` decorator and a `render()` method that returns JSX:
|
|
133
133
|
|
|
134
134
|
```tsx
|
|
135
135
|
import { View } from "@helfy/helfy";
|
|
@@ -146,19 +146,19 @@ class Hello {
|
|
|
146
146
|
}
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
149
|
+
The `@View` decorator automatically:
|
|
150
|
+
- calls `render()` **once** and mounts the result into a DOM fragment
|
|
151
|
+
- adds a `view` property (reference to the root DOM element)
|
|
152
|
+
- wraps `this.props` in a reactive Proxy (each prop is a signal)
|
|
153
|
+
- configures fine-grained effects: when a signal changes, only the specific DOM node that reads it is updated, not the entire component
|
|
154
154
|
|
|
155
|
-
>
|
|
155
|
+
> **Important:** When inheriting from `@View`, apply the decorator on the child class as well. Otherwise `@ctx`, `scheduleUpdate`, and DOM updates will not work.
|
|
156
156
|
|
|
157
157
|
### Props
|
|
158
158
|
|
|
159
|
-
Props
|
|
159
|
+
Props are passed via the constructor. The `@View` decorator wraps `this.props` in a reactive Proxy — each prop becomes a signal. This means that when the parent updates props, only the DOM nodes that read them are updated, without a full re-render.
|
|
160
160
|
|
|
161
|
-
>
|
|
161
|
+
> **Important:** Do not destructure `this.props` in `render()`. Access fields directly via `this.props.field` — this ensures correct signal subscription.
|
|
162
162
|
|
|
163
163
|
```tsx
|
|
164
164
|
import { View } from "@helfy/helfy";
|
|
@@ -186,18 +186,17 @@ class Button {
|
|
|
186
186
|
}
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
Usage:
|
|
190
190
|
|
|
191
191
|
```tsx
|
|
192
|
-
<Button label="
|
|
192
|
+
<Button label="Click" onClick={() => console.log('clicked')} />
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
When the parent updates props, only the DOM nodes that read the changed props are updated.
|
|
196
196
|
|
|
197
|
-
###
|
|
197
|
+
### Local state (@state)
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
При изменении значения обновляются только те DOM-узлы и эффекты, которые читают это поле (fine-grained). Полный перерендер компонента не происходит:
|
|
199
|
+
The `@state` decorator makes a field reactive. When its value changes, only the DOM nodes and effects that read it are updated (fine-grained). The component is not fully re-rendered:
|
|
201
200
|
|
|
202
201
|
```tsx
|
|
203
202
|
import { View, state } from "@helfy/helfy";
|
|
@@ -207,7 +206,7 @@ class Counter {
|
|
|
207
206
|
@state private count = 0;
|
|
208
207
|
|
|
209
208
|
increment() {
|
|
210
|
-
this.count++; //
|
|
209
|
+
this.count++; // updates only nodes that read count
|
|
211
210
|
}
|
|
212
211
|
|
|
213
212
|
decrement() {
|
|
@@ -226,13 +225,13 @@ class Counter {
|
|
|
226
225
|
}
|
|
227
226
|
```
|
|
228
227
|
|
|
229
|
-
|
|
228
|
+
You can declare multiple `@state` fields. Each independently triggers updates only in the DOM nodes that read it.
|
|
230
229
|
|
|
231
|
-
|
|
230
|
+
Signal primitives (`createSignal`, `createEffect`, `createComputed`, `batch`, `onCleanup`) are also exported from `helfy` and can be used directly, but in most cases `@state`, `@computed`, and `@effect` are sufficient.
|
|
232
231
|
|
|
233
|
-
###
|
|
232
|
+
### Nested components
|
|
234
233
|
|
|
235
|
-
|
|
234
|
+
Components are used in JSX as tags. Props are passed as attributes:
|
|
236
235
|
|
|
237
236
|
```tsx
|
|
238
237
|
import { View } from "@helfy/helfy";
|
|
@@ -250,9 +249,9 @@ class App {
|
|
|
250
249
|
}
|
|
251
250
|
```
|
|
252
251
|
|
|
253
|
-
###
|
|
252
|
+
### DOM mounting
|
|
254
253
|
|
|
255
|
-
|
|
254
|
+
Typical bootstrap is via `createApp()`: an instance is created, `attach(root)` is called internally, and the `onAttached()` hook runs after insertion into the document.
|
|
256
255
|
|
|
257
256
|
```typescript
|
|
258
257
|
import { createApp } from "@helfy/helfy";
|
|
@@ -262,19 +261,19 @@ createApp({ root: document.getElementById("root")! })
|
|
|
262
261
|
.mount(App);
|
|
263
262
|
```
|
|
264
263
|
|
|
265
|
-
|
|
264
|
+
For scenarios without routing:
|
|
266
265
|
|
|
267
266
|
```typescript
|
|
268
267
|
createApp({ root: document.getElementById("root")! }).mount(App);
|
|
269
268
|
```
|
|
270
269
|
|
|
271
|
-
|
|
270
|
+
Manual mounting (without `createApp`): `const app = new App(); app.attach(root)`.
|
|
272
271
|
|
|
273
|
-
###
|
|
272
|
+
### DOM and component refs (@ref)
|
|
274
273
|
|
|
275
|
-
|
|
274
|
+
The `@ref` decorator marks a field to receive a reference to a DOM element or component instance. In JSX use the `@ref(this.fieldName)` directive. The reference is available after `onMount()`. For operations like `focus()` that require the element in the document, use `onAttached()`.
|
|
276
275
|
|
|
277
|
-
|
|
276
|
+
**For components** — the parent receives a proxy with access only to methods marked with `@expose`. This lets you explicitly define the component's public API.
|
|
278
277
|
|
|
279
278
|
```tsx
|
|
280
279
|
import { View, ref, expose } from "@helfy/helfy";
|
|
@@ -297,7 +296,7 @@ class Form {
|
|
|
297
296
|
@ref private input!: Input;
|
|
298
297
|
|
|
299
298
|
onAttached() {
|
|
300
|
-
this.input.focus(); //
|
|
299
|
+
this.input.focus(); // only the exposed method is available
|
|
301
300
|
}
|
|
302
301
|
|
|
303
302
|
render() {
|
|
@@ -306,15 +305,15 @@ class Form {
|
|
|
306
305
|
}
|
|
307
306
|
```
|
|
308
307
|
|
|
309
|
-
- DOM
|
|
310
|
-
-
|
|
311
|
-
-
|
|
308
|
+
- DOM elements via `@ref` are passed as-is
|
|
309
|
+
- Components with `@expose` — parent sees only the exposed methods
|
|
310
|
+
- Components without `@expose` — the full instance is passed (backward compatibility)
|
|
312
311
|
|
|
313
|
-
|
|
312
|
+
With conditional rendering (`@if`), the ref is cleared when the element is removed. Do not use `@ref` and `@state` on the same field.
|
|
314
313
|
|
|
315
|
-
###
|
|
314
|
+
### Two-way binding (@bind)
|
|
316
315
|
|
|
317
|
-
|
|
316
|
+
The `@bind` directive is syntactic sugar for two-way binding of a `@state` field to a form element. Syntax: `@bind(expr)` — parentheses, like `@ref`. The compiler generates the `value`/`checked` pair and event handler.
|
|
318
317
|
|
|
319
318
|
```tsx
|
|
320
319
|
import { View, state } from "@helfy/helfy";
|
|
@@ -332,8 +331,8 @@ class LoginForm {
|
|
|
332
331
|
<input @bind(this.email) type="email" />
|
|
333
332
|
<input @bind(this.isActive) type="checkbox" />
|
|
334
333
|
<select @bind(this.priority)>
|
|
335
|
-
<option value="low"
|
|
336
|
-
<option value="high"
|
|
334
|
+
<option value="low">Low</option>
|
|
335
|
+
<option value="high">High</option>
|
|
337
336
|
</select>
|
|
338
337
|
</form>
|
|
339
338
|
);
|
|
@@ -341,24 +340,24 @@ class LoginForm {
|
|
|
341
340
|
}
|
|
342
341
|
```
|
|
343
342
|
|
|
344
|
-
|
|
343
|
+
Transformation by element type:
|
|
345
344
|
|
|
346
|
-
|
|
|
347
|
-
|
|
345
|
+
| Element | Attribute | Event |
|
|
346
|
+
|---------|-----------|-------|
|
|
348
347
|
| `input[text|email|password|...]` | `value` | `oninput` |
|
|
349
348
|
| `input[checkbox|radio]` | `checked` | `onchange` |
|
|
350
349
|
| `select` | `value` | `onchange` |
|
|
351
350
|
| `textarea` | `value` | `oninput` |
|
|
352
351
|
|
|
353
|
-
|
|
352
|
+
The field must be marked with `@state` for reactive UI updates. For union types (e.g. `TodoPriority`), use `as typeof expr` — type safety is preserved.
|
|
354
353
|
|
|
355
|
-
|
|
354
|
+
**Custom components:** for binding to a component with a `value` prop, use named binding `@bind:value(expr)`:
|
|
356
355
|
|
|
357
356
|
```tsx
|
|
358
|
-
//
|
|
359
|
-
<Input @bind:value(this.title) placeholder="
|
|
357
|
+
// Parent
|
|
358
|
+
<Input @bind:value(this.title) placeholder="Title" />
|
|
360
359
|
|
|
361
|
-
//
|
|
360
|
+
// Input component with @binded("value")
|
|
362
361
|
@View
|
|
363
362
|
class Input {
|
|
364
363
|
@binded("value") private bindedVal!: string;
|
|
@@ -369,11 +368,11 @@ class Input {
|
|
|
369
368
|
}
|
|
370
369
|
```
|
|
371
370
|
|
|
372
|
-
###
|
|
371
|
+
### Forms (@Form, @field, @field in JSX)
|
|
373
372
|
|
|
374
|
-
|
|
373
|
+
Centralized form handling with validation via context: `@Form` — form context class, `@field` — field decorator (creates `FieldState`), `@useForm` — injects the form into a component. The JSX directive `@field(expr)` connects an input to `FieldState` in one line.
|
|
375
374
|
|
|
376
|
-
**FormContext** —
|
|
375
|
+
**FormContext** — a class with `@Form` and `@field` fields:
|
|
377
376
|
|
|
378
377
|
```tsx
|
|
379
378
|
import { Form, field, logger, type FieldState, type ILogger } from "@helfy/helfy";
|
|
@@ -392,7 +391,7 @@ export class LoginFormContext {
|
|
|
392
391
|
rememberMe!: FieldState<boolean>;
|
|
393
392
|
|
|
394
393
|
validateAll(): boolean {
|
|
395
|
-
//
|
|
394
|
+
// validate fields, set field.error
|
|
396
395
|
return true;
|
|
397
396
|
}
|
|
398
397
|
|
|
@@ -406,9 +405,9 @@ export class LoginFormContext {
|
|
|
406
405
|
}
|
|
407
406
|
```
|
|
408
407
|
|
|
409
|
-
**FieldState**
|
|
408
|
+
**FieldState** has `value`, `isDirty`, `isTouched`, `error`, `isValid`. When `value`/`error`/`isTouched` changes, components with `@useForm` re-render.
|
|
410
409
|
|
|
411
|
-
|
|
410
|
+
**Form provider** — the parent component wraps the form in context:
|
|
412
411
|
|
|
413
412
|
```tsx
|
|
414
413
|
// LoginPage.tsx
|
|
@@ -424,7 +423,7 @@ export class LoginPage {
|
|
|
424
423
|
}
|
|
425
424
|
```
|
|
426
425
|
|
|
427
|
-
|
|
426
|
+
**Form component** — inject via `@useForm`, access fields via `this.form`:
|
|
428
427
|
|
|
429
428
|
```tsx
|
|
430
429
|
import { View, useForm } from "@helfy/helfy";
|
|
@@ -443,72 +442,72 @@ export class LoginForm {
|
|
|
443
442
|
)}
|
|
444
443
|
<input @field(this.form.password) type="password" class="input" />
|
|
445
444
|
<input @field(this.form.rememberMe) type="checkbox" id="remember" />
|
|
446
|
-
<label for="remember"
|
|
447
|
-
<button type="submit"
|
|
445
|
+
<label for="remember">Remember me</label>
|
|
446
|
+
<button type="submit">Sign in</button>
|
|
448
447
|
</form>
|
|
449
448
|
);
|
|
450
449
|
}
|
|
451
450
|
}
|
|
452
451
|
```
|
|
453
452
|
|
|
454
|
-
|
|
453
|
+
Wrapper components (TextField, CheckboxField, etc.) accept `field` as a prop: `<TextField field={this.form.email} label="Email" />` and use `<input @field(this.props.field) />` internally.
|
|
455
454
|
|
|
456
|
-
**JSX
|
|
455
|
+
**JSX directive `@field(expr)`** — one directive replaces `@bind` + `onblur` + error `class` + `aria-invalid`. The compiler generates:
|
|
457
456
|
|
|
458
|
-
- `value`/`checked`
|
|
457
|
+
- `value`/`checked` and `oninput`/`onchange`
|
|
459
458
|
- `onblur` → `expr.isTouched = true`
|
|
460
|
-
- `class` —
|
|
459
|
+
- `class` — merged with existing; when `expr.isTouched && expr.error` adds `input-error`
|
|
461
460
|
- `aria-invalid={expr.isTouched && expr.error ? "true" : "false"}`
|
|
462
461
|
|
|
463
|
-
|
|
462
|
+
Supports `input` (text, email, password, checkbox, radio), `select`, `textarea`. The `.input-error` class can be defined in global styles (e.g. `@apply border-red-500` in Tailwind).
|
|
464
463
|
|
|
465
464
|
---
|
|
466
465
|
|
|
467
|
-
##
|
|
466
|
+
## Template directives
|
|
468
467
|
|
|
469
|
-
Helfy
|
|
468
|
+
Helfy extends JSX with `@if`, `@elseif`, `@else`, `@for`, and `@empty` directives.
|
|
470
469
|
|
|
471
470
|
### @if / @elseif / @else
|
|
472
471
|
|
|
473
|
-
|
|
472
|
+
Conditional rendering:
|
|
474
473
|
|
|
475
474
|
```tsx
|
|
476
475
|
render() {
|
|
477
476
|
return (
|
|
478
477
|
<div>
|
|
479
478
|
@if (this.count > 0) {
|
|
480
|
-
<span
|
|
479
|
+
<span>Positive: {this.count}</span>
|
|
481
480
|
}
|
|
482
481
|
</div>
|
|
483
482
|
);
|
|
484
483
|
}
|
|
485
484
|
```
|
|
486
485
|
|
|
487
|
-
|
|
486
|
+
Condition chains:
|
|
488
487
|
|
|
489
488
|
```tsx
|
|
490
489
|
render() {
|
|
491
490
|
return (
|
|
492
491
|
<div>
|
|
493
492
|
@if (this.status === 'loading') {
|
|
494
|
-
<span
|
|
493
|
+
<span>Loading...</span>
|
|
495
494
|
} @elseif (this.status === 'error') {
|
|
496
|
-
<span
|
|
495
|
+
<span>Error!</span>
|
|
497
496
|
} @else {
|
|
498
|
-
<span
|
|
497
|
+
<span>Data loaded</span>
|
|
499
498
|
}
|
|
500
499
|
</div>
|
|
501
500
|
);
|
|
502
501
|
}
|
|
503
502
|
```
|
|
504
503
|
|
|
505
|
-
|
|
504
|
+
Nested conditions:
|
|
506
505
|
|
|
507
506
|
```tsx
|
|
508
507
|
@if (this.isVisible) {
|
|
509
508
|
<div>
|
|
510
509
|
@if (this.count > 10) {
|
|
511
|
-
<span
|
|
510
|
+
<span>More than ten</span>
|
|
512
511
|
}
|
|
513
512
|
</div>
|
|
514
513
|
}
|
|
@@ -516,7 +515,7 @@ render() {
|
|
|
516
515
|
|
|
517
516
|
### @for
|
|
518
517
|
|
|
519
|
-
|
|
518
|
+
Array iteration. Syntax: `@for (item of array)` or `@for (item, index of array)`.
|
|
520
519
|
|
|
521
520
|
```tsx
|
|
522
521
|
@state private items = ['apple', 'banana', 'cherry'];
|
|
@@ -532,7 +531,7 @@ render() {
|
|
|
532
531
|
}
|
|
533
532
|
```
|
|
534
533
|
|
|
535
|
-
`track`
|
|
534
|
+
`track` sets the key for DOM diffing optimization (like `key` in React):
|
|
536
535
|
|
|
537
536
|
```tsx
|
|
538
537
|
@for (user of this.users; track user.id) {
|
|
@@ -542,13 +541,13 @@ render() {
|
|
|
542
541
|
|
|
543
542
|
### @empty
|
|
544
543
|
|
|
545
|
-
|
|
544
|
+
The `@empty` block after `@for` renders when the array is empty:
|
|
546
545
|
|
|
547
546
|
```tsx
|
|
548
547
|
@for (item of this.items; track item) {
|
|
549
548
|
<div>{item}</div>
|
|
550
549
|
} @empty {
|
|
551
|
-
<span
|
|
550
|
+
<span>List is empty</span>
|
|
552
551
|
}
|
|
553
552
|
```
|
|
554
553
|
|
|
@@ -556,9 +555,9 @@ render() {
|
|
|
556
555
|
|
|
557
556
|
## State Management
|
|
558
557
|
|
|
559
|
-
###
|
|
558
|
+
### Creating a Store
|
|
560
559
|
|
|
561
|
-
Store
|
|
560
|
+
A Store is a global reactive store. Create a class with the `@Store` decorator; reactive fields use `@observable`:
|
|
562
561
|
|
|
563
562
|
```typescript
|
|
564
563
|
import { Store, observable } from "@helfy/helfy";
|
|
@@ -570,7 +569,7 @@ class UserStore {
|
|
|
570
569
|
|
|
571
570
|
login(name: string) {
|
|
572
571
|
this.name = name;
|
|
573
|
-
this.isLoggedIn = true; //
|
|
572
|
+
this.isLoggedIn = true; // subscribers of name and isLoggedIn are notified
|
|
574
573
|
}
|
|
575
574
|
|
|
576
575
|
logout() {
|
|
@@ -582,15 +581,15 @@ class UserStore {
|
|
|
582
581
|
export default UserStore;
|
|
583
582
|
```
|
|
584
583
|
|
|
585
|
-
|
|
586
|
-
- `subscribe(field, callback)`
|
|
587
|
-
- `unsubscribe(field, callback)`
|
|
584
|
+
The `@Store` decorator adds:
|
|
585
|
+
- `subscribe(field, callback)` — subscribe to field changes, returns unsubscribe function
|
|
586
|
+
- `unsubscribe(field, callback)` — unsubscribe
|
|
588
587
|
|
|
589
|
-
|
|
588
|
+
The `@observable` decorator turns a field into a reactive property with getter/setter. On write, all subscribers are notified.
|
|
590
589
|
|
|
591
|
-
###
|
|
590
|
+
### Subscribing with @observe
|
|
592
591
|
|
|
593
|
-
|
|
592
|
+
The `@observe` decorator binds a component field to a store field. When the store value changes, the component re-renders:
|
|
594
593
|
|
|
595
594
|
```tsx
|
|
596
595
|
import { View, observe } from "@helfy/helfy";
|
|
@@ -609,9 +608,9 @@ class Header {
|
|
|
609
608
|
return (
|
|
610
609
|
<header>
|
|
611
610
|
@if (this.isLoggedIn) {
|
|
612
|
-
<span
|
|
611
|
+
<span>Hello, {this.userName}!</span>
|
|
613
612
|
} @else {
|
|
614
|
-
<span
|
|
613
|
+
<span>Sign in</span>
|
|
615
614
|
}
|
|
616
615
|
</header>
|
|
617
616
|
);
|
|
@@ -619,11 +618,11 @@ class Header {
|
|
|
619
618
|
}
|
|
620
619
|
```
|
|
621
620
|
|
|
622
|
-
|
|
621
|
+
The field type is inferred from the store via lookup type (`UserStore['name']`), giving full type safety.
|
|
623
622
|
|
|
624
|
-
###
|
|
623
|
+
### Store context
|
|
625
624
|
|
|
626
|
-
|
|
625
|
+
Create a single context for all stores:
|
|
627
626
|
|
|
628
627
|
```typescript
|
|
629
628
|
import UserStore from "./UserStore";
|
|
@@ -637,26 +636,26 @@ class Stores {
|
|
|
637
636
|
export default Stores;
|
|
638
637
|
```
|
|
639
638
|
|
|
640
|
-
|
|
639
|
+
Usage in components:
|
|
641
640
|
|
|
642
641
|
```tsx
|
|
643
642
|
import Stores from "./StoreContext";
|
|
644
643
|
|
|
645
|
-
//
|
|
644
|
+
// subscribe via decorator
|
|
646
645
|
@observe(Stores.userStore, 'name')
|
|
647
646
|
private userName: string;
|
|
648
647
|
|
|
649
|
-
//
|
|
648
|
+
// direct store method call
|
|
650
649
|
Stores.userStore.login("Alice");
|
|
651
650
|
```
|
|
652
651
|
|
|
653
652
|
---
|
|
654
653
|
|
|
655
|
-
##
|
|
654
|
+
## Logging (@logger)
|
|
656
655
|
|
|
657
|
-
|
|
656
|
+
The `@logger` decorator injects a logger into View, Context, Store, and Injectable classes. The class name is set at compile time (Babel plugin), keeping readable names even after minification.
|
|
658
657
|
|
|
659
|
-
###
|
|
658
|
+
### Usage
|
|
660
659
|
|
|
661
660
|
```tsx
|
|
662
661
|
import { View, logger, type ILogger } from "@helfy/helfy";
|
|
@@ -673,22 +672,22 @@ class TodoInput {
|
|
|
673
672
|
}
|
|
674
673
|
```
|
|
675
674
|
|
|
676
|
-
|
|
677
|
-
- `@logger()` —
|
|
678
|
-
- `@logger("my-tag")` —
|
|
675
|
+
**Variants:**
|
|
676
|
+
- `@logger()` — class name is set at compile-time as `<ClassName>`, format and color depend on class type
|
|
677
|
+
- `@logger("my-tag")` — custom tag, gray color
|
|
679
678
|
|
|
680
|
-
###
|
|
679
|
+
### Tag format and color by class type
|
|
681
680
|
|
|
682
|
-
|
|
|
683
|
-
|
|
684
|
-
| View (
|
|
685
|
-
| Injectable (
|
|
681
|
+
| Type | Format | Color |
|
|
682
|
+
|------|--------|-------|
|
|
683
|
+
| View (components) | `<TodoInput>` | skyblue |
|
|
684
|
+
| Injectable (services) | `TodoValidateService()` | pink |
|
|
686
685
|
| Context | `{TodoContext}` | khaki |
|
|
687
686
|
| Form | `[LoginFormContext]` | bright blue |
|
|
688
687
|
| Store | `TodoStore[]` | lime green |
|
|
689
|
-
|
|
|
688
|
+
| Custom `@logger("...")` | as specified | gray |
|
|
690
689
|
|
|
691
|
-
### API
|
|
690
|
+
### Logger API
|
|
692
691
|
|
|
693
692
|
```typescript
|
|
694
693
|
interface ILogger {
|
|
@@ -696,13 +695,13 @@ interface ILogger {
|
|
|
696
695
|
info(message: string, meta?: Record<string, unknown>): void;
|
|
697
696
|
warn(message: string, meta?: Record<string, unknown>): void;
|
|
698
697
|
error(messageOrError: string | Error, meta?: Record<string, unknown>): void;
|
|
699
|
-
withContext(ctx: string): ILogger; //
|
|
698
|
+
withContext(ctx: string): ILogger; // child logger with extra prefix
|
|
700
699
|
}
|
|
701
700
|
```
|
|
702
701
|
|
|
703
|
-
### DI
|
|
702
|
+
### DI and transports
|
|
704
703
|
|
|
705
|
-
`LoggerService`
|
|
704
|
+
`LoggerService` is registered under `ILoggerToken` via `registerAllServices`. Custom transports (Sentry, file, etc.) implementing `ILogTransport` can be wired up:
|
|
706
705
|
|
|
707
706
|
```typescript
|
|
708
707
|
import { LoggerService, ConsoleTransport, ILoggerToken } from "@helfy/helfy";
|
|
@@ -715,17 +714,17 @@ createApp({ root })
|
|
|
715
714
|
.mount(App);
|
|
716
715
|
```
|
|
717
716
|
|
|
718
|
-
|
|
717
|
+
Without a registered logger in the container, a fallback that logs to `console` is used.
|
|
719
718
|
|
|
720
719
|
---
|
|
721
720
|
|
|
722
721
|
## Context & DI
|
|
723
722
|
|
|
724
|
-
Helfy
|
|
723
|
+
Helfy supports hierarchical Context and Dependency Injection: a provider wraps a subtree in JSX, and child components receive values via `@ctx`. Useful for theme, forms, routing, and other shared dependencies.
|
|
725
724
|
|
|
726
|
-
###
|
|
725
|
+
### Provider (@Context, @provide)
|
|
727
726
|
|
|
728
|
-
|
|
727
|
+
A class with the `@Context` decorator is a non-rendering provider: it has no `render()` and only renders its children. Fields with `@provide` are available to consumers down the tree.
|
|
729
728
|
|
|
730
729
|
```tsx
|
|
731
730
|
import { Context, provide } from "@helfy/helfy";
|
|
@@ -744,7 +743,7 @@ export class ThemeContext {
|
|
|
744
743
|
}
|
|
745
744
|
```
|
|
746
745
|
|
|
747
|
-
|
|
746
|
+
Usage in JSX — wrap a subtree:
|
|
748
747
|
|
|
749
748
|
```tsx
|
|
750
749
|
<ThemeContext>
|
|
@@ -753,11 +752,11 @@ export class ThemeContext {
|
|
|
753
752
|
</ThemeContext>
|
|
754
753
|
```
|
|
755
754
|
|
|
756
|
-
|
|
755
|
+
The framework only renders the children of `ThemeContext`; the provider itself does not create DOM nodes.
|
|
757
756
|
|
|
758
|
-
###
|
|
757
|
+
### Consumer (@ctx)
|
|
759
758
|
|
|
760
|
-
|
|
759
|
+
A component with `@View` can receive context values via `@ctx`:
|
|
761
760
|
|
|
762
761
|
```tsx
|
|
763
762
|
import { View, ctx } from "@helfy/helfy";
|
|
@@ -771,14 +770,14 @@ class ThemeToggle {
|
|
|
771
770
|
render() {
|
|
772
771
|
return (
|
|
773
772
|
<button onclick={this.theme.toggle}>
|
|
774
|
-
{this.theme.mode === "dark" ? "
|
|
773
|
+
{this.theme.mode === "dark" ? "Light theme" : "Dark theme"}
|
|
775
774
|
</button>
|
|
776
775
|
);
|
|
777
776
|
}
|
|
778
777
|
}
|
|
779
778
|
```
|
|
780
779
|
|
|
781
|
-
|
|
780
|
+
You can inject only a context field:
|
|
782
781
|
|
|
783
782
|
```tsx
|
|
784
783
|
@View
|
|
@@ -787,18 +786,18 @@ class ModeDisplay {
|
|
|
787
786
|
private mode!: TThemeMode;
|
|
788
787
|
|
|
789
788
|
render() {
|
|
790
|
-
return <span
|
|
789
|
+
return <span>Current theme: {this.mode}</span>;
|
|
791
790
|
}
|
|
792
791
|
}
|
|
793
792
|
```
|
|
794
793
|
|
|
795
|
-
|
|
794
|
+
Provider lookup goes up the tree (`_parentView`); the nearest provider with the matching key is used.
|
|
796
795
|
|
|
797
|
-
###
|
|
796
|
+
### Reactive fields
|
|
798
797
|
|
|
799
|
-
`@provide({ reactive: true })`
|
|
798
|
+
`@provide({ reactive: true })` makes a field reactive: when it changes, all consumers re-render. For methods, `@provide()` without `reactive` is enough.
|
|
800
799
|
|
|
801
|
-
|
|
800
|
+
**Computed fields** — getters with `@provide({ computed: true, deps: ["field1", "field2"] })` recompute when dependencies change and trigger consumer re-renders:
|
|
802
801
|
|
|
803
802
|
```tsx
|
|
804
803
|
@provide({ computed: true, deps: ["todos", "filter"] })
|
|
@@ -809,9 +808,9 @@ get filteredTodos(): Todo[] {
|
|
|
809
808
|
}
|
|
810
809
|
```
|
|
811
810
|
|
|
812
|
-
###
|
|
811
|
+
### Optional injection
|
|
813
812
|
|
|
814
|
-
|
|
813
|
+
The third argument of `@ctx` — options `{ optional?, defaultValue? }`:
|
|
815
814
|
|
|
816
815
|
```tsx
|
|
817
816
|
@ctx(ThemeContext, { optional: true })
|
|
@@ -821,15 +820,15 @@ private theme?: ThemeContext;
|
|
|
821
820
|
private mode = "light";
|
|
822
821
|
```
|
|
823
822
|
|
|
824
|
-
|
|
823
|
+
With no provider in the tree, `optional: true` yields `undefined`; `defaultValue` is used when the provider is absent.
|
|
825
824
|
|
|
826
|
-
### DI Container (
|
|
825
|
+
### DI Container (constructor injection)
|
|
827
826
|
|
|
828
|
-
Helfy
|
|
827
|
+
Helfy supports a DI container for global services with **compile-time** magic: services are marked with `@Injectable<IX>()`, consumers only specify the type in the constructor — no `@inject` or explicit tokens.
|
|
829
828
|
|
|
830
|
-
|
|
829
|
+
**Convention:** the last constructor parameter is props from the parent; preceding parameters are resolved from the container or from @Context up the tree.
|
|
831
830
|
|
|
832
|
-
|
|
831
|
+
**Service** — must use `@Injectable<IX>()` (interface in generic):
|
|
833
832
|
|
|
834
833
|
```typescript
|
|
835
834
|
export interface ITodoValidateService {
|
|
@@ -842,7 +841,7 @@ export class TodoValidateService implements ITodoValidateService {
|
|
|
842
841
|
}
|
|
843
842
|
```
|
|
844
843
|
|
|
845
|
-
|
|
844
|
+
**Consumer** — only the type in the constructor (plugin injects the token):
|
|
846
845
|
|
|
847
846
|
```tsx
|
|
848
847
|
@View
|
|
@@ -854,7 +853,7 @@ class TodoInput {
|
|
|
854
853
|
}
|
|
855
854
|
```
|
|
856
855
|
|
|
857
|
-
**Bootstrap** —
|
|
856
|
+
**Bootstrap** — DI registration is attached automatically at `createApp` (plugin injects `.useDI(registerAllServices)` in the chain):
|
|
858
857
|
|
|
859
858
|
```typescript
|
|
860
859
|
createApp({ root: document.getElementById("root")! })
|
|
@@ -862,9 +861,9 @@ createApp({ root: document.getElementById("root")! })
|
|
|
862
861
|
.mount(App);
|
|
863
862
|
```
|
|
864
863
|
|
|
865
|
-
|
|
864
|
+
The scanner is run by the Babel plugin before transformation, finds all `@Injectable<IX>()`, and generates `.helfy/di-tokens.ts` and `.helfy/di-registry.ts`.
|
|
866
865
|
|
|
867
|
-
**Fallback** —
|
|
866
|
+
**Fallback** — for manual setup: `.configureContainer()`, `@inject(token)`, `@Injectable(token)`:
|
|
868
867
|
|
|
869
868
|
```typescript
|
|
870
869
|
createApp({ root })
|
|
@@ -876,38 +875,38 @@ createApp({ root })
|
|
|
876
875
|
.mount(App);
|
|
877
876
|
```
|
|
878
877
|
|
|
879
|
-
|
|
878
|
+
Optionally: pass a custom container via `.container(myContainer)` or `createApp({ root, container })`.
|
|
880
879
|
|
|
881
|
-
`@Context`
|
|
880
|
+
`@Context` and the container coexist: Context is for tree-scoped values (theme, router), Container for global services.
|
|
882
881
|
|
|
883
882
|
---
|
|
884
883
|
|
|
885
884
|
## JSX
|
|
886
885
|
|
|
887
|
-
Helfy
|
|
886
|
+
Helfy uses a custom JSX runtime (`@helfy/helfy/jsx-runtime`). JSX is translated to `jsx()` / `jsxs()` calls that build a virtual DOM representation.
|
|
888
887
|
|
|
889
|
-
###
|
|
888
|
+
### Attributes
|
|
890
889
|
|
|
891
|
-
|
|
890
|
+
Standard HTML attributes are passed through:
|
|
892
891
|
|
|
893
892
|
```tsx
|
|
894
|
-
<input type="text" placeholder="
|
|
893
|
+
<input type="text" placeholder="Enter name" />
|
|
895
894
|
<img src="/logo.png" alt="Logo" />
|
|
896
895
|
<div id="container" class="wrapper"></div>
|
|
897
896
|
```
|
|
898
897
|
|
|
899
|
-
###
|
|
898
|
+
### Events
|
|
900
899
|
|
|
901
|
-
|
|
900
|
+
Event handlers use **lowercase** attributes (`onclick`, `oninput`, not `onClick`):
|
|
902
901
|
|
|
903
902
|
```tsx
|
|
904
903
|
<button onclick={() => this.increment()}>+</button>
|
|
905
904
|
<input oninput={(e) => this.handleInput(e)} />
|
|
906
905
|
```
|
|
907
906
|
|
|
908
|
-
###
|
|
907
|
+
### Styles
|
|
909
908
|
|
|
910
|
-
|
|
909
|
+
Inline styles use an object with camelCase keys:
|
|
911
910
|
|
|
912
911
|
```tsx
|
|
913
912
|
<div style={{
|
|
@@ -916,26 +915,26 @@ Helfy использует кастомный JSX runtime (`@helfy/helfy/jsx-run
|
|
|
916
915
|
padding: '8px 16px',
|
|
917
916
|
borderRadius: '4px'
|
|
918
917
|
}}>
|
|
919
|
-
|
|
918
|
+
Styled block
|
|
920
919
|
</div>
|
|
921
920
|
```
|
|
922
921
|
|
|
923
|
-
### CSS
|
|
922
|
+
### CSS classes
|
|
924
923
|
|
|
925
|
-
|
|
924
|
+
The `class` attribute accepts a string or an array with conditional classes:
|
|
926
925
|
|
|
927
926
|
```tsx
|
|
928
|
-
//
|
|
927
|
+
// string
|
|
929
928
|
<div class="container">...</div>
|
|
930
929
|
|
|
931
930
|
// CSS Modules
|
|
932
931
|
import styles from './App.module.css';
|
|
933
932
|
<div class={styles.wrapper}>...</div>
|
|
934
933
|
|
|
935
|
-
//
|
|
934
|
+
// conditional classes via array
|
|
936
935
|
<div class={[
|
|
937
936
|
styles.cell,
|
|
938
|
-
[styles.active, this.isActive], //
|
|
937
|
+
[styles.active, this.isActive], // applied when this.isActive === true
|
|
939
938
|
[styles.disabled, this.isDisabled],
|
|
940
939
|
]}>...</div>
|
|
941
940
|
```
|
|
@@ -944,33 +943,33 @@ import styles from './App.module.css';
|
|
|
944
943
|
|
|
945
944
|
## API
|
|
946
945
|
|
|
947
|
-
###
|
|
948
|
-
|
|
949
|
-
|
|
|
950
|
-
|
|
951
|
-
| `@View` |
|
|
952
|
-
| `@state` |
|
|
953
|
-
| `@Store` |
|
|
954
|
-
| `@observable` |
|
|
955
|
-
| `@observe(store, field)` |
|
|
956
|
-
| `@Context` |
|
|
957
|
-
| `@provide()` / `@provide({ reactive: true })` / `@provide({ computed: true, deps })` |
|
|
958
|
-
| `@ctx(ContextClass)` / `@ctx(ContextClass, field)` |
|
|
959
|
-
| `@Injectable<IX>()` / `@Injectable<IX>('singleton' \| 'transient' \| 'scoped')` / `@Injectable(token)` |
|
|
960
|
-
| `@inject(token)` |
|
|
961
|
-
| `@logger()` / `@logger("tag")` |
|
|
962
|
-
| `@ref` |
|
|
963
|
-
| `@expose` |
|
|
964
|
-
| `@binded(name)` |
|
|
965
|
-
| `@bind(expr)` / `@bind:name(expr)` | JSX |
|
|
966
|
-
| `@Form` |
|
|
967
|
-
| `@field(options)` |
|
|
968
|
-
| `@useForm(FormContext)` |
|
|
969
|
-
| `@field(expr)` | JSX |
|
|
970
|
-
|
|
971
|
-
###
|
|
972
|
-
|
|
973
|
-
Helfy
|
|
946
|
+
### Decorators
|
|
947
|
+
|
|
948
|
+
| Decorator | Scope | Description |
|
|
949
|
+
|-----------|-------|-------------|
|
|
950
|
+
| `@View` | Class | Turns a class into a component with `render()`, `view`, `update()` |
|
|
951
|
+
| `@state` | Field | Component local state on signals (write updates only this component) |
|
|
952
|
+
| `@Store` | Class | Adds `subscribe`/`unsubscribe` and `@observable` field reactivity |
|
|
953
|
+
| `@observable` | Field | Makes a store field reactive — write notifies subscribers |
|
|
954
|
+
| `@observe(store, field)` | Field | Binds a component field to a store field |
|
|
955
|
+
| `@Context` | Class | Non-rendering context provider; renders only children |
|
|
956
|
+
| `@provide()` / `@provide({ reactive: true })` / `@provide({ computed: true, deps })` | Field | Marks a field as available to `@ctx`. `reactive` — consumers re-render on change; `computed` + `deps` — for getters |
|
|
957
|
+
| `@ctx(ContextClass)` / `@ctx(ContextClass, field)` | Field | Injects context or a context field from the nearest provider up the tree |
|
|
958
|
+
| `@Injectable<IX>()` / `@Injectable<IX>('singleton' \| 'transient' \| 'scoped')` / `@Injectable(token)` | Class | Marks a class as injectable. Scope in (): `'singleton'` (default), `'transient'`, `'scoped'`. Fallback: `@Injectable(token)` with Symbol/string |
|
|
959
|
+
| `@inject(token)` | Constructor param | Sets the token for the parameter (fallback; with `@Injectable<IX>()` the plugin injects it automatically) |
|
|
960
|
+
| `@logger()` / `@logger("tag")` | Field | Injects ILogger. No arg — compile-time class name and color by type (View/Context/Store/Injectable). With arg — custom tag (gray) |
|
|
961
|
+
| `@ref` | Field | Marks a field to receive a DOM or component reference (use with `@ref(this.fieldName)` in JSX) |
|
|
962
|
+
| `@expose` | Method | Makes a method available to the parent when using `@ref` on a component (without `@expose` the parent gets the full instance) |
|
|
963
|
+
| `@binded(name)` | Field | Binds a field to `@bind:name` from the parent (for custom components) |
|
|
964
|
+
| `@bind(expr)` / `@bind:name(expr)` | JSX | Two-way binding with `@state` field (value/checked + oninput/onchange). For components: `@bind:value(expr)` |
|
|
965
|
+
| `@Form` | Class | Form context with `@field` fields |
|
|
966
|
+
| `@field(options)` | FormContext field | Creates FieldState for a form field (value, isTouched, error, isDirty) |
|
|
967
|
+
| `@useForm(FormContext)` | Field | Injects the form into a component with subscription to field changes |
|
|
968
|
+
| `@field(expr)` | JSX | Connects an input to FieldState: value/checked + onblur + error class + aria-invalid |
|
|
969
|
+
|
|
970
|
+
### Routing (SPA)
|
|
971
|
+
|
|
972
|
+
Helfy includes a lightweight SPA router. When you call `createApp().router({ routes }).mount(App)`, the framework automatically wraps the app in `RouterContext`, so you don't need to wrap your App manually:
|
|
974
973
|
|
|
975
974
|
```tsx
|
|
976
975
|
// index.ts
|
|
@@ -1012,15 +1011,15 @@ class Sidebar {
|
|
|
1012
1011
|
|
|
1013
1012
|
return (
|
|
1014
1013
|
<nav>
|
|
1015
|
-
<Link to="/" label="
|
|
1016
|
-
<Link to="/analytics" label="
|
|
1014
|
+
<Link to="/" label="Home" class={isHome ? "font-bold" : ""} />
|
|
1015
|
+
<Link to="/analytics" label="Analytics" class={isAnalytics ? "font-bold" : ""} />
|
|
1017
1016
|
</nav>
|
|
1018
1017
|
);
|
|
1019
1018
|
}
|
|
1020
1019
|
}
|
|
1021
1020
|
```
|
|
1022
1021
|
|
|
1023
|
-
|
|
1022
|
+
A typical page component can use router decorators:
|
|
1024
1023
|
|
|
1025
1024
|
```tsx
|
|
1026
1025
|
import { View, path, search, params, router, type RouterAPI } from "@helfy/helfy";
|
|
@@ -1047,7 +1046,7 @@ class DebugPage {
|
|
|
1047
1046
|
<pre>params: {JSON.stringify(this.routeParams)}</pre>
|
|
1048
1047
|
<pre>query: {JSON.stringify(this.query)}</pre>
|
|
1049
1048
|
<button onclick={() => this.rtr.push("/analytics")}>
|
|
1050
|
-
|
|
1049
|
+
Go to analytics
|
|
1051
1050
|
</button>
|
|
1052
1051
|
</section>
|
|
1053
1052
|
);
|
|
@@ -1055,7 +1054,7 @@ class DebugPage {
|
|
|
1055
1054
|
}
|
|
1056
1055
|
```
|
|
1057
1056
|
|
|
1058
|
-
|
|
1057
|
+
**Custom 404 page.** Wrap `RouterView` and override the `notFound` slot:
|
|
1059
1058
|
|
|
1060
1059
|
```tsx
|
|
1061
1060
|
@View
|
|
@@ -1070,35 +1069,35 @@ class AppRouter {
|
|
|
1070
1069
|
);
|
|
1071
1070
|
}
|
|
1072
1071
|
}
|
|
1073
|
-
```
|
|
1072
|
+
```
|
|
1074
1073
|
|
|
1075
|
-
###
|
|
1074
|
+
### Component lifecycle
|
|
1076
1075
|
|
|
1077
|
-
1. `constructor(props)`
|
|
1078
|
-
2. `render()`
|
|
1079
|
-
3. `mount()`
|
|
1080
|
-
4. `onMount()`
|
|
1081
|
-
5. `onAttached()`
|
|
1082
|
-
6. `update()`
|
|
1083
|
-
7. `updateProps(newProps)`
|
|
1076
|
+
1. `constructor(props)` — instance creation, `this.props` wrapped in reactive Proxy
|
|
1077
|
+
2. `render()` — returns JSX (**called once** on mount)
|
|
1078
|
+
3. `mount()` — initial JSX render to DOM fragment
|
|
1079
|
+
4. `onMount()` — hook after first mount (optional)
|
|
1080
|
+
5. `onAttached()` — hook after insertion into document (optional). Called on `attach(parent)`.
|
|
1081
|
+
6. `update()` — structural update (e.g. on route change). For the same child component, only signals are updated via `updateProps`
|
|
1082
|
+
7. `updateProps(newProps)` — updates prop signals (fine-grained, no full re-render)
|
|
1084
1083
|
|
|
1085
1084
|
#### onMount vs onAttached
|
|
1086
1085
|
|
|
1087
1086
|
| | `onMount()` | `onAttached()` |
|
|
1088
1087
|
|---|---|---|
|
|
1089
|
-
|
|
|
1090
|
-
|
|
|
1091
|
-
|
|
|
1088
|
+
| **When** | Right after `mount()`, tree built, refs assigned | After `attach(parent)`, element in document |
|
|
1089
|
+
| **Element in document** | May not be yet (root in fragment) | Yes |
|
|
1090
|
+
| **Refs** | Available | Available |
|
|
1092
1091
|
|
|
1093
|
-
**`onMount()`** —
|
|
1094
|
-
-
|
|
1095
|
-
-
|
|
1096
|
-
-
|
|
1092
|
+
**`onMount()`** — for initialization that doesn't need the document:
|
|
1093
|
+
- Subscriptions to store/observable/services
|
|
1094
|
+
- Internal state setup
|
|
1095
|
+
- Adding handlers (works on detached nodes too)
|
|
1097
1096
|
|
|
1098
|
-
**`onAttached()`** —
|
|
1097
|
+
**`onAttached()`** — for operations that require the element in the document:
|
|
1099
1098
|
- `focus()`, `scrollIntoView()`
|
|
1100
|
-
- `getBoundingClientRect()`,
|
|
1101
|
-
-
|
|
1099
|
+
- `getBoundingClientRect()`, layout measurement
|
|
1100
|
+
- Any DOM API that only works on attached nodes
|
|
1102
1101
|
|
|
1103
1102
|
```tsx
|
|
1104
1103
|
@View
|
|
@@ -1106,22 +1105,22 @@ class SearchInput {
|
|
|
1106
1105
|
@ref private input!: HTMLInputElement;
|
|
1107
1106
|
|
|
1108
1107
|
onMount() {
|
|
1109
|
-
this.store.subscribe(this.handleChange); //
|
|
1108
|
+
this.store.subscribe(this.handleChange); // subscription — document not needed
|
|
1110
1109
|
}
|
|
1111
1110
|
|
|
1112
1111
|
onAttached() {
|
|
1113
|
-
this.input.focus(); //
|
|
1112
|
+
this.input.focus(); // focus — needs document
|
|
1114
1113
|
}
|
|
1115
1114
|
}
|
|
1116
1115
|
```
|
|
1117
1116
|
|
|
1118
|
-
###
|
|
1117
|
+
### Slots (content projection)
|
|
1119
1118
|
|
|
1120
|
-
Helfy
|
|
1119
|
+
Helfy supports named slots with fallback content and override in child components.
|
|
1121
1120
|
|
|
1122
|
-
####
|
|
1121
|
+
#### Slot provider (`@View` component)
|
|
1123
1122
|
|
|
1124
|
-
|
|
1123
|
+
Slots are declared in JSX via the `@slot:name(...)` directive inside `render()`:
|
|
1125
1124
|
|
|
1126
1125
|
```tsx
|
|
1127
1126
|
import { View } from "@helfy/helfy";
|
|
@@ -1131,24 +1130,24 @@ class AppLayout {
|
|
|
1131
1130
|
render() {
|
|
1132
1131
|
return (
|
|
1133
1132
|
<section class="layout">
|
|
1134
|
-
{/*
|
|
1135
|
-
@slot:header({ title: "
|
|
1133
|
+
{/* Named slot header with fallback markup */}
|
|
1134
|
+
@slot:header({ title: "Task list" }) fallback {
|
|
1136
1135
|
<header class="mb-4">
|
|
1137
1136
|
<h1 class="text-2xl font-bold text-gray-900">
|
|
1138
|
-
|
|
1137
|
+
Task list
|
|
1139
1138
|
</h1>
|
|
1140
1139
|
</header>
|
|
1141
1140
|
}
|
|
1142
1141
|
|
|
1143
|
-
{/*
|
|
1142
|
+
{/* Named slot content with fallback and @if inside */}
|
|
1144
1143
|
@slot:content({ store: this.props.store, filtered: this.props.filtered, hasTodos: this.props.hasTodos }) fallback {
|
|
1145
1144
|
@if (this.props.hasTodos) {
|
|
1146
1145
|
<section class="pt-3 border-t border-gray-200 text-sm text-gray-600">
|
|
1147
1146
|
<p class="mb-1">
|
|
1148
|
-
|
|
1149
|
-
|
|
1147
|
+
Total: {this.props.store.todos.length}, active: {this.props.store.activeCount},
|
|
1148
|
+
completed: {this.props.store.completedCount}
|
|
1150
1149
|
</p>
|
|
1151
|
-
<p
|
|
1150
|
+
<p>Filtered: {this.props.filtered.length}</p>
|
|
1152
1151
|
</section>
|
|
1153
1152
|
}
|
|
1154
1153
|
}
|
|
@@ -1158,15 +1157,15 @@ class AppLayout {
|
|
|
1158
1157
|
}
|
|
1159
1158
|
```
|
|
1160
1159
|
|
|
1161
|
-
|
|
1160
|
+
Provider rules:
|
|
1162
1161
|
|
|
1163
|
-
- `@slot:header({ ... })` —
|
|
1164
|
-
-
|
|
1165
|
-
-
|
|
1162
|
+
- `@slot:header({ ... })` — declares the named slot `header` and invokes it.
|
|
1163
|
+
- The `fallback { ... }` block (optional) defines default markup when the slot is not overridden.
|
|
1164
|
+
- Inside `fallback` you can use `@if`, `@for`, and regular JSX.
|
|
1166
1165
|
|
|
1167
|
-
####
|
|
1166
|
+
#### Slot consumer (override in JSX)
|
|
1168
1167
|
|
|
1169
|
-
|
|
1168
|
+
Override a slot in a child component via `@slot.name(...) { ... }` inside JSX children:
|
|
1170
1169
|
|
|
1171
1170
|
```tsx
|
|
1172
1171
|
@View
|
|
@@ -1182,20 +1181,20 @@ class TodoApp {
|
|
|
1182
1181
|
filtered={filtered}
|
|
1183
1182
|
hasTodos={this.hasTodos}
|
|
1184
1183
|
>
|
|
1185
|
-
{/*
|
|
1184
|
+
{/* Override header slot */}
|
|
1186
1185
|
@slot.header({ title }) {
|
|
1187
1186
|
<header class="mb-5">
|
|
1188
1187
|
<h1 class="mb-4 text-2xl font-bold text-gray-900">
|
|
1189
|
-
|
|
1188
|
+
Tasks ({title})
|
|
1190
1189
|
</h1>
|
|
1191
1190
|
<TodoInput
|
|
1192
|
-
placeholder="
|
|
1191
|
+
placeholder="Add task…"
|
|
1193
1192
|
onSubmit={(text) => store.add(text)}
|
|
1194
1193
|
/>
|
|
1195
1194
|
</header>
|
|
1196
1195
|
}
|
|
1197
1196
|
|
|
1198
|
-
{/*
|
|
1197
|
+
{/* Override content slot, @if/@for allowed inside */}
|
|
1199
1198
|
@slot.content({ store, filtered, hasTodos }) {
|
|
1200
1199
|
@if (hasTodos) {
|
|
1201
1200
|
<section class="pt-3 border-t border-gray-200">
|
|
@@ -1221,8 +1220,8 @@ class TodoApp {
|
|
|
1221
1220
|
}
|
|
1222
1221
|
```
|
|
1223
1222
|
|
|
1224
|
-
|
|
1223
|
+
Syntax summary:
|
|
1225
1224
|
|
|
1226
|
-
- `@slot
|
|
1227
|
-
- `@slot
|
|
1228
|
-
-
|
|
1225
|
+
- `@slot:name({ props }) fallback { FallbackJSX }` — declare and invoke the slot in the provider.
|
|
1226
|
+
- `@slot.name({ ctx }) { OverrideJSX }` — override the slot in the consumer.
|
|
1227
|
+
- All directives (`@if`, `@for`, `@empty`) and regular JSX work inside `FallbackJSX` and `OverrideJSX`.
|