@lankajs/angular 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lankajs contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @lankajs/angular
2
+
3
+ **▸ module** · Angular binding
4
+
5
+ > One function — `useLankaVM` — over a signal, and the access tracking core already does.
6
+
7
+ A library in the same box. The app imports and calls it; core does not know it exists.
8
+
9
+ **Runs in:** the browser.
10
+
11
+ **Requires:** Angular. Enforced by `check-runtime.mjs`, which refuses an import of any other.
12
+
13
+ **How to use it:** [GUIDE.md](./GUIDE.md) — the user guide, with examples. **How to change it:** [SKILL.md](./SKILL.md).
14
+
15
+ ## Contents
16
+
17
+ - `useLankaVM` — the one name, and the same one every member of this shelf publishes
18
+
19
+ ## What an Angular call answers
20
+
21
+ A `Signal`: `state().rows` in a component, `{{ state().rows }}` in a template. Zoneless
22
+ works with no extra step, because a signal is what zoneless change detection reads.
23
+
24
+ ## The injection context, and why it is required rather than optional
25
+
26
+ `useLankaVM` must be called where Angular can inject — a constructor, a field
27
+ initialiser, a factory, or inside `runInInjectionContext`. It asserts that, and the
28
+ message names the fix.
29
+
30
+ That is stricter than the Vue and Solid bindings, which publish a `stop()` for a call
31
+ made outside their scope. Angular's reason is different: `DestroyRef` is the ONLY way
32
+ to know when the caller goes away, and a subscription with no way to learn that is a
33
+ leak with no owner. Where Vue and Solid degrade, Angular refuses — and refusing at the
34
+ call is better than a leak discovered in production.
35
+
36
+ ## The injection context reaches a test too
37
+
38
+ `renderWithLanka` from `@lankajs/angular/testing` renders a component with a live
39
+ framework behind it, the way every other member of this shelf does. What differs is
40
+ underneath: Angular Testing Library drives `TestBed`, so a component here is compiled
41
+ rather than merely mounted — and a ViewModel read in a field initialiser is inside an
42
+ injection context, which is the one thing this binding insists on.
43
+
44
+ ---
45
+
46
+ Repository map: [../../../README.md](../../../README.md)
@@ -0,0 +1,31 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
26
+
27
+ export {
28
+ __commonJS,
29
+ __toESM
30
+ };
31
+ //# sourceMappingURL=chunk-5WRI5ZAA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,166 @@
1
+ import { ILankaReadableVM } from 'lanka/viewmodel';
2
+ import { Signal } from '@angular/core';
3
+
4
+ /** What a subscriber hands in, and what it gets back. */
5
+ interface ILankaObserver<TValue> {
6
+ next?: (value: TValue) => void;
7
+ error?: (failure: unknown) => void;
8
+ complete?: () => void;
9
+ }
10
+ /** What unsubscribing looks like, in RxJS's own vocabulary. */
11
+ interface ILankaUnsubscribable {
12
+ unsubscribe: () => void;
13
+ }
14
+ /**
15
+ * A ViewModel as something the `async` pipe and an RxJS chain accept.
16
+ *
17
+ * `Subscribable` is the whole contract: one method, and `AsyncPipe` takes it as
18
+ * readily as an `Observable`.
19
+ */
20
+ interface ILankaObservableVM<TState extends object> {
21
+ subscribe: (observer: ILankaObserver<TState> | ((value: TState) => void)) => ILankaUnsubscribable;
22
+ }
23
+ /**
24
+ * Reads a ViewModel as a stream, for the half of Angular that speaks RxJS.
25
+ *
26
+ * ```ts
27
+ * @Component({ template: `@if (todos$ | async; as todos) { … }` })
28
+ * export class TodoScreen {
29
+ * protected readonly todos$ = toLankaObservable(todosVM);
30
+ * }
31
+ * ```
32
+ *
33
+ * ```ts
34
+ * // or in a chain, where signals cannot go
35
+ * toLankaObservable(todosVM).subscribe(({ rows }) => this.log(rows.length));
36
+ * ```
37
+ *
38
+ * ## Why this exists beside `useLankaVM` and `toLankaSignals`
39
+ *
40
+ * Angular is signals-first now and those two answer signals, which is the right
41
+ * default. It is also a framework with fifteen years of `Observable` in it: the
42
+ * `async` pipe, `HttpClient`, the router's events, every `switchMap` a codebase
43
+ * already has. A consumer with a stream in hand reaches for `combineLatest`, and
44
+ * a signal is not something they can pass to it.
45
+ *
46
+ * ## No `rxjs` import, deliberately
47
+ *
48
+ * `AsyncPipe` accepts `Subscribable<T>`, which is an INTERFACE — one method — so
49
+ * this satisfies it structurally and adds no dependency. The parity canon's
50
+ * order for an idiom is the framework's own library first, then what it already
51
+ * requires, then a few lines written here, and only then somebody else's
52
+ * package. This is the third rung, and it keeps `@lankajs/angular` importing
53
+ * nothing but `@angular/core`.
54
+ *
55
+ * A consumer who wants the operators pipes it: `from(toLankaObservable(vm))`
56
+ * takes a subscribable, and `toObservable` from `@angular/core/rxjs-interop`
57
+ * takes the signal `useLankaVM` answers.
58
+ *
59
+ * ## It emits the CURRENT state first
60
+ *
61
+ * Like a `BehaviorSubject` and like every store an Angular consumer has met: a
62
+ * subscriber gets the state it subscribed to before anything changes, because a
63
+ * template rendering `| async` would otherwise show nothing until the first
64
+ * write.
65
+ *
66
+ * ## No injection context needed
67
+ *
68
+ * Unlike `useLankaVM` and `toLankaSignals`, which take a `DestroyRef` because a
69
+ * signal has no other way to learn its reader has gone. A stream's subscriber
70
+ * holds its own unsubscribe, which is RxJS's answer to the same question — so
71
+ * this works in a service, a resolver, an interceptor and a plain function.
72
+ */
73
+ declare const toLankaObservable: <TState extends object>(viewModel: ILankaReadableVM<TState>) => ILankaObservableVM<TState>;
74
+
75
+ /**
76
+ * A ViewModel split the way an Angular service exposes state: a signal per
77
+ * value, and the actions as themselves.
78
+ *
79
+ * An action is one object for the life of the store, so wrapping it in a signal
80
+ * would make every call site write `load()()`. A value changes, so it is a
81
+ * signal; a function does not, so it is a function.
82
+ */
83
+ type TLankaSignals<TState extends object> = {
84
+ [TKey in keyof TState]: TState[TKey] extends (...args: never[]) => unknown ? TState[TKey] : Signal<TState[TKey]>;
85
+ };
86
+ /**
87
+ * Reads a ViewModel as the signals an Angular component expects.
88
+ *
89
+ * ```ts
90
+ * @Component({ template: `@if (todos.isLoading()) { … } @for (row of todos.rows(); track row) { … }` })
91
+ * export class TodoScreen {
92
+ * protected readonly todos = toLankaSignals(todosVM);
93
+ * }
94
+ * ```
95
+ *
96
+ * ## Why this exists beside `useLankaVM`
97
+ *
98
+ * `useLankaVM` answers ONE `Signal` over the whole state, which is the shape
99
+ * every other binding on the shelf parallels: `state().rows`. It is also not how
100
+ * Angular holds state. An Angular service exposes a signal per field —
101
+ * `readonly rows = signal([])` — and a template reads `rows()`, never
102
+ * `state().rows`. A consumer with that habit reaches for `todos.rows()` and finds
103
+ * a call on a plain object.
104
+ *
105
+ * ## One subscription, and each signal is a `computed` over it
106
+ *
107
+ * There is one `subscribe` on the ViewModel and one version signal behind every
108
+ * field, so a change wakes Angular once and each `computed` decides for itself
109
+ * whether its own value moved. That is Angular's own deduplication — a `computed`
110
+ * whose result is unchanged notifies nobody — arriving for free, and it is why
111
+ * this is not a second subscription per field.
112
+ *
113
+ * ## The keys are read ONCE, at the call
114
+ *
115
+ * A ViewModel declares its state up front, so the field list is fixed at the
116
+ * moment this is called. A key added to the state later has no signal here, and
117
+ * that is the price of the shape: Angular's own services name their fields too.
118
+ * `useLankaVM` is the answer for a state whose shape is genuinely dynamic.
119
+ *
120
+ * ## It needs an injection context, for the reason `useLankaVM` does
121
+ *
122
+ * `DestroyRef` is the only way to learn the caller has gone, and a subscription
123
+ * that cannot learn that is a leak with no owner.
124
+ */
125
+ declare const toLankaSignals: <TState extends object>(viewModel: ILankaReadableVM<TState>) => TLankaSignals<TState>;
126
+
127
+ /**
128
+ * Reads a ViewModel from Angular.
129
+ *
130
+ * ```ts
131
+ * @Component({ template: `<li *ngFor="let row of state().rows">{{ row }}</li>` })
132
+ * export class TodoScreen {
133
+ * protected readonly state = useLankaVM(todoVM);
134
+ * }
135
+ * ```
136
+ *
137
+ * Without a selector the signal carries a value that RECORDS which keys were
138
+ * read, and changes only when one of THOSE moves. With a selector the selector
139
+ * decides and tracking is bypassed.
140
+ *
141
+ * Zoneless needs no extra step: a signal is what zoneless change detection
142
+ * reads, so this is the shape Angular is moving towards rather than a bridge to
143
+ * it.
144
+ *
145
+ * ## Why an injection context is REQUIRED, not preferred
146
+ *
147
+ * `@lankajs/vue` and `@lankajs/solid` publish a `stop()` for a call made outside
148
+ * their framework's scope, because both can still work without one. Angular
149
+ * cannot: `DestroyRef` is the only way to learn that the caller has gone, and a
150
+ * subscription with no way to learn that is a leak with no owner.
151
+ *
152
+ * So this refuses at the call rather than leaking quietly, and the message names
153
+ * the fix. Where the other two degrade, this one stops — and a refusal a
154
+ * developer reads once beats a leak found in production.
155
+ *
156
+ * ## What this function does NOT contain
157
+ *
158
+ * The recording, the comparison and the blind-spot warning are
159
+ * `createLankaAccessTracker` in core. Every binding on this shelf calls it,
160
+ * which is what makes "a screen updates for the keys it read" a fact about lanka
161
+ * rather than a fact about Angular.
162
+ */
163
+ declare function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): Signal<TState>;
164
+ declare function useLankaVM<TState extends object, TSelected>(viewModel: ILankaReadableVM<TState>, selector: (state: TState) => TSelected): Signal<TSelected>;
165
+
166
+ export { type ILankaObservableVM, type ILankaObserver, type ILankaUnsubscribable, type TLankaSignals, toLankaObservable, toLankaSignals, useLankaVM };
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import "./chunk-5WRI5ZAA.js";
2
+
3
+ // src/to-lanka-observable/toLankaObservable.ts
4
+ import { createLankaViewSubscription } from "lanka/extend";
5
+ var toLankaObservable = (viewModel) => ({
6
+ subscribe: (observer) => {
7
+ const next = typeof observer === "function" ? observer : observer.next ?? (() => void 0);
8
+ const view = createLankaViewSubscription(viewModel, () => next(view.read()));
9
+ next(view.read());
10
+ return { unsubscribe: view.stop };
11
+ }
12
+ });
13
+
14
+ // src/to-lanka-signals/toLankaSignals.ts
15
+ import { DestroyRef, assertInInjectionContext, computed, inject, signal } from "@angular/core";
16
+ import { createLankaViewSubscription as createLankaViewSubscription2 } from "lanka/extend";
17
+ var toLankaSignals = (viewModel) => {
18
+ assertInInjectionContext(toLankaSignals);
19
+ const version = signal(0);
20
+ const view = createLankaViewSubscription2(viewModel, () => {
21
+ version.update((seen) => seen + 1);
22
+ });
23
+ inject(DestroyRef).onDestroy(view.stop);
24
+ const signals = {};
25
+ for (const [key, value] of Object.entries(viewModel.getState())) {
26
+ signals[key] = typeof value === "function" ? value : computed(() => {
27
+ version();
28
+ return view.read()[key];
29
+ });
30
+ }
31
+ return signals;
32
+ };
33
+
34
+ // src/use-lanka-vm/useLankaVM.ts
35
+ import { DestroyRef as DestroyRef2, assertInInjectionContext as assertInInjectionContext2, inject as inject2, signal as signal2 } from "@angular/core";
36
+ import { createLankaAccessTracker } from "lanka/extend";
37
+ function useLankaVM(viewModel, selector) {
38
+ assertInInjectionContext2(useLankaVM);
39
+ const tracker = createLankaAccessTracker(viewModel);
40
+ const read = () => selector ? selector(viewModel.getState()) : tracker.read();
41
+ const state = signal2(read(), { equal: () => false });
42
+ const stop = viewModel.subscribe((next, prev) => {
43
+ if (selector) {
44
+ const picked = read();
45
+ if (Object.is(picked, state())) return;
46
+ state.set(picked);
47
+ return;
48
+ }
49
+ if (!tracker.shouldNotify(next, prev)) {
50
+ tracker.reportSkipped(next, prev);
51
+ return;
52
+ }
53
+ state.set(read());
54
+ });
55
+ inject2(DestroyRef2).onDestroy(stop);
56
+ return state.asReadonly();
57
+ }
58
+ export {
59
+ toLankaObservable,
60
+ toLankaSignals,
61
+ useLankaVM
62
+ };
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/to-lanka-observable/toLankaObservable.ts","../src/to-lanka-signals/toLankaSignals.ts","../src/use-lanka-vm/useLankaVM.ts"],"sourcesContent":["import { createLankaViewSubscription } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/** What a subscriber hands in, and what it gets back. */\nexport interface ILankaObserver<TValue> {\n\tnext?: (value: TValue) => void;\n\terror?: (failure: unknown) => void;\n\tcomplete?: () => void;\n}\n\n/** What unsubscribing looks like, in RxJS's own vocabulary. */\nexport interface ILankaUnsubscribable {\n\tunsubscribe: () => void;\n}\n\n/**\n * A ViewModel as something the `async` pipe and an RxJS chain accept.\n *\n * `Subscribable` is the whole contract: one method, and `AsyncPipe` takes it as\n * readily as an `Observable`.\n */\nexport interface ILankaObservableVM<TState extends object> {\n\tsubscribe: (\n\t\tobserver: ILankaObserver<TState> | ((value: TState) => void),\n\t) => ILankaUnsubscribable;\n}\n\n/**\n * Reads a ViewModel as a stream, for the half of Angular that speaks RxJS.\n *\n * ```ts\n * @Component({ template: `@if (todos$ | async; as todos) { … }` })\n * export class TodoScreen {\n * \tprotected readonly todos$ = toLankaObservable(todosVM);\n * }\n * ```\n *\n * ```ts\n * // or in a chain, where signals cannot go\n * toLankaObservable(todosVM).subscribe(({ rows }) => this.log(rows.length));\n * ```\n *\n * ## Why this exists beside `useLankaVM` and `toLankaSignals`\n *\n * Angular is signals-first now and those two answer signals, which is the right\n * default. It is also a framework with fifteen years of `Observable` in it: the\n * `async` pipe, `HttpClient`, the router's events, every `switchMap` a codebase\n * already has. A consumer with a stream in hand reaches for `combineLatest`, and\n * a signal is not something they can pass to it.\n *\n * ## No `rxjs` import, deliberately\n *\n * `AsyncPipe` accepts `Subscribable<T>`, which is an INTERFACE — one method — so\n * this satisfies it structurally and adds no dependency. The parity canon's\n * order for an idiom is the framework's own library first, then what it already\n * requires, then a few lines written here, and only then somebody else's\n * package. This is the third rung, and it keeps `@lankajs/angular` importing\n * nothing but `@angular/core`.\n *\n * A consumer who wants the operators pipes it: `from(toLankaObservable(vm))`\n * takes a subscribable, and `toObservable` from `@angular/core/rxjs-interop`\n * takes the signal `useLankaVM` answers.\n *\n * ## It emits the CURRENT state first\n *\n * Like a `BehaviorSubject` and like every store an Angular consumer has met: a\n * subscriber gets the state it subscribed to before anything changes, because a\n * template rendering `| async` would otherwise show nothing until the first\n * write.\n *\n * ## No injection context needed\n *\n * Unlike `useLankaVM` and `toLankaSignals`, which take a `DestroyRef` because a\n * signal has no other way to learn its reader has gone. A stream's subscriber\n * holds its own unsubscribe, which is RxJS's answer to the same question — so\n * this works in a service, a resolver, an interceptor and a plain function.\n */\nexport const toLankaObservable = <TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ILankaObservableVM<TState> => ({\n\tsubscribe: (observer) => {\n\t\tconst next =\n\t\t\ttypeof observer === \"function\" ? observer : (observer.next ?? (() => undefined));\n\n\t\tconst view = createLankaViewSubscription(viewModel, () => next(view.read()));\n\n\t\tnext(view.read());\n\n\t\treturn { unsubscribe: view.stop };\n\t},\n});\n","import { DestroyRef, assertInInjectionContext, computed, inject, signal } from \"@angular/core\";\nimport { createLankaViewSubscription } from \"lanka/extend\";\nimport type { Signal } from \"@angular/core\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel split the way an Angular service exposes state: a signal per\n * value, and the actions as themselves.\n *\n * An action is one object for the life of the store, so wrapping it in a signal\n * would make every call site write `load()()`. A value changes, so it is a\n * signal; a function does not, so it is a function.\n */\nexport type TLankaSignals<TState extends object> = {\n\t[TKey in keyof TState]: TState[TKey] extends (...args: never[]) => unknown\n\t\t? TState[TKey]\n\t\t: Signal<TState[TKey]>;\n};\n\n/**\n * Reads a ViewModel as the signals an Angular component expects.\n *\n * ```ts\n * @Component({ template: `@if (todos.isLoading()) { … } @for (row of todos.rows(); track row) { … }` })\n * export class TodoScreen {\n * \tprotected readonly todos = toLankaSignals(todosVM);\n * }\n * ```\n *\n * ## Why this exists beside `useLankaVM`\n *\n * `useLankaVM` answers ONE `Signal` over the whole state, which is the shape\n * every other binding on the shelf parallels: `state().rows`. It is also not how\n * Angular holds state. An Angular service exposes a signal per field —\n * `readonly rows = signal([])` — and a template reads `rows()`, never\n * `state().rows`. A consumer with that habit reaches for `todos.rows()` and finds\n * a call on a plain object.\n *\n * ## One subscription, and each signal is a `computed` over it\n *\n * There is one `subscribe` on the ViewModel and one version signal behind every\n * field, so a change wakes Angular once and each `computed` decides for itself\n * whether its own value moved. That is Angular's own deduplication — a `computed`\n * whose result is unchanged notifies nobody — arriving for free, and it is why\n * this is not a second subscription per field.\n *\n * ## The keys are read ONCE, at the call\n *\n * A ViewModel declares its state up front, so the field list is fixed at the\n * moment this is called. A key added to the state later has no signal here, and\n * that is the price of the shape: Angular's own services name their fields too.\n * `useLankaVM` is the answer for a state whose shape is genuinely dynamic.\n *\n * ## It needs an injection context, for the reason `useLankaVM` does\n *\n * `DestroyRef` is the only way to learn the caller has gone, and a subscription\n * that cannot learn that is a leak with no owner.\n */\nexport const toLankaSignals = <TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): TLankaSignals<TState> => {\n\tassertInInjectionContext(toLankaSignals);\n\n\tconst version = signal(0);\n\tconst view = createLankaViewSubscription(viewModel, () => {\n\t\tversion.update((seen) => seen + 1);\n\t});\n\n\tinject(DestroyRef).onDestroy(view.stop);\n\n\tconst signals = {} as Record<string, unknown>;\n\n\tfor (const [key, value] of Object.entries(viewModel.getState())) {\n\t\tsignals[key] =\n\t\t\ttypeof value === \"function\"\n\t\t\t\t? value\n\t\t\t\t: computed(() => {\n\t\t\t\t\t\t// Read for the DEPENDENCY, discard the number. The value itself\n\t\t\t\t\t\t// comes from a tracked read, so the key is recorded and this reader\n\t\t\t\t\t\t// is woken only for the keys it has signals for.\n\t\t\t\t\t\tversion();\n\n\t\t\t\t\t\treturn (view.read() as Record<string, unknown>)[key];\n\t\t\t\t\t});\n\t}\n\n\treturn signals as TLankaSignals<TState>;\n};\n","import { DestroyRef, assertInInjectionContext, inject, signal } from \"@angular/core\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { Signal } from \"@angular/core\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * Reads a ViewModel from Angular.\n *\n * ```ts\n * @Component({ template: `<li *ngFor=\"let row of state().rows\">{{ row }}</li>` })\n * export class TodoScreen {\n * \tprotected readonly state = useLankaVM(todoVM);\n * }\n * ```\n *\n * Without a selector the signal carries a value that RECORDS which keys were\n * read, and changes only when one of THOSE moves. With a selector the selector\n * decides and tracking is bypassed.\n *\n * Zoneless needs no extra step: a signal is what zoneless change detection\n * reads, so this is the shape Angular is moving towards rather than a bridge to\n * it.\n *\n * ## Why an injection context is REQUIRED, not preferred\n *\n * `@lankajs/vue` and `@lankajs/solid` publish a `stop()` for a call made outside\n * their framework's scope, because both can still work without one. Angular\n * cannot: `DestroyRef` is the only way to learn that the caller has gone, and a\n * subscription with no way to learn that is a leak with no owner.\n *\n * So this refuses at the call rather than leaking quietly, and the message names\n * the fix. Where the other two degrade, this one stops — and a refusal a\n * developer reads once beats a leak found in production.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. Every binding on this shelf calls it,\n * which is what makes \"a screen updates for the keys it read\" a fact about lanka\n * rather than a fact about Angular.\n */\nexport function useLankaVM<TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): Signal<TState>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): Signal<TSelected>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): Signal<TState | TSelected> {\n\tassertInInjectionContext(useLankaVM);\n\n\tconst tracker = createLankaAccessTracker(viewModel);\n\tconst read = (): TState | TSelected =>\n\t\tselector ? selector(viewModel.getState()) : tracker.read();\n\n\t// `equal: () => false` because a tracked read hands back the SAME proxy while\n\t// the state object is unchanged, and a signal compares by identity — so\n\t// setting it would be a no-op exactly when the tracker did its job. What\n\t// decides whether anything happens is `shouldNotify` below, which is the\n\t// framework's answer rather than the signal's.\n\tconst state = signal<TState | TSelected>(read(), { equal: () => false });\n\n\tconst stop = viewModel.subscribe((next, prev) => {\n\t\tif (selector) {\n\t\t\tconst picked = read();\n\n\t\t\t// Only when the SELECTION moved. The signal is `equal: () => false`, so\n\t\t\t// setting it always wakes — which is right for a tracked read and wrong\n\t\t\t// for a selected one, and is what the suite's selector scenes refuse.\n\t\t\tif (Object.is(picked, state())) return;\n\n\t\t\tstate.set(picked);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!tracker.shouldNotify(next, prev)) {\n\t\t\t// No update will follow. If the changed key is linked to this component\n\t\t\t// through a getter it read, the screen froze — and in development core\n\t\t\t// says so by name.\n\t\t\ttracker.reportSkipped(next, prev);\n\t\t\treturn;\n\t\t}\n\n\t\tstate.set(read());\n\t});\n\tinject(DestroyRef).onDestroy(stop);\n\n\treturn state.asReadonly();\n}\n"],"mappings":";;;AAAA,SAAS,mCAAmC;AA6ErC,IAAM,oBAAoB,CAChC,eACiC;AAAA,EACjC,WAAW,CAAC,aAAa;AACxB,UAAM,OACL,OAAO,aAAa,aAAa,WAAY,SAAS,SAAS,MAAM;AAEtE,UAAM,OAAO,4BAA4B,WAAW,MAAM,KAAK,KAAK,KAAK,CAAC,CAAC;AAE3E,SAAK,KAAK,KAAK,CAAC;AAEhB,WAAO,EAAE,aAAa,KAAK,KAAK;AAAA,EACjC;AACD;;;AC1FA,SAAS,YAAY,0BAA0B,UAAU,QAAQ,cAAc;AAC/E,SAAS,+BAAAA,oCAAmC;AAyDrC,IAAM,iBAAiB,CAC7B,cAC2B;AAC3B,2BAAyB,cAAc;AAEvC,QAAM,UAAU,OAAO,CAAC;AACxB,QAAM,OAAOA,6BAA4B,WAAW,MAAM;AACzD,YAAQ,OAAO,CAAC,SAAS,OAAO,CAAC;AAAA,EAClC,CAAC;AAED,SAAO,UAAU,EAAE,UAAU,KAAK,IAAI;AAEtC,QAAM,UAAU,CAAC;AAEjB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,SAAS,CAAC,GAAG;AAChE,YAAQ,GAAG,IACV,OAAO,UAAU,aACd,QACA,SAAS,MAAM;AAIf,cAAQ;AAER,aAAQ,KAAK,KAAK,EAA8B,GAAG;AAAA,IACpD,CAAC;AAAA,EACL;AAEA,SAAO;AACR;;;ACvFA,SAAS,cAAAC,aAAY,4BAAAC,2BAA0B,UAAAC,SAAQ,UAAAC,eAAc;AACrE,SAAS,gCAAgC;AAiDlC,SAAS,WACf,WACA,UAC6B;AAC7B,EAAAF,0BAAyB,UAAU;AAEnC,QAAM,UAAU,yBAAyB,SAAS;AAClD,QAAM,OAAO,MACZ,WAAW,SAAS,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AAO1D,QAAM,QAAQE,QAA2B,KAAK,GAAG,EAAE,OAAO,MAAM,MAAM,CAAC;AAEvE,QAAM,OAAO,UAAU,UAAU,CAAC,MAAM,SAAS;AAChD,QAAI,UAAU;AACb,YAAM,SAAS,KAAK;AAKpB,UAAI,OAAO,GAAG,QAAQ,MAAM,CAAC,EAAG;AAEhC,YAAM,IAAI,MAAM;AAEhB;AAAA,IACD;AAEA,QAAI,CAAC,QAAQ,aAAa,MAAM,IAAI,GAAG;AAItC,cAAQ,cAAc,MAAM,IAAI;AAChC;AAAA,IACD;AAEA,UAAM,IAAI,KAAK,CAAC;AAAA,EACjB,CAAC;AACD,EAAAD,QAAOF,WAAU,EAAE,UAAU,IAAI;AAEjC,SAAO,MAAM,WAAW;AACzB;","names":["createLankaViewSubscription","DestroyRef","assertInInjectionContext","inject","signal"]}
@@ -0,0 +1,46 @@
1
+ import { RenderComponentOptions, RenderResult } from '@testing-library/angular';
2
+ import { Type } from '@angular/core';
3
+ import { IPrepareLankaRenderOptions } from '@lankajs/tool-testing';
4
+ import { ILankaInstance } from 'lanka';
5
+
6
+ interface IRenderWithLankaOptions<TComponent> extends Omit<RenderComponentOptions<TComponent>, "wrapper">, IPrepareLankaRenderOptions {
7
+ }
8
+ /**
9
+ * What a render with a bootstrapped framework ADDS to the library's own result.
10
+ *
11
+ * An interface over the addition rather than over the whole result, and the same
12
+ * in all five bindings: Svelte Testing Library's result carries a string index
13
+ * signature for its bound queries, so a named member added by extension has to
14
+ * satisfy it — and `lanka` is an instance, not a query. Describing only the
15
+ * addition is true of every library and needs no cast anywhere.
16
+ */
17
+ interface IRenderWithLankaResult {
18
+ /** The instance the render used. */
19
+ lanka: ILankaInstance;
20
+ }
21
+ /**
22
+ * Rendering an Angular component with a bootstrapped framework.
23
+ *
24
+ * ## Why
25
+ *
26
+ * A component reading a ViewModel needs a live instance: without one the first
27
+ * scenario or locator access fails. Assembling bootstrap in every component test
28
+ * is twenty lines of preamble that diverge between files silently.
29
+ *
30
+ * ## What is here, and what is in the kit
31
+ *
32
+ * The five bindings publish this name and differ only in which `render` they
33
+ * call. Everything else — a fresh instance, the doubles, the caller's setup and
34
+ * the scenario layer brought up in that order — is `prepareLankaRender` in
35
+ * `@lankajs/tool-testing`, which is the one place all five already look.
36
+ *
37
+ * ## Angular's is asynchronous, and that is its own
38
+ *
39
+ * Angular Testing Library drives `TestBed`, which COMPILES a component rather
40
+ * than merely mounting one, so `render` answers a promise. The four other
41
+ * bindings answer directly. That difference belongs to Angular's testing story
42
+ * and not to lanka, which is why it is not hidden behind a synchronous wrapper.
43
+ */
44
+ declare const renderWithLanka: <TComponent>(component: Type<TComponent>, options?: IRenderWithLankaOptions<TComponent>) => Promise<RenderResult<TComponent, TComponent> & IRenderWithLankaResult>;
45
+
46
+ export { type IRenderWithLankaOptions, type IRenderWithLankaResult, renderWithLanka };