@helfy/helfy 0.0.8 → 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 +264 -109
- 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
|
@@ -21,16 +21,20 @@ Components are defined as classes with the `@View` decorator, state is managed v
|
|
|
21
21
|
- [@for](#for)
|
|
22
22
|
- [@empty](#empty)
|
|
23
23
|
- [State Management](#state-management)
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
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)
|
|
27
30
|
- [Logging (@logger)](#logging-logger)
|
|
28
31
|
- [Context & DI](#context--di)
|
|
29
|
-
- [Provider (@Context
|
|
30
|
-
- [Consumer (@
|
|
32
|
+
- [Provider (@Context)](#provider-context)
|
|
33
|
+
- [Consumer (@useCtx)](#consumer-usectx)
|
|
31
34
|
- [Reactive fields](#reactive-fields)
|
|
32
35
|
- [Optional injection](#optional-injection)
|
|
33
|
-
- [DI
|
|
36
|
+
- [DI: props vs dependencies](#di-props-vs-dependencies)
|
|
37
|
+
- [Global services (@inject)](#global-services-inject)
|
|
34
38
|
- [JSX](#jsx)
|
|
35
39
|
- [Attributes](#attributes)
|
|
36
40
|
- [Events](#events)
|
|
@@ -78,7 +82,7 @@ Or in `package.json`:
|
|
|
78
82
|
|
|
79
83
|
### Build setup
|
|
80
84
|
|
|
81
|
-
The Babel plugin runs the DI scanner before transformation (no pre-scripts in `package.json` needed). The
|
|
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
|
|
|
@@ -90,9 +94,9 @@ A single preset in `.babelrc` is enough:
|
|
|
90
94
|
}
|
|
91
95
|
```
|
|
92
96
|
|
|
93
|
-
The preset includes: JSX runtime, TypeScript, legacy decorators, class properties, babel-plugin-transform-typescript-metadata, **helfy-di** (compile-time DI for `@Injectable<IX>()`, auto-registration at `createApp`, `@logger()` → `@logger("<ClassName>")` transformation).
|
|
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
|
|
|
@@ -106,6 +110,8 @@ To process directives (`@if`, `@for`, `@ref`, `@bind`, `@field`) in `.tsx` files
|
|
|
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:
|
|
@@ -152,7 +158,7 @@ The `@View` decorator automatically:
|
|
|
152
158
|
- wraps `this.props` in a reactive Proxy (each prop is a signal)
|
|
153
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
|
-
> **Important:** When inheriting from `@View`, apply the decorator on the child class as well. Otherwise `@
|
|
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
|
|
|
@@ -450,7 +456,7 @@ export class LoginForm {
|
|
|
450
456
|
}
|
|
451
457
|
```
|
|
452
458
|
|
|
453
|
-
Wrapper components (TextField, CheckboxField, etc.) accept
|
|
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.
|
|
454
460
|
|
|
455
461
|
**JSX directive `@field(expr)`** — one directive replaces `@bind` + `onblur` + error `class` + `aria-invalid`. The compiler generates:
|
|
456
462
|
|
|
@@ -555,105 +561,212 @@ The `@empty` block after `@for` renders when the array is empty:
|
|
|
555
561
|
|
|
556
562
|
## State Management
|
|
557
563
|
|
|
558
|
-
|
|
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.
|
|
559
577
|
|
|
560
|
-
|
|
578
|
+
### Creating a Store (@Store)
|
|
579
|
+
|
|
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
|
|
561
585
|
|
|
562
586
|
```typescript
|
|
563
|
-
import { Store,
|
|
587
|
+
import { Store, state, computed } from "@helfy/helfy";
|
|
564
588
|
|
|
565
589
|
@Store
|
|
566
|
-
class
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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;
|
|
573
599
|
}
|
|
600
|
+
}
|
|
574
601
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
}
|
|
602
|
+
add(title: string) {
|
|
603
|
+
this.todos = [...this.todos, { id: crypto.randomUUID(), title, completed: false }];
|
|
604
|
+
}
|
|
580
605
|
|
|
581
|
-
|
|
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
|
+
}
|
|
582
616
|
```
|
|
583
617
|
|
|
584
|
-
|
|
585
|
-
- `subscribe(field, callback)` — subscribe to field changes, returns unsubscribe function
|
|
586
|
-
- `unsubscribe(field, callback)` — unsubscribe
|
|
618
|
+
Fields use `@state` or `@observable` — both create signals.
|
|
587
619
|
|
|
588
|
-
|
|
620
|
+
### Infrastructure services (@Service)
|
|
589
621
|
|
|
590
|
-
|
|
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
|
|
591
627
|
|
|
592
|
-
|
|
628
|
+
```typescript
|
|
629
|
+
import { Service } from "@helfy/helfy";
|
|
593
630
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
import UserStore from "./UserStore";
|
|
631
|
+
export interface ITodoValidateService {
|
|
632
|
+
validateTitle(title: string): ValidationResult;
|
|
633
|
+
}
|
|
598
634
|
|
|
599
|
-
@
|
|
600
|
-
class
|
|
601
|
-
|
|
602
|
-
|
|
635
|
+
@Service<ITodoValidateService>()
|
|
636
|
+
export class TodoValidateService implements ITodoValidateService {
|
|
637
|
+
validateTitle(title: string) {
|
|
638
|
+
// ...
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
603
642
|
|
|
604
|
-
|
|
605
|
-
private isLoggedIn: UserStore['isLoggedIn'];
|
|
643
|
+
### Business use cases (@UseCase)
|
|
606
644
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
+
}
|
|
618
669
|
}
|
|
619
670
|
```
|
|
620
671
|
|
|
621
|
-
|
|
672
|
+
### HttpClient and ApiClient
|
|
622
673
|
|
|
623
|
-
|
|
674
|
+
Helfy provides an HTTP client and a Query/Mutation layer (similar to TanStack Query) for API data fetching.
|
|
675
|
+
|
|
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
|
+
```
|
|
624
689
|
|
|
625
|
-
|
|
690
|
+
**Define API interface and implementation** with `@ApiClient` and `@queryConfig`:
|
|
626
691
|
|
|
627
692
|
```typescript
|
|
628
|
-
import
|
|
629
|
-
|
|
693
|
+
import { ApiClient, queryConfig, QueryBuilder, type Query, type HttpClient } from "@helfy/helfy";
|
|
694
|
+
|
|
695
|
+
export interface TodoApi {
|
|
696
|
+
todos(): Query<Todo[]>;
|
|
697
|
+
}
|
|
698
|
+
|
|
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
|
+
```
|
|
630
712
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
+
}
|
|
634
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>;
|
|
635
735
|
|
|
636
|
-
|
|
736
|
+
async handleSubmit() {
|
|
737
|
+
await this.createTodo.mutateAsync({ title: this.title });
|
|
738
|
+
}
|
|
637
739
|
```
|
|
638
740
|
|
|
639
|
-
|
|
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:
|
|
640
744
|
|
|
641
745
|
```tsx
|
|
642
|
-
import
|
|
746
|
+
import { View, inject } from "@helfy/helfy";
|
|
643
747
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
private
|
|
748
|
+
@View
|
|
749
|
+
class TodoList {
|
|
750
|
+
@inject<ITodoStore>() private store!: ITodoStore;
|
|
751
|
+
@inject<ICreateTodoUseCase>() private createTodo!: ICreateTodoUseCase;
|
|
647
752
|
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
}
|
|
650
763
|
```
|
|
651
764
|
|
|
652
765
|
---
|
|
653
766
|
|
|
654
767
|
## Logging (@logger)
|
|
655
768
|
|
|
656
|
-
The `@logger` decorator injects a logger into View, Context, Store, and
|
|
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.
|
|
657
770
|
|
|
658
771
|
### Usage
|
|
659
772
|
|
|
@@ -681,7 +794,8 @@ class TodoInput {
|
|
|
681
794
|
| Type | Format | Color |
|
|
682
795
|
|------|--------|-------|
|
|
683
796
|
| View (components) | `<TodoInput>` | skyblue |
|
|
684
|
-
| Injectable
|
|
797
|
+
| Service / Injectable | `TodoValidateService()` | pink |
|
|
798
|
+
| UseCase | `CreateTodoUseCase()` | pink |
|
|
685
799
|
| Context | `{TodoContext}` | khaki |
|
|
686
800
|
| Form | `[LoginFormContext]` | bright blue |
|
|
687
801
|
| Store | `TodoStore[]` | lime green |
|
|
@@ -720,23 +834,24 @@ Without a registered logger in the container, a fallback that logs to `console`
|
|
|
720
834
|
|
|
721
835
|
## Context & DI
|
|
722
836
|
|
|
723
|
-
Helfy supports hierarchical Context and Dependency Injection: a provider wraps a subtree in JSX, and child components receive values via `@
|
|
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)
|
|
724
840
|
|
|
725
|
-
|
|
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.
|
|
726
842
|
|
|
727
|
-
|
|
843
|
+
**Constructor:** receives only props from JSX. Define your own props interface (same as View). Use `@inject` for container dependencies.
|
|
728
844
|
|
|
729
845
|
```tsx
|
|
730
|
-
import { Context,
|
|
846
|
+
import { Context, state } from "@helfy/helfy";
|
|
731
847
|
|
|
732
848
|
export type TThemeMode = "light" | "dark";
|
|
733
849
|
|
|
734
850
|
@Context
|
|
735
851
|
export class ThemeContext {
|
|
736
|
-
@
|
|
852
|
+
@state
|
|
737
853
|
mode: TThemeMode = "dark";
|
|
738
854
|
|
|
739
|
-
@provide()
|
|
740
855
|
toggle = () => {
|
|
741
856
|
this.mode = this.mode === "dark" ? "light" : "dark";
|
|
742
857
|
};
|
|
@@ -754,17 +869,31 @@ Usage in JSX — wrap a subtree:
|
|
|
754
869
|
|
|
755
870
|
The framework only renders the children of `ThemeContext`; the provider itself does not create DOM nodes.
|
|
756
871
|
|
|
757
|
-
|
|
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
|
+
```
|
|
885
|
+
|
|
886
|
+
### Consumer (@useCtx)
|
|
758
887
|
|
|
759
|
-
A component with `@View` can receive context values via `@
|
|
888
|
+
A component with `@View` can receive context values via `@useCtx`:
|
|
760
889
|
|
|
761
890
|
```tsx
|
|
762
|
-
import { View,
|
|
891
|
+
import { View, useCtx } from "@helfy/helfy";
|
|
763
892
|
import { ThemeContext } from "./ThemeContext";
|
|
764
893
|
|
|
765
894
|
@View
|
|
766
895
|
class ThemeToggle {
|
|
767
|
-
@
|
|
896
|
+
@useCtx(ThemeContext)
|
|
768
897
|
private theme!: ThemeContext;
|
|
769
898
|
|
|
770
899
|
render() {
|
|
@@ -782,7 +911,7 @@ You can inject only a context field:
|
|
|
782
911
|
```tsx
|
|
783
912
|
@View
|
|
784
913
|
class ModeDisplay {
|
|
785
|
-
@
|
|
914
|
+
@useCtx(ThemeContext, "mode")
|
|
786
915
|
private mode!: TThemeMode;
|
|
787
916
|
|
|
788
917
|
render() {
|
|
@@ -791,16 +920,26 @@ class ModeDisplay {
|
|
|
791
920
|
}
|
|
792
921
|
```
|
|
793
922
|
|
|
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
|
+
|
|
794
933
|
Provider lookup goes up the tree (`_parentView`); the nearest provider with the matching key is used.
|
|
795
934
|
|
|
796
935
|
### Reactive fields
|
|
797
936
|
|
|
798
|
-
`@
|
|
937
|
+
`@state` makes a field reactive: when it changes, all consumers re-render. Public methods are exposed automatically.
|
|
799
938
|
|
|
800
|
-
**Computed fields** — getters with `@
|
|
939
|
+
**Computed fields** — getters with `@computed` recompute when dependencies change and trigger consumer re-renders:
|
|
801
940
|
|
|
802
941
|
```tsx
|
|
803
|
-
@
|
|
942
|
+
@computed
|
|
804
943
|
get filteredTodos(): Todo[] {
|
|
805
944
|
return this.filter === "active"
|
|
806
945
|
? this.todos.filter(t => !t.completed)
|
|
@@ -810,50 +949,60 @@ get filteredTodos(): Todo[] {
|
|
|
810
949
|
|
|
811
950
|
### Optional injection
|
|
812
951
|
|
|
813
|
-
The third argument of `@
|
|
952
|
+
The third argument of `@useCtx` — options `{ optional?, defaultValue? }`:
|
|
814
953
|
|
|
815
954
|
```tsx
|
|
816
|
-
@
|
|
955
|
+
@useCtx(ThemeContext, { optional: true })
|
|
817
956
|
private theme?: ThemeContext;
|
|
818
957
|
|
|
819
|
-
@
|
|
958
|
+
@useCtx(ThemeContext, "mode", { defaultValue: "light" })
|
|
820
959
|
private mode = "light";
|
|
821
960
|
```
|
|
822
961
|
|
|
823
962
|
With no provider in the tree, `optional: true` yields `undefined`; `defaultValue` is used when the provider is absent.
|
|
824
963
|
|
|
825
|
-
### DI
|
|
964
|
+
### DI: props vs dependencies
|
|
965
|
+
|
|
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
|
|
826
969
|
|
|
827
|
-
|
|
970
|
+
**@Service and @UseCase:** constructor DI via `@diParams` (added by Babel plugin) for injected dependencies.
|
|
828
971
|
|
|
829
|
-
|
|
972
|
+
### Global services (@inject)
|
|
830
973
|
|
|
831
|
-
|
|
974
|
+
Use `@inject<IX>()` in View/Context to access Store, Service, and UseCase from the container:
|
|
832
975
|
|
|
833
976
|
```typescript
|
|
977
|
+
// Define interface and implement with @Service
|
|
834
978
|
export interface ITodoValidateService {
|
|
835
979
|
validate(title: string): ValidationResult;
|
|
836
980
|
}
|
|
837
981
|
|
|
838
|
-
@
|
|
982
|
+
@Service<ITodoValidateService>()
|
|
839
983
|
export class TodoValidateService implements ITodoValidateService {
|
|
840
984
|
validate(title: string) { ... }
|
|
841
985
|
}
|
|
842
986
|
```
|
|
843
987
|
|
|
844
|
-
**Consumer** —
|
|
988
|
+
**Consumer (View)** — inject Store or UseCase:
|
|
845
989
|
|
|
846
990
|
```tsx
|
|
847
991
|
@View
|
|
848
992
|
class TodoInput {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
) {}
|
|
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
|
+
}
|
|
853
1002
|
}
|
|
854
1003
|
```
|
|
855
1004
|
|
|
856
|
-
**Bootstrap** — DI registration is
|
|
1005
|
+
**Bootstrap** — DI registration is automatic at `createApp` (plugin injects `.useDI(registerAllServices)`):
|
|
857
1006
|
|
|
858
1007
|
```typescript
|
|
859
1008
|
createApp({ root: document.getElementById("root")! })
|
|
@@ -861,9 +1010,9 @@ createApp({ root: document.getElementById("root")! })
|
|
|
861
1010
|
.mount(App);
|
|
862
1011
|
```
|
|
863
1012
|
|
|
864
|
-
The scanner
|
|
1013
|
+
The DI scanner finds `@Store`, `@Service<IX>()`, `@UseCase<IX>()` and generates `.helfy/di-tokens.ts`, `.helfy/di-registry.ts`.
|
|
865
1014
|
|
|
866
|
-
**Fallback** — for manual setup: `.configureContainer()`, `@
|
|
1015
|
+
**Fallback** — for manual setup: `.configureContainer()`, `@Injectable(token)`:
|
|
867
1016
|
|
|
868
1017
|
```typescript
|
|
869
1018
|
createApp({ root })
|
|
@@ -949,19 +1098,25 @@ import styles from './App.module.css';
|
|
|
949
1098
|
|-----------|-------|-------------|
|
|
950
1099
|
| `@View` | Class | Turns a class into a component with `render()`, `view`, `update()` |
|
|
951
1100
|
| `@state` | Field | Component local state on signals (write updates only this component) |
|
|
952
|
-
| `@Store` | Class |
|
|
953
|
-
| `@observable` | Field |
|
|
954
|
-
| `@
|
|
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` |
|
|
955
1105
|
| `@Context` | Class | Non-rendering context provider; renders only children |
|
|
956
|
-
| `@
|
|
957
|
-
| `@
|
|
958
|
-
| `@
|
|
959
|
-
| `@
|
|
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 |
|
|
960
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) |
|
|
961
1111
|
| `@ref` | Field | Marks a field to receive a DOM or component reference (use with `@ref(this.fieldName)` in JSX) |
|
|
962
1112
|
| `@expose` | Method | Makes a method available to the parent when using `@ref` on a component (without `@expose` the parent gets the full instance) |
|
|
963
1113
|
| `@binded(name)` | Field | Binds a field to `@bind:name` from the parent (for custom components) |
|
|
964
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 |
|
|
965
1120
|
| `@Form` | Class | Form context with `@field` fields |
|
|
966
1121
|
| `@field(options)` | FormContext field | Creates FieldState for a form field (value, isTouched, error, isDirty) |
|
|
967
1122
|
| `@useForm(FormContext)` | Field | Injects the form into a component with subscription to field changes |
|
package/babel-preset.js
CHANGED
|
@@ -15,7 +15,8 @@ module.exports = (api, opts = {}) => ({
|
|
|
15
15
|
"@babel/preset-typescript",
|
|
16
16
|
],
|
|
17
17
|
plugins: [
|
|
18
|
-
|
|
18
|
+
// Plugins run before Presets — di-plugin must run before TS strips constructor param types
|
|
19
|
+
[diPlugin, opts.di || opts || {}],
|
|
19
20
|
"babel-plugin-transform-typescript-metadata",
|
|
20
21
|
["@babel/plugin-proposal-decorators", { legacy: true }],
|
|
21
22
|
["@babel/plugin-proposal-class-properties", { loose: true }],
|
|
@@ -41,6 +41,8 @@ export default class DOM {
|
|
|
41
41
|
/**
|
|
42
42
|
* Mount a reactive @if / @ifelse. Uses createEffect to reactively
|
|
43
43
|
* evaluate the condition and mount/unmount the body.
|
|
44
|
+
* Only destroys/remounts when the condition actually changes (prevents
|
|
45
|
+
* form state loss on blur/input when effect re-runs spuriously).
|
|
44
46
|
*/
|
|
45
47
|
private static mountReactiveIf;
|
|
46
48
|
/**
|
package/dist/app/createApp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from "../di/Container";
|
|
2
|
-
import type { RouterOptions } from "./types";
|
|
2
|
+
import type { HttpConfig, RouterOptions } from "./types";
|
|
3
3
|
import type { ViewInstance } from "../decorators/View.decorator";
|
|
4
4
|
export interface CreateAppOptions {
|
|
5
5
|
root: Element;
|
|
@@ -14,6 +14,8 @@ export interface AppBuilder {
|
|
|
14
14
|
configureContainer(fn: (container: Container) => void): AppBuilder;
|
|
15
15
|
/** Register services. Optional — plugin injects .useDI(registerAllServices) on createApp chains. */
|
|
16
16
|
useDI(registerAllServices?: RegisterServicesFn): AppBuilder;
|
|
17
|
+
/** Configure HTTP client (baseUrl, headers, timeout, etc.) and register HttpClient in DI. */
|
|
18
|
+
http(config: HttpConfig): AppBuilder;
|
|
17
19
|
router(options: RouterOptions): AppBuilder;
|
|
18
20
|
mount(app: new (props?: any) => any): ViewInstance;
|
|
19
21
|
}
|
package/dist/app/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createApp } from "./createApp";
|
|
2
2
|
export { HelfyApp } from "./HelfyApp";
|
|
3
3
|
export type { AppBuilder, CreateAppOptions, RegisterServicesFn } from "./createApp";
|
|
4
|
-
export type { AppConfig, RouterOptions } from "./types";
|
|
4
|
+
export type { AppConfig, HttpConfig, RouterOptions } from "./types";
|
package/dist/app/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RouteConfig } from "../router/types";
|
|
2
|
+
import type { HttpClient } from "../http/HttpClient.types";
|
|
2
3
|
export interface AppConfig {
|
|
3
4
|
root: Element;
|
|
4
5
|
routes: RouteConfig[];
|
|
@@ -7,3 +8,12 @@ export interface AppConfig {
|
|
|
7
8
|
export interface RouterOptions {
|
|
8
9
|
routes: RouteConfig[];
|
|
9
10
|
}
|
|
11
|
+
export interface HttpConfig {
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Custom HttpClient implementation (otherwise FetchHttpClient) */
|
|
16
|
+
client?: new (config: HttpConfig) => HttpClient;
|
|
17
|
+
/** QueryCache max entries for LRU eviction. Default 100 */
|
|
18
|
+
queryCacheMaxSize?: number;
|
|
19
|
+
}
|