gonia 0.1.2 → 0.1.3
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/dist/client/hydrate.js +40 -58
- package/dist/context-registry.d.ts +130 -0
- package/dist/context-registry.js +153 -0
- 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/index.d.ts +6 -2
- package/dist/index.js +4 -2
- package/dist/inject.d.ts +47 -6
- package/dist/inject.js +66 -4
- package/dist/providers.js +9 -12
- package/dist/scope.js +13 -22
- package/dist/server/render.js +29 -40
- package/dist/types.d.ts +88 -11
- package/dist/types.js +41 -0
- package/dist/vite/plugin.d.ts +27 -0
- package/dist/vite/plugin.js +56 -3
- package/package.json +5 -1
package/dist/client/hydrate.js
CHANGED
|
@@ -9,7 +9,8 @@ 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';
|
|
13
14
|
// Built-in directives
|
|
14
15
|
import { text } from '../directives/text.js';
|
|
15
16
|
import { show } from '../directives/show.js';
|
|
@@ -73,7 +74,14 @@ function getDirectivesForElement(el, registry) {
|
|
|
73
74
|
for (const [name, directive] of registry) {
|
|
74
75
|
const attr = el.getAttribute(`g-${name}`);
|
|
75
76
|
if (attr !== null) {
|
|
76
|
-
|
|
77
|
+
// Look up options from the global directive registry
|
|
78
|
+
const registration = getDirective(`g-${name}`);
|
|
79
|
+
directives.push({
|
|
80
|
+
name,
|
|
81
|
+
directive,
|
|
82
|
+
expr: attr,
|
|
83
|
+
using: registration?.options.using
|
|
84
|
+
});
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
// Sort by priority (higher first)
|
|
@@ -124,48 +132,28 @@ export function setElementContext(el, ctx) {
|
|
|
124
132
|
contextCache.set(el, ctx);
|
|
125
133
|
}
|
|
126
134
|
/**
|
|
127
|
-
*
|
|
135
|
+
* Create resolver config for client-side dependency resolution.
|
|
128
136
|
*
|
|
129
137
|
* @internal
|
|
130
138
|
*/
|
|
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
|
-
});
|
|
139
|
+
function createClientResolverConfig(el, ctx) {
|
|
140
|
+
return {
|
|
141
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
142
|
+
resolveState: () => findParentScope(el, true) ?? getLocalState(el),
|
|
143
|
+
resolveCustom: (name) => {
|
|
144
|
+
// Look up in ancestor DI providers first (provide option)
|
|
145
|
+
const diProvided = resolveFromDIProviders(el, name);
|
|
146
|
+
if (diProvided !== undefined)
|
|
147
|
+
return diProvided;
|
|
148
|
+
// Look up in global services registry
|
|
149
|
+
const service = services.get(name);
|
|
150
|
+
if (service !== undefined)
|
|
151
|
+
return service;
|
|
152
|
+
// Look up in ancestor context providers ($context)
|
|
153
|
+
return resolveFromProviders(el, name);
|
|
154
|
+
},
|
|
155
|
+
mode: 'client'
|
|
156
|
+
};
|
|
169
157
|
}
|
|
170
158
|
/**
|
|
171
159
|
* Process directives on a single element.
|
|
@@ -190,9 +178,10 @@ function processElement(el, registry) {
|
|
|
190
178
|
const ctx = getContextForElement(el);
|
|
191
179
|
// Process directives sequentially, handling async ones properly
|
|
192
180
|
let chain;
|
|
193
|
-
for (const { directive, expr } of directives) {
|
|
181
|
+
for (const { directive, expr, using } of directives) {
|
|
194
182
|
const processDirective = () => {
|
|
195
|
-
const
|
|
183
|
+
const config = createClientResolverConfig(el, ctx);
|
|
184
|
+
const args = resolveInjectables(directive, expr, el, ctx.eval.bind(ctx), config, using);
|
|
196
185
|
const result = directive(...args);
|
|
197
186
|
// Register as provider if directive declares $context
|
|
198
187
|
if (directive.$context?.length) {
|
|
@@ -330,8 +319,8 @@ async function processDirectiveElements() {
|
|
|
330
319
|
}
|
|
331
320
|
const { fn, options } = registration;
|
|
332
321
|
// Only process directives with templates (web components),
|
|
333
|
-
// scope: true,
|
|
334
|
-
if (!options.template && !options.scope && !options.provide) {
|
|
322
|
+
// scope: true, provide (DI overrides), or using (context dependencies)
|
|
323
|
+
if (!options.template && !options.scope && !options.provide && !options.using) {
|
|
335
324
|
continue;
|
|
336
325
|
}
|
|
337
326
|
// Find all elements matching this directive's tag name
|
|
@@ -357,19 +346,12 @@ async function processDirectiveElements() {
|
|
|
357
346
|
// 3. Call directive function if present (initializes state)
|
|
358
347
|
if (fn) {
|
|
359
348
|
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
|
-
});
|
|
349
|
+
const config = {
|
|
350
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
351
|
+
resolveState: () => scope,
|
|
352
|
+
mode: 'client'
|
|
353
|
+
};
|
|
354
|
+
const args = resolveInjectables(fn, '', el, ctx.eval.bind(ctx), config, options.using);
|
|
373
355
|
const result = fn(...args);
|
|
374
356
|
if (result instanceof Promise) {
|
|
375
357
|
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
|
+
}
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* @packageDocumentation
|
|
10
10
|
*/
|
|
11
11
|
import { Directive } from '../types.js';
|
|
12
|
+
import { SlotContentContext } from './template.js';
|
|
12
13
|
/**
|
|
13
14
|
* Slot directive for content transclusion.
|
|
14
15
|
*
|
|
15
16
|
* @remarks
|
|
16
17
|
* Finds the nearest template ancestor and transcludes the
|
|
17
|
-
* matching slot content into itself.
|
|
18
|
+
* matching slot content into itself. The SlotContentContext
|
|
19
|
+
* is automatically injected via DI.
|
|
18
20
|
*
|
|
19
21
|
* If the slot name is an expression, wraps in an effect
|
|
20
22
|
* for reactivity.
|
|
@@ -35,7 +37,7 @@ import { Directive } from '../types.js';
|
|
|
35
37
|
* <slot></slot>
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
|
-
export declare const slot: Directive<['$expr', '$element', '$eval']>;
|
|
40
|
+
export declare const slot: Directive<['$expr', '$element', '$eval', typeof SlotContentContext]>;
|
|
39
41
|
/**
|
|
40
42
|
* Process native <slot> elements.
|
|
41
43
|
*
|
package/dist/directives/slot.js
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { directive } from '../types.js';
|
|
12
12
|
import { effect } from '../reactivity.js';
|
|
13
|
-
import {
|
|
13
|
+
import { resolveContext } from '../context-registry.js';
|
|
14
|
+
import { SlotContentContext } from './template.js';
|
|
14
15
|
/**
|
|
15
16
|
* Slot directive for content transclusion.
|
|
16
17
|
*
|
|
17
18
|
* @remarks
|
|
18
19
|
* Finds the nearest template ancestor and transcludes the
|
|
19
|
-
* matching slot content into itself.
|
|
20
|
+
* matching slot content into itself. The SlotContentContext
|
|
21
|
+
* is automatically injected via DI.
|
|
20
22
|
*
|
|
21
23
|
* If the slot name is an expression, wraps in an effect
|
|
22
24
|
* for reactivity.
|
|
@@ -37,7 +39,7 @@ import { findTemplateAncestor, getSavedContent } from './template.js';
|
|
|
37
39
|
* <slot></slot>
|
|
38
40
|
* ```
|
|
39
41
|
*/
|
|
40
|
-
export const slot = function slot($expr, $element, $eval) {
|
|
42
|
+
export const slot = function slot($expr, $element, $eval, $slotContent) {
|
|
41
43
|
// Determine slot name
|
|
42
44
|
// If expr is empty, check for name attribute, otherwise use 'default'
|
|
43
45
|
const getName = () => {
|
|
@@ -50,16 +52,11 @@ export const slot = function slot($expr, $element, $eval) {
|
|
|
50
52
|
};
|
|
51
53
|
const transclude = () => {
|
|
52
54
|
const name = getName();
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
// No template ancestor - leave slot as-is or clear it
|
|
55
|
+
// SlotContentContext is injected via DI
|
|
56
|
+
if (!$slotContent) {
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
|
-
const
|
|
59
|
-
if (!content) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const slotContent = content.slots.get(name);
|
|
59
|
+
const slotContent = $slotContent.slots.get(name);
|
|
63
60
|
if (slotContent) {
|
|
64
61
|
$element.innerHTML = slotContent;
|
|
65
62
|
}
|
|
@@ -73,6 +70,7 @@ export const slot = function slot($expr, $element, $eval) {
|
|
|
73
70
|
transclude();
|
|
74
71
|
}
|
|
75
72
|
};
|
|
73
|
+
slot.$inject = ['$expr', '$element', '$eval', SlotContentContext];
|
|
76
74
|
directive('g-slot', slot);
|
|
77
75
|
/**
|
|
78
76
|
* Process native <slot> elements.
|
|
@@ -85,11 +83,8 @@ directive('g-slot', slot);
|
|
|
85
83
|
*/
|
|
86
84
|
export function processNativeSlot(el) {
|
|
87
85
|
const name = el.getAttribute('name') ?? 'default';
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const content = getSavedContent(templateEl);
|
|
86
|
+
// Resolve slot content from nearest template ancestor
|
|
87
|
+
const content = resolveContext(el, SlotContentContext);
|
|
93
88
|
if (!content) {
|
|
94
89
|
return;
|
|
95
90
|
}
|
|
@@ -10,15 +10,22 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { Directive } from '../types.js';
|
|
12
12
|
import { EffectScope } from '../reactivity.js';
|
|
13
|
+
import { ContextKey } from '../context-registry.js';
|
|
13
14
|
/**
|
|
14
15
|
* Saved slot content for an element.
|
|
15
|
-
*
|
|
16
|
-
* @internal
|
|
17
16
|
*/
|
|
18
17
|
export interface SlotContent {
|
|
19
18
|
/** Content by slot name. 'default' for unnamed content. */
|
|
20
19
|
slots: Map<string, string>;
|
|
21
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Context key for slot content.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Templates register their slot content using this context, and
|
|
26
|
+
* slot directives resolve it to find their content.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SlotContentContext: ContextKey<SlotContent>;
|
|
22
29
|
/**
|
|
23
30
|
* Get saved slot content for an element.
|
|
24
31
|
*
|
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { directive, DirectivePriority } from '../types.js';
|
|
12
12
|
import { createEffectScope } from '../reactivity.js';
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
import { findAncestor } from '../dom.js';
|
|
14
|
+
import { createContextKey, registerContext, resolveContext, hasContext } from '../context-registry.js';
|
|
15
|
+
/**
|
|
16
|
+
* Context key for slot content.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Templates register their slot content using this context, and
|
|
20
|
+
* slot directives resolve it to find their content.
|
|
21
|
+
*/
|
|
22
|
+
export const SlotContentContext = createContextKey('SlotContent');
|
|
15
23
|
/** WeakMap storing effect scopes per element for cleanup. */
|
|
16
24
|
const elementScopes = new WeakMap();
|
|
17
25
|
/** Set tracking which templates are currently rendering (cycle detection). */
|
|
@@ -22,7 +30,7 @@ const renderingChain = new WeakMap();
|
|
|
22
30
|
* @internal
|
|
23
31
|
*/
|
|
24
32
|
export function getSavedContent(el) {
|
|
25
|
-
return
|
|
33
|
+
return resolveContext(el, SlotContentContext, true);
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* Find the nearest ancestor with saved content (the template element).
|
|
@@ -30,14 +38,7 @@ export function getSavedContent(el) {
|
|
|
30
38
|
* @internal
|
|
31
39
|
*/
|
|
32
40
|
export function findTemplateAncestor(el) {
|
|
33
|
-
|
|
34
|
-
while (current) {
|
|
35
|
-
if (savedContent.has(current)) {
|
|
36
|
-
return current;
|
|
37
|
-
}
|
|
38
|
-
current = current.parentElement;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
+
return findAncestor(el, (e) => hasContext(e, SlotContentContext) ? e : undefined) ?? null;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
44
|
* Check if a node is an Element.
|
|
@@ -116,11 +117,11 @@ export const template = async function template($expr, $element, $templates) {
|
|
|
116
117
|
console.error(`Cycle detected: template "${templateName}" is already being rendered`);
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
|
-
// Save children for slots
|
|
120
|
+
// Save children for slots using typed context
|
|
120
121
|
const slotContent = {
|
|
121
122
|
slots: extractSlotContent($element)
|
|
122
123
|
};
|
|
123
|
-
|
|
124
|
+
registerContext($element, SlotContentContext, slotContent);
|
|
124
125
|
// Track this template in the chain
|
|
125
126
|
const newChain = new Set(chain);
|
|
126
127
|
newChain.add(templateName);
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM traversal utilities.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Find an ancestor element matching a predicate.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Walks up the DOM tree starting from the element's parent (or the element
|
|
11
|
+
* itself if `includeSelf` is true), calling the predicate on each ancestor.
|
|
12
|
+
* Returns the first non-undefined result from the predicate.
|
|
13
|
+
*
|
|
14
|
+
* @typeParam T - The type returned by the predicate
|
|
15
|
+
* @param el - The element to start from
|
|
16
|
+
* @param predicate - Function called on each ancestor, returns a value or undefined
|
|
17
|
+
* @param includeSelf - Whether to check the element itself (default: false)
|
|
18
|
+
* @returns The first non-undefined result from the predicate, or undefined
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Find ancestor with a specific attribute
|
|
23
|
+
* const ancestor = findAncestor(el, (e) => e.hasAttribute('data-scope') ? e : undefined);
|
|
24
|
+
*
|
|
25
|
+
* // Find value from a WeakMap on an ancestor
|
|
26
|
+
* const value = findAncestor(el, (e) => myWeakMap.get(e));
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function findAncestor<T>(el: Element, predicate: (el: Element) => T | undefined, includeSelf?: boolean): T | undefined;
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM traversal utilities.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Find an ancestor element matching a predicate.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Walks up the DOM tree starting from the element's parent (or the element
|
|
11
|
+
* itself if `includeSelf` is true), calling the predicate on each ancestor.
|
|
12
|
+
* Returns the first non-undefined result from the predicate.
|
|
13
|
+
*
|
|
14
|
+
* @typeParam T - The type returned by the predicate
|
|
15
|
+
* @param el - The element to start from
|
|
16
|
+
* @param predicate - Function called on each ancestor, returns a value or undefined
|
|
17
|
+
* @param includeSelf - Whether to check the element itself (default: false)
|
|
18
|
+
* @returns The first non-undefined result from the predicate, or undefined
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Find ancestor with a specific attribute
|
|
23
|
+
* const ancestor = findAncestor(el, (e) => e.hasAttribute('data-scope') ? e : undefined);
|
|
24
|
+
*
|
|
25
|
+
* // Find value from a WeakMap on an ancestor
|
|
26
|
+
* const value = findAncestor(el, (e) => myWeakMap.get(e));
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function findAncestor(el, predicate, includeSelf = false) {
|
|
30
|
+
let current = includeSelf ? el : el.parentElement;
|
|
31
|
+
while (current) {
|
|
32
|
+
const result = predicate(current);
|
|
33
|
+
if (result !== undefined) {
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
current = current.parentElement;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|