@helfy/helfy 0.0.7 → 0.0.9
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 +477 -323
- package/babel-preset.js +2 -1
- package/dist/Helfy/Render/DOM.d.ts +2 -0
- package/dist/app/createApp.d.ts +3 -1
- package/dist/app/index.d.ts +1 -1
- package/dist/app/types.d.ts +10 -0
- package/dist/compiler/babel.js +20 -13
- package/dist/decorators/Context.decorator.d.ts +22 -5
- package/dist/decorators/Effect.decorator.d.ts +1 -1
- package/dist/decorators/Inject.decorator.d.ts +8 -3
- package/dist/decorators/InjectContainer.decorator.d.ts +16 -0
- package/dist/decorators/Store.decorator.d.ts +12 -3
- package/dist/decorators/UseCase.decorator.d.ts +21 -0
- package/dist/di/InjectParam.decorator.d.ts +0 -11
- package/dist/di/Injectable.decorator.d.ts +2 -0
- package/dist/di/createViewInstance.d.ts +4 -5
- package/dist/di/index.d.ts +3 -2
- package/dist/http/ApiClient.decorator.d.ts +19 -0
- package/dist/http/FetchHttpClient.d.ts +17 -0
- package/dist/http/HttpClient.types.d.ts +19 -0
- package/dist/http/Mutation.types.d.ts +22 -0
- package/dist/http/MutationBuilder.d.ts +17 -0
- package/dist/http/Query.types.d.ts +22 -0
- package/dist/http/QueryBuilder.d.ts +16 -0
- package/dist/http/QueryCache.d.ts +13 -0
- package/dist/http/index.d.ts +18 -0
- package/dist/http/mutationConfig.decorator.d.ts +19 -0
- package/dist/http/queryConfig.decorator.d.ts +9 -0
- package/dist/http/useInfiniteQuery.decorator.d.ts +45 -0
- package/dist/http/useMutation.decorator.d.ts +21 -0
- package/dist/http/useQuery.decorator.d.ts +25 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/router/core.d.ts +2 -2
- package/dist/router/decorators.d.ts +4 -4
- package/dist/router.js +1 -1
- package/dist/signals.d.ts +7 -1
- package/package.json +1 -1
- package/dist/decorators/Observe.decorator.d.ts +0 -1
- package/dist/decorators/Provide.decorator.d.ts +0 -22
package/README.md
CHANGED
|
@@ -1,50 +1,54 @@
|
|
|
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
|
+
- [Architecture layers (Store, Service, UseCase)](#state-management)
|
|
25
|
+
- [Creating a Store](#creating-a-store-store)
|
|
26
|
+
- [Infrastructure services](#infrastructure-services-service)
|
|
27
|
+
- [Business use cases](#business-use-cases-usecase)
|
|
28
|
+
- [Accessing Store and UseCase from components](#accessing-store-and-usecase-from-components)
|
|
29
|
+
- [Store context (optional)](#store-context-optional)
|
|
30
|
+
- [Logging (@logger)](#logging-logger)
|
|
28
31
|
- [Context & DI](#context--di)
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [DI
|
|
32
|
+
- [Provider (@Context)](#provider-context)
|
|
33
|
+
- [Consumer (@useCtx)](#consumer-usectx)
|
|
34
|
+
- [Reactive fields](#reactive-fields)
|
|
35
|
+
- [Optional injection](#optional-injection)
|
|
36
|
+
- [DI: props vs dependencies](#di-props-vs-dependencies)
|
|
37
|
+
- [Global services (@inject)](#global-services-inject)
|
|
34
38
|
- [JSX](#jsx)
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [CSS
|
|
39
|
+
- [Attributes](#attributes)
|
|
40
|
+
- [Events](#events)
|
|
41
|
+
- [Styles](#styles)
|
|
42
|
+
- [CSS classes](#css-classes)
|
|
39
43
|
- [API](#api)
|
|
40
44
|
|
|
41
45
|
---
|
|
42
46
|
|
|
43
|
-
##
|
|
47
|
+
## Installation
|
|
44
48
|
|
|
45
|
-
###
|
|
49
|
+
### Creating a new project (helfy-create)
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
Quick start — create a project with a single command:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
54
|
npx helfy-create my-app
|
|
@@ -52,21 +56,21 @@ cd my-app
|
|
|
52
56
|
npm run dev
|
|
53
57
|
```
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
Without a project name (creates `helfy-app`):
|
|
56
60
|
|
|
57
61
|
```bash
|
|
58
62
|
npx helfy-create
|
|
59
63
|
```
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
The `--skip-install` option — create without running `npm install` (e.g. for offline use or custom registry).
|
|
62
66
|
|
|
63
|
-
###
|
|
67
|
+
### Adding to an existing project
|
|
64
68
|
|
|
65
69
|
```bash
|
|
66
70
|
npm install @helfy/helfy
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
Or in `package.json`:
|
|
70
74
|
|
|
71
75
|
```json
|
|
72
76
|
{
|
|
@@ -78,11 +82,11 @@ npm install @helfy/helfy
|
|
|
78
82
|
|
|
79
83
|
### Build setup
|
|
80
84
|
|
|
81
|
-
Babel
|
|
85
|
+
The Babel plugin runs the DI scanner and context scanner before transformation (no pre-scripts in `package.json` needed). The scanners generate `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`, and `.helfy/ctx-tokens.ts`. Add `.helfy` to `.gitignore`.
|
|
82
86
|
|
|
83
87
|
### Babel
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
A single preset in `.babelrc` is enough:
|
|
86
90
|
|
|
87
91
|
```json
|
|
88
92
|
{
|
|
@@ -90,11 +94,11 @@ Babel-плагин автоматически запускает DI-сканер
|
|
|
90
94
|
}
|
|
91
95
|
```
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
The preset includes: JSX runtime, TypeScript, legacy decorators, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI for `@Injectable<IX>()`, `@Service<IX>()`, `@UseCase<IX>()`, `@Store`, `@inject<IX>()`, `@useCtx<IX>()`, auto-registration at `createApp`, `@logger()` → `@logger("<ClassName>")` transformation).
|
|
94
98
|
|
|
95
|
-
### Webpack
|
|
99
|
+
### Webpack / Rspack
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
To process directives (`@if`, `@for`, `@ref`, `@bind`, `@field`) in `.tsx` files, add the loader:
|
|
98
102
|
|
|
99
103
|
```javascript
|
|
100
104
|
{
|
|
@@ -106,9 +110,11 @@ Babel-плагин автоматически запускает DI-сканер
|
|
|
106
110
|
}
|
|
107
111
|
```
|
|
108
112
|
|
|
113
|
+
The same configuration works with both Webpack and Rspack. Rspack is a faster, Rust-based bundler with webpack-compatible API.
|
|
114
|
+
|
|
109
115
|
### TypeScript
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
In `tsconfig.json` set:
|
|
112
118
|
|
|
113
119
|
```json
|
|
114
120
|
{
|
|
@@ -121,15 +127,15 @@ Babel-плагин автоматически запускает DI-сканер
|
|
|
121
127
|
}
|
|
122
128
|
```
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
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
131
|
|
|
126
132
|
---
|
|
127
133
|
|
|
128
|
-
##
|
|
134
|
+
## Components
|
|
129
135
|
|
|
130
|
-
###
|
|
136
|
+
### Basic component
|
|
131
137
|
|
|
132
|
-
|
|
138
|
+
A component is a class with the `@View` decorator and a `render()` method that returns JSX:
|
|
133
139
|
|
|
134
140
|
```tsx
|
|
135
141
|
import { View } from "@helfy/helfy";
|
|
@@ -146,19 +152,19 @@ class Hello {
|
|
|
146
152
|
}
|
|
147
153
|
```
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
155
|
+
The `@View` decorator automatically:
|
|
156
|
+
- calls `render()` **once** and mounts the result into a DOM fragment
|
|
157
|
+
- adds a `view` property (reference to the root DOM element)
|
|
158
|
+
- wraps `this.props` in a reactive Proxy (each prop is a signal)
|
|
159
|
+
- configures fine-grained effects: when a signal changes, only the specific DOM node that reads it is updated, not the entire component
|
|
154
160
|
|
|
155
|
-
>
|
|
161
|
+
> **Important:** When inheriting from `@View`, apply the decorator on the child class as well. Otherwise `@useCtx`, `scheduleUpdate`, and DOM updates will not work.
|
|
156
162
|
|
|
157
163
|
### Props
|
|
158
164
|
|
|
159
|
-
Props
|
|
165
|
+
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
166
|
|
|
161
|
-
>
|
|
167
|
+
> **Important:** Do not destructure `this.props` in `render()`. Access fields directly via `this.props.field` — this ensures correct signal subscription.
|
|
162
168
|
|
|
163
169
|
```tsx
|
|
164
170
|
import { View } from "@helfy/helfy";
|
|
@@ -186,18 +192,17 @@ class Button {
|
|
|
186
192
|
}
|
|
187
193
|
```
|
|
188
194
|
|
|
189
|
-
|
|
195
|
+
Usage:
|
|
190
196
|
|
|
191
197
|
```tsx
|
|
192
|
-
<Button label="
|
|
198
|
+
<Button label="Click" onClick={() => console.log('clicked')} />
|
|
193
199
|
```
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
When the parent updates props, only the DOM nodes that read the changed props are updated.
|
|
196
202
|
|
|
197
|
-
###
|
|
203
|
+
### Local state (@state)
|
|
198
204
|
|
|
199
|
-
|
|
200
|
-
При изменении значения обновляются только те DOM-узлы и эффекты, которые читают это поле (fine-grained). Полный перерендер компонента не происходит:
|
|
205
|
+
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
206
|
|
|
202
207
|
```tsx
|
|
203
208
|
import { View, state } from "@helfy/helfy";
|
|
@@ -207,7 +212,7 @@ class Counter {
|
|
|
207
212
|
@state private count = 0;
|
|
208
213
|
|
|
209
214
|
increment() {
|
|
210
|
-
this.count++; //
|
|
215
|
+
this.count++; // updates only nodes that read count
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
decrement() {
|
|
@@ -226,13 +231,13 @@ class Counter {
|
|
|
226
231
|
}
|
|
227
232
|
```
|
|
228
233
|
|
|
229
|
-
|
|
234
|
+
You can declare multiple `@state` fields. Each independently triggers updates only in the DOM nodes that read it.
|
|
230
235
|
|
|
231
|
-
|
|
236
|
+
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
237
|
|
|
233
|
-
###
|
|
238
|
+
### Nested components
|
|
234
239
|
|
|
235
|
-
|
|
240
|
+
Components are used in JSX as tags. Props are passed as attributes:
|
|
236
241
|
|
|
237
242
|
```tsx
|
|
238
243
|
import { View } from "@helfy/helfy";
|
|
@@ -250,9 +255,9 @@ class App {
|
|
|
250
255
|
}
|
|
251
256
|
```
|
|
252
257
|
|
|
253
|
-
###
|
|
258
|
+
### DOM mounting
|
|
254
259
|
|
|
255
|
-
|
|
260
|
+
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
261
|
|
|
257
262
|
```typescript
|
|
258
263
|
import { createApp } from "@helfy/helfy";
|
|
@@ -262,19 +267,19 @@ createApp({ root: document.getElementById("root")! })
|
|
|
262
267
|
.mount(App);
|
|
263
268
|
```
|
|
264
269
|
|
|
265
|
-
|
|
270
|
+
For scenarios without routing:
|
|
266
271
|
|
|
267
272
|
```typescript
|
|
268
273
|
createApp({ root: document.getElementById("root")! }).mount(App);
|
|
269
274
|
```
|
|
270
275
|
|
|
271
|
-
|
|
276
|
+
Manual mounting (without `createApp`): `const app = new App(); app.attach(root)`.
|
|
272
277
|
|
|
273
|
-
###
|
|
278
|
+
### DOM and component refs (@ref)
|
|
274
279
|
|
|
275
|
-
|
|
280
|
+
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
281
|
|
|
277
|
-
|
|
282
|
+
**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
283
|
|
|
279
284
|
```tsx
|
|
280
285
|
import { View, ref, expose } from "@helfy/helfy";
|
|
@@ -297,7 +302,7 @@ class Form {
|
|
|
297
302
|
@ref private input!: Input;
|
|
298
303
|
|
|
299
304
|
onAttached() {
|
|
300
|
-
this.input.focus(); //
|
|
305
|
+
this.input.focus(); // only the exposed method is available
|
|
301
306
|
}
|
|
302
307
|
|
|
303
308
|
render() {
|
|
@@ -306,15 +311,15 @@ class Form {
|
|
|
306
311
|
}
|
|
307
312
|
```
|
|
308
313
|
|
|
309
|
-
- DOM
|
|
310
|
-
-
|
|
311
|
-
-
|
|
314
|
+
- DOM elements via `@ref` are passed as-is
|
|
315
|
+
- Components with `@expose` — parent sees only the exposed methods
|
|
316
|
+
- Components without `@expose` — the full instance is passed (backward compatibility)
|
|
312
317
|
|
|
313
|
-
|
|
318
|
+
With conditional rendering (`@if`), the ref is cleared when the element is removed. Do not use `@ref` and `@state` on the same field.
|
|
314
319
|
|
|
315
|
-
###
|
|
320
|
+
### Two-way binding (@bind)
|
|
316
321
|
|
|
317
|
-
|
|
322
|
+
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
323
|
|
|
319
324
|
```tsx
|
|
320
325
|
import { View, state } from "@helfy/helfy";
|
|
@@ -332,8 +337,8 @@ class LoginForm {
|
|
|
332
337
|
<input @bind(this.email) type="email" />
|
|
333
338
|
<input @bind(this.isActive) type="checkbox" />
|
|
334
339
|
<select @bind(this.priority)>
|
|
335
|
-
<option value="low"
|
|
336
|
-
<option value="high"
|
|
340
|
+
<option value="low">Low</option>
|
|
341
|
+
<option value="high">High</option>
|
|
337
342
|
</select>
|
|
338
343
|
</form>
|
|
339
344
|
);
|
|
@@ -341,24 +346,24 @@ class LoginForm {
|
|
|
341
346
|
}
|
|
342
347
|
```
|
|
343
348
|
|
|
344
|
-
|
|
349
|
+
Transformation by element type:
|
|
345
350
|
|
|
346
|
-
|
|
|
347
|
-
|
|
351
|
+
| Element | Attribute | Event |
|
|
352
|
+
|---------|-----------|-------|
|
|
348
353
|
| `input[text|email|password|...]` | `value` | `oninput` |
|
|
349
354
|
| `input[checkbox|radio]` | `checked` | `onchange` |
|
|
350
355
|
| `select` | `value` | `onchange` |
|
|
351
356
|
| `textarea` | `value` | `oninput` |
|
|
352
357
|
|
|
353
|
-
|
|
358
|
+
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
359
|
|
|
355
|
-
|
|
360
|
+
**Custom components:** for binding to a component with a `value` prop, use named binding `@bind:value(expr)`:
|
|
356
361
|
|
|
357
362
|
```tsx
|
|
358
|
-
//
|
|
359
|
-
<Input @bind:value(this.title) placeholder="
|
|
363
|
+
// Parent
|
|
364
|
+
<Input @bind:value(this.title) placeholder="Title" />
|
|
360
365
|
|
|
361
|
-
//
|
|
366
|
+
// Input component with @binded("value")
|
|
362
367
|
@View
|
|
363
368
|
class Input {
|
|
364
369
|
@binded("value") private bindedVal!: string;
|
|
@@ -369,11 +374,11 @@ class Input {
|
|
|
369
374
|
}
|
|
370
375
|
```
|
|
371
376
|
|
|
372
|
-
###
|
|
377
|
+
### Forms (@Form, @field, @field in JSX)
|
|
373
378
|
|
|
374
|
-
|
|
379
|
+
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
380
|
|
|
376
|
-
**FormContext** —
|
|
381
|
+
**FormContext** — a class with `@Form` and `@field` fields:
|
|
377
382
|
|
|
378
383
|
```tsx
|
|
379
384
|
import { Form, field, logger, type FieldState, type ILogger } from "@helfy/helfy";
|
|
@@ -392,7 +397,7 @@ export class LoginFormContext {
|
|
|
392
397
|
rememberMe!: FieldState<boolean>;
|
|
393
398
|
|
|
394
399
|
validateAll(): boolean {
|
|
395
|
-
//
|
|
400
|
+
// validate fields, set field.error
|
|
396
401
|
return true;
|
|
397
402
|
}
|
|
398
403
|
|
|
@@ -406,9 +411,9 @@ export class LoginFormContext {
|
|
|
406
411
|
}
|
|
407
412
|
```
|
|
408
413
|
|
|
409
|
-
**FieldState**
|
|
414
|
+
**FieldState** has `value`, `isDirty`, `isTouched`, `error`, `isValid`. When `value`/`error`/`isTouched` changes, components with `@useForm` re-render.
|
|
410
415
|
|
|
411
|
-
|
|
416
|
+
**Form provider** — the parent component wraps the form in context:
|
|
412
417
|
|
|
413
418
|
```tsx
|
|
414
419
|
// LoginPage.tsx
|
|
@@ -424,7 +429,7 @@ export class LoginPage {
|
|
|
424
429
|
}
|
|
425
430
|
```
|
|
426
431
|
|
|
427
|
-
|
|
432
|
+
**Form component** — inject via `@useForm`, access fields via `this.form`:
|
|
428
433
|
|
|
429
434
|
```tsx
|
|
430
435
|
import { View, useForm } from "@helfy/helfy";
|
|
@@ -443,72 +448,72 @@ export class LoginForm {
|
|
|
443
448
|
)}
|
|
444
449
|
<input @field(this.form.password) type="password" class="input" />
|
|
445
450
|
<input @field(this.form.rememberMe) type="checkbox" id="remember" />
|
|
446
|
-
<label for="remember"
|
|
447
|
-
<button type="submit"
|
|
451
|
+
<label for="remember">Remember me</label>
|
|
452
|
+
<button type="submit">Sign in</button>
|
|
448
453
|
</form>
|
|
449
454
|
);
|
|
450
455
|
}
|
|
451
456
|
}
|
|
452
457
|
```
|
|
453
458
|
|
|
454
|
-
|
|
459
|
+
Wrapper components (TextField, CheckboxField, etc.) accept `$field` as a writable prop: `<TextField $field={this.form.email} label="Email" />` and use `<input @field(this.props.$field) />` internally. The `$` prefix marks a writable prop — mutable objects like FieldState bypass the default signal-based (readonly) props so form inputs work correctly.
|
|
455
460
|
|
|
456
|
-
**JSX
|
|
461
|
+
**JSX directive `@field(expr)`** — one directive replaces `@bind` + `onblur` + error `class` + `aria-invalid`. The compiler generates:
|
|
457
462
|
|
|
458
|
-
- `value`/`checked`
|
|
463
|
+
- `value`/`checked` and `oninput`/`onchange`
|
|
459
464
|
- `onblur` → `expr.isTouched = true`
|
|
460
|
-
- `class` —
|
|
465
|
+
- `class` — merged with existing; when `expr.isTouched && expr.error` adds `input-error`
|
|
461
466
|
- `aria-invalid={expr.isTouched && expr.error ? "true" : "false"}`
|
|
462
467
|
|
|
463
|
-
|
|
468
|
+
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
469
|
|
|
465
470
|
---
|
|
466
471
|
|
|
467
|
-
##
|
|
472
|
+
## Template directives
|
|
468
473
|
|
|
469
|
-
Helfy
|
|
474
|
+
Helfy extends JSX with `@if`, `@elseif`, `@else`, `@for`, and `@empty` directives.
|
|
470
475
|
|
|
471
476
|
### @if / @elseif / @else
|
|
472
477
|
|
|
473
|
-
|
|
478
|
+
Conditional rendering:
|
|
474
479
|
|
|
475
480
|
```tsx
|
|
476
481
|
render() {
|
|
477
482
|
return (
|
|
478
483
|
<div>
|
|
479
484
|
@if (this.count > 0) {
|
|
480
|
-
<span
|
|
485
|
+
<span>Positive: {this.count}</span>
|
|
481
486
|
}
|
|
482
487
|
</div>
|
|
483
488
|
);
|
|
484
489
|
}
|
|
485
490
|
```
|
|
486
491
|
|
|
487
|
-
|
|
492
|
+
Condition chains:
|
|
488
493
|
|
|
489
494
|
```tsx
|
|
490
495
|
render() {
|
|
491
496
|
return (
|
|
492
497
|
<div>
|
|
493
498
|
@if (this.status === 'loading') {
|
|
494
|
-
<span
|
|
499
|
+
<span>Loading...</span>
|
|
495
500
|
} @elseif (this.status === 'error') {
|
|
496
|
-
<span
|
|
501
|
+
<span>Error!</span>
|
|
497
502
|
} @else {
|
|
498
|
-
<span
|
|
503
|
+
<span>Data loaded</span>
|
|
499
504
|
}
|
|
500
505
|
</div>
|
|
501
506
|
);
|
|
502
507
|
}
|
|
503
508
|
```
|
|
504
509
|
|
|
505
|
-
|
|
510
|
+
Nested conditions:
|
|
506
511
|
|
|
507
512
|
```tsx
|
|
508
513
|
@if (this.isVisible) {
|
|
509
514
|
<div>
|
|
510
515
|
@if (this.count > 10) {
|
|
511
|
-
<span
|
|
516
|
+
<span>More than ten</span>
|
|
512
517
|
}
|
|
513
518
|
</div>
|
|
514
519
|
}
|
|
@@ -516,7 +521,7 @@ render() {
|
|
|
516
521
|
|
|
517
522
|
### @for
|
|
518
523
|
|
|
519
|
-
|
|
524
|
+
Array iteration. Syntax: `@for (item of array)` or `@for (item, index of array)`.
|
|
520
525
|
|
|
521
526
|
```tsx
|
|
522
527
|
@state private items = ['apple', 'banana', 'cherry'];
|
|
@@ -532,7 +537,7 @@ render() {
|
|
|
532
537
|
}
|
|
533
538
|
```
|
|
534
539
|
|
|
535
|
-
`track`
|
|
540
|
+
`track` sets the key for DOM diffing optimization (like `key` in React):
|
|
536
541
|
|
|
537
542
|
```tsx
|
|
538
543
|
@for (user of this.users; track user.id) {
|
|
@@ -542,13 +547,13 @@ render() {
|
|
|
542
547
|
|
|
543
548
|
### @empty
|
|
544
549
|
|
|
545
|
-
|
|
550
|
+
The `@empty` block after `@for` renders when the array is empty:
|
|
546
551
|
|
|
547
552
|
```tsx
|
|
548
553
|
@for (item of this.items; track item) {
|
|
549
554
|
<div>{item}</div>
|
|
550
555
|
} @empty {
|
|
551
|
-
<span
|
|
556
|
+
<span>List is empty</span>
|
|
552
557
|
}
|
|
553
558
|
```
|
|
554
559
|
|
|
@@ -556,107 +561,214 @@ render() {
|
|
|
556
561
|
|
|
557
562
|
## State Management
|
|
558
563
|
|
|
559
|
-
|
|
564
|
+
Helfy uses a three-layer architecture: **@Store** (pure state), **@Service** (infrastructure), **@UseCase** (business logic).
|
|
565
|
+
|
|
566
|
+
### Dependency flow
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
@UseCase ──→ @Store ←── @Service
|
|
570
|
+
│ ↑
|
|
571
|
+
└─────────────────┘
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
- **@Store** — pure reactive state; no dependencies, no side effects. Store does not know about anyone.
|
|
575
|
+
- **@Service** — infrastructure (HTTP, validation, storage); can inject Store and other Services.
|
|
576
|
+
- **@UseCase** — orchestrates business scenarios; injects Store + Services.
|
|
577
|
+
|
|
578
|
+
### Creating a Store (@Store)
|
|
560
579
|
|
|
561
|
-
Store
|
|
580
|
+
A Store is a global reactive state container. **Requirements:**
|
|
581
|
+
- Empty constructor (no parameters)
|
|
582
|
+
- Only `@state` on fields and `@computed` on getters
|
|
583
|
+
- Mutation methods that only change own state (synchronous; no `await` — async work belongs in @Service)
|
|
584
|
+
- **Forbidden:** `@inject`, `@useCtx`, `@effect`, direct access to HTTP/storage/timers
|
|
562
585
|
|
|
563
586
|
```typescript
|
|
564
|
-
import { Store,
|
|
587
|
+
import { Store, state, computed } from "@helfy/helfy";
|
|
565
588
|
|
|
566
589
|
@Store
|
|
567
|
-
class
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
590
|
+
class TodoStore {
|
|
591
|
+
@state todos: Todo[] = [];
|
|
592
|
+
@state filter: "all" | "active" | "completed" = "all";
|
|
593
|
+
|
|
594
|
+
@computed get filteredTodos() {
|
|
595
|
+
switch (this.filter) {
|
|
596
|
+
case "active": return this.todos.filter(t => !t.completed);
|
|
597
|
+
case "completed": return this.todos.filter(t => t.completed);
|
|
598
|
+
default: return this.todos;
|
|
574
599
|
}
|
|
600
|
+
}
|
|
575
601
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
}
|
|
602
|
+
add(title: string) {
|
|
603
|
+
this.todos = [...this.todos, { id: crypto.randomUUID(), title, completed: false }];
|
|
604
|
+
}
|
|
581
605
|
|
|
582
|
-
|
|
606
|
+
toggle(id: string) {
|
|
607
|
+
this.todos = this.todos.map(t =>
|
|
608
|
+
t.id === id ? { ...t, completed: !t.completed } : t
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
setFilter(f: typeof this.filter) {
|
|
613
|
+
this.filter = f;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
583
616
|
```
|
|
584
617
|
|
|
585
|
-
|
|
586
|
-
- `subscribe(field, callback)` -- подписка на изменение поля, возвращает функцию отписки
|
|
587
|
-
- `unsubscribe(field, callback)` -- отписка
|
|
618
|
+
Fields use `@state` or `@observable` — both create signals.
|
|
588
619
|
|
|
589
|
-
|
|
620
|
+
### Infrastructure services (@Service)
|
|
590
621
|
|
|
591
|
-
|
|
622
|
+
Use `@Service` for infrastructure: HTTP clients, validators, repositories. **Requirements:**
|
|
623
|
+
- Constructor with DI dependencies (other Services, Store)
|
|
624
|
+
- Provides atomic technical capabilities
|
|
625
|
+
- Can directly mutate Store (e.g. after fetching data)
|
|
626
|
+
- **Forbidden:** `@state`, `@computed`, `@effect` on fields
|
|
592
627
|
|
|
593
|
-
|
|
628
|
+
```typescript
|
|
629
|
+
import { Service } from "@helfy/helfy";
|
|
594
630
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
import UserStore from "./UserStore";
|
|
631
|
+
export interface ITodoValidateService {
|
|
632
|
+
validateTitle(title: string): ValidationResult;
|
|
633
|
+
}
|
|
599
634
|
|
|
600
|
-
@
|
|
601
|
-
class
|
|
602
|
-
|
|
603
|
-
|
|
635
|
+
@Service<ITodoValidateService>()
|
|
636
|
+
export class TodoValidateService implements ITodoValidateService {
|
|
637
|
+
validateTitle(title: string) {
|
|
638
|
+
// ...
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
604
642
|
|
|
605
|
-
|
|
606
|
-
private isLoggedIn: UserStore['isLoggedIn'];
|
|
643
|
+
### Business use cases (@UseCase)
|
|
607
644
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
645
|
+
Use `@UseCase` to orchestrate business scenarios. **Requirements:**
|
|
646
|
+
- Constructor with DI (Store, Services)
|
|
647
|
+
- Main public method: `execute`, `perform`, or `handle`
|
|
648
|
+
- Typical flow: validation → call services → update Store → side effects (notifications, analytics)
|
|
649
|
+
- **Forbidden:** `@state`, `@computed`, `@effect` on fields
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
import { UseCase } from "@helfy/helfy";
|
|
653
|
+
|
|
654
|
+
@UseCase<ICreateTodoUseCase>()
|
|
655
|
+
export class CreateTodoUseCase implements ICreateTodoUseCase {
|
|
656
|
+
constructor(
|
|
657
|
+
private store: TodoStore,
|
|
658
|
+
private validateService: ITodoValidateService
|
|
659
|
+
) {}
|
|
660
|
+
|
|
661
|
+
async execute(dto: CreateTodoDto): Promise<Result<Todo, AppError>> {
|
|
662
|
+
const validation = this.validateService.validateTitle(dto.title);
|
|
663
|
+
if (!validation.valid) return Err(validation.error);
|
|
664
|
+
|
|
665
|
+
const todo = { id: crypto.randomUUID(), ...dto, completed: false };
|
|
666
|
+
this.store.add(todo);
|
|
667
|
+
return Ok(todo);
|
|
668
|
+
}
|
|
619
669
|
}
|
|
620
670
|
```
|
|
621
671
|
|
|
622
|
-
|
|
672
|
+
### HttpClient and ApiClient
|
|
673
|
+
|
|
674
|
+
Helfy provides an HTTP client and a Query/Mutation layer (similar to TanStack Query) for API data fetching.
|
|
623
675
|
|
|
624
|
-
|
|
676
|
+
**Configure HttpClient** in `createApp`:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
createApp({ root: document.getElementById("root")! })
|
|
680
|
+
.http({
|
|
681
|
+
baseUrl: "/api",
|
|
682
|
+
timeout: 10000,
|
|
683
|
+
headers: { "X-App-Version": "1.0" },
|
|
684
|
+
queryCacheMaxSize: 100,
|
|
685
|
+
})
|
|
686
|
+
.router({ routes })
|
|
687
|
+
.mount(App);
|
|
688
|
+
```
|
|
625
689
|
|
|
626
|
-
|
|
690
|
+
**Define API interface and implementation** with `@ApiClient` and `@queryConfig`:
|
|
627
691
|
|
|
628
692
|
```typescript
|
|
629
|
-
import
|
|
630
|
-
import AppStore from "./AppStore";
|
|
693
|
+
import { ApiClient, queryConfig, QueryBuilder, type Query, type HttpClient } from "@helfy/helfy";
|
|
631
694
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
static readonly appStore = new AppStore();
|
|
695
|
+
export interface TodoApi {
|
|
696
|
+
todos(): Query<Todo[]>;
|
|
635
697
|
}
|
|
636
698
|
|
|
637
|
-
|
|
699
|
+
@ApiClient<TodoApi>()
|
|
700
|
+
export class TodoApiImpl implements TodoApi {
|
|
701
|
+
constructor(private readonly http: HttpClient) {}
|
|
702
|
+
|
|
703
|
+
@queryConfig("todos")
|
|
704
|
+
todos() {
|
|
705
|
+
return new QueryBuilder<Todo[]>(["todo"])
|
|
706
|
+
.fn(() => this.http.get<Todo[]>("/todos"))
|
|
707
|
+
.staleTime(5 * 60 * 1000)
|
|
708
|
+
.refetchOnWindowFocus(true);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Use `@useQuery` in View** for automatic fetch on mount:
|
|
714
|
+
|
|
715
|
+
```tsx
|
|
716
|
+
@View
|
|
717
|
+
class TodoList {
|
|
718
|
+
@useQuery<TodoApi>("todos") private todosQuery!: Query<Todo[]>;
|
|
719
|
+
|
|
720
|
+
render() {
|
|
721
|
+
const q = this.todosQuery;
|
|
722
|
+
if (q.isLoading) return <Spinner />;
|
|
723
|
+
if (q.isError) return <ErrorMessage error={q.error} />;
|
|
724
|
+
return <ul>{q.data?.map((t) => <li>{t.text}</li>)}</ul>;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Use `@useMutation`** for imperative mutations:
|
|
730
|
+
|
|
731
|
+
```tsx
|
|
732
|
+
import type { Mutation } from "@helfy/helfy";
|
|
733
|
+
|
|
734
|
+
@useMutation<TodoApi>("create") private createTodo!: Mutation<TodoItem, AddTodoDto>;
|
|
735
|
+
|
|
736
|
+
async handleSubmit() {
|
|
737
|
+
await this.createTodo.mutateAsync({ title: this.title });
|
|
738
|
+
}
|
|
638
739
|
```
|
|
639
740
|
|
|
640
|
-
|
|
741
|
+
### Accessing Store and UseCase from components
|
|
742
|
+
|
|
743
|
+
Inject Store and UseCase via `@inject` in View/Context. Prefer UseCase for mutations, Store for reads:
|
|
641
744
|
|
|
642
745
|
```tsx
|
|
643
|
-
import
|
|
746
|
+
import { View, inject } from "@helfy/helfy";
|
|
644
747
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
private
|
|
748
|
+
@View
|
|
749
|
+
class TodoList {
|
|
750
|
+
@inject<ITodoStore>() private store!: ITodoStore;
|
|
751
|
+
@inject<ICreateTodoUseCase>() private createTodo!: ICreateTodoUseCase;
|
|
648
752
|
|
|
649
|
-
|
|
650
|
-
|
|
753
|
+
render() {
|
|
754
|
+
return (
|
|
755
|
+
<ul>
|
|
756
|
+
@for (todo of this.store.filteredTodos; track todo.id) {
|
|
757
|
+
<li>{todo.title}</li>
|
|
758
|
+
}
|
|
759
|
+
</ul>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
651
763
|
```
|
|
652
764
|
|
|
653
765
|
---
|
|
654
766
|
|
|
655
|
-
##
|
|
767
|
+
## Logging (@logger)
|
|
656
768
|
|
|
657
|
-
|
|
769
|
+
The `@logger` decorator injects a logger into View, Context, Store, Service, and UseCase classes. The class name is set at compile time (Babel plugin), keeping readable names even after minification.
|
|
658
770
|
|
|
659
|
-
###
|
|
771
|
+
### Usage
|
|
660
772
|
|
|
661
773
|
```tsx
|
|
662
774
|
import { View, logger, type ILogger } from "@helfy/helfy";
|
|
@@ -673,22 +785,23 @@ class TodoInput {
|
|
|
673
785
|
}
|
|
674
786
|
```
|
|
675
787
|
|
|
676
|
-
|
|
677
|
-
- `@logger()` —
|
|
678
|
-
- `@logger("my-tag")` —
|
|
788
|
+
**Variants:**
|
|
789
|
+
- `@logger()` — class name is set at compile-time as `<ClassName>`, format and color depend on class type
|
|
790
|
+
- `@logger("my-tag")` — custom tag, gray color
|
|
679
791
|
|
|
680
|
-
###
|
|
792
|
+
### Tag format and color by class type
|
|
681
793
|
|
|
682
|
-
|
|
|
683
|
-
|
|
684
|
-
| View (
|
|
685
|
-
| Injectable
|
|
794
|
+
| Type | Format | Color |
|
|
795
|
+
|------|--------|-------|
|
|
796
|
+
| View (components) | `<TodoInput>` | skyblue |
|
|
797
|
+
| Service / Injectable | `TodoValidateService()` | pink |
|
|
798
|
+
| UseCase | `CreateTodoUseCase()` | pink |
|
|
686
799
|
| Context | `{TodoContext}` | khaki |
|
|
687
800
|
| Form | `[LoginFormContext]` | bright blue |
|
|
688
801
|
| Store | `TodoStore[]` | lime green |
|
|
689
|
-
|
|
|
802
|
+
| Custom `@logger("...")` | as specified | gray |
|
|
690
803
|
|
|
691
|
-
### API
|
|
804
|
+
### Logger API
|
|
692
805
|
|
|
693
806
|
```typescript
|
|
694
807
|
interface ILogger {
|
|
@@ -696,13 +809,13 @@ interface ILogger {
|
|
|
696
809
|
info(message: string, meta?: Record<string, unknown>): void;
|
|
697
810
|
warn(message: string, meta?: Record<string, unknown>): void;
|
|
698
811
|
error(messageOrError: string | Error, meta?: Record<string, unknown>): void;
|
|
699
|
-
withContext(ctx: string): ILogger; //
|
|
812
|
+
withContext(ctx: string): ILogger; // child logger with extra prefix
|
|
700
813
|
}
|
|
701
814
|
```
|
|
702
815
|
|
|
703
|
-
### DI
|
|
816
|
+
### DI and transports
|
|
704
817
|
|
|
705
|
-
`LoggerService`
|
|
818
|
+
`LoggerService` is registered under `ILoggerToken` via `registerAllServices`. Custom transports (Sentry, file, etc.) implementing `ILogTransport` can be wired up:
|
|
706
819
|
|
|
707
820
|
```typescript
|
|
708
821
|
import { LoggerService, ConsoleTransport, ILoggerToken } from "@helfy/helfy";
|
|
@@ -715,36 +828,37 @@ createApp({ root })
|
|
|
715
828
|
.mount(App);
|
|
716
829
|
```
|
|
717
830
|
|
|
718
|
-
|
|
831
|
+
Without a registered logger in the container, a fallback that logs to `console` is used.
|
|
719
832
|
|
|
720
833
|
---
|
|
721
834
|
|
|
722
835
|
## Context & DI
|
|
723
836
|
|
|
724
|
-
Helfy
|
|
837
|
+
Helfy supports hierarchical Context and Dependency Injection: a provider wraps a subtree in JSX, and child components receive values via `@useCtx`. Useful for theme, forms, routing, and other shared dependencies.
|
|
838
|
+
|
|
839
|
+
### Provider (@Context)
|
|
725
840
|
|
|
726
|
-
|
|
841
|
+
A class with the `@Context` decorator is a non-rendering provider: it has no `render()` and only renders its children. Use `@state` for reactive fields, `@computed` for derived values; public methods are exposed automatically.
|
|
727
842
|
|
|
728
|
-
|
|
843
|
+
**Constructor:** receives only props from JSX. Define your own props interface (same as View). Use `@inject` for container dependencies.
|
|
729
844
|
|
|
730
845
|
```tsx
|
|
731
|
-
import { Context,
|
|
846
|
+
import { Context, state } from "@helfy/helfy";
|
|
732
847
|
|
|
733
848
|
export type TThemeMode = "light" | "dark";
|
|
734
849
|
|
|
735
850
|
@Context
|
|
736
851
|
export class ThemeContext {
|
|
737
|
-
@
|
|
852
|
+
@state
|
|
738
853
|
mode: TThemeMode = "dark";
|
|
739
854
|
|
|
740
|
-
@provide()
|
|
741
855
|
toggle = () => {
|
|
742
856
|
this.mode = this.mode === "dark" ? "light" : "dark";
|
|
743
857
|
};
|
|
744
858
|
}
|
|
745
859
|
```
|
|
746
860
|
|
|
747
|
-
|
|
861
|
+
Usage in JSX — wrap a subtree:
|
|
748
862
|
|
|
749
863
|
```tsx
|
|
750
864
|
<ThemeContext>
|
|
@@ -753,55 +867,79 @@ export class ThemeContext {
|
|
|
753
867
|
</ThemeContext>
|
|
754
868
|
```
|
|
755
869
|
|
|
756
|
-
|
|
870
|
+
The framework only renders the children of `ThemeContext`; the provider itself does not create DOM nodes.
|
|
871
|
+
|
|
872
|
+
**Context with typed props** — define an interface and use it in the constructor (same pattern as View):
|
|
873
|
+
|
|
874
|
+
```tsx
|
|
875
|
+
interface ApiContextProps {
|
|
876
|
+
baseUrl: string;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
@Context
|
|
880
|
+
export class ApiContext {
|
|
881
|
+
constructor(readonly props: ApiContextProps) {}
|
|
882
|
+
// ...
|
|
883
|
+
}
|
|
884
|
+
```
|
|
757
885
|
|
|
758
|
-
###
|
|
886
|
+
### Consumer (@useCtx)
|
|
759
887
|
|
|
760
|
-
|
|
888
|
+
A component with `@View` can receive context values via `@useCtx`:
|
|
761
889
|
|
|
762
890
|
```tsx
|
|
763
|
-
import { View,
|
|
891
|
+
import { View, useCtx } from "@helfy/helfy";
|
|
764
892
|
import { ThemeContext } from "./ThemeContext";
|
|
765
893
|
|
|
766
894
|
@View
|
|
767
895
|
class ThemeToggle {
|
|
768
|
-
@
|
|
896
|
+
@useCtx(ThemeContext)
|
|
769
897
|
private theme!: ThemeContext;
|
|
770
898
|
|
|
771
899
|
render() {
|
|
772
900
|
return (
|
|
773
901
|
<button onclick={this.theme.toggle}>
|
|
774
|
-
{this.theme.mode === "dark" ? "
|
|
902
|
+
{this.theme.mode === "dark" ? "Light theme" : "Dark theme"}
|
|
775
903
|
</button>
|
|
776
904
|
);
|
|
777
905
|
}
|
|
778
906
|
}
|
|
779
907
|
```
|
|
780
908
|
|
|
781
|
-
|
|
909
|
+
You can inject only a context field:
|
|
782
910
|
|
|
783
911
|
```tsx
|
|
784
912
|
@View
|
|
785
913
|
class ModeDisplay {
|
|
786
|
-
@
|
|
914
|
+
@useCtx(ThemeContext, "mode")
|
|
787
915
|
private mode!: TThemeMode;
|
|
788
916
|
|
|
789
917
|
render() {
|
|
790
|
-
return <span
|
|
918
|
+
return <span>Current theme: {this.mode}</span>;
|
|
791
919
|
}
|
|
792
920
|
}
|
|
793
921
|
```
|
|
794
922
|
|
|
795
|
-
|
|
923
|
+
**Interface-based injection** — for contexts with `@Context<IX>()`, use `@useCtx<ITodoContext>()`:
|
|
924
|
+
|
|
925
|
+
```tsx
|
|
926
|
+
@useCtx<ITodoContext>()
|
|
927
|
+
private ctx!: ITodoContext;
|
|
928
|
+
|
|
929
|
+
@useCtx<ITodoContext>("filteredTodos")
|
|
930
|
+
private filteredTodos!: ITodoContext["filteredTodos"];
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Provider lookup goes up the tree (`_parentView`); the nearest provider with the matching key is used.
|
|
796
934
|
|
|
797
|
-
###
|
|
935
|
+
### Reactive fields
|
|
798
936
|
|
|
799
|
-
`@
|
|
937
|
+
`@state` makes a field reactive: when it changes, all consumers re-render. Public methods are exposed automatically.
|
|
800
938
|
|
|
801
|
-
|
|
939
|
+
**Computed fields** — getters with `@computed` recompute when dependencies change and trigger consumer re-renders:
|
|
802
940
|
|
|
803
941
|
```tsx
|
|
804
|
-
@
|
|
942
|
+
@computed
|
|
805
943
|
get filteredTodos(): Todo[] {
|
|
806
944
|
return this.filter === "active"
|
|
807
945
|
? this.todos.filter(t => !t.completed)
|
|
@@ -809,52 +947,62 @@ get filteredTodos(): Todo[] {
|
|
|
809
947
|
}
|
|
810
948
|
```
|
|
811
949
|
|
|
812
|
-
###
|
|
950
|
+
### Optional injection
|
|
813
951
|
|
|
814
|
-
|
|
952
|
+
The third argument of `@useCtx` — options `{ optional?, defaultValue? }`:
|
|
815
953
|
|
|
816
954
|
```tsx
|
|
817
|
-
@
|
|
955
|
+
@useCtx(ThemeContext, { optional: true })
|
|
818
956
|
private theme?: ThemeContext;
|
|
819
957
|
|
|
820
|
-
@
|
|
958
|
+
@useCtx(ThemeContext, "mode", { defaultValue: "light" })
|
|
821
959
|
private mode = "light";
|
|
822
960
|
```
|
|
823
961
|
|
|
824
|
-
|
|
962
|
+
With no provider in the tree, `optional: true` yields `undefined`; `defaultValue` is used when the provider is absent.
|
|
963
|
+
|
|
964
|
+
### DI: props vs dependencies
|
|
825
965
|
|
|
826
|
-
|
|
966
|
+
**View and Context:** constructor receives only props from JSX. Dependencies are injected via field decorators:
|
|
967
|
+
- `@inject<IX>()` — from global container (Store, Service, UseCase)
|
|
968
|
+
- `@useCtx<IX>()` — from tree @Context / @Form providers
|
|
827
969
|
|
|
828
|
-
|
|
970
|
+
**@Service and @UseCase:** constructor DI via `@diParams` (added by Babel plugin) for injected dependencies.
|
|
829
971
|
|
|
830
|
-
|
|
972
|
+
### Global services (@inject)
|
|
831
973
|
|
|
832
|
-
|
|
974
|
+
Use `@inject<IX>()` in View/Context to access Store, Service, and UseCase from the container:
|
|
833
975
|
|
|
834
976
|
```typescript
|
|
977
|
+
// Define interface and implement with @Service
|
|
835
978
|
export interface ITodoValidateService {
|
|
836
979
|
validate(title: string): ValidationResult;
|
|
837
980
|
}
|
|
838
981
|
|
|
839
|
-
@
|
|
982
|
+
@Service<ITodoValidateService>()
|
|
840
983
|
export class TodoValidateService implements ITodoValidateService {
|
|
841
984
|
validate(title: string) { ... }
|
|
842
985
|
}
|
|
843
986
|
```
|
|
844
987
|
|
|
845
|
-
|
|
988
|
+
**Consumer (View)** — inject Store or UseCase:
|
|
846
989
|
|
|
847
990
|
```tsx
|
|
848
991
|
@View
|
|
849
992
|
class TodoInput {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
) {}
|
|
993
|
+
@inject<ITodoValidateService>() private validateService!: ITodoValidateService;
|
|
994
|
+
@inject<ICreateTodoUseCase>() private createTodo!: ICreateTodoUseCase;
|
|
995
|
+
|
|
996
|
+
constructor(readonly props: Props) {}
|
|
997
|
+
|
|
998
|
+
render() {
|
|
999
|
+
// Call UseCase for mutations, Service for validation
|
|
1000
|
+
return <form onsubmit={() => this.createTodo.execute(this.form.getPayload())}>...</form>;
|
|
1001
|
+
}
|
|
854
1002
|
}
|
|
855
1003
|
```
|
|
856
1004
|
|
|
857
|
-
**Bootstrap** —
|
|
1005
|
+
**Bootstrap** — DI registration is automatic at `createApp` (plugin injects `.useDI(registerAllServices)`):
|
|
858
1006
|
|
|
859
1007
|
```typescript
|
|
860
1008
|
createApp({ root: document.getElementById("root")! })
|
|
@@ -862,9 +1010,9 @@ createApp({ root: document.getElementById("root")! })
|
|
|
862
1010
|
.mount(App);
|
|
863
1011
|
```
|
|
864
1012
|
|
|
865
|
-
|
|
1013
|
+
The DI scanner finds `@Store`, `@Service<IX>()`, `@UseCase<IX>()` and generates `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`.
|
|
866
1014
|
|
|
867
|
-
**Fallback** —
|
|
1015
|
+
**Fallback** — for manual setup: `.configureContainer()`, `@Injectable(token)`:
|
|
868
1016
|
|
|
869
1017
|
```typescript
|
|
870
1018
|
createApp({ root })
|
|
@@ -876,38 +1024,38 @@ createApp({ root })
|
|
|
876
1024
|
.mount(App);
|
|
877
1025
|
```
|
|
878
1026
|
|
|
879
|
-
|
|
1027
|
+
Optionally: pass a custom container via `.container(myContainer)` or `createApp({ root, container })`.
|
|
880
1028
|
|
|
881
|
-
`@Context`
|
|
1029
|
+
`@Context` and the container coexist: Context is for tree-scoped values (theme, router), Container for global services.
|
|
882
1030
|
|
|
883
1031
|
---
|
|
884
1032
|
|
|
885
1033
|
## JSX
|
|
886
1034
|
|
|
887
|
-
Helfy
|
|
1035
|
+
Helfy uses a custom JSX runtime (`@helfy/helfy/jsx-runtime`). JSX is translated to `jsx()` / `jsxs()` calls that build a virtual DOM representation.
|
|
888
1036
|
|
|
889
|
-
###
|
|
1037
|
+
### Attributes
|
|
890
1038
|
|
|
891
|
-
|
|
1039
|
+
Standard HTML attributes are passed through:
|
|
892
1040
|
|
|
893
1041
|
```tsx
|
|
894
|
-
<input type="text" placeholder="
|
|
1042
|
+
<input type="text" placeholder="Enter name" />
|
|
895
1043
|
<img src="/logo.png" alt="Logo" />
|
|
896
1044
|
<div id="container" class="wrapper"></div>
|
|
897
1045
|
```
|
|
898
1046
|
|
|
899
|
-
###
|
|
1047
|
+
### Events
|
|
900
1048
|
|
|
901
|
-
|
|
1049
|
+
Event handlers use **lowercase** attributes (`onclick`, `oninput`, not `onClick`):
|
|
902
1050
|
|
|
903
1051
|
```tsx
|
|
904
1052
|
<button onclick={() => this.increment()}>+</button>
|
|
905
1053
|
<input oninput={(e) => this.handleInput(e)} />
|
|
906
1054
|
```
|
|
907
1055
|
|
|
908
|
-
###
|
|
1056
|
+
### Styles
|
|
909
1057
|
|
|
910
|
-
|
|
1058
|
+
Inline styles use an object with camelCase keys:
|
|
911
1059
|
|
|
912
1060
|
```tsx
|
|
913
1061
|
<div style={{
|
|
@@ -916,26 +1064,26 @@ Helfy использует кастомный JSX runtime (`@helfy/helfy/jsx-run
|
|
|
916
1064
|
padding: '8px 16px',
|
|
917
1065
|
borderRadius: '4px'
|
|
918
1066
|
}}>
|
|
919
|
-
|
|
1067
|
+
Styled block
|
|
920
1068
|
</div>
|
|
921
1069
|
```
|
|
922
1070
|
|
|
923
|
-
### CSS
|
|
1071
|
+
### CSS classes
|
|
924
1072
|
|
|
925
|
-
|
|
1073
|
+
The `class` attribute accepts a string or an array with conditional classes:
|
|
926
1074
|
|
|
927
1075
|
```tsx
|
|
928
|
-
//
|
|
1076
|
+
// string
|
|
929
1077
|
<div class="container">...</div>
|
|
930
1078
|
|
|
931
1079
|
// CSS Modules
|
|
932
1080
|
import styles from './App.module.css';
|
|
933
1081
|
<div class={styles.wrapper}>...</div>
|
|
934
1082
|
|
|
935
|
-
//
|
|
1083
|
+
// conditional classes via array
|
|
936
1084
|
<div class={[
|
|
937
1085
|
styles.cell,
|
|
938
|
-
[styles.active, this.isActive], //
|
|
1086
|
+
[styles.active, this.isActive], // applied when this.isActive === true
|
|
939
1087
|
[styles.disabled, this.isDisabled],
|
|
940
1088
|
]}>...</div>
|
|
941
1089
|
```
|
|
@@ -944,33 +1092,39 @@ import styles from './App.module.css';
|
|
|
944
1092
|
|
|
945
1093
|
## API
|
|
946
1094
|
|
|
947
|
-
###
|
|
948
|
-
|
|
949
|
-
|
|
|
950
|
-
|
|
951
|
-
| `@View` |
|
|
952
|
-
| `@state` |
|
|
953
|
-
| `@Store` |
|
|
954
|
-
| `@observable` |
|
|
955
|
-
| `@
|
|
956
|
-
| `@
|
|
957
|
-
| `@
|
|
958
|
-
| `@
|
|
959
|
-
| `@
|
|
960
|
-
| `@
|
|
961
|
-
| `@
|
|
962
|
-
| `@
|
|
963
|
-
| `@
|
|
964
|
-
| `@
|
|
965
|
-
| `@
|
|
966
|
-
| `@
|
|
967
|
-
| `@
|
|
968
|
-
| `@
|
|
969
|
-
| `@
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1095
|
+
### Decorators
|
|
1096
|
+
|
|
1097
|
+
| Decorator | Scope | Description |
|
|
1098
|
+
|-----------|-------|-------------|
|
|
1099
|
+
| `@View` | Class | Turns a class into a component with `render()`, `view`, `update()` |
|
|
1100
|
+
| `@state` | Field | Component local state on signals (write updates only this component) |
|
|
1101
|
+
| `@Store` | Class | Global reactive state. Empty constructor; only `@state`/`@computed`. No `@inject`, `@useCtx`, `@effect`, or external I/O |
|
|
1102
|
+
| `@observable` | Field | Alias for `@state` in Store; makes field reactive |
|
|
1103
|
+
| `@Service<IX>()` | Class | Infrastructure (HTTP, validation, repos). Constructor DI. No `@state`/`@computed`. Can mutate Store |
|
|
1104
|
+
| `@UseCase<IX>()` | Class | Business scenarios. Injects Store + Service. Main method: `execute`/`perform`/`handle`. No `@state`/`@computed` |
|
|
1105
|
+
| `@Context` | Class | Non-rendering context provider; renders only children |
|
|
1106
|
+
| `@state` / `@computed` | Context field | In `@Context`: `@state` for reactive fields, `@computed` for derived getters. Public methods exposed automatically |
|
|
1107
|
+
| `@inject<IX>()` | Field | Injects Store, Service, or UseCase from container. Use in View/Context |
|
|
1108
|
+
| `@useCtx(ContextClass)` / `@useCtx<IX>()` / `@useCtx(ContextClass, field)` | Field | Injects context or a context field from the nearest provider up the tree |
|
|
1109
|
+
| `@Injectable<IX>()` | Class | Generic injectable. Prefer `@Service` for infrastructure, `@UseCase` for business logic |
|
|
1110
|
+
| `@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) |
|
|
1111
|
+
| `@ref` | Field | Marks a field to receive a DOM or component reference (use with `@ref(this.fieldName)` in JSX) |
|
|
1112
|
+
| `@expose` | Method | Makes a method available to the parent when using `@ref` on a component (without `@expose` the parent gets the full instance) |
|
|
1113
|
+
| `@binded(name)` | Field | Binds a field to `@bind:name` from the parent (for custom components) |
|
|
1114
|
+
| `@bind(expr)` / `@bind:name(expr)` | JSX | Two-way binding with `@state` field (value/checked + oninput/onchange). For components: `@bind:value(expr)` |
|
|
1115
|
+
| `@ApiClient<IX>()` | Class | API client with `@queryConfig` / `@mutationConfig` methods. Registers as Service by interface |
|
|
1116
|
+
| `@queryConfig(keyTemplate)` | Method | Marks method as Query; returns QueryBuilder chain, decorator calls `.build()` |
|
|
1117
|
+
| `@mutationConfig(options?)` | Method | Marks method as Mutation; merges invalidateQueries, optimisticFn into Mutation |
|
|
1118
|
+
| `@useQuery<ApiInterface>(keyOrGetter)` | Field | Injects Query from ApiClient, refetch on mount, reactive data/isLoading/isError |
|
|
1119
|
+
| `@useMutation<ApiInterface>(methodName)` | Field | Injects Mutation from ApiClient by method name |
|
|
1120
|
+
| `@Form` | Class | Form context with `@field` fields |
|
|
1121
|
+
| `@field(options)` | FormContext field | Creates FieldState for a form field (value, isTouched, error, isDirty) |
|
|
1122
|
+
| `@useForm(FormContext)` | Field | Injects the form into a component with subscription to field changes |
|
|
1123
|
+
| `@field(expr)` | JSX | Connects an input to FieldState: value/checked + onblur + error class + aria-invalid |
|
|
1124
|
+
|
|
1125
|
+
### Routing (SPA)
|
|
1126
|
+
|
|
1127
|
+
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
1128
|
|
|
975
1129
|
```tsx
|
|
976
1130
|
// index.ts
|
|
@@ -1012,15 +1166,15 @@ class Sidebar {
|
|
|
1012
1166
|
|
|
1013
1167
|
return (
|
|
1014
1168
|
<nav>
|
|
1015
|
-
<Link to="/" label="
|
|
1016
|
-
<Link to="/analytics" label="
|
|
1169
|
+
<Link to="/" label="Home" class={isHome ? "font-bold" : ""} />
|
|
1170
|
+
<Link to="/analytics" label="Analytics" class={isAnalytics ? "font-bold" : ""} />
|
|
1017
1171
|
</nav>
|
|
1018
1172
|
);
|
|
1019
1173
|
}
|
|
1020
1174
|
}
|
|
1021
1175
|
```
|
|
1022
1176
|
|
|
1023
|
-
|
|
1177
|
+
A typical page component can use router decorators:
|
|
1024
1178
|
|
|
1025
1179
|
```tsx
|
|
1026
1180
|
import { View, path, search, params, router, type RouterAPI } from "@helfy/helfy";
|
|
@@ -1047,7 +1201,7 @@ class DebugPage {
|
|
|
1047
1201
|
<pre>params: {JSON.stringify(this.routeParams)}</pre>
|
|
1048
1202
|
<pre>query: {JSON.stringify(this.query)}</pre>
|
|
1049
1203
|
<button onclick={() => this.rtr.push("/analytics")}>
|
|
1050
|
-
|
|
1204
|
+
Go to analytics
|
|
1051
1205
|
</button>
|
|
1052
1206
|
</section>
|
|
1053
1207
|
);
|
|
@@ -1055,7 +1209,7 @@ class DebugPage {
|
|
|
1055
1209
|
}
|
|
1056
1210
|
```
|
|
1057
1211
|
|
|
1058
|
-
|
|
1212
|
+
**Custom 404 page.** Wrap `RouterView` and override the `notFound` slot:
|
|
1059
1213
|
|
|
1060
1214
|
```tsx
|
|
1061
1215
|
@View
|
|
@@ -1070,35 +1224,35 @@ class AppRouter {
|
|
|
1070
1224
|
);
|
|
1071
1225
|
}
|
|
1072
1226
|
}
|
|
1073
|
-
```
|
|
1227
|
+
```
|
|
1074
1228
|
|
|
1075
|
-
###
|
|
1229
|
+
### Component lifecycle
|
|
1076
1230
|
|
|
1077
|
-
1. `constructor(props)`
|
|
1078
|
-
2. `render()`
|
|
1079
|
-
3. `mount()`
|
|
1080
|
-
4. `onMount()`
|
|
1081
|
-
5. `onAttached()`
|
|
1082
|
-
6. `update()`
|
|
1083
|
-
7. `updateProps(newProps)`
|
|
1231
|
+
1. `constructor(props)` — instance creation, `this.props` wrapped in reactive Proxy
|
|
1232
|
+
2. `render()` — returns JSX (**called once** on mount)
|
|
1233
|
+
3. `mount()` — initial JSX render to DOM fragment
|
|
1234
|
+
4. `onMount()` — hook after first mount (optional)
|
|
1235
|
+
5. `onAttached()` — hook after insertion into document (optional). Called on `attach(parent)`.
|
|
1236
|
+
6. `update()` — structural update (e.g. on route change). For the same child component, only signals are updated via `updateProps`
|
|
1237
|
+
7. `updateProps(newProps)` — updates prop signals (fine-grained, no full re-render)
|
|
1084
1238
|
|
|
1085
1239
|
#### onMount vs onAttached
|
|
1086
1240
|
|
|
1087
1241
|
| | `onMount()` | `onAttached()` |
|
|
1088
1242
|
|---|---|---|
|
|
1089
|
-
|
|
|
1090
|
-
|
|
|
1091
|
-
|
|
|
1243
|
+
| **When** | Right after `mount()`, tree built, refs assigned | After `attach(parent)`, element in document |
|
|
1244
|
+
| **Element in document** | May not be yet (root in fragment) | Yes |
|
|
1245
|
+
| **Refs** | Available | Available |
|
|
1092
1246
|
|
|
1093
|
-
**`onMount()`** —
|
|
1094
|
-
-
|
|
1095
|
-
-
|
|
1096
|
-
-
|
|
1247
|
+
**`onMount()`** — for initialization that doesn't need the document:
|
|
1248
|
+
- Subscriptions to store/observable/services
|
|
1249
|
+
- Internal state setup
|
|
1250
|
+
- Adding handlers (works on detached nodes too)
|
|
1097
1251
|
|
|
1098
|
-
**`onAttached()`** —
|
|
1252
|
+
**`onAttached()`** — for operations that require the element in the document:
|
|
1099
1253
|
- `focus()`, `scrollIntoView()`
|
|
1100
|
-
- `getBoundingClientRect()`,
|
|
1101
|
-
-
|
|
1254
|
+
- `getBoundingClientRect()`, layout measurement
|
|
1255
|
+
- Any DOM API that only works on attached nodes
|
|
1102
1256
|
|
|
1103
1257
|
```tsx
|
|
1104
1258
|
@View
|
|
@@ -1106,22 +1260,22 @@ class SearchInput {
|
|
|
1106
1260
|
@ref private input!: HTMLInputElement;
|
|
1107
1261
|
|
|
1108
1262
|
onMount() {
|
|
1109
|
-
this.store.subscribe(this.handleChange); //
|
|
1263
|
+
this.store.subscribe(this.handleChange); // subscription — document not needed
|
|
1110
1264
|
}
|
|
1111
1265
|
|
|
1112
1266
|
onAttached() {
|
|
1113
|
-
this.input.focus(); //
|
|
1267
|
+
this.input.focus(); // focus — needs document
|
|
1114
1268
|
}
|
|
1115
1269
|
}
|
|
1116
1270
|
```
|
|
1117
1271
|
|
|
1118
|
-
###
|
|
1272
|
+
### Slots (content projection)
|
|
1119
1273
|
|
|
1120
|
-
Helfy
|
|
1274
|
+
Helfy supports named slots with fallback content and override in child components.
|
|
1121
1275
|
|
|
1122
|
-
####
|
|
1276
|
+
#### Slot provider (`@View` component)
|
|
1123
1277
|
|
|
1124
|
-
|
|
1278
|
+
Slots are declared in JSX via the `@slot:name(...)` directive inside `render()`:
|
|
1125
1279
|
|
|
1126
1280
|
```tsx
|
|
1127
1281
|
import { View } from "@helfy/helfy";
|
|
@@ -1131,24 +1285,24 @@ class AppLayout {
|
|
|
1131
1285
|
render() {
|
|
1132
1286
|
return (
|
|
1133
1287
|
<section class="layout">
|
|
1134
|
-
{/*
|
|
1135
|
-
@slot:header({ title: "
|
|
1288
|
+
{/* Named slot header with fallback markup */}
|
|
1289
|
+
@slot:header({ title: "Task list" }) fallback {
|
|
1136
1290
|
<header class="mb-4">
|
|
1137
1291
|
<h1 class="text-2xl font-bold text-gray-900">
|
|
1138
|
-
|
|
1292
|
+
Task list
|
|
1139
1293
|
</h1>
|
|
1140
1294
|
</header>
|
|
1141
1295
|
}
|
|
1142
1296
|
|
|
1143
|
-
{/*
|
|
1297
|
+
{/* Named slot content with fallback and @if inside */}
|
|
1144
1298
|
@slot:content({ store: this.props.store, filtered: this.props.filtered, hasTodos: this.props.hasTodos }) fallback {
|
|
1145
1299
|
@if (this.props.hasTodos) {
|
|
1146
1300
|
<section class="pt-3 border-t border-gray-200 text-sm text-gray-600">
|
|
1147
1301
|
<p class="mb-1">
|
|
1148
|
-
|
|
1149
|
-
|
|
1302
|
+
Total: {this.props.store.todos.length}, active: {this.props.store.activeCount},
|
|
1303
|
+
completed: {this.props.store.completedCount}
|
|
1150
1304
|
</p>
|
|
1151
|
-
<p
|
|
1305
|
+
<p>Filtered: {this.props.filtered.length}</p>
|
|
1152
1306
|
</section>
|
|
1153
1307
|
}
|
|
1154
1308
|
}
|
|
@@ -1158,15 +1312,15 @@ class AppLayout {
|
|
|
1158
1312
|
}
|
|
1159
1313
|
```
|
|
1160
1314
|
|
|
1161
|
-
|
|
1315
|
+
Provider rules:
|
|
1162
1316
|
|
|
1163
|
-
- `@slot:header({ ... })` —
|
|
1164
|
-
-
|
|
1165
|
-
-
|
|
1317
|
+
- `@slot:header({ ... })` — declares the named slot `header` and invokes it.
|
|
1318
|
+
- The `fallback { ... }` block (optional) defines default markup when the slot is not overridden.
|
|
1319
|
+
- Inside `fallback` you can use `@if`, `@for`, and regular JSX.
|
|
1166
1320
|
|
|
1167
|
-
####
|
|
1321
|
+
#### Slot consumer (override in JSX)
|
|
1168
1322
|
|
|
1169
|
-
|
|
1323
|
+
Override a slot in a child component via `@slot.name(...) { ... }` inside JSX children:
|
|
1170
1324
|
|
|
1171
1325
|
```tsx
|
|
1172
1326
|
@View
|
|
@@ -1182,20 +1336,20 @@ class TodoApp {
|
|
|
1182
1336
|
filtered={filtered}
|
|
1183
1337
|
hasTodos={this.hasTodos}
|
|
1184
1338
|
>
|
|
1185
|
-
{/*
|
|
1339
|
+
{/* Override header slot */}
|
|
1186
1340
|
@slot.header({ title }) {
|
|
1187
1341
|
<header class="mb-5">
|
|
1188
1342
|
<h1 class="mb-4 text-2xl font-bold text-gray-900">
|
|
1189
|
-
|
|
1343
|
+
Tasks ({title})
|
|
1190
1344
|
</h1>
|
|
1191
1345
|
<TodoInput
|
|
1192
|
-
placeholder="
|
|
1346
|
+
placeholder="Add task…"
|
|
1193
1347
|
onSubmit={(text) => store.add(text)}
|
|
1194
1348
|
/>
|
|
1195
1349
|
</header>
|
|
1196
1350
|
}
|
|
1197
1351
|
|
|
1198
|
-
{/*
|
|
1352
|
+
{/* Override content slot, @if/@for allowed inside */}
|
|
1199
1353
|
@slot.content({ store, filtered, hasTodos }) {
|
|
1200
1354
|
@if (hasTodos) {
|
|
1201
1355
|
<section class="pt-3 border-t border-gray-200">
|
|
@@ -1221,8 +1375,8 @@ class TodoApp {
|
|
|
1221
1375
|
}
|
|
1222
1376
|
```
|
|
1223
1377
|
|
|
1224
|
-
|
|
1378
|
+
Syntax summary:
|
|
1225
1379
|
|
|
1226
|
-
- `@slot
|
|
1227
|
-
- `@slot
|
|
1228
|
-
-
|
|
1380
|
+
- `@slot:name({ props }) fallback { FallbackJSX }` — declare and invoke the slot in the provider.
|
|
1381
|
+
- `@slot.name({ ctx }) { OverrideJSX }` — override the slot in the consumer.
|
|
1382
|
+
- All directives (`@if`, `@for`, `@empty`) and regular JSX work inside `FallbackJSX` and `OverrideJSX`.
|