gonia 0.1.2 → 0.2.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 +24 -7
- package/dist/client/hydrate.js +94 -59
- package/dist/context-registry.d.ts +130 -0
- package/dist/context-registry.js +153 -0
- package/dist/directives/for.d.ts +1 -1
- package/dist/directives/for.js +12 -76
- package/dist/directives/if.d.ts +4 -1
- package/dist/directives/if.js +69 -66
- package/dist/directives/slot.d.ts +4 -2
- package/dist/directives/slot.js +11 -16
- package/dist/directives/template.d.ts +9 -2
- package/dist/directives/template.js +14 -13
- package/dist/dom.d.ts +29 -0
- package/dist/dom.js +39 -0
- package/dist/expression.js +17 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -2
- package/dist/inject.d.ts +47 -6
- package/dist/inject.js +66 -4
- package/dist/process.d.ts +61 -0
- package/dist/process.js +215 -0
- package/dist/providers.js +9 -12
- package/dist/scope.js +17 -22
- package/dist/server/render.js +70 -41
- package/dist/types.d.ts +130 -20
- package/dist/types.js +48 -2
- package/dist/vite/plugin.d.ts +27 -0
- package/dist/vite/plugin.js +56 -3
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -58,18 +58,16 @@ hydrate();
|
|
|
58
58
|
```typescript
|
|
59
59
|
import { directive, Directive } from 'gonia';
|
|
60
60
|
|
|
61
|
-
const myApp: Directive = ($element, $
|
|
62
|
-
// Initialize
|
|
63
|
-
$
|
|
61
|
+
const myApp: Directive<['$element', '$scope']> = ($element, $scope) => {
|
|
62
|
+
// Initialize scope
|
|
63
|
+
$scope.count = 0;
|
|
64
64
|
|
|
65
65
|
// Define methods
|
|
66
|
-
$
|
|
67
|
-
$
|
|
66
|
+
$scope.increment = () => {
|
|
67
|
+
$scope.count++;
|
|
68
68
|
};
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
myApp.$inject = ['$element', '$state'];
|
|
72
|
-
|
|
73
71
|
// Register with scope: true to create isolated state
|
|
74
72
|
directive('my-app', myApp, { scope: true });
|
|
75
73
|
```
|
|
@@ -92,6 +90,8 @@ directive('my-app', myApp, { scope: true });
|
|
|
92
90
|
| `g-class` | Dynamic classes | `<div g-class="{ active: isActive }">` |
|
|
93
91
|
| `g-model` | Two-way binding | `<input g-model="name">` |
|
|
94
92
|
| `g-on` | Event handling | `<button g-on="click: handleClick">` |
|
|
93
|
+
| `g-scope` | Inline scope init | `<div g-scope="{ count: 0 }">` |
|
|
94
|
+
| `g-bind:*` | Dynamic attributes | `<a g-bind:href="link">` |
|
|
95
95
|
|
|
96
96
|
## Vite Integration
|
|
97
97
|
|
|
@@ -114,6 +114,23 @@ See the [docs](./docs) folder for detailed documentation:
|
|
|
114
114
|
- [SSR Guide](./docs/ssr.md)
|
|
115
115
|
- [Reactivity](./docs/reactivity.md)
|
|
116
116
|
|
|
117
|
+
## Roadmap
|
|
118
|
+
|
|
119
|
+
### Done
|
|
120
|
+
- [x] Core directives (`g-text`, `g-show`, `g-if`, `g-for`, `g-class`, `g-model`, `g-on`, `g-scope`, `g-bind:*`, `g-html`)
|
|
121
|
+
- [x] Directive options (`scope`, `template`, `assign`, `provide`, `using`)
|
|
122
|
+
- [x] SSR with client hydration
|
|
123
|
+
- [x] Vite plugin with `$inject` transformation
|
|
124
|
+
- [x] Typed context registry
|
|
125
|
+
- [x] Persistent scopes for `g-if` toggles
|
|
126
|
+
|
|
127
|
+
### Planned
|
|
128
|
+
- [ ] Reducer-based two-way bindings (`scope: { prop: '=' }`)
|
|
129
|
+
- [ ] Scoped CSS with automatic class mangling
|
|
130
|
+
- [ ] Async components with suspense boundaries
|
|
131
|
+
- [ ] Browser devtools extension
|
|
132
|
+
- [ ] Transition system for `g-if`/`g-for`
|
|
133
|
+
|
|
117
134
|
## License
|
|
118
135
|
|
|
119
136
|
MIT
|
package/dist/client/hydrate.js
CHANGED
|
@@ -9,7 +9,9 @@ import { processNativeSlot } from '../directives/slot.js';
|
|
|
9
9
|
import { getLocalState, registerProvider, resolveFromProviders, registerDIProviders, resolveFromDIProviders } from '../providers.js';
|
|
10
10
|
import { FOR_PROCESSED_ATTR } from '../directives/for.js';
|
|
11
11
|
import { findParentScope, createElementScope, getElementScope } from '../scope.js';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveDependencies as resolveInjectables } from '../inject.js';
|
|
13
|
+
import { resolveContext } from '../context-registry.js';
|
|
14
|
+
import { effect } from '../reactivity.js';
|
|
13
15
|
// Built-in directives
|
|
14
16
|
import { text } from '../directives/text.js';
|
|
15
17
|
import { show } from '../directives/show.js';
|
|
@@ -59,6 +61,10 @@ function getSelector(registry) {
|
|
|
59
61
|
}
|
|
60
62
|
// Also match native <slot> elements
|
|
61
63
|
directiveSelectors.push('slot');
|
|
64
|
+
// Match template placeholders from SSR (g-if with false condition)
|
|
65
|
+
directiveSelectors.push('template[data-g-if]');
|
|
66
|
+
// Match g-scope for inline scope initialization
|
|
67
|
+
directiveSelectors.push('[g-scope]');
|
|
62
68
|
cachedSelector = directiveSelectors.join(',');
|
|
63
69
|
}
|
|
64
70
|
return cachedSelector;
|
|
@@ -73,7 +79,14 @@ function getDirectivesForElement(el, registry) {
|
|
|
73
79
|
for (const [name, directive] of registry) {
|
|
74
80
|
const attr = el.getAttribute(`g-${name}`);
|
|
75
81
|
if (attr !== null) {
|
|
76
|
-
|
|
82
|
+
// Look up options from the global directive registry
|
|
83
|
+
const registration = getDirective(`g-${name}`);
|
|
84
|
+
directives.push({
|
|
85
|
+
name,
|
|
86
|
+
directive,
|
|
87
|
+
expr: attr,
|
|
88
|
+
using: registration?.options.using
|
|
89
|
+
});
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
// Sort by priority (higher first)
|
|
@@ -124,48 +137,28 @@ export function setElementContext(el, ctx) {
|
|
|
124
137
|
contextCache.set(el, ctx);
|
|
125
138
|
}
|
|
126
139
|
/**
|
|
127
|
-
*
|
|
140
|
+
* Create resolver config for client-side dependency resolution.
|
|
128
141
|
*
|
|
129
142
|
* @internal
|
|
130
143
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
default: {
|
|
150
|
-
// Look up in ancestor DI providers first (provide option)
|
|
151
|
-
const diProvided = resolveFromDIProviders(el, name);
|
|
152
|
-
if (diProvided !== undefined) {
|
|
153
|
-
return diProvided;
|
|
154
|
-
}
|
|
155
|
-
// Look up in global services registry
|
|
156
|
-
const service = services.get(name);
|
|
157
|
-
if (service !== undefined) {
|
|
158
|
-
return service;
|
|
159
|
-
}
|
|
160
|
-
// Look up in ancestor context providers ($context)
|
|
161
|
-
const contextProvided = resolveFromProviders(el, name);
|
|
162
|
-
if (contextProvided !== undefined) {
|
|
163
|
-
return contextProvided;
|
|
164
|
-
}
|
|
165
|
-
throw new Error(`Unknown injectable: ${name}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
144
|
+
function createClientResolverConfig(el, ctx) {
|
|
145
|
+
return {
|
|
146
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
147
|
+
resolveState: () => findParentScope(el, true) ?? getLocalState(el),
|
|
148
|
+
resolveCustom: (name) => {
|
|
149
|
+
// Look up in ancestor DI providers first (provide option)
|
|
150
|
+
const diProvided = resolveFromDIProviders(el, name);
|
|
151
|
+
if (diProvided !== undefined)
|
|
152
|
+
return diProvided;
|
|
153
|
+
// Look up in global services registry
|
|
154
|
+
const service = services.get(name);
|
|
155
|
+
if (service !== undefined)
|
|
156
|
+
return service;
|
|
157
|
+
// Look up in ancestor context providers ($context)
|
|
158
|
+
return resolveFromProviders(el, name);
|
|
159
|
+
},
|
|
160
|
+
mode: 'client'
|
|
161
|
+
};
|
|
169
162
|
}
|
|
170
163
|
/**
|
|
171
164
|
* Process directives on a single element.
|
|
@@ -184,15 +177,60 @@ function processElement(el, registry) {
|
|
|
184
177
|
processNativeSlot(el);
|
|
185
178
|
return;
|
|
186
179
|
}
|
|
180
|
+
// Handle template placeholders from SSR (g-if with false condition)
|
|
181
|
+
if (el.tagName === 'TEMPLATE' && el.hasAttribute('data-g-if')) {
|
|
182
|
+
const ifDirective = registry.get('if');
|
|
183
|
+
if (ifDirective) {
|
|
184
|
+
const expr = el.getAttribute('data-g-if') || '';
|
|
185
|
+
const ctx = getContextForElement(el);
|
|
186
|
+
const config = createClientResolverConfig(el, ctx);
|
|
187
|
+
const registration = getDirective('g-if');
|
|
188
|
+
const args = resolveInjectables(ifDirective, expr, el, ctx.eval.bind(ctx), config, registration?.options.using);
|
|
189
|
+
const result = ifDirective(...args);
|
|
190
|
+
if (result instanceof Promise) {
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
187
196
|
const directives = getDirectivesForElement(el, registry);
|
|
188
|
-
|
|
197
|
+
const hasScopeAttr = el.hasAttribute('g-scope');
|
|
198
|
+
const hasBindAttrs = [...el.attributes].some(a => a.name.startsWith('g-bind:'));
|
|
199
|
+
// Skip if nothing to process
|
|
200
|
+
if (directives.length === 0 && !hasScopeAttr && !hasBindAttrs)
|
|
189
201
|
return;
|
|
190
202
|
const ctx = getContextForElement(el);
|
|
203
|
+
const scope = findParentScope(el, true) ?? {};
|
|
204
|
+
// Process g-scope first (inline scope initialization)
|
|
205
|
+
if (hasScopeAttr) {
|
|
206
|
+
const scopeAttr = el.getAttribute('g-scope');
|
|
207
|
+
const scopeValues = ctx.eval(scopeAttr);
|
|
208
|
+
if (scopeValues && typeof scopeValues === 'object') {
|
|
209
|
+
Object.assign(scope, scopeValues);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Process g-bind:* attributes (dynamic attribute binding with reactivity)
|
|
213
|
+
for (const attr of [...el.attributes]) {
|
|
214
|
+
if (attr.name.startsWith('g-bind:')) {
|
|
215
|
+
const targetAttr = attr.name.slice('g-bind:'.length);
|
|
216
|
+
const valueExpr = attr.value;
|
|
217
|
+
effect(() => {
|
|
218
|
+
const value = ctx.eval(valueExpr);
|
|
219
|
+
if (value === null || value === undefined) {
|
|
220
|
+
el.removeAttribute(targetAttr);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
el.setAttribute(targetAttr, String(value));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
191
228
|
// Process directives sequentially, handling async ones properly
|
|
192
229
|
let chain;
|
|
193
|
-
for (const { directive, expr } of directives) {
|
|
230
|
+
for (const { directive, expr, using } of directives) {
|
|
194
231
|
const processDirective = () => {
|
|
195
|
-
const
|
|
232
|
+
const config = createClientResolverConfig(el, ctx);
|
|
233
|
+
const args = resolveInjectables(directive, expr, el, ctx.eval.bind(ctx), config, using);
|
|
196
234
|
const result = directive(...args);
|
|
197
235
|
// Register as provider if directive declares $context
|
|
198
236
|
if (directive.$context?.length) {
|
|
@@ -330,8 +368,8 @@ async function processDirectiveElements() {
|
|
|
330
368
|
}
|
|
331
369
|
const { fn, options } = registration;
|
|
332
370
|
// Only process directives with templates (web components),
|
|
333
|
-
// scope: true,
|
|
334
|
-
if (!options.template && !options.scope && !options.provide) {
|
|
371
|
+
// scope: true, provide (DI overrides), or using (context dependencies)
|
|
372
|
+
if (!options.template && !options.scope && !options.provide && !options.using) {
|
|
335
373
|
continue;
|
|
336
374
|
}
|
|
337
375
|
// Find all elements matching this directive's tag name
|
|
@@ -346,6 +384,10 @@ async function processDirectiveElements() {
|
|
|
346
384
|
if (options.scope) {
|
|
347
385
|
const parentScope = findParentScope(el);
|
|
348
386
|
scope = createElementScope(el, parentScope);
|
|
387
|
+
// Apply assigned values to scope
|
|
388
|
+
if (options.assign) {
|
|
389
|
+
Object.assign(scope, options.assign);
|
|
390
|
+
}
|
|
349
391
|
}
|
|
350
392
|
else {
|
|
351
393
|
scope = findParentScope(el, true) ?? {};
|
|
@@ -357,19 +399,12 @@ async function processDirectiveElements() {
|
|
|
357
399
|
// 3. Call directive function if present (initializes state)
|
|
358
400
|
if (fn) {
|
|
359
401
|
const ctx = createContext(Mode.CLIENT, scope);
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return scope;
|
|
367
|
-
case '$eval':
|
|
368
|
-
return ctx.eval.bind(ctx);
|
|
369
|
-
default:
|
|
370
|
-
return undefined;
|
|
371
|
-
}
|
|
372
|
-
});
|
|
402
|
+
const config = {
|
|
403
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
404
|
+
resolveState: () => scope,
|
|
405
|
+
mode: 'client'
|
|
406
|
+
};
|
|
407
|
+
const args = resolveInjectables(fn, '', el, ctx.eval.bind(ctx), config, options.using);
|
|
373
408
|
const result = fn(...args);
|
|
374
409
|
if (result instanceof Promise) {
|
|
375
410
|
await result;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe context registry for sharing data across DOM ancestors/descendants.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Provides a unified system for registering and resolving typed context values
|
|
6
|
+
* on DOM elements. Similar to React's Context or Vue's provide/inject but with
|
|
7
|
+
* full type safety through branded context keys.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* A branded context key that provides type safety for context values.
|
|
13
|
+
*
|
|
14
|
+
* @typeParam T - The type of value this context holds
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* The `__type` property is a phantom type - it doesn't exist at runtime
|
|
18
|
+
* but provides compile-time type checking when registering and resolving.
|
|
19
|
+
*/
|
|
20
|
+
export interface ContextKey<T> {
|
|
21
|
+
/** Unique identifier for this context */
|
|
22
|
+
readonly id: symbol;
|
|
23
|
+
/** Debug name for error messages */
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** Phantom type for TypeScript inference */
|
|
26
|
+
readonly __type?: T;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a typed context key.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam T - The type of value this context will hold
|
|
32
|
+
* @param name - Debug name for the context (used in error messages)
|
|
33
|
+
* @returns A unique context key
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* interface UserData {
|
|
38
|
+
* name: string;
|
|
39
|
+
* email: string;
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* const UserContext = createContextKey<UserData>('User');
|
|
43
|
+
*
|
|
44
|
+
* // Register on an element
|
|
45
|
+
* registerContext(el, UserContext, { name: 'Alice', email: 'alice@example.com' });
|
|
46
|
+
*
|
|
47
|
+
* // Resolve from a descendant - fully typed!
|
|
48
|
+
* const user = resolveContext(childEl, UserContext);
|
|
49
|
+
* // user is UserData | undefined
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function createContextKey<T>(name: string): ContextKey<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Register a context value on an element.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam T - The context value type (inferred from key)
|
|
57
|
+
* @param el - The element to register the context on
|
|
58
|
+
* @param key - The context key
|
|
59
|
+
* @param value - The value to store
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* Descendants can resolve this context using `resolveContext`.
|
|
63
|
+
* If a context with the same key is already registered on this element,
|
|
64
|
+
* it will be overwritten.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
|
|
69
|
+
*
|
|
70
|
+
* registerContext(rootEl, ThemeContext, { mode: 'dark' });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function registerContext<T>(el: Element, key: ContextKey<T>, value: T): void;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a context value from an ancestor element.
|
|
76
|
+
*
|
|
77
|
+
* @typeParam T - The context value type (inferred from key)
|
|
78
|
+
* @param el - The element to start searching from
|
|
79
|
+
* @param key - The context key to look for
|
|
80
|
+
* @param includeSelf - Whether to check the element itself (default: false)
|
|
81
|
+
* @returns The context value, or undefined if not found
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* Walks up the DOM tree from the element's parent (or the element itself
|
|
85
|
+
* if `includeSelf` is true), looking for an ancestor with the specified
|
|
86
|
+
* context registered.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
|
|
91
|
+
*
|
|
92
|
+
* // Somewhere up the tree, ThemeContext was registered
|
|
93
|
+
* const theme = resolveContext(el, ThemeContext);
|
|
94
|
+
* if (theme) {
|
|
95
|
+
* console.log(theme.mode); // 'light' or 'dark'
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveContext<T>(el: Element, key: ContextKey<T>, includeSelf?: boolean): T | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Check if a context is registered on an element.
|
|
102
|
+
*
|
|
103
|
+
* @param el - The element to check
|
|
104
|
+
* @param key - The context key
|
|
105
|
+
* @returns True if the context is registered on this specific element
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* This only checks the element itself, not ancestors.
|
|
109
|
+
* Use `resolveContext` to search up the tree.
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasContext<T>(el: Element, key: ContextKey<T>): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Remove a context from an element.
|
|
114
|
+
*
|
|
115
|
+
* @param el - The element to remove the context from
|
|
116
|
+
* @param key - The context key to remove
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* Does nothing if the context wasn't registered.
|
|
120
|
+
*/
|
|
121
|
+
export declare function removeContext<T>(el: Element, key: ContextKey<T>): void;
|
|
122
|
+
/**
|
|
123
|
+
* Clear all contexts from an element.
|
|
124
|
+
*
|
|
125
|
+
* @param el - The element to clear
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* Called during element cleanup/removal.
|
|
129
|
+
*/
|
|
130
|
+
export declare function clearContexts(el: Element): void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe context registry for sharing data across DOM ancestors/descendants.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Provides a unified system for registering and resolving typed context values
|
|
6
|
+
* on DOM elements. Similar to React's Context or Vue's provide/inject but with
|
|
7
|
+
* full type safety through branded context keys.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { findAncestor } from './dom.js';
|
|
12
|
+
/**
|
|
13
|
+
* Storage for all contexts per element.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
const contexts = new WeakMap();
|
|
18
|
+
/**
|
|
19
|
+
* Create a typed context key.
|
|
20
|
+
*
|
|
21
|
+
* @typeParam T - The type of value this context will hold
|
|
22
|
+
* @param name - Debug name for the context (used in error messages)
|
|
23
|
+
* @returns A unique context key
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* interface UserData {
|
|
28
|
+
* name: string;
|
|
29
|
+
* email: string;
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* const UserContext = createContextKey<UserData>('User');
|
|
33
|
+
*
|
|
34
|
+
* // Register on an element
|
|
35
|
+
* registerContext(el, UserContext, { name: 'Alice', email: 'alice@example.com' });
|
|
36
|
+
*
|
|
37
|
+
* // Resolve from a descendant - fully typed!
|
|
38
|
+
* const user = resolveContext(childEl, UserContext);
|
|
39
|
+
* // user is UserData | undefined
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function createContextKey(name) {
|
|
43
|
+
return {
|
|
44
|
+
id: Symbol(name),
|
|
45
|
+
name,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register a context value on an element.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam T - The context value type (inferred from key)
|
|
52
|
+
* @param el - The element to register the context on
|
|
53
|
+
* @param key - The context key
|
|
54
|
+
* @param value - The value to store
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* Descendants can resolve this context using `resolveContext`.
|
|
58
|
+
* If a context with the same key is already registered on this element,
|
|
59
|
+
* it will be overwritten.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
|
|
64
|
+
*
|
|
65
|
+
* registerContext(rootEl, ThemeContext, { mode: 'dark' });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function registerContext(el, key, value) {
|
|
69
|
+
let map = contexts.get(el);
|
|
70
|
+
if (!map) {
|
|
71
|
+
map = new Map();
|
|
72
|
+
contexts.set(el, map);
|
|
73
|
+
}
|
|
74
|
+
map.set(key.id, value);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a context value from an ancestor element.
|
|
78
|
+
*
|
|
79
|
+
* @typeParam T - The context value type (inferred from key)
|
|
80
|
+
* @param el - The element to start searching from
|
|
81
|
+
* @param key - The context key to look for
|
|
82
|
+
* @param includeSelf - Whether to check the element itself (default: false)
|
|
83
|
+
* @returns The context value, or undefined if not found
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* Walks up the DOM tree from the element's parent (or the element itself
|
|
87
|
+
* if `includeSelf` is true), looking for an ancestor with the specified
|
|
88
|
+
* context registered.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
|
|
93
|
+
*
|
|
94
|
+
* // Somewhere up the tree, ThemeContext was registered
|
|
95
|
+
* const theme = resolveContext(el, ThemeContext);
|
|
96
|
+
* if (theme) {
|
|
97
|
+
* console.log(theme.mode); // 'light' or 'dark'
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function resolveContext(el, key, includeSelf = false) {
|
|
102
|
+
return findAncestor(el, (e) => {
|
|
103
|
+
const map = contexts.get(e);
|
|
104
|
+
if (map?.has(key.id)) {
|
|
105
|
+
return map.get(key.id);
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}, includeSelf);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if a context is registered on an element.
|
|
112
|
+
*
|
|
113
|
+
* @param el - The element to check
|
|
114
|
+
* @param key - The context key
|
|
115
|
+
* @returns True if the context is registered on this specific element
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* This only checks the element itself, not ancestors.
|
|
119
|
+
* Use `resolveContext` to search up the tree.
|
|
120
|
+
*/
|
|
121
|
+
export function hasContext(el, key) {
|
|
122
|
+
const map = contexts.get(el);
|
|
123
|
+
return map?.has(key.id) ?? false;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Remove a context from an element.
|
|
127
|
+
*
|
|
128
|
+
* @param el - The element to remove the context from
|
|
129
|
+
* @param key - The context key to remove
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* Does nothing if the context wasn't registered.
|
|
133
|
+
*/
|
|
134
|
+
export function removeContext(el, key) {
|
|
135
|
+
const map = contexts.get(el);
|
|
136
|
+
if (map) {
|
|
137
|
+
map.delete(key.id);
|
|
138
|
+
if (map.size === 0) {
|
|
139
|
+
contexts.delete(el);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clear all contexts from an element.
|
|
145
|
+
*
|
|
146
|
+
* @param el - The element to clear
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
149
|
+
* Called during element cleanup/removal.
|
|
150
|
+
*/
|
|
151
|
+
export function clearContexts(el) {
|
|
152
|
+
contexts.delete(el);
|
|
153
|
+
}
|
package/dist/directives/for.d.ts
CHANGED
|
@@ -26,4 +26,4 @@ export declare const FOR_TEMPLATE_ATTR = "data-g-for-template";
|
|
|
26
26
|
* <div g-for="(value, key) in object" g-text="key + ': ' + value"></div>
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
export declare const cfor: Directive<['$expr', '$element', '$eval', '$
|
|
29
|
+
export declare const cfor: Directive<['$expr', '$element', '$eval', '$scope', '$mode']>;
|