@nisli/core 0.43.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/README.md +100 -0
- package/dist/component.d.ts +75 -0
- package/dist/component.js +260 -0
- package/dist/component.js.map +1 -0
- package/dist/context.d.ts +43 -0
- package/dist/context.js +55 -0
- package/dist/context.js.map +1 -0
- package/dist/emitter.d.ts +51 -0
- package/dist/emitter.js +90 -0
- package/dist/emitter.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/injector.d.ts +65 -0
- package/dist/injector.js +100 -0
- package/dist/injector.js.map +1 -0
- package/dist/lifecycle.d.ts +57 -0
- package/dist/lifecycle.js +107 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/query.d.ts +81 -0
- package/dist/query.js +231 -0
- package/dist/query.js.map +1 -0
- package/dist/ref.d.ts +36 -0
- package/dist/ref.js +43 -0
- package/dist/ref.js.map +1 -0
- package/dist/signal.d.ts +93 -0
- package/dist/signal.js +403 -0
- package/dist/signal.js.map +1 -0
- package/dist/template.d.ts +49 -0
- package/dist/template.js +753 -0
- package/dist/template.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# nisli
|
|
2
|
+
|
|
3
|
+
A reactive web component framework. Signals, templates, dependency injection — no build step, no virtual DOM, no dependencies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nisli/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { signal, component, html } from '@nisli/core';
|
|
15
|
+
|
|
16
|
+
const Counter = component('x-counter', () => {
|
|
17
|
+
const count = signal(0);
|
|
18
|
+
return html`
|
|
19
|
+
<button @click=${() => count.set(count() + 1)}>
|
|
20
|
+
Count: ${count}
|
|
21
|
+
</button>
|
|
22
|
+
`;
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Signals** — Fine-grained reactivity with `signal`, `computed`, `effect`
|
|
29
|
+
- **Components** — Web Components with a composition-style setup function
|
|
30
|
+
- **Templates** — Tagged template literals with automatic signal binding
|
|
31
|
+
- **Dependency Injection** — `inject` any class as a singleton, `provide` overrides for testing
|
|
32
|
+
- **Queries** — Declarative async data loading with caching and auto-refetch
|
|
33
|
+
- **Event Emitters** — Typed event bus with auto-disposal in component context
|
|
34
|
+
- **Lifecycle** — `onMount`, `onCleanup`, `useHostEvent`
|
|
35
|
+
- **Refs** — Direct element access via `ref()`
|
|
36
|
+
- **Control Flow** — `when()` for toggles, `each()` for keyed list rendering
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Reactivity
|
|
42
|
+
signal(value) // Reactive signal
|
|
43
|
+
computed(() => expr) // Derived signal (lazy, cached)
|
|
44
|
+
effect(() => { ... }) // Side effect that tracks dependencies
|
|
45
|
+
|
|
46
|
+
// Components
|
|
47
|
+
component('tag-name', (props, host) => html`...`)
|
|
48
|
+
component<Props>('tag-name', { props: [...] }, (props, host) => html`...`)
|
|
49
|
+
|
|
50
|
+
// Templates — signals are implicit, no .value needed
|
|
51
|
+
html`<div>${count}</div>`
|
|
52
|
+
html`<button @click=${handler}>Go</button>`
|
|
53
|
+
html`<div class:active=${isActive}>...</div>`
|
|
54
|
+
|
|
55
|
+
// Control flow
|
|
56
|
+
when(condition, () => html`...`)
|
|
57
|
+
each(items, item => item.id, (item) => html`...`)
|
|
58
|
+
|
|
59
|
+
// Dependency injection — class IS the token
|
|
60
|
+
inject(MyService) // Auto-creates singleton
|
|
61
|
+
provide(MyService, () => mock) // Override (testing)
|
|
62
|
+
|
|
63
|
+
// Queries
|
|
64
|
+
const { data, loading, error } = query(
|
|
65
|
+
() => ['tasks', id.value], // Cache key (tracked)
|
|
66
|
+
() => api.getTasks(id.value), // Fetcher
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Lifecycle
|
|
70
|
+
onMount(() => { ... })
|
|
71
|
+
onCleanup(() => { ... })
|
|
72
|
+
useHostEvent('click', handler)
|
|
73
|
+
|
|
74
|
+
// Refs
|
|
75
|
+
const el = ref<HTMLDivElement>()
|
|
76
|
+
html`<div ${el}>...</div>`
|
|
77
|
+
|
|
78
|
+
// Events
|
|
79
|
+
class Nav extends Emitter<{ select: { id: string } }> {}
|
|
80
|
+
inject(Nav).emit('select', { id })
|
|
81
|
+
inject(Nav).on('select', ({ id }) => { ... })
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Size
|
|
85
|
+
|
|
86
|
+
~2,600 lines of TypeScript. Zero dependencies.
|
|
87
|
+
|
|
88
|
+
## Inspiration
|
|
89
|
+
|
|
90
|
+
nisli stands on the shoulders of giants:
|
|
91
|
+
|
|
92
|
+
- [React](https://react.dev) — Component model, declarative UI
|
|
93
|
+
- [Solid](https://www.solidjs.com) — Signals, fine-grained reactivity, no virtual DOM
|
|
94
|
+
- [Lit](https://lit.dev) — Web Components, tagged template literals
|
|
95
|
+
- [Angular](https://angular.dev) — Dependency injection, typed tokens
|
|
96
|
+
- [Vue](https://vuejs.org) — Composition-style setup functions, reactive system design
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* component.ts — Web component shell with typed props and factory composition
|
|
3
|
+
*
|
|
4
|
+
* `component()` is the single entry point for defining components.
|
|
5
|
+
* It registers a custom element and returns a typed factory function.
|
|
6
|
+
*
|
|
7
|
+
* Component authors write a setup function that receives typed props
|
|
8
|
+
* (each wrapped as a Signal) and optionally the host element.
|
|
9
|
+
* The setup function returns a TemplateResult from html`...`.
|
|
10
|
+
*
|
|
11
|
+
* The class internals (connectedCallback, etc.) are completely hidden.
|
|
12
|
+
*/
|
|
13
|
+
import { type Signal, type ReadonlySignal } from './signal.js';
|
|
14
|
+
import type { TemplateResult } from './template.js';
|
|
15
|
+
/**
|
|
16
|
+
* Maps a props interface to reactive signals.
|
|
17
|
+
* Each prop `P[K]` becomes `Signal<P[K]>`.
|
|
18
|
+
*/
|
|
19
|
+
export type ReactiveProps<P> = {
|
|
20
|
+
readonly [K in keyof P]: Signal<P[K]>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The setup function signature. Receives reactive props and host element.
|
|
24
|
+
* Returns a TemplateResult to be mounted into the component.
|
|
25
|
+
*/
|
|
26
|
+
export type SetupFunction<P> = (props: ReactiveProps<P>, host: HTMLElement) => TemplateResult;
|
|
27
|
+
/**
|
|
28
|
+
* Accepts either a Signal<T> (reactive) or a plain T (static, auto-wrapped).
|
|
29
|
+
* This eliminates the need to wrap static values in `signal()` at call sites.
|
|
30
|
+
* See ADR 0009 Gap 4.
|
|
31
|
+
*/
|
|
32
|
+
export type PropInput<T> = T | Signal<T> | ReadonlySignal<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Host-level attributes that every factory accepts, independent of component props.
|
|
35
|
+
* `class` is applied to the host element via classList — it does not collide with
|
|
36
|
+
* the component's own class attribute or class:name directives.
|
|
37
|
+
* See ADR 0009 Gap 3.
|
|
38
|
+
*/
|
|
39
|
+
export interface HostAttrs {
|
|
40
|
+
class?: PropInput<string>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A factory function for type-safe component composition.
|
|
44
|
+
* Props accept both Signal<T> (reactive) and plain T (static, auto-wrapped).
|
|
45
|
+
* An optional second argument provides host-level attributes (class).
|
|
46
|
+
*/
|
|
47
|
+
export type ComponentFactory<P> = (props: {
|
|
48
|
+
[K in keyof P]: PropInput<P[K]>;
|
|
49
|
+
}, hostAttrs?: HostAttrs) => TemplateResult;
|
|
50
|
+
/** Options for component() registration */
|
|
51
|
+
export interface ComponentOptions {
|
|
52
|
+
/** Custom error fallback renderer */
|
|
53
|
+
onError?: (error: Error, host: HTMLElement) => TemplateResult | string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Define a web component with typed props and a setup function.
|
|
57
|
+
*
|
|
58
|
+
* Returns a typed factory function for compile-time-safe composition.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* interface TaskItemProps {
|
|
62
|
+
* task: Task;
|
|
63
|
+
* selected: boolean;
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* const TaskItem = component<TaskItemProps>('task-item', (props, host) => {
|
|
67
|
+
* const title = computed(() => props.task.value.title);
|
|
68
|
+
* return html`<div>${title}</div>`;
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Factory usage:
|
|
72
|
+
* TaskItem({ task: taskSignal, selected: isSelected })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function component<P extends Record<string, unknown> = Record<string, never>>(tagName: string, setup: SetupFunction<P>, options?: ComponentOptions): ComponentFactory<P>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* component.ts — Web component shell with typed props and factory composition
|
|
3
|
+
*
|
|
4
|
+
* `component()` is the single entry point for defining components.
|
|
5
|
+
* It registers a custom element and returns a typed factory function.
|
|
6
|
+
*
|
|
7
|
+
* Component authors write a setup function that receives typed props
|
|
8
|
+
* (each wrapped as a Signal) and optionally the host element.
|
|
9
|
+
* The setup function returns a TemplateResult from html`...`.
|
|
10
|
+
*
|
|
11
|
+
* The class internals (connectedCallback, etc.) are completely hidden.
|
|
12
|
+
*/
|
|
13
|
+
import { signal, effect, setContextHook, isSignal, untrack } from './signal.js';
|
|
14
|
+
import { runWithContext, hasContext, getCurrentComponent } from './context.js';
|
|
15
|
+
import { runMountCallbacks } from './lifecycle.js';
|
|
16
|
+
// Wire up effect auto-disposal: when effect() is called during component setup,
|
|
17
|
+
// auto-register the dispose function with the component host.
|
|
18
|
+
setContextHook(() => {
|
|
19
|
+
if (hasContext()) {
|
|
20
|
+
const comp = getCurrentComponent();
|
|
21
|
+
return (disposer) => comp.addDisposer(disposer);
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
});
|
|
25
|
+
// ── Internal component host ─────────────────────────────────────────
|
|
26
|
+
class ComponentHostImpl {
|
|
27
|
+
element;
|
|
28
|
+
disposers = [];
|
|
29
|
+
setupComplete = false;
|
|
30
|
+
constructor(element) {
|
|
31
|
+
this.element = element;
|
|
32
|
+
}
|
|
33
|
+
addDisposer(fn) {
|
|
34
|
+
this.disposers.push(fn);
|
|
35
|
+
}
|
|
36
|
+
dispose() {
|
|
37
|
+
for (const fn of this.disposers) {
|
|
38
|
+
try {
|
|
39
|
+
fn();
|
|
40
|
+
}
|
|
41
|
+
catch (_) { /* swallow disposal errors */ }
|
|
42
|
+
}
|
|
43
|
+
this.disposers.length = 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ── Prop signal management via Proxy ────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Creates a Proxy-based props object that lazily creates signals
|
|
49
|
+
* for each property on first access. When a parent sets a property
|
|
50
|
+
* on the element, the Proxy intercepts it and updates (or creates)
|
|
51
|
+
* the backing signal.
|
|
52
|
+
*/
|
|
53
|
+
function createPropsProxy() {
|
|
54
|
+
const signals = new Map();
|
|
55
|
+
const getOrCreate = (key, initialValue) => {
|
|
56
|
+
let s = signals.get(key);
|
|
57
|
+
if (!s) {
|
|
58
|
+
s = signal(initialValue);
|
|
59
|
+
signals.set(key, s);
|
|
60
|
+
}
|
|
61
|
+
return s;
|
|
62
|
+
};
|
|
63
|
+
const props = new Proxy({}, {
|
|
64
|
+
get(_target, key) {
|
|
65
|
+
return getOrCreate(key);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const setProperty = (key, value) => {
|
|
69
|
+
const s = getOrCreate(key, value);
|
|
70
|
+
s.value = value;
|
|
71
|
+
};
|
|
72
|
+
return { props, setProperty };
|
|
73
|
+
}
|
|
74
|
+
// ── component() — the main entry point ──────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Define a web component with typed props and a setup function.
|
|
77
|
+
*
|
|
78
|
+
* Returns a typed factory function for compile-time-safe composition.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* interface TaskItemProps {
|
|
82
|
+
* task: Task;
|
|
83
|
+
* selected: boolean;
|
|
84
|
+
* }
|
|
85
|
+
*
|
|
86
|
+
* const TaskItem = component<TaskItemProps>('task-item', (props, host) => {
|
|
87
|
+
* const title = computed(() => props.task.value.title);
|
|
88
|
+
* return html`<div>${title}</div>`;
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* // Factory usage:
|
|
92
|
+
* TaskItem({ task: taskSignal, selected: isSelected })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function component(tagName, setup, options) {
|
|
96
|
+
// The custom element class
|
|
97
|
+
class FrameworkComponent extends HTMLElement {
|
|
98
|
+
_host = null;
|
|
99
|
+
_propsProxy = null;
|
|
100
|
+
_mounted = false;
|
|
101
|
+
_templateResult = null;
|
|
102
|
+
constructor() {
|
|
103
|
+
super();
|
|
104
|
+
this._propsProxy = createPropsProxy();
|
|
105
|
+
}
|
|
106
|
+
connectedCallback() {
|
|
107
|
+
if (this._mounted)
|
|
108
|
+
return; // guard against re-connection
|
|
109
|
+
this._mounted = true;
|
|
110
|
+
// Isolate activeObserver: child component setup signal reads must NOT
|
|
111
|
+
// leak into a parent effect's dependency set. Without this, a parent
|
|
112
|
+
// effect running replaceChildren/appendChild triggers child
|
|
113
|
+
// connectedCallback synchronously, and child signal reads would be
|
|
114
|
+
// tracked as parent dependencies — causing infinite re-render loops.
|
|
115
|
+
// See ADR 0008 Gap 1 / ADR 0009.
|
|
116
|
+
untrack(() => {
|
|
117
|
+
const host = new ComponentHostImpl(this);
|
|
118
|
+
this._host = host;
|
|
119
|
+
try {
|
|
120
|
+
runWithContext(host, () => {
|
|
121
|
+
this._templateResult = setup(this._propsProxy.props, this);
|
|
122
|
+
});
|
|
123
|
+
// Mount the template result
|
|
124
|
+
if (this._templateResult) {
|
|
125
|
+
mountTemplate(this, this._templateResult, host);
|
|
126
|
+
}
|
|
127
|
+
// Run onMount() callbacks — DOM is now committed
|
|
128
|
+
runMountCallbacks(host);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
132
|
+
console.error(`Component <${tagName}> setup error:`, error);
|
|
133
|
+
if (options?.onError) {
|
|
134
|
+
try {
|
|
135
|
+
const fallback = options.onError(error, this);
|
|
136
|
+
if (typeof fallback === 'string') {
|
|
137
|
+
this.innerHTML = fallback;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
mountTemplate(this, fallback);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (_) {
|
|
144
|
+
this.innerHTML = `<div style="color:red;padding:4px">Error in <${tagName}></div>`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.innerHTML = `<div style="color:red;padding:4px">Error in <${tagName}></div>`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
disconnectedCallback() {
|
|
154
|
+
this._mounted = false;
|
|
155
|
+
if (this._templateResult?.dispose) {
|
|
156
|
+
this._templateResult.dispose();
|
|
157
|
+
}
|
|
158
|
+
if (this._host) {
|
|
159
|
+
this._host.dispose();
|
|
160
|
+
this._host = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Set a prop value. Called by the factory and by direct property assignment.
|
|
165
|
+
* The Proxy intercepts and creates/updates backing signals.
|
|
166
|
+
*/
|
|
167
|
+
_setProp(key, value) {
|
|
168
|
+
this._propsProxy?.setProperty(key, value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Register the custom element (if not already registered)
|
|
172
|
+
if (!customElements.get(tagName)) {
|
|
173
|
+
customElements.define(tagName, FrameworkComponent);
|
|
174
|
+
}
|
|
175
|
+
// Return the typed factory function
|
|
176
|
+
const factory = (props, hostAttrs) => {
|
|
177
|
+
return {
|
|
178
|
+
__type: 'factory',
|
|
179
|
+
tagName,
|
|
180
|
+
props: props,
|
|
181
|
+
hostAttrs,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
return factory;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Ensure a value is a signal. If it already is, return as-is.
|
|
188
|
+
* If it's a plain value, wrap it in signal().
|
|
189
|
+
* Used by factory mount to support PropInput<T> = T | Signal<T>.
|
|
190
|
+
*/
|
|
191
|
+
function ensureSignal(value) {
|
|
192
|
+
if (isSignal(value))
|
|
193
|
+
return value;
|
|
194
|
+
return signal(value);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Mount a TemplateResult into a host element.
|
|
198
|
+
* This is the bridge between component.ts and template.ts.
|
|
199
|
+
* Template.ts will provide the actual implementation via the
|
|
200
|
+
* TemplateResult.mount() method.
|
|
201
|
+
*
|
|
202
|
+
* @param hostImpl — optional ComponentHostImpl for registering disposers
|
|
203
|
+
*/
|
|
204
|
+
function mountTemplate(host, result, hostImpl) {
|
|
205
|
+
if (result && typeof result === 'object' && 'mount' in result && typeof result.mount === 'function') {
|
|
206
|
+
result.mount(host);
|
|
207
|
+
}
|
|
208
|
+
else if (result && typeof result === 'object' && '__type' in result) {
|
|
209
|
+
// Factory result — create the child element
|
|
210
|
+
const factoryResult = result;
|
|
211
|
+
const el = document.createElement(factoryResult.tagName);
|
|
212
|
+
// Forward props as signals (auto-wrap plain values)
|
|
213
|
+
for (const [key, raw] of Object.entries(factoryResult.props)) {
|
|
214
|
+
const sig = ensureSignal(raw);
|
|
215
|
+
el._setProp(key, sig.value);
|
|
216
|
+
// Subscribe to signal changes to update the child's props
|
|
217
|
+
const unsub = sig.subscribe((newVal) => {
|
|
218
|
+
el._setProp(key, newVal);
|
|
219
|
+
});
|
|
220
|
+
// Register unsub for cleanup on disconnect
|
|
221
|
+
if (hostImpl) {
|
|
222
|
+
hostImpl.addDisposer(unsub);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Apply host-level attributes (class)
|
|
226
|
+
if (factoryResult.hostAttrs?.class != null) {
|
|
227
|
+
const classSig = ensureSignal(factoryResult.hostAttrs.class);
|
|
228
|
+
// Apply initial value
|
|
229
|
+
applyHostClass(el, '', classSig.value);
|
|
230
|
+
// Subscribe to reactive changes
|
|
231
|
+
let prevClass = classSig.value;
|
|
232
|
+
const unsub = classSig.subscribe((newClass) => {
|
|
233
|
+
applyHostClass(el, prevClass, newClass);
|
|
234
|
+
prevClass = newClass;
|
|
235
|
+
});
|
|
236
|
+
if (hostImpl) {
|
|
237
|
+
hostImpl.addDisposer(unsub);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
host.appendChild(el);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Apply a class string to an element via classList, replacing the previous
|
|
245
|
+
* class string's classes. Uses add/remove to avoid interfering with classes
|
|
246
|
+
* set by the component's own template (class:name directives, etc.).
|
|
247
|
+
*/
|
|
248
|
+
function applyHostClass(el, prev, next) {
|
|
249
|
+
const prevClasses = prev ? prev.split(/\s+/).filter(Boolean) : [];
|
|
250
|
+
const nextClasses = next ? next.split(/\s+/).filter(Boolean) : [];
|
|
251
|
+
for (const cls of prevClasses) {
|
|
252
|
+
if (!nextClasses.includes(cls))
|
|
253
|
+
el.classList.remove(cls);
|
|
254
|
+
}
|
|
255
|
+
for (const cls of nextClasses) {
|
|
256
|
+
if (!prevClasses.includes(cls))
|
|
257
|
+
el.classList.add(cls);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.js","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — Setup context for pure function DI
|
|
3
|
+
*
|
|
4
|
+
* The setup context lets pure functions (inject(), listen(), effect())
|
|
5
|
+
* access the component they belong to without needing `this`.
|
|
6
|
+
*
|
|
7
|
+
* The context exists ONLY during component initialization (setup()).
|
|
8
|
+
* It is NOT a runtime thing — it's a synchronous initialization-time concept.
|
|
9
|
+
*
|
|
10
|
+
* Same pattern as Angular's inject(), Solid's createSignal(), Vue's setup().
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Minimal interface for a component host. component.ts provides the
|
|
14
|
+
* full implementation; this file only needs the disposal hook.
|
|
15
|
+
*/
|
|
16
|
+
export interface ComponentHost {
|
|
17
|
+
/** Register a cleanup function to run on disconnectedCallback */
|
|
18
|
+
addDisposer(fn: () => void): void;
|
|
19
|
+
/** The raw HTMLElement */
|
|
20
|
+
element: HTMLElement;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run a function with a component as the active context.
|
|
24
|
+
* Pure functions called during `fn` can access the component via
|
|
25
|
+
* `getCurrentComponent()`.
|
|
26
|
+
*
|
|
27
|
+
* Context is strictly synchronous — it does NOT survive across
|
|
28
|
+
* await boundaries or microtasks.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runWithContext(component: ComponentHost, fn: () => void): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get the currently active component host.
|
|
33
|
+
* Throws if called outside a setup() context.
|
|
34
|
+
*
|
|
35
|
+
* This is framework-internal — component authors use inject(), effect(),
|
|
36
|
+
* etc., which call this internally.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getCurrentComponent(): ComponentHost;
|
|
39
|
+
/**
|
|
40
|
+
* Check if there is an active setup context.
|
|
41
|
+
* Returns true during component setup, false otherwise.
|
|
42
|
+
*/
|
|
43
|
+
export declare function hasContext(): boolean;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — Setup context for pure function DI
|
|
3
|
+
*
|
|
4
|
+
* The setup context lets pure functions (inject(), listen(), effect())
|
|
5
|
+
* access the component they belong to without needing `this`.
|
|
6
|
+
*
|
|
7
|
+
* The context exists ONLY during component initialization (setup()).
|
|
8
|
+
* It is NOT a runtime thing — it's a synchronous initialization-time concept.
|
|
9
|
+
*
|
|
10
|
+
* Same pattern as Angular's inject(), Solid's createSignal(), Vue's setup().
|
|
11
|
+
*/
|
|
12
|
+
// ── Context state ───────────────────────────────────────────────────
|
|
13
|
+
let currentComponent = null;
|
|
14
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Run a function with a component as the active context.
|
|
17
|
+
* Pure functions called during `fn` can access the component via
|
|
18
|
+
* `getCurrentComponent()`.
|
|
19
|
+
*
|
|
20
|
+
* Context is strictly synchronous — it does NOT survive across
|
|
21
|
+
* await boundaries or microtasks.
|
|
22
|
+
*/
|
|
23
|
+
export function runWithContext(component, fn) {
|
|
24
|
+
const prev = currentComponent;
|
|
25
|
+
currentComponent = component;
|
|
26
|
+
try {
|
|
27
|
+
fn();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
currentComponent = prev;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the currently active component host.
|
|
35
|
+
* Throws if called outside a setup() context.
|
|
36
|
+
*
|
|
37
|
+
* This is framework-internal — component authors use inject(), effect(),
|
|
38
|
+
* etc., which call this internally.
|
|
39
|
+
*/
|
|
40
|
+
export function getCurrentComponent() {
|
|
41
|
+
if (!currentComponent) {
|
|
42
|
+
throw new Error('getCurrentComponent() called outside setup(). ' +
|
|
43
|
+
'inject(), effect(), and other context-dependent functions ' +
|
|
44
|
+
'can only be called during component initialization.');
|
|
45
|
+
}
|
|
46
|
+
return currentComponent;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if there is an active setup context.
|
|
50
|
+
* Returns true during component setup, false otherwise.
|
|
51
|
+
*/
|
|
52
|
+
export function hasContext() {
|
|
53
|
+
return currentComponent !== null;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAeH,uEAAuE;AAEvE,IAAI,gBAAgB,GAAyB,IAAI,CAAC;AAElD,uEAAuE;AAEvE;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,SAAwB,EAAE,EAAc;IACrE,MAAM,IAAI,GAAG,gBAAgB,CAAC;IAC9B,gBAAgB,GAAG,SAAS,CAAC;IAC7B,IAAI,CAAC;QACH,EAAE,EAAE,CAAC;IACP,CAAC;YAAS,CAAC;QACT,gBAAgB,GAAG,IAAI,CAAC;IAC1B,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,gDAAgD;YAChD,4DAA4D;YAC5D,qDAAqD,CACtD,CAAC;IACJ,CAAC;IACD,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,gBAAgB,KAAK,IAAI,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emitter.ts — Typed pub/sub base class for services.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `document.dispatchEvent(new CustomEvent(...))` with typed,
|
|
5
|
+
* scoped event emitters. Services extend Emitter<T> where T defines
|
|
6
|
+
* the event contract as a record of event names → payload types.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* class NavigationEvents extends Emitter<{
|
|
10
|
+
* select: { id: string };
|
|
11
|
+
* filter: { filter: string };
|
|
12
|
+
* }> {}
|
|
13
|
+
*
|
|
14
|
+
* const nav = inject(NavigationEvents);
|
|
15
|
+
* nav.emit('select', { id: 'TASK-1' }); // typed
|
|
16
|
+
* nav.on('select', ({ id }) => console.log(id)); // typed
|
|
17
|
+
* const selectedId = nav.toSignal('select', e => e.id, null);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { type Signal } from './signal.js';
|
|
21
|
+
type EventMap = Record<string, unknown>;
|
|
22
|
+
type Listener<T> = (payload: T) => void;
|
|
23
|
+
export declare class Emitter<Events extends EventMap> {
|
|
24
|
+
private listeners;
|
|
25
|
+
/**
|
|
26
|
+
* Emit a typed event. All subscribers for this event name are called
|
|
27
|
+
* synchronously. If a subscriber throws, other subscribers still fire.
|
|
28
|
+
*/
|
|
29
|
+
emit<K extends keyof Events>(event: K, payload: Events[K]): void;
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to a typed event. Returns an unsubscribe function.
|
|
32
|
+
*
|
|
33
|
+
* If called during component setup, the subscription is automatically
|
|
34
|
+
* disposed on disconnectedCallback.
|
|
35
|
+
*/
|
|
36
|
+
on<K extends keyof Events>(event: K, fn: Listener<Events[K]>): () => void;
|
|
37
|
+
/**
|
|
38
|
+
* Bridge an event into the signal system.
|
|
39
|
+
* Returns a signal that updates whenever the event fires.
|
|
40
|
+
*
|
|
41
|
+
* @param event - Event name to subscribe to
|
|
42
|
+
* @param selector - Transform the payload into the signal value
|
|
43
|
+
* @param initial - Initial signal value before any event fires
|
|
44
|
+
*/
|
|
45
|
+
toSignal<K extends keyof Events, V>(event: K, selector: (payload: Events[K]) => V, initial: V): Signal<V>;
|
|
46
|
+
/**
|
|
47
|
+
* Remove all subscribers. Useful for testing cleanup.
|
|
48
|
+
*/
|
|
49
|
+
clear(): void;
|
|
50
|
+
}
|
|
51
|
+
export {};
|