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/dist/inject.d.ts CHANGED
@@ -11,11 +11,21 @@
11
11
  *
12
12
  * @packageDocumentation
13
13
  */
14
+ import type { ContextKey } from './context-registry.js';
15
+ import type { Expression, EvalFn } from './types.js';
16
+ /**
17
+ * An injectable dependency - either a string name or a typed context key.
18
+ */
19
+ export type Injectable = string | ContextKey<unknown>;
20
+ /**
21
+ * Check if a value is a ContextKey.
22
+ */
23
+ export declare function isContextKey(value: unknown): value is ContextKey<unknown>;
14
24
  /**
15
25
  * A function with optional `$inject` annotation.
16
26
  */
17
27
  interface InjectableFunction extends Function {
18
- $inject?: readonly string[];
28
+ $inject?: readonly Injectable[];
19
29
  }
20
30
  /**
21
31
  * Get the list of injectable dependencies for a function.
@@ -25,7 +35,7 @@ interface InjectableFunction extends Function {
25
35
  * In production, always use `$inject` to survive minification.
26
36
  *
27
37
  * @param fn - The function to inspect
28
- * @returns Array of dependency names
38
+ * @returns Array of dependency names or context keys
29
39
  *
30
40
  * @example
31
41
  * ```ts
@@ -33,10 +43,41 @@ interface InjectableFunction extends Function {
33
43
  * const myDirective = (expr, ctx, el, http, userService) => {};
34
44
  * getInjectables(myDirective); // ['expr', 'ctx', 'el', 'http', 'userService']
35
45
  *
36
- * // Production - explicit annotation
37
- * myDirective.$inject = ['http', 'userService'];
38
- * getInjectables(myDirective); // ['http', 'userService']
46
+ * // Production - explicit annotation with context keys
47
+ * myDirective.$inject = ['$element', SlotContentContext];
48
+ * getInjectables(myDirective); // ['$element', SlotContentContext]
39
49
  * ```
40
50
  */
41
- export declare function getInjectables(fn: InjectableFunction): string[];
51
+ export declare function getInjectables(fn: InjectableFunction): Injectable[];
52
+ /**
53
+ * Configuration for dependency resolution.
54
+ */
55
+ export interface DependencyResolverConfig {
56
+ /** Resolve a ContextKey to its value */
57
+ resolveContext: (key: ContextKey<unknown>) => unknown;
58
+ /** Resolve $scope injectable */
59
+ resolveState: () => Record<string, unknown>;
60
+ /** Resolve $rootState injectable (may be same as state) */
61
+ resolveRootState?: () => Record<string, unknown>;
62
+ /** Resolve custom injectable by name (services, providers) */
63
+ resolveCustom?: (name: string) => unknown | undefined;
64
+ /** Current mode */
65
+ mode: 'server' | 'client';
66
+ }
67
+ /**
68
+ * Resolve dependencies for a directive function.
69
+ *
70
+ * @remarks
71
+ * Unified dependency resolution used by both client and server.
72
+ * Handles $inject arrays, ContextKey injection, and the `using` option.
73
+ *
74
+ * @param fn - The directive function (with optional $inject)
75
+ * @param expr - The expression string from the directive attribute
76
+ * @param element - The target DOM element
77
+ * @param evalFn - Function to evaluate expressions
78
+ * @param config - Resolution configuration
79
+ * @param using - Optional array of context keys to append
80
+ * @returns Array of resolved dependency values
81
+ */
82
+ export declare function resolveDependencies(fn: InjectableFunction, expr: Expression | string, element: Element, evalFn: EvalFn, config: DependencyResolverConfig, using?: ContextKey<unknown>[]): unknown[];
42
83
  export {};
package/dist/inject.js CHANGED
@@ -11,6 +11,12 @@
11
11
  *
12
12
  * @packageDocumentation
13
13
  */
