helfy-create 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,15 @@ npx helfy-create
19
19
  **Options:**
20
20
  - `--skip-install` — create project without running `npm install` (e.g. for custom registry or offline)
21
21
 
22
+ ## Editor setup (VS Code)
23
+
24
+ **Syntax highlighting.** The helfy-syntax extension is not in the Marketplace. Install manually from [GitHub](https://github.com/ikrymsaev/helfy-syntax/tree/main/dist):
25
+
26
+ - Download the **archive** and extract to the extensions folder, or
27
+ - Download the **.vsix** file and install: `Extensions` → `...` → `Install from VSIX...`
28
+
29
+ **TypeScript.** Use **workspace TypeScript**: `Ctrl+Shift+P` (Windows/Linux) or `Cmd+Shift+P` (Mac) → `TypeScript: Select TypeScript Version` → `Use Workspace Version`. Required because the project includes `@helfy/helfy-ts-plugin`.
30
+
22
31
  ## What you get
23
32
 
24
33
  - Helfy + TypeScript + Webpack setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helfy-create",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Create a new Helfy project",
5
5
  "bin": {
6
6
  "helfy-create": "./bin/create-helfy.js"
@@ -14,7 +14,6 @@
14
14
  "template/tsconfig.json",
15
15
  "template/webpack.config.js",
16
16
  "template/declaration.d.ts",
17
- "template/docs",
18
17
  "template/src"
19
18
  ],
20
19
  "keywords": [
@@ -27,7 +26,6 @@
27
26
  "license": "MIT",
28
27
  "repository": {
29
28
  "type": "git",
30
- "url": "https://github.com/ikrymsaev/helfy.git",
31
- "directory": "packages/create-helfy"
29
+ "url": "https://github.com/ikrymsaev/helfy-create"
32
30
  }
33
31
  }
@@ -1,1300 +0,0 @@
1
- # Helfy
2
-
3
- Helfy -- TypeScript UI-фреймворк с декоратор-ориентированным API, кастомным JSX и fine-grained reactivity.
4
-
5
- Компоненты описываются классами с декоратором `@View`, состояние управляется через `@state` и `@observable`, а шаблоны используют расширенный JSX с директивами `@if`, `@for`, `@else`. Реактивность построена на signals (аналог SolidJS): `render()` вызывается один раз, а последующие обновления DOM происходят точечно — только для тех узлов, которые читают изменившийся signal.
6
-
7
- ## Содержание
8
-
9
- - [Установка](#установка)
10
- - [Компоненты](#компоненты)
11
- - [Базовый компонент](#базовый-компонент)
12
- - [Props](#props)
13
- - [Локальное состояние (@state)](#локальное-состояние-state)
14
- - [Вложенные компоненты](#вложенные-компоненты)
15
- - [Монтирование в DOM](#монтирование-в-dom)
16
- - [Привязка к DOM и компонентам (@ref)](#привязка-к-dom-и-компонентам-ref)
17
- - [Двустороннее связывание (@bind)](#двустороннее-связывание-bind)
18
- - [Формы (@Form, @field, @field в JSX)](#формы-form-field-field-в-jsx)
19
- - [Шаблонные директивы](#шаблонные-директивы)
20
- - [@if / @elseif / @else](#if--elseif--else)
21
- - [@for](#for)
22
- - [@empty](#empty)
23
- - [State Management](#state-management)
24
- - [Создание Store](#создание-store)
25
- - [Подписка через @observe](#подписка-через-observe)
26
- - [Контекст хранилищ](#контекст-хранилищ)
27
- - [Логирование (@logger)](#логирование-logger)
28
- - [Context & DI](#context--di)
29
- - [Провайдер (@Context, @provide)](#провайдер-context-provide)
30
- - [Потребитель (@ctx)](#потребитель-ctx)
31
- - [Реактивные поля](#реактивные-поля)
32
- - [Опциональная инжекция](#опциональная-инжекция)
33
- - [DI Container](#di-container-конструкторный-инжект)
34
- - [JSX](#jsx)
35
- - [Атрибуты](#атрибуты)
36
- - [События](#события)
37
- - [Стили](#стили)
38
- - [CSS-классы](#css-классы)
39
- - [API](#api)
40
-
41
- ---
42
-
43
- ## Установка
44
-
45
- ```bash
46
- npm install @helfy/helfy
47
- ```
48
-
49
- или в `package.json`:
50
-
51
- ```json
52
- {
53
- "dependencies": {
54
- "@helfy/helfy": "^0.0.1"
55
- }
56
- }
57
- ```
58
-
59
- ### Build setup
60
-
61
- Babel-плагин автоматически запускает DI-сканер перед трансформацией (pre-скрипты в `package.json` не нужны). Сканер генерирует `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`. Добавьте `.helfy` в `.gitignore`.
62
-
63
- ### Babel
64
-
65
- В `.babelrc` достаточно одного пресета:
66
-
67
- ```json
68
- {
69
- "presets": ["@helfy/helfy/babel-preset"]
70
- }
71
- ```
72
-
73
- Пресет включает: JSX runtime, TypeScript, legacy-декораторы, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI для `@Injectable<IX>()`, автоинъекция регистрации при `createApp`, трансформация `@logger()` → `@logger("<ClassName>")`).
74
-
75
- ### Webpack
76
-
77
- Для обработки директив (`@if`, `@for`, `@ref`, `@bind`, `@field`) в `.tsx` файлах подключить loader:
78
-
79
- ```javascript
80
- {
81
- test: /\.tsx$/,
82
- use: [
83
- 'babel-loader',
84
- require.resolve('@helfy/helfy/compiler/helfy-loader'),
85
- ],
86
- }
87
- ```
88
-
89
- ### TypeScript
90
-
91
- В `tsconfig.json` указать:
92
-
93
- ```json
94
- {
95
- "compilerOptions": {
96
- "jsx": "preserve",
97
- "experimentalDecorators": true,
98
- "emitDecoratorMetadata": true,
99
- "plugins": [{ "name": "@helfy/helfy/ts-plugin" }]
100
- }
101
- }
102
- ```
103
-
104
- Плагин `@helfy/helfy/ts-plugin` даёт поддержку директив `@if`, `@for`, `@ref`, `@bind`, `@field` в IDE (автодополнение, типизация, переход к определению). Идёт в комплекте с пакетом helfy.
105
-
106
- ---
107
-
108
- ## Компоненты
109
-
110
- ### Базовый компонент
111
-
112
- Компонент -- это класс с декоратором `@View` и методом `render()`, возвращающим JSX:
113
-
114
- ```tsx
115
- import { View } from "@helfy/helfy";
116
-
117
- @View
118
- class Hello {
119
- render() {
120
- return (
121
- <div>
122
- <h1>Hello, Helfy!</h1>
123
- </div>
124
- );
125
- }
126
- }
127
- ```
128
-
129
- Декоратор `@View` автоматически:
130
- - вызывает `render()` **один раз** и монтирует результат в DOM-фрагмент
131
- - добавляет свойство `view` (ссылка на корневой DOM-элемент)
132
- - оборачивает `this.props` в реактивный Proxy (каждый prop — signal)
133
- - настраивает fine-grained эффекты: при изменении signal обновляется только конкретный DOM-узел, а не весь компонент
134
-
135
- > **Важно:** При наследовании `@View` нужно указывать и на дочернем классе. Без этого не будут работать `@ctx`, `scheduleUpdate` и обновление DOM.
136
-
137
- ### Props
138
-
139
- Props передаются через конструктор. Декоратор `@View` оборачивает `this.props` в реактивный Proxy — каждый prop становится signal. Это означает, что при изменении props родителем обновляются только те DOM-узлы, которые их читают, без полного перерендера компонента.
140
-
141
- > **Важно:** Не деструктуризируйте `this.props` в `render()`. Обращайтесь к полям напрямую через `this.props.field` — это обеспечивает корректную подписку на signals.
142
-
143
- ```tsx
144
- import { View } from "@helfy/helfy";
145
-
146
- interface ButtonProps {
147
- label: string;
148
- type?: "button" | "submit";
149
- onClick?: () => void;
150
- }
151
-
152
- @View
153
- class Button {
154
- constructor(private readonly props: ButtonProps) {}
155
-
156
- render() {
157
- return (
158
- <button
159
- type={this.props.type ?? "button"}
160
- onclick={this.props.onClick}
161
- >
162
- {this.props.label}
163
- </button>
164
- );
165
- }
166
- }
167
- ```
168
-
169
- Использование:
170
-
171
- ```tsx
172
- <Button label="Нажми" onClick={() => console.log('clicked')} />
173
- ```
174
-
175
- При изменении props родителем `updateProps()` обновляет соответствующие signals через `batch()`. Fine-grained эффекты, созданные Babel-плагином (`createReactiveChild`), отслеживают эти signals и точечно обновляют DOM.
176
-
177
- ### Локальное состояние (@state)
178
-
179
- Декоратор `@state` делает поле реактивным — под капотом каждое поле — это signal из `createSignal()`.
180
- При изменении значения обновляются только те DOM-узлы и эффекты, которые читают это поле (fine-grained). Полный перерендер компонента не происходит:
181
-
182
- ```tsx
183
- import { View, state } from "@helfy/helfy";
184
-
185
- @View
186
- class Counter {
187
- @state private count = 0;
188
-
189
- increment() {
190
- this.count++; // обновит только узлы, читающие count
191
- }
192
-
193
- decrement() {
194
- this.count--;
195
- }
196
-
197
- render() {
198
- return (
199
- <div>
200
- <span>{this.count}</span>
201
- <button onclick={this.increment.bind(this)}>+</button>
202
- <button onclick={this.decrement.bind(this)}>-</button>
203
- </div>
204
- );
205
- }
206
- }
207
- ```
208
-
209
- Можно объявить несколько `@state`-полей. Каждое из них независимо триггерит обновление только тех узлов DOM, которые его читают.
210
-
211
- Signals‑примитивы (`createSignal`, `createEffect`, `createComputed`, `batch`, `onCleanup`) также экспортируются из `helfy` и могут использоваться напрямую, но в большинстве случаев достаточно `@state`, `@computed` и `@effect`.
212
-
213
- ### Вложенные компоненты
214
-
215
- Компоненты используются в JSX как теги. Props передаются как атрибуты:
216
-
217
- ```tsx
218
- import { View } from "@helfy/helfy";
219
-
220
- @View
221
- class App {
222
- render() {
223
- return (
224
- <div>
225
- <Counter />
226
- <Button label="Hello" onClick={() => {}} />
227
- </div>
228
- );
229
- }
230
- }
231
- ```
232
-
233
- ### Монтирование в DOM
234
-
235
- Корневой компонент монтируется через `attach()`, чтобы вызвать хук `onAttached()` (после вставки в document):
236
-
237
- ```typescript
238
- const app = new App();
239
- app.attach(document.getElementById('root'));
240
- ```
241
-
242
- ### Привязка к DOM и компонентам (@ref)
243
-
244
- Декоратор `@ref` помечает поле для получения ссылки на DOM-элемент или экземпляр компонента. В JSX используется директива `@ref(this.имяПоля)`. Доступ к ссылке возможен после `onMount()`. Для операций вроде `focus()`, требующих элемент в document, используйте `onAttached()`.
245
-
246
- **Для компонентов** — родитель получает прокси с доступом только к методам, помеченным `@expose`. Это позволяет явно определить публичный API компонента.
247
-
248
- ```tsx
249
- import { View, ref, expose } from "@helfy/helfy";
250
-
251
- @View
252
- class Input {
253
- @ref private inputEl!: HTMLInputElement;
254
-
255
- @expose focus() {
256
- this.inputEl?.focus();
257
- }
258
-
259
- render() {
260
- return <input @ref(this.inputEl) type="text" />;
261
- }
262
- }
263
-
264
- @View
265
- class Form {
266
- @ref private input!: Input;
267
-
268
- onAttached() {
269
- this.input.focus(); // доступен только помеченный метод
270
- }
271
-
272
- render() {
273
- return <Input @ref(this.input) />;
274
- }
275
- }
276
- ```
277
-
278
- - DOM-элементы по `@ref` передаются как есть
279
- - Компоненты с `@expose` — родитель видит только помеченные методы
280
- - Компоненты без `@expose` — передаётся экземпляр целиком (обратная совместимость)
281
-
282
- При условном рендере (`@if`) ссылка обнуляется при удалении элемента. Запрещено использовать `@ref` и `@state` на одном поле.
283
-
284
- ### Двустороннее связывание (@bind)
285
-
286
- Директива `@bind` — синтаксический сахар для двусторонней привязки значения поля `@state` к элементу формы. Синтаксис: `@bind(expr)` — круглые скобки, как у `@ref`. Компилятор автоматически генерирует пару `value`/`checked` и обработчик события.
287
-
288
- ```tsx
289
- import { View, state } from "@helfy/helfy";
290
-
291
- @View
292
- class LoginForm {
293
- @state private username = '';
294
- @state private email = '';
295
- @state private isActive = true;
296
-
297
- render() {
298
- return (
299
- <form>
300
- <input @bind(this.username) type="text" />
301
- <input @bind(this.email) type="email" />
302
- <input @bind(this.isActive) type="checkbox" />
303
- <select @bind(this.priority)>
304
- <option value="low">Низкий</option>
305
- <option value="high">Высокий</option>
306
- </select>
307
- </form>
308
- );
309
- }
310
- }
311
- ```
312
-
313
- Трансформация по типу элемента:
314
-
315
- | Элемент | Атрибут | Событие |
316
- |---------|---------|---------|
317
- | `input[text|email|password|...]` | `value` | `oninput` |
318
- | `input[checkbox|radio]` | `checked` | `onchange` |
319
- | `select` | `value` | `onchange` |
320
- | `textarea` | `value` | `oninput` |
321
-
322
- Поле должно быть помечено `@state` для реактивного обновления UI. Для union-типов (например, `TodoPriority`) используется приведение `as typeof expr` — типобезопасность сохраняется.
323
-
324
- **Кастомные компоненты:** для привязки к компоненту с пропом `value` используется именованный биндинг `@bind:value(expr)`:
325
-
326
- ```tsx
327
- // Родитель
328
- <Input @bind:value(this.title) placeholder="Заголовок" />
329
-
330
- // Компонент Input с @binded("value")
331
- @View
332
- class Input {
333
- @binded("value") private bindedVal!: string;
334
- // ...
335
- render() {
336
- return <input @bind(this.bindedVal) type="text" />;
337
- }
338
- }
339
- ```
340
-
341
- ### Формы (@Form, @field, @field в JSX)
342
-
343
- Централизованное управление формами с валидацией через контекст: `@Form` — класс контекста формы, `@field` — декоратор полей (создаёт `FieldState`), `@useForm` — инжект формы в компонент. JSX-директива `@field(expr)` подключает инпут к `FieldState` одной строкой.
344
-
345
- **FormContext** — класс с `@Form` и полями `@field`:
346
-
347
- ```tsx
348
- import { Form, field, logger, type FieldState, type ILogger } from "@helfy/helfy";
349
-
350
- @Form
351
- export class LoginFormContext {
352
- @logger() private log!: ILogger;
353
-
354
- @field({ defaultValue: "" })
355
- email!: FieldState<string>;
356
-
357
- @field({ defaultValue: "" })
358
- password!: FieldState<string>;
359
-
360
- @field({ defaultValue: false })
361
- rememberMe!: FieldState<boolean>;
362
-
363
- validateAll(): boolean {
364
- // проверка полей, установка field.error
365
- return true;
366
- }
367
-
368
- submit() {
369
- if (!this.validateAll()) {
370
- this.log.warn("Submit validation failed");
371
- return;
372
- }
373
- this.log.info("Submit", { email: this.email.value });
374
- }
375
- }
376
- ```
377
-
378
- **FieldState** содержит `value`, `isDirty`, `isTouched`, `error`, `isValid`. При изменении `value`/`error`/`isTouched` компоненты с `@useForm` перерендериваются.
379
-
380
- **Компонент формы** — инжект через `@useForm`, рендер через провайдер:
381
-
382
- ```tsx
383
- import { View, useForm } from "@helfy/helfy";
384
- import { LoginFormContext } from "./LoginFormContext";
385
-
386
- @View
387
- export class LoginForm {
388
- @useForm(LoginFormContext) private form!: LoginFormContext;
389
-
390
- render() {
391
- return (
392
- <LoginFormContext>
393
- <form onsubmit={(e: Event) => { e.preventDefault(); this.form.submit(); }}>
394
- <input @field(this.form.email) type="email" class="input" />
395
- {this.form.email.isTouched && this.form.email.error && (
396
- <span class="error">{this.form.email.error}</span>
397
- )}
398
- <input @field(this.form.password) type="password" class="input" />
399
- <input @field(this.form.rememberMe) type="checkbox" id="remember" />
400
- <label for="remember">Запомнить меня</label>
401
- <button type="submit">Войти</button>
402
- </form>
403
- </LoginFormContext>
404
- );
405
- }
406
- }
407
- ```
408
-
409
- **JSX-директива `@field(expr)`** — одна директива заменяет `@bind` + `onblur` + `class` для ошибок + `aria-invalid`. Компилятор генерирует:
410
-
411
- - `value`/`checked` и `oninput`/`onchange`
412
- - `onblur` → `expr.isTouched = true`
413
- - `class` — объединение с существующим; при `expr.isTouched && expr.error` добавляется `input-error`
414
- - `aria-invalid={expr.isTouched && expr.error ? "true" : "false"}`
415
-
416
- Поддерживаются `input` (text, email, password, checkbox, radio), `select`, `textarea`. Класс `.input-error` можно задать в глобальных стилях (например, `@apply border-red-500` в Tailwind).
417
-
418
- ---
419
-
420
- ## Шаблонные директивы
421
-
422
- Helfy расширяет JSX директивами `@if`, `@elseif`, `@else`, `@for`, `@empty`. Компилятор трансформирует их в вызовы `$._if()`, `$._ifelse()`, `$._forin()` на этапе сборки.
423
-
424
- ### @if / @elseif / @else
425
-
426
- Условный рендеринг:
427
-
428
- ```tsx
429
- render() {
430
- return (
431
- <div>
432
- @if (this.count > 0) {
433
- <span>Положительное: {this.count}</span>
434
- }
435
- </div>
436
- );
437
- }
438
- ```
439
-
440
- Цепочка условий:
441
-
442
- ```tsx
443
- render() {
444
- return (
445
- <div>
446
- @if (this.status === 'loading') {
447
- <span>Загрузка...</span>
448
- } @elseif (this.status === 'error') {
449
- <span>Ошибка!</span>
450
- } @else {
451
- <span>Данные загружены</span>
452
- }
453
- </div>
454
- );
455
- }
456
- ```
457
-
458
- Вложенные условия:
459
-
460
- ```tsx
461
- @if (this.isVisible) {
462
- <div>
463
- @if (this.count > 10) {
464
- <span>Больше десяти</span>
465
- }
466
- </div>
467
- }
468
- ```
469
-
470
- ### @for
471
-
472
- Итерация по массиву. Синтаксис: `@for (item of array)` или `@for (item, index of array)`.
473
-
474
- ```tsx
475
- @state private items = ['apple', 'banana', 'cherry'];
476
-
477
- render() {
478
- return (
479
- <ul>
480
- @for (item, i of this.items; track item) {
481
- <li>{i}: {item}</li>
482
- }
483
- </ul>
484
- );
485
- }
486
- ```
487
-
488
- `track` задает ключ для оптимизации DOM-диффинга (аналог `key` в React):
489
-
490
- ```tsx
491
- @for (user of this.users; track user.id) {
492
- <UserCard name={user.name} />
493
- }
494
- ```
495
-
496
- ### @empty
497
-
498
- Блок `@empty` после `@for` рендерится, когда массив пуст:
499
-
500
- ```tsx
501
- @for (item of this.items; track item) {
502
- <div>{item}</div>
503
- } @empty {
504
- <span>Список пуст</span>
505
- }
506
- ```
507
-
508
- ---
509
-
510
- ## State Management
511
-
512
- ### Создание Store
513
-
514
- Store -- глобальное реактивное хранилище. Создается классом с декоратором `@Store`, реактивные поля помечаются `@observable`:
515
-
516
- ```typescript
517
- import { Store, observable } from "@helfy/helfy";
518
-
519
- @Store
520
- class UserStore {
521
- @observable name = "Guest";
522
- @observable isLoggedIn = false;
523
-
524
- login(name: string) {
525
- this.name = name;
526
- this.isLoggedIn = true; // подписчики name и isLoggedIn уведомлены
527
- }
528
-
529
- logout() {
530
- this.name = "Guest";
531
- this.isLoggedIn = false;
532
- }
533
- }
534
-
535
- export default UserStore;
536
- ```
537
-
538
- Декоратор `@Store` добавляет классу:
539
- - `subscribe(field, callback)` -- подписка на изменение поля, возвращает функцию отписки
540
- - `unsubscribe(field, callback)` -- отписка
541
-
542
- Декоратор `@observable` превращает поле в реактивное свойство с getter/setter. При записи нового значения все подписчики уведомляются.
543
-
544
- ### Подписка через @observe
545
-
546
- Декоратор `@observe` связывает поле компонента с полем store. При изменении значения в store компонент автоматически перерендерится:
547
-
548
- ```tsx
549
- import { View, observe } from "@helfy/helfy";
550
- import Stores from "./StoreContext";
551
- import UserStore from "./UserStore";
552
-
553
- @View
554
- class Header {
555
- @observe(Stores.userStore, 'name')
556
- private userName: UserStore['name'];
557
-
558
- @observe(Stores.userStore, 'isLoggedIn')
559
- private isLoggedIn: UserStore['isLoggedIn'];
560
-
561
- render() {
562
- return (
563
- <header>
564
- @if (this.isLoggedIn) {
565
- <span>Привет, {this.userName}!</span>
566
- } @else {
567
- <span>Войдите в систему</span>
568
- }
569
- </header>
570
- );
571
- }
572
- }
573
- ```
574
-
575
- Тип поля берется из store через lookup-тип (`UserStore['name']`), что дает полную типобезопасность.
576
-
577
- ### Контекст хранилищ
578
-
579
- Рекомендуется создать единый контекст для всех store:
580
-
581
- ```typescript
582
- import UserStore from "./UserStore";
583
- import AppStore from "./AppStore";
584
-
585
- class Stores {
586
- static readonly userStore = new UserStore();
587
- static readonly appStore = new AppStore();
588
- }
589
-
590
- export default Stores;
591
- ```
592
-
593
- Использование в компонентах:
594
-
595
- ```tsx
596
- import Stores from "./StoreContext";
597
-
598
- // подписка через декоратор
599
- @observe(Stores.userStore, 'name')
600
- private userName: string;
601
-
602
- // прямой вызов метода store
603
- Stores.userStore.login("Alice");
604
- ```
605
-
606
- ---
607
-
608
- ## Логирование (@logger)
609
-
610
- Декоратор `@logger` инжектирует логгер в классы View, Context, Store и Injectable. Имя класса подставляется на этапе компиляции (Babel-плагин), что сохраняет читаемые имена даже при минификации.
611
-
612
- ### Использование
613
-
614
- ```tsx
615
- import { View, logger, type ILogger } from "@helfy/helfy";
616
-
617
- @View
618
- class TodoInput {
619
- @logger() private log!: ILogger; // auto: <TodoInput>
620
-
621
- onAttached() {
622
- this.log.debug("todo input attached");
623
- this.log.info("todo added", { title: this.title });
624
- this.log.warn("validation failed", { error: result.error });
625
- }
626
- }
627
- ```
628
-
629
- **Варианты:**
630
- - `@logger()` — имя класса подставляется на compile-time как `<ClassName>`, формат и цвет зависят от типа класса
631
- - `@logger("my-tag")` — кастомный тег, серый цвет
632
-
633
- ### Формат и цвет тегов по типу класса
634
-
635
- | Тип | Формат | Цвет |
636
- |-----|--------|------|
637
- | View (компоненты) | `<TodoInput>` | skyblue |
638
- | Injectable (сервисы) | `TodoValidateService()` | pink |
639
- | Context | `{TodoContext}` | khaki |
640
- | Form | `[LoginFormContext]` | bright blue |
641
- | Store | `TodoStore[]` | lime green |
642
- | Кастомный `@logger("...")` | как указано | gray |
643
-
644
- ### API логгера
645
-
646
- ```typescript
647
- interface ILogger {
648
- debug(message: string, meta?: Record<string, unknown>): void;
649
- info(message: string, meta?: Record<string, unknown>): void;
650
- warn(message: string, meta?: Record<string, unknown>): void;
651
- error(messageOrError: string | Error, meta?: Record<string, unknown>): void;
652
- withContext(ctx: string): ILogger; // дочерний логгер с доп. префиксом
653
- }
654
- ```
655
-
656
- ### DI и транспорты
657
-
658
- `LoggerService` регистрируется под `ILoggerToken` при `registerAllServices`. Можно подключить кастомные транспорты (Sentry, файл и т.д.), реализующие `ILogTransport`:
659
-
660
- ```typescript
661
- import { LoggerService, ConsoleTransport, ILoggerToken } from "@helfy/helfy";
662
-
663
- const transports = [new ConsoleTransport(), /* mySentryTransport */];
664
- createApp({ root })
665
- .configureContainer((c) => {
666
- c.bind(ILoggerToken, { useClass: LoggerService, deps: [transports] });
667
- })
668
- .mount(App);
669
- ```
670
-
671
- Без зарегистрированного логгера в контейнере используется fallback с выводом в `console`.
672
-
673
- ---
674
-
675
- ## Context & DI
676
-
677
- Helfy поддерживает иерархический Context и Dependency Injection: провайдер оборачивает поддерево в JSX, а дочерние компоненты получают значения через `@ctx`. Это удобно для темы, форм, роутинга и других общих зависимостей.
678
-
679
- ### Провайдер (@Context, @provide)
680
-
681
- Класс с декоратором `@Context` — не-рендерящий провайдер: он не имеет `render()` и рендерит только своих детей. Поля с `@provide` становятся доступны потребителям ниже по дереву.
682
-
683
- ```tsx
684
- import { Context, provide } from "@helfy/helfy";
685
-
686
- export type TThemeMode = "light" | "dark";
687
-
688
- @Context
689
- export class ThemeContext {
690
- @provide({ reactive: true })
691
- mode: TThemeMode = "dark";
692
-
693
- @provide()
694
- toggle = () => {
695
- this.mode = this.mode === "dark" ? "light" : "dark";
696
- };
697
- }
698
- ```
699
-
700
- Использование в JSX — обёртка вокруг поддерева:
701
-
702
- ```tsx
703
- <ThemeContext>
704
- <Header />
705
- <Main />
706
- </ThemeContext>
707
- ```
708
-
709
- Фреймворк рендерит только детей `ThemeContext`; сам провайдер не создаёт DOM-узлов.
710
-
711
- ### Потребитель (@ctx)
712
-
713
- Компонент с `@View` может получать значение контекста через `@ctx`:
714
-
715
- ```tsx
716
- import { View, ctx } from "@helfy/helfy";
717
- import { ThemeContext } from "./ThemeContext";
718
-
719
- @View
720
- class ThemeToggle {
721
- @ctx(ThemeContext)
722
- private theme!: ThemeContext;
723
-
724
- render() {
725
- return (
726
- <button onclick={this.theme.toggle}>
727
- {this.theme.mode === "dark" ? "Светлая тема" : "Тёмная тема"}
728
- </button>
729
- );
730
- }
731
- }
732
- ```
733
-
734
- Можно инжектировать только поле контекста:
735
-
736
- ```tsx
737
- @View
738
- class ModeDisplay {
739
- @ctx(ThemeContext, "mode")
740
- private mode!: TThemeMode;
741
-
742
- render() {
743
- return <span>Текущая тема: {this.mode}</span>;
744
- }
745
- }
746
- ```
747
-
748
- Поиск провайдера идёт вверх по дереву (`_parentView`); используется ближайший провайдер с нужным ключом.
749
-
750
- ### Реактивные поля
751
-
752
- `@provide({ reactive: true })` делает поле реактивным: при изменении все потребители перерендериваются (под капотом используется `subscribe`). Для методов достаточно `@provide()` без `reactive`.
753
-
754
- ### Опциональная инжекция
755
-
756
- Третий аргумент `@ctx` — опции `{ optional?, defaultValue? }`:
757
-
758
- ```tsx
759
- @ctx(ThemeContext, { optional: true })
760
- private theme?: ThemeContext;
761
-
762
- @ctx(ThemeContext, "mode", { defaultValue: "light" })
763
- private mode = "light";
764
- ```
765
-
766
- Без провайдера в дереве `optional: true` даёт `undefined`; `defaultValue` используется, когда провайдера нет.
767
-
768
- ### DI Container (конструкторный инжект)
769
-
770
- Helfy поддерживает DI-контейнер для глобальных сервисов с **compile-time** магией: сервисы помечаются `@Injectable<IX>()`, потребители указывают только тип в конструкторе — без `@inject` и явных токенов.
771
-
772
- **Конвенция:** последний параметр конструктора — props от родителя; предшествующие параметры резолвятся из контейнера или из @Context по дереву.
773
-
774
- **Сервис** — обязательно `@Injectable<IX>()` (интерфейс в generic):
775
-
776
- ```typescript
777
- export interface ITodoValidateService {
778
- validate(title: string): ValidationResult;
779
- }
780
-
781
- @Injectable<ITodoValidateService>()
782
- export class TodoValidateService implements ITodoValidateService {
783
- validate(title: string) { ... }
784
- }
785
- ```
786
-
787
- **Потребитель** — только тип в конструкторе (плагин подставляет токен):
788
-
789
- ```tsx
790
- @View
791
- class TodoInput {
792
- constructor(
793
- private readonly validateService: ITodoValidateService,
794
- readonly props: Props
795
- ) {}
796
- }
797
- ```
798
-
799
- **Bootstrap** — регистрация DI подключается автоматически при `createApp` (плагин подставляет `.useDI(registerAllServices)` в цепочку):
800
-
801
- ```typescript
802
- createApp({ root: document.getElementById("root")! })
803
- .router({ routes })
804
- .mount(App);
805
- ```
806
-
807
- Сканер вызывается Babel-плагином перед трансформацией, находит все `@Injectable<IX>()`, генерирует `.helfy/di-tokens.ts` и `.helfy/di-registry.ts`.
808
-
809
- **Fallback** — для ручной настройки доступны `.configureContainer()`, `@inject(token)`, `@Injectable(token)`:
810
-
811
- ```typescript
812
- createApp({ root })
813
- .configureContainer((c) => {
814
- c.bind(HttpClient, { useClass: AxiosHttpClient });
815
- c.autoRegister(SomeService);
816
- })
817
- .router({ routes })
818
- .mount(App);
819
- ```
820
-
821
- Опционально: передать свой контейнер через `.container(myContainer)` или `createApp({ root, container })`.
822
-
823
- `@Context` и контейнер сосуществуют: Context — для tree-scoped значений (тема, роутер), Container — для глобальных сервисов.
824
-
825
- ---
826
-
827
- ## JSX
828
-
829
- Helfy использует кастомный JSX runtime (`@helfy/helfy/jsx-runtime`). JSX транслируется в вызовы `jsx()` / `jsxs()`, которые строят виртуальное представление DOM.
830
-
831
- ### Атрибуты
832
-
833
- Стандартные HTML-атрибуты передаются напрямую:
834
-
835
- ```tsx
836
- <input type="text" placeholder="Введите имя" />
837
- <img src="/logo.png" alt="Logo" />
838
- <div id="container" class="wrapper"></div>
839
- ```
840
-
841
- ### События
842
-
843
- Обработчики событий передаются через атрибуты в **нижнем регистре** (`onclick`, `oninput`, а не `onClick`):
844
-
845
- ```tsx
846
- <button onclick={() => this.increment()}>+</button>
847
- <input oninput={(e) => this.handleInput(e)} />
848
- ```
849
-
850
- ### Стили
851
-
852
- Инлайн-стили передаются объектом с camelCase-ключами:
853
-
854
- ```tsx
855
- <div style={{
856
- backgroundColor: '#f0f0f0',
857
- fontSize: '16px',
858
- padding: '8px 16px',
859
- borderRadius: '4px'
860
- }}>
861
- Стилизованный блок
862
- </div>
863
- ```
864
-
865
- ### CSS-классы
866
-
867
- Атрибут `class` принимает строку или массив с условными классами:
868
-
869
- ```tsx
870
- // строка
871
- <div class="container">...</div>
872
-
873
- // CSS Modules
874
- import styles from './App.module.css';
875
- <div class={styles.wrapper}>...</div>
876
-
877
- // условные классы через массив
878
- <div class={[
879
- styles.cell,
880
- [styles.active, this.isActive], // применяется если this.isActive === true
881
- [styles.disabled, this.isDisabled],
882
- ]}>...</div>
883
- ```
884
-
885
- ---
886
-
887
- ## API
888
-
889
- ### Декораторы
890
-
891
- | Декоратор | Область | Описание |
892
- |-----------|---------|----------|
893
- | `@View` | Класс | Превращает класс в компонент с `render()`, `view`, `update()` |
894
- | `@state` | Поле | Локальное состояние компонента на базе signals (запись обновляет только этот компонент) |
895
- | `@Store` | Класс | Добавляет `subscribe`/`unsubscribe` и реактивность `@observable`-полей |
896
- | `@observable` | Поле | Делает поле store реактивным -- запись уведомляет подписчиков |
897
- | `@observe(store, field)` | Поле | Связывает поле компонента с полем store |
898
- | `@Context` | Класс | Не-рендерящий провайдер контекста; рендерит только детей |
899
- | `@provide()` / `@provide({ reactive: true })` | Поле | Помечает поле как доступное для `@ctx` (reactive — потребители перерендериваются) |
900
- | `@ctx(ContextClass)` / `@ctx(ContextClass, field)` | Поле | Инжектирует контекст или поле контекста из ближайшего провайдера вверх по дереву |
901
- | `@Injectable<IX>()` / `@Injectable<IX>('singleton' \| 'transient' \| 'scoped')` / `@Injectable(token)` | Класс | Помечает класс как injectable. Scope в (): `'singleton'` (по умолчанию), `'transient'`, `'scoped'`. Fallback: `@Injectable(token)` с Symbol/строкой |
902
- | `@inject(token)` | Параметр конструктора | Задаёт токен для параметра (fallback; при `@Injectable<IX>()` плагин подставляет токен автоматически) |
903
- | `@logger()` / `@logger("tag")` | Поле | Инжектирует ILogger. Без аргумента — compile-time имя класса и цвет по типу (View/Context/Store/Injectable). С аргументом — кастомный тег (серый) |
904
- | `@ref` | Поле | Помечает поле для получения ссылки на DOM или компонент (использовать с `@ref(this.имяПоля)` в JSX) |
905
- | `@binded(name)` | Поле | Связывает поле с биндингом `@bind:name` от родителя (для кастомных компонентов) |
906
- | `@bind(expr)` / `@bind:name(expr)` | JSX | Двустороннее связывание с `@state` полем (value/checked + oninput/onchange). Для компонентов: `@bind:value(expr)` |
907
- | `@Form` | Класс | Контекст формы с полями `@field` |
908
- | `@field(options)` | Поле FormContext | Создаёт FieldState для поля формы (value, isTouched, error, isDirty) |
909
- | `@useForm(FormContext)` | Поле | Инжектирует форму в компонент с подпиской на изменения полей |
910
- | `@field(expr)` | JSX | Подключает инпут к FieldState: value/checked + onblur + error class + aria-invalid |
911
-
912
- ### Экспорты пакета
913
-
914
- ```typescript
915
- import {
916
- $, // утилиты рендеринга ($._if, $._ifelse, $._forin, $._slot, $._callSlot)
917
- View, // декоратор @View
918
- state, // декоратор @state (signal-backed локальное состояние)
919
- Store, // декоратор @Store
920
- observable, // декоратор @observable
921
- observe, // декоратор @observe
922
- computed, // @computed для Store/@Context/@View (getter-based)
923
- effect, // @effect для реактивных сайд-эффектов во View
924
- Context, // декоратор @Context
925
- provide, // декоратор @provide
926
- ctx, // декоратор @ctx (поле, Context)
927
- Container, // DI-контейнер
928
- Injectable, // декоратор @Injectable
929
- inject, // декоратор @inject (параметр конструктора)
930
- createViewInstance,
931
- setRootContainer,
932
- getRootContainer,
933
- ref, // декоратор @ref
934
- binded, // декоратор @binded (для компонентов с @bind:value)
935
- Form, // декоратор @Form (контекст формы)
936
- field, // декоратор @field (поле FormContext)
937
- useForm, // декоратор @useForm (инжект формы)
938
- type FieldState,
939
- type FieldOptions,
940
- StoreBase, // базовый класс Store (для расширения)
941
- Subscriber, // тип callback-подписчика
942
- Router, // ядро роутера (Router)
943
- RouterContext, // контекст роутера для дерева компонентов
944
- RouterView, // компонент, рендерящий текущий маршрут
945
- Link, // SPA-ссылка
946
- DefaultNotFound, // fallback-компонент 404 (используется RouterView при отсутствии слота)
947
- path, // @path() — декоратор для pathname
948
- search, // @search() — декоратор для query-параметров
949
- params, // @params() — декоратор для route-параметров
950
- router, // @router() — декоратор для API роутера
951
- logger, // @logger() — декоратор для ILogger
952
- LoggerService, // реализация ILogger (DI)
953
- ConsoleTransport,
954
- ILoggerToken,
955
- // primitives для fine-grained reactivity
956
- createSignal,
957
- createEffect,
958
- createComputed,
959
- batch,
960
- onCleanup,
961
- type Signal,
962
- type SignalGetter,
963
- type SignalSetter,
964
- type EffectDisposer,
965
- type RouteConfig,
966
- type RouterLocation,
967
- type ILogger,
968
- type ILogTransport,
969
- type LogLevel,
970
- } from "@helfy/helfy";
971
- ```
972
-
973
- ### Subpath-экспорты
974
-
975
- | Путь | Назначение |
976
- |------|------------|
977
- | `helfy` | Основной API (декораторы, $, типы) |
978
- | `@helfy/helfy/router` | Тот же API роутера, но отдельным entry (для tree-shaking) |
979
- | `@helfy/helfy/jsx-runtime` | JSX runtime (`jsx`, `jsxs`) |
980
- | `@helfy/helfy/compiler/helfy-loader` | Webpack-loader для директив `@if`/`@for`/`@ref`/`@bind`/`@field` |
981
- | `@helfy/helfy/babel-preset` | Babel-пресет (JSX, TypeScript, декораторы) |
982
-
983
- ### Роутинг (SPA)
984
-
985
- Helfy включает лёгкий SPA‑роутер, построенный на `Context & DI`:
986
-
987
- ```tsx
988
- import {
989
- View,
990
- RouterContext,
991
- RouterView,
992
- Link,
993
- type RouteConfig,
994
- type RouterLocation,
995
- ctx,
996
- } from "@helfy/helfy";
997
-
998
- // Конфиг маршрутов
999
- const routes: RouteConfig[] = [
1000
- { path: "/", component: HomePage },
1001
- { path: "/analytics", component: AnalyticsPage },
1002
- { path: "/settings", component: SettingsPage },
1003
- { path: "/debug/:id", component: DebugPage },
1004
- ];
1005
-
1006
- @View
1007
- class App {
1008
- render() {
1009
- return (
1010
- <RouterContext routes={routes}>
1011
- <Layout>
1012
- @slot.sidebar() {<Sidebar />}
1013
- @slot.content() {<RouterView />}
1014
- </Layout>
1015
- </RouterContext>
1016
- );
1017
- }
1018
- }
1019
-
1020
- @View
1021
- class Sidebar {
1022
- @ctx(RouterContext, "location")
1023
- private location!: RouterLocation;
1024
-
1025
- render() {
1026
- const pathname = this.location.pathname;
1027
- const isHome = pathname === "/";
1028
- const isAnalytics = pathname.startsWith("/analytics");
1029
-
1030
- return (
1031
- <nav>
1032
- <Link
1033
- to="/"
1034
- label="Главная"
1035
- class={isHome ? "font-bold" : ""}
1036
- />
1037
- <Link
1038
- to="/analytics"
1039
- label="Аналитика"
1040
- class={isAnalytics ? "font-bold" : ""}
1041
- />
1042
- </nav>
1043
- );
1044
- }
1045
- }
1046
- ```
1047
-
1048
- Типичный компонент‑страница может использовать утилитарные декораторы роутера:
1049
-
1050
- ```tsx
1051
- import { View, path, search, params, router, type RouterAPI } from "@helfy/helfy";
1052
-
1053
- @View
1054
- class DebugPage {
1055
- @path()
1056
- private pathname!: string;
1057
-
1058
- @params()
1059
- private routeParams!: Record<string, string>;
1060
-
1061
- @search()
1062
- private query!: Record<string, string>;
1063
-
1064
- @router()
1065
- private rtr!: RouterAPI;
1066
-
1067
- render() {
1068
- return (
1069
- <section>
1070
- <h1>Debug</h1>
1071
- <pre>pathname: {this.pathname}</pre>
1072
- <pre>params: {JSON.stringify(this.routeParams)}</pre>
1073
- <pre>query: {JSON.stringify(this.query)}</pre>
1074
- <button onclick={() => this.rtr.push("/analytics")}>
1075
- Перейти в аналитику
1076
- </button>
1077
- </section>
1078
- );
1079
- }
1080
- }
1081
- ```
1082
-
1083
- Под капотом:
1084
-
1085
- - `Router` использует `history.pushState/replaceState` и `popstate` для синхронизации с URL.
1086
- - `RouterContext` хранит реактивное `location` (`@provide({ reactive: true })`), так что `@ctx(RouterContext, "location")` и декораторы `@path/@search/@params/@router` автоматически триггерят перерендер компонентов при смене маршрута.
1087
- - `RouterView` сопоставляет текущий `pathname` с `RouteConfig` (в том числе с параметрами `:id` и вложенными маршрутами) и рендерит нужный `@View`‑класс. При отсутствии совпадения показывается `DefaultNotFound` или содержимое слота `@slot.notFound`.
1088
- - `Link` перехватывает `click`, не перезагружает страницу и вызывает `router.push/replace`, сохраняя поведение обычных ссылок (`href` остаётся).
1089
-
1090
- **Кастомная страница 404.** Оберните `RouterView` и переопределите слот `notFound`:
1091
-
1092
- ```tsx
1093
- @View
1094
- class AppRouter {
1095
- render() {
1096
- return (
1097
- <RouterView>
1098
- @slot.notFound({ pathname }) {
1099
- <MyNotFound pathname={pathname} />
1100
- }
1101
- </RouterView>
1102
- );
1103
- }
1104
- }
1105
- ```
1106
-
1107
- ### Жизненный цикл компонента
1108
-
1109
- 1. `constructor(props)` -- создание экземпляра, `this.props` оборачивается в реактивный Proxy
1110
- 2. `render()` -- возврат JSX (**вызывается один раз** при монтировании; Babel-плагин оборачивает выражения в `createReactiveChild` для fine-grained обновлений)
1111
- 3. `mount()` -- первичный рендер JSX в DOM-фрагмент
1112
- 4. `onMount()` -- хук после первого монтирования (опциональный)
1113
- 5. `onAttached()` -- хук после вставки в document (опциональный). Вызывается при `attach(parent)`.
1114
- 6. `update()` -- структурное обновление (при смене корневого компонента, например при роутинге). Для одного и того же дочернего компонента обновляются только signals через `updateProps`
1115
- 7. `updateProps(newProps)` -- обновление prop-signals через `batch()` (fine-grained, без полного перерендера)
1116
-
1117
- #### onMount vs onAttached
1118
-
1119
- | | `onMount()` | `onAttached()` |
1120
- |---|---|---|
1121
- | **Когда** | Сразу после `mount()`, дерево построено, рефы назначены | После `attach(parent)`, элемент в document |
1122
- | **Элемент в document** | Может быть ещё нет (корень в fragment) | Да |
1123
- | **Рефы** | Доступны | Доступны |
1124
-
1125
- **`onMount()`** — для инициализации, не требующей document:
1126
- - Подписки на store/observable/сервисы
1127
- - Настройка внутреннего состояния
1128
- - Добавление обработчиков (работают и на detached-узлах)
1129
-
1130
- **`onAttached()`** — для операций, требующих элемент в document:
1131
- - `focus()`, `scrollIntoView()`
1132
- - `getBoundingClientRect()`, замер layout
1133
- - Любые DOM API, работающие только с подключённым узлом
1134
-
1135
- ```tsx
1136
- @View
1137
- class SearchInput {
1138
- @ref private input!: HTMLInputElement;
1139
-
1140
- onMount() {
1141
- this.store.subscribe(this.handleChange); // подписка — document не нужен
1142
- }
1143
-
1144
- onAttached() {
1145
- this.input.focus(); // фокус — нужен document
1146
- }
1147
- }
1148
- ```
1149
-
1150
- ### Утилиты рендеринга ($)
1151
-
1152
- Компилятор трансформирует директивы в вызовы `$`, но их можно использовать и напрямую.
1153
-
1154
- **Статические** (условие вычисляется один раз):
1155
-
1156
- ```tsx
1157
- import { $ } from "@helfy/helfy";
1158
-
1159
- render() {
1160
- return (
1161
- <div>
1162
- {$._if(this.visible, <span>Видно</span>)}
1163
- {$._ifelse(this.count > 0,
1164
- <span>Положительное</span>,
1165
- <span>Отрицательное или ноль</span>
1166
- )}
1167
- {$._forin(this.items, (item, index) => (
1168
- <div $_key={item.id}>{index}: {item.name}</div>
1169
- ))}
1170
- </div>
1171
- );
1172
- }
1173
- ```
1174
-
1175
- **Реактивные** (условие и ветки — thunks, пересчитываются при изменении signals):
1176
-
1177
- ```tsx
1178
- {$._rIf(() => this.visible, () => <span>Видно</span>)}
1179
- {$._rIfElse(() => this.count > 0,
1180
- () => <span>Положительное</span>,
1181
- () => <span>Отрицательное или ноль</span>
1182
- )}
1183
- {$._rForin(() => this.items, (item, index) => (
1184
- <div $_key={item.id}>{index}: {item.name}</div>
1185
- ))}
1186
- ```
1187
-
1188
- Компилятор автоматически выбирает реактивный вариант (`_rIf`, `_rIfElse`, `_rForin`) при генерации кода из директив `@if` / `@for`.
1189
-
1190
- ### Слоты (content projection)
1191
-
1192
- Helfy поддерживает именованные слоты с fallback‑содержимым и переопределением в дочерних компонентах.
1193
-
1194
- #### Провайдер слота (`@View`‑компонент)
1195
-
1196
- Слоты объявляются прямо в JSX через директиву `@slot:имя(...)` внутри `render()`:
1197
-
1198
- ```tsx
1199
- import { View } from "@helfy/helfy";
1200
-
1201
- @View
1202
- class AppLayout {
1203
- render() {
1204
- return (
1205
- <section class="layout">
1206
- {/* Именованный слот header с fallback-разметкой */}
1207
- @slot:header({ title: "Список задач" }) fallback {
1208
- <header class="mb-4">
1209
- <h1 class="text-2xl font-bold text-gray-900">
1210
- Список задач
1211
- </h1>
1212
- </header>
1213
- }
1214
-
1215
- {/* Именованный слот content с fallback и директивой @if внутри */}
1216
- @slot:content({ store: this.props.store, filtered: this.props.filtered, hasTodos: this.props.hasTodos }) fallback {
1217
- @if (this.props.hasTodos) {
1218
- <section class="pt-3 border-t border-gray-200 text-sm text-gray-600">
1219
- <p class="mb-1">
1220
- Всего задач: {this.props.store.todos.length}, активных: {this.props.store.activeCount},
1221
- завершённых: {this.props.store.completedCount}
1222
- </p>
1223
- <p>Фильтрованных к показу: {this.props.filtered.length}</p>
1224
- </section>
1225
- }
1226
- }
1227
- </section>
1228
- );
1229
- }
1230
- }
1231
- ```
1232
-
1233
- Правила для провайдера:
1234
-
1235
- - `@slot:header({ ... })` — объявляет именованный слот `header` и вызывает его.
1236
- - Блок `fallback { ... }` (опциональный) задаёт разметку по умолчанию, если слот не переопределён.
1237
- - Внутри `fallback` можно использовать директивы `@if`, `@for` и обычный JSX.
1238
-
1239
- #### Потребитель слота (override в JSX)
1240
-
1241
- Переопределение слота в дочернем компоненте делается через `@slot.имя(...) { ... }` внутри JSX‑детей:
1242
-
1243
- ```tsx
1244
- @View
1245
- class TodoApp {
1246
- render() {
1247
- const store = this.store;
1248
- const filtered = this.filteredTodos;
1249
-
1250
- return (
1251
- <AppLayout
1252
- user={store.user}
1253
- store={store}
1254
- filtered={filtered}
1255
- hasTodos={this.hasTodos}
1256
- >
1257
- {/* Переопределяем слот header */}
1258
- @slot.header({ title }) {
1259
- <header class="mb-5">
1260
- <h1 class="mb-4 text-2xl font-bold text-gray-900">
1261
- Задачи ({title})
1262
- </h1>
1263
- <TodoInput
1264
- placeholder="Добавить задачу…"
1265
- onSubmit={(text) => store.add(text)}
1266
- />
1267
- </header>
1268
- }
1269
-
1270
- {/* Переопределяем слот content, внутри можно использовать @if/@for */}
1271
- @slot.content({ store, filtered, hasTodos }) {
1272
- @if (hasTodos) {
1273
- <section class="pt-3 border-t border-gray-200">
1274
- <TodoList
1275
- todos={filtered}
1276
- onToggle={(id) => store.toggle(id)}
1277
- onRemove={(id) => store.remove(id)}
1278
- />
1279
- <TodoFooter
1280
- filter={this.filter}
1281
- activeCount={store.activeCount}
1282
- completedCount={store.completedCount}
1283
- summary={store.filterSummary}
1284
- onFilter={(f) => store.setFilter(f)}
1285
- onClearCompleted={() => store.clearCompleted()}
1286
- />
1287
- </section>
1288
- }
1289
- }
1290
- </AppLayout>
1291
- );
1292
- }
1293
- }
1294
- ```
1295
-
1296
- Кратко по синтаксису:
1297
-
1298
- - `@slot:имя({ props }) fallback { FallbackJSX }` — объявление и вызов слота в провайдере.
1299
- - `@slot.имя({ ctx }) { OverrideJSX }` — переопределение слота в потребителе.
1300
- - Внутри `FallbackJSX` и `OverrideJSX` поддерживаются все директивы (`@if`, `@for`, `@empty`) и обычный JSX.