14
+ /**
15
+ * Check if a value is a ContextKey.
16
+ */
17
+ export function isContextKey(value) {
18
+ return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'symbol';
19
+ }
14
20
  /**
15
21
  * Get the list of injectable dependencies for a function.
16
22
  *
@@ -19,7 +25,7 @@
19
25
  * In production, always use `$inject` to survive minification.
20
26
  *
21
27
  * @param fn - The function to inspect
22
- * @returns Array of dependency names
28
+ * @returns Array of dependency names or context keys
23
29
  *
24
30
  * @example
25
31
  * ```ts
@@ -27,9 +33,9 @@
27
33
  * const myDirective = (expr, ctx, el, http, userService) => {};
28
34
  * getInjectables(myDirective); // ['expr', 'ctx', 'el', 'http', 'userService']
29
35
  *
30
- * // Production - explicit annotation
31
- * myDirective.$inject = ['http', 'userService'];
32
- * getInjectables(myDirective); // ['http', 'userService']
36
+ * // Production - explicit annotation with context keys
37
+ * myDirective.$inject = ['$element', SlotContentContext];
38
+ * getInjectables(myDirective); // ['$element', SlotContentContext]
33
39
  * ```
34
40
  */
35
41
  export function getInjectables(fn) {
@@ -61,3 +67,59 @@ function parseFunctionParams(fn) {
61
67
  .map(p => p.replace(/\s*=.*$/, ''))
62
68
  .filter(Boolean);
63
69
  }
70
+ /**
71
+ * Resolve dependencies for a directive function.
72
+ *
73
+ * @remarks
74
+ * Unified dependency resolution used by both client and server.
75
+ * Handles $inject arrays, ContextKey injection, and the `using` option.
76
+ *
77
+ * @param fn - The directive function (with optional $inject)
78
+ * @param expr - The expression string from the directive attribute
79
+ * @param element - The target DOM element
80
+ * @param evalFn - Function to evaluate expressions
81
+ * @param config - Resolution configuration
82
+ * @param using - Optional array of context keys to append
83
+ * @returns Array of resolved dependency values
84
+ */
85
+ export function resolveDependencies(fn, expr, element, evalFn, config, using) {
86
+ const inject = getInjectables(fn);
87
+ const args = inject.map(dep => {
88
+ // Handle ContextKey injection
89
+ if (isContextKey(dep)) {
90
+ return config.resolveContext(dep);
91
+ }
92
+ // Handle string-based injection
93
+ switch (dep) {
94
+ case '$expr':
95
+ return expr;
96
+ case '$element':
97
+ return element;
98
+ case '$eval':
99
+ return evalFn;
100
+ case '$scope':
101
+ return config.resolveState();
102
+ case '$rootState':
103
+ return config.resolveRootState?.() ?? config.resolveState();
104
+ case '$mode':
105
+ return config.mode;
106
+ default: {
107
+ // Look up in custom resolver (services, providers, etc.)
108
+ if (config.resolveCustom) {
109
+ const resolved = config.resolveCustom(dep);
110
+ if (resolved !== undefined) {
111
+ return resolved;
112
+ }
113
+ }
114
+ throw new Error(`Unknown injectable: ${dep}`);
115
+ }
116
+ }
117
+ });
118
+ // Append contexts from `using` option
119
+ if (using?.length) {
120
+ for (const key of using) {
121
+ args.push(config.resolveContext(key));
122
+ }
123
+ }
124
+ return args;
125
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared element processing for structural directives.
3
+ *
4
+ * @remarks
5
+ * Provides a unified way to process directives on elements created by
6
+ * structural directives like g-if and g-for. Supports scope reuse for
7
+ * state preservation across re-renders.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { Mode } from './types.js';
12
+ /** Attribute used to mark elements processed by structural directives */
13
+ export declare const PROCESSED_ATTR = "data-g-processed";
14
+ /**
15
+ * Options for processing element directives.
16
+ */
17
+ export interface ProcessOptions {
18
+ /**
19
+ * Existing scope to use instead of creating a new one.
20
+ * Use this to preserve state across re-renders (e.g., g-if toggle).
21
+ */
22
+ existingScope?: Record<string, unknown>;
23
+ /**
24
+ * Additional properties to add to the scope.
25
+ * Used by g-for to add item/index variables.
26
+ */
27
+ scopeAdditions?: Record<string, unknown>;
28
+ /**
29
+ * Skip processing structural directives (g-for, g-if).
30
+ * Set to true when processing content inside a structural directive
31
+ * to avoid infinite recursion.
32
+ */
33
+ skipStructural?: boolean;
34
+ }
35
+ /**
36
+ * Process directives on an element using registered directives.
37
+ *
38
+ * @remarks
39
+ * This processes all non-structural directives (g-text, g-class, g-show, g-on, etc.)
40
+ * on an element. For structural directives, use the directives directly.
41
+ *
42
+ * @param el - The element to process
43
+ * @param parentScope - The parent scope for variable resolution
44
+ * @param mode - Server or client mode
45
+ * @param options - Processing options
46
+ * @returns The scope used for this element (for chaining/children)
47
+ */
48
+ export declare function processElementDirectives(el: Element, parentScope: Record<string, unknown>, mode: Mode, options?: ProcessOptions): Record<string, unknown>;
49
+ /**
50
+ * Process an element tree (element and all descendants).
51
+ *
52
+ * @remarks
53
+ * Recursively processes directives on an element and all its children.
54
+ * Each child gets its own scope that inherits from the parent.
55
+ *
56
+ * @param el - The root element to process
57
+ * @param parentScope - The parent scope
58
+ * @param mode - Server or client mode
59
+ * @param options - Processing options (existingScope only applies to root element)
60
+ */
61
+ export declare function processElementTree(el: Element, parentScope: Record<string, unknown>, mode: Mode, options?: ProcessOptions): void;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Shared element processing for structural directives.
3
+ *
4
+ * @remarks
5
+ * Provides a unified way to process directives on elements created by
6
+ * structural directives like g-if and g-for. Supports scope reuse for
7
+ * state preservation across re-renders.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { Mode, getDirective } from './types.js';
12
+ import { createContext } from './context.js';
13
+ import { createScope, effect } from './reactivity.js';
14
+ import { resolveDependencies } from './inject.js';
15
+ import { resolveContext } from './context-registry.js';
16
+ import { resolveFromProviders, resolveFromDIProviders } from './providers.js';
17
+ /** Attribute used to mark elements processed by structural directives */
18
+ export const PROCESSED_ATTR = 'data-g-processed';
19
+ /**
20
+ * Create resolver config for dependency resolution.
21
+ *
22
+ * @internal
23
+ */
24
+ function createResolverConfig(el, scope, mode) {
25
+ return {
26
+ resolveContext: (key) => resolveContext(el, key),
27
+ resolveState: () => scope,
28
+ resolveRootState: () => scope,
29
+ resolveCustom: (name) => {
30
+ const diProvided = resolveFromDIProviders(el, name);
31
+ if (diProvided !== undefined)
32
+ return diProvided;
33
+ return resolveFromProviders(el, name);
34
+ },
35
+ mode: mode === Mode.SERVER ? 'server' : 'client'
36
+ };
37
+ }
38
+ /**
39
+ * Set up an event handler on an element.
40
+ *
41
+ * @internal
42
+ */
43
+ function setupEventHandler(el, expr, ctx, scope) {
44
+ const colonIdx = expr.indexOf(':');
45
+ if (colonIdx === -1) {
46
+ return;
47
+ }
48
+ const eventName = expr.slice(0, colonIdx).trim();
49
+ const handlerExpr = expr.slice(colonIdx + 1).trim();
50
+ el.addEventListener(eventName, (event) => {
51
+ const result = ctx.eval(handlerExpr);
52
+ if (typeof result === 'function') {
53
+ result.call(scope, event);
54
+ }
55
+ });
56
+ }
57
+ /**
58
+ * Process directives on an element using registered directives.
59
+ *
60
+ * @remarks
61
+ * This processes all non-structural directives (g-text, g-class, g-show, g-on, etc.)
62
+ * on an element. For structural directives, use the directives directly.
63
+ *
64
+ * @param el - The element to process
65
+ * @param parentScope - The parent scope for variable resolution
66
+ * @param mode - Server or client mode
67
+ * @param options - Processing options
68
+ * @returns The scope used for this element (for chaining/children)
69
+ */
70
+ export function processElementDirectives(el, parentScope, mode, options = {}) {
71
+ const { existingScope, scopeAdditions = {}, skipStructural = true } = options;
72
+ // Mark element as processed
73
+ el.setAttribute(PROCESSED_ATTR, '');
74
+ // Use existing scope or create a new child scope
75
+ const scope = existingScope
76
+ ? (Object.keys(scopeAdditions).length > 0
77
+ ? createScope(existingScope, scopeAdditions)
78
+ : existingScope)
79
+ : createScope(parentScope, scopeAdditions);
80
+ const ctx = createContext(mode, scope);
81
+ // Process g-scope (inline scope initialization)
82
+ const scopeAttr = el.getAttribute('g-scope');
83
+ if (scopeAttr) {
84
+ const scopeValues = ctx.eval(scopeAttr);
85
+ if (scopeValues && typeof scopeValues === 'object') {
86
+ Object.assign(scope, scopeValues);
87
+ }
88
+ }
89
+ // Process g-text
90
+ const textAttr = el.getAttribute('g-text');
91
+ if (textAttr) {
92
+ if (mode === Mode.CLIENT) {
93
+ effect(() => {
94
+ const value = ctx.eval(textAttr);
95
+ el.textContent = String(value ?? '');
96
+ });
97
+ }
98
+ else {
99
+ const value = ctx.eval(textAttr);
100
+ el.textContent = String(value ?? '');
101
+ }
102
+ }
103
+ // Process g-class
104
+ const classAttr = el.getAttribute('g-class');
105
+ if (classAttr) {
106
+ const applyClasses = () => {
107
+ const classObj = ctx.eval(classAttr);
108
+ if (classObj && typeof classObj === 'object') {
109
+ for (const [className, shouldAdd] of Object.entries(classObj)) {
110
+ if (shouldAdd) {
111
+ el.classList.add(className);
112
+ }
113
+ else {
114
+ el.classList.remove(className);
115
+ }
116
+ }
117
+ }
118
+ };
119
+ if (mode === Mode.CLIENT) {
120
+ effect(applyClasses);
121
+ }
122
+ else {
123
+ applyClasses();
124
+ }
125
+ }
126
+ // Process g-show
127
+ const showAttr = el.getAttribute('g-show');
128
+ if (showAttr) {
129
+ const applyShow = () => {
130
+ const value = ctx.eval(showAttr);
131
+ el.style.display = value ? '' : 'none';
132
+ };
133
+ if (mode === Mode.CLIENT) {
134
+ effect(applyShow);
135
+ }
136
+ else {
137
+ applyShow();
138
+ }
139
+ }
140
+ // Process g-on (client only)
141
+ if (mode === Mode.CLIENT) {
142
+ const onAttr = el.getAttribute('g-on');
143
+ if (onAttr) {
144
+ setupEventHandler(el, onAttr, ctx, scope);
145
+ }
146
+ }
147
+ // Process g-model (client only)
148
+ if (mode === Mode.CLIENT) {
149
+ const modelAttr = el.getAttribute('g-model');
150
+ if (modelAttr) {
151
+ const registration = getDirective('g-model');
152
+ if (registration?.fn) {
153
+ const config = createResolverConfig(el, scope, mode);
154
+ const args = resolveDependencies(registration.fn, modelAttr, el, ctx.eval.bind(ctx), config, registration.options.using);
155
+ registration.fn(...args);
156
+ }
157
+ }
158
+ }
159
+ // Process g-html
160
+ const htmlAttr = el.getAttribute('g-html');
161
+ if (htmlAttr) {
162
+ const applyHtml = () => {
163
+ const value = ctx.eval(htmlAttr);
164
+ el.innerHTML = String(value ?? '');
165
+ };
166
+ if (mode === Mode.CLIENT) {
167
+ effect(applyHtml);
168
+ }
169
+ else {
170
+ applyHtml();
171
+ }
172
+ }
173
+ // Process g-bind:* attributes
174
+ const bindAttrs = [...el.attributes].filter(a => a.name.startsWith('g-bind:'));
175
+ for (const attr of bindAttrs) {
176
+ const targetAttr = attr.name.slice('g-bind:'.length);
177
+ const valueExpr = attr.value;
178
+ const applyBinding = () => {
179
+ const value = ctx.eval(valueExpr);
180
+ if (value === null || value === undefined) {
181
+ el.removeAttribute(targetAttr);
182
+ }
183
+ else {
184
+ el.setAttribute(targetAttr, String(value));
185
+ }
186
+ };
187
+ if (mode === Mode.CLIENT) {
188
+ effect(applyBinding);
189
+ }
190
+ else {
191
+ applyBinding();
192
+ }
193
+ }
194
+ return scope;
195
+ }
196
+ /**
197
+ * Process an element tree (element and all descendants).
198
+ *
199
+ * @remarks
200
+ * Recursively processes directives on an element and all its children.
201
+ * Each child gets its own scope that inherits from the parent.
202
+ *
203
+ * @param el - The root element to process
204
+ * @param parentScope - The parent scope
205
+ * @param mode - Server or client mode
206
+ * @param options - Processing options (existingScope only applies to root element)
207
+ */
208
+ export function processElementTree(el, parentScope, mode, options = {}) {
209
+ // Process the root element
210
+ const scope = processElementDirectives(el, parentScope, mode, options);
211
+ // Process children recursively (they get fresh child scopes)
212
+ for (const child of el.children) {
213
+ processElementTree(child, scope, mode, { skipStructural: true });
214
+ }
215
+ }
package/dist/providers.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  import { reactive } from './reactivity.js';
7
+ import { findAncestor } from './dom.js';
7
8
  /**
8
9
  * Local state stored per element.
9
10
  *
@@ -83,15 +84,13 @@ export function registerDIProviders(el, provideMap) {
83
84
  * @internal
84
85
  */
85
86
  export function resolveFromDIProviders(el, name) {
86
- let current = el.parentElement;
87
- while (current) {
88
- const provideMap = diProviders.get(current);
87
+ return findAncestor(el, (e) => {
88
+ const provideMap = diProviders.get(e);
89
89
  if (provideMap && name in provideMap) {
90
90
  return provideMap[name];
91
91
  }
92
- current = current.parentElement;
93
- }
94
- return undefined;
92
+ return undefined;
93
+ });
95
94
  }
96
95
  /**
97
96
  * Resolve a context value from ancestor elements.
@@ -107,15 +106,13 @@ export function resolveFromDIProviders(el, name) {
107
106
  * @internal
108
107
  */
109
108
  export function resolveFromProviders(el, name) {
110
- let current = el.parentElement;
111
- while (current) {
112
- const info = contextProviders.get(current);
109
+ return findAncestor(el, (e) => {
110
+ const info = contextProviders.get(e);
113
111
  if (info?.directive.$context?.includes(name)) {
114
112
  return info.state;
115
113
  }
116
- current = current.parentElement;
117
- }
118
- return undefined;
114
+ return undefined;
115
+ });
119
116
  }
120
117
  /**
121
118
  * Clear local state for an element.
package/dist/scope.js CHANGED
@@ -6,7 +6,9 @@
6
6
  import { reactive } from './reactivity.js';
7
7
  import { createContext } from './context.js';
8
8
  import { Mode } from './types.js';
9
- import { getInjectables } from './inject.js';
9
+ import { resolveDependencies } from './inject.js';
10
+ import { findAncestor } from './dom.js';
11
+ import { resolveContext } from './context-registry.js';
10
12
  /** WeakMap to store element scopes */
11
13
  const elementScopes = new WeakMap();
12
14
  /** Root scope for top-level directives without explicit parent scope */
@@ -73,13 +75,9 @@ export function getElementScope(el) {
73
75
  * @returns The nearest scope, or root scope if none found and fallback enabled
74
76
  */
75
77
  export function findParentScope(el, includeSelf = false, useRootFallback = true) {
76
- let current = includeSelf ? el : el.parentElement;
77
- while (current) {
78
- const scope = elementScopes.get(current);
79
- if (scope) {
80
- return scope;
81
- }
82
- current = current.parentElement;
78
+ const scope = findAncestor(el, (e) => elementScopes.get(e), includeSelf);
79
+ if (scope) {
80
+ return scope;
83
81
  }
84
82
  // Fall back to root scope for top-level directives
85
83
  return useRootFallback ? getRootScope() : undefined;
@@ -112,22 +110,19 @@ fn, options) {
112
110
  const parentScope = findParentScope(this);
113
111
  // Create this element's scope
114
112
  const scope = createElementScope(this, parentScope);
113
+ // Apply assigned values to scope
114
+ if (options.assign) {
115
+ Object.assign(scope, options.assign);
116
+ }
115
117
  // Create context for expression evaluation
116
118
  const ctx = createContext(Mode.CLIENT, scope);
117
- // Resolve dependencies and call directive
118
- const inject = getInjectables(fn);
119
- const args = inject.map((dep) => {
120
- switch (dep) {
121
- case '$element':
122
- return this;
123
- case '$state':
124
- return scope;
125
- case '$eval':
126
- return ctx.eval.bind(ctx);
127
- default:
128
- return undefined;
129
- }
130
- });
119
+ // Resolve dependencies using shared resolver
120
+ const config = {
121
+ resolveContext: (key) => resolveContext(this, key),
122
+ resolveState: () => scope,
123
+ mode: 'client'
124
+ };
125
+ const args = resolveDependencies(fn, '', this, ctx.eval.bind(ctx), config, options.using);
131
126
  const result = fn(...args);
132
127
  // Handle async directives
133
128
  if (result instanceof Promise) {