gonia 0.2.1 → 0.2.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.
@@ -70,8 +70,8 @@ export const slot = function slot($expr, $element, $eval, $slotContent) {
70
70
  transclude();
71
71
  }
72
72
  };
73
- slot.$inject = ['$expr', '$element', '$eval', SlotContentContext];
74
- directive('g-slot', slot);
73
+ slot.$inject = ['$expr', '$element', '$eval'];
74
+ directive('g-slot', slot, { using: [SlotContentContext] });
75
75
  /**
76
76
  * Process native <slot> elements.
77
77
  *
package/dist/inject.d.ts CHANGED
@@ -14,9 +14,10 @@
14
14
  import type { ContextKey } from './context-registry.js';
15
15
  import type { Expression, EvalFn } from './types.js';
16
16
  /**
17
- * An injectable dependency - either a string name or a typed context key.
17
+ * An injectable dependency name.
18
+ * For ContextKey injection, use the `using` option on directive registration.
18
19
  */
19
- export type Injectable = string | ContextKey<unknown>;
20
+ export type Injectable = string;
20
21
  /**
21
22
  * Check if a value is a ContextKey.
22
23
  */
@@ -25,7 +26,7 @@ export declare function isContextKey(value: unknown): value is ContextKey<unknow
25
26
  * A function with optional `$inject` annotation.
26
27
  */
27
28
  interface InjectableFunction extends Function {
28
- $inject?: readonly Injectable[];
29
+ $inject?: readonly string[];
29
30
  }
30
31
  /**
31
32
  * Get the list of injectable dependencies for a function.
@@ -43,10 +44,12 @@ interface InjectableFunction extends Function {
43
44
  * const myDirective = (expr, ctx, el, http, userService) => {};
44
45
  * getInjectables(myDirective); // ['expr', 'ctx', 'el', 'http', 'userService']
45
46
  *
46
- * // Production - explicit annotation with context keys
47
- * myDirective.$inject = ['$element', SlotContentContext];
48
- * getInjectables(myDirective); // ['$element', SlotContentContext]
47
+ * // Production - explicit $inject array (survives minification)
48
+ * myDirective.$inject = ['$element', '$scope'];
49
+ * getInjectables(myDirective); // ['$element', '$scope']
49
50
  * ```
51
+ *
52
+ * For ContextKey injection, use the `using` option on directive registration.
50
53
  */
51
54
  export declare function getInjectables(fn: InjectableFunction): Injectable[];
52
55
  /**
package/dist/inject.js CHANGED
@@ -33,10 +33,12 @@ export function isContextKey(value) {
33
33
  * const myDirective = (expr, ctx, el, http, userService) => {};
34
34
  * getInjectables(myDirective); // ['expr', 'ctx', 'el', 'http', 'userService']
35
35
  *
36
- * // Production - explicit annotation with context keys
37
- * myDirective.$inject = ['$element', SlotContentContext];
38
- * getInjectables(myDirective); // ['$element', SlotContentContext]
36
+ * // Production - explicit $inject array (survives minification)
37
+ * myDirective.$inject = ['$element', '$scope'];
38
+ * getInjectables(myDirective); // ['$element', '$scope']
39
39
  * ```
40
+ *
41
+ * For ContextKey injection, use the `using` option on directive registration.
40
42
  */
41
43
  export function getInjectables(fn) {
42
44
  if ('$inject' in fn && Array.isArray(fn.$inject)) {
@@ -85,11 +87,6 @@ function parseFunctionParams(fn) {
85
87
  export function resolveDependencies(fn, expr, element, evalFn, config, using) {
86
88
  const inject = getInjectables(fn);
87
89
  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
90
  switch (dep) {
94
91
  case '$expr':
95
92
  return expr;
@@ -7,11 +7,26 @@ import { Window } from 'happy-dom';
7
7
  import { Mode, DirectivePriority, getDirective } from '../types.js';
8
8
  import { createContext } from '../context.js';
9
9
  import { processNativeSlot } from '../directives/slot.js';
10
- import { getLocalState, registerProvider, resolveFromProviders, resolveFromDIProviders } from '../providers.js';
10
+ import { registerProvider, resolveFromProviders, resolveFromDIProviders } from '../providers.js';
11
+ import { createElementScope, getElementScope } from '../scope.js';
11
12
  import { FOR_PROCESSED_ATTR, FOR_TEMPLATE_ATTR } from '../directives/for.js';
12
13
  import { IF_PROCESSED_ATTR } from '../directives/if.js';
13
14
  import { resolveDependencies as resolveInjectables } from '../inject.js';
14
15
  import { resolveContext } from '../context-registry.js';
16
+ /**
17
+ * Decode HTML entities that happy-dom doesn't decode.
18
+ *
19
+ * @internal
20
+ */
21
+ function decodeHTMLEntities(str) {
22
+ return str
23
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
24
+ .replace(/&quot;/g, '"')
25
+ .replace(/&apos;/g, "'")
26
+ .replace(/&lt;/g, '<')
27
+ .replace(/&gt;/g, '>')
28
+ .replace(/&amp;/g, '&');
29
+ }
15
30
  /** Registered services */
16
31
  let services = new Map();
17
32
  const selectorCache = new WeakMap();
@@ -69,15 +84,32 @@ export function registerDirective(registry, name, fn) {
69
84
  export function registerService(name, service) {
70
85
  services.set(name, service);
71
86
  }
87
+ /**
88
+ * Find the nearest scope by walking up the DOM tree.
89
+ * Falls back to rootState if no element scope found.
90
+ *
91
+ * @internal
92
+ */
93
+ function findServerScope(el, rootState) {
94
+ let current = el;
95
+ while (current) {
96
+ const scope = getElementScope(current);
97
+ if (scope) {
98
+ return scope;
99
+ }
100
+ current = current.parentElement;
101
+ }
102
+ return rootState;
103
+ }
72
104
  /**
73
105
  * Create resolver config for server-side dependency resolution.
74
106
  *
75
107
  * @internal
76
108
  */
77
- function createServerResolverConfig(el, rootState) {
109
+ function createServerResolverConfig(el, scopeState, rootState) {
78
110
  return {
79
111
  resolveContext: (key) => resolveContext(el, key),
80
- resolveState: () => getLocalState(el) ?? rootState,
112
+ resolveState: () => scopeState,
81
113
  resolveRootState: () => rootState,
82
114
  resolveCustom: (name) => {
83
115
  // Look up in ancestor DI providers first (provide option)
@@ -152,6 +184,27 @@ export async function render(html, state, registry) {
152
184
  });
153
185
  continue;
154
186
  }
187
+ // Handle g-scope elements that don't have other directives
188
+ // Add a placeholder entry so they get processed
189
+ if (match.hasAttribute('g-scope')) {
190
+ let hasDirective = false;
191
+ for (const [name] of registry) {
192
+ if (match.hasAttribute(`g-${name}`)) {
193
+ hasDirective = true;
194
+ break;
195
+ }
196
+ }
197
+ if (!hasDirective) {
198
+ index.push({
199
+ el: match,
200
+ name: 'scope',
201
+ directive: null,
202
+ expr: '',
203
+ priority: DirectivePriority.STRUCTURAL,
204
+ isNativeSlot: false
205
+ });
206
+ }
207
+ }
155
208
  for (const [name, directive] of registry) {
156
209
  const attr = match.getAttribute(`g-${name}`);
157
210
  if (attr !== null) {
@@ -161,7 +214,7 @@ export async function render(html, state, registry) {
161
214
  el: match,
162
215
  name,
163
216
  directive,
164
- expr: attr,
217
+ expr: decodeHTMLEntities(attr),
165
218
  priority: directive.priority ?? DirectivePriority.NORMAL,
166
219
  using: registration?.options.using
167
220
  });
@@ -227,7 +280,7 @@ export async function render(html, state, registry) {
227
280
  // Process g-scope first (inline scope initialization)
228
281
  const scopeAttr = el.getAttribute('g-scope');
229
282
  if (scopeAttr) {
230
- const scopeValues = ctx.eval(scopeAttr);
283
+ const scopeValues = ctx.eval(decodeHTMLEntities(scopeAttr));
231
284
  if (scopeValues && typeof scopeValues === 'object') {
232
285
  Object.assign(state, scopeValues);
233
286
  }
@@ -236,7 +289,7 @@ export async function render(html, state, registry) {
236
289
  for (const attr of [...el.attributes]) {
237
290
  if (attr.name.startsWith('g-bind:')) {
238
291
  const targetAttr = attr.name.slice('g-bind:'.length);
239
- const value = ctx.eval(attr.value);
292
+ const value = ctx.eval(decodeHTMLEntities(attr.value));
240
293
  if (value === null || value === undefined) {
241
294
  el.removeAttribute(targetAttr);
242
295
  }
@@ -253,14 +306,28 @@ export async function render(html, state, registry) {
253
306
  if (item.isNativeSlot) {
254
307
  processNativeSlot(item.el);
255
308
  }
309
+ else if (item.directive === null) {
310
+ // Placeholder for g-scope - already processed above
311
+ continue;
312
+ }
256
313
  else {
257
- const config = createServerResolverConfig(item.el, state);
314
+ // Determine scope for this directive
315
+ let scopeState;
316
+ if (item.directive.$context?.length) {
317
+ // Directives with $context get their own scope to populate
318
+ const parentScope = findServerScope(item.el, state);
319
+ scopeState = createElementScope(item.el, parentScope);
320
+ }
321
+ else {
322
+ // Other directives use nearest ancestor scope or root
323
+ scopeState = findServerScope(item.el, state);
324
+ }
325
+ const config = createServerResolverConfig(item.el, scopeState, state);
258
326
  const args = resolveInjectables(item.directive, item.expr, item.el, ctx.eval.bind(ctx), config, item.using);
259
327
  await item.directive(...args);
260
328
  // Register as context provider if directive declares $context
261
329
  if (item.directive.$context?.length) {
262
- const localState = getLocalState(item.el);
263
- registerProvider(item.el, item.directive, localState);
330
+ registerProvider(item.el, item.directive, scopeState);
264
331
  }
265
332
  }
266
333
  }
package/dist/types.d.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  import type { ContextKey } from './context-registry.js';
7
- import type { Injectable } from './inject.js';
8
7
  /**
9
8
  * Execution mode for the framework.
10
9
  */
@@ -160,14 +159,15 @@ export interface DirectiveMeta<T = InjectableRegistry> {
160
159
  *
161
160
  * @example
162
161
  * ```ts
163
- * // String-based injection
164
162
  * myDirective.$inject = ['$element', '$scope'];
163
+ * ```
165
164
  *
166
- * // With typed context keys
167
- * myDirective.$inject = ['$element', SlotContentContext];
165
+ * For typed context keys, use the `using` option on directive registration:
166
+ * ```ts
167
+ * directive('my-directive', myDirective, { using: [SlotContentContext] });
168
168
  * ```
169
169
  */
170
- $inject?: readonly Injectable[];
170
+ $inject?: readonly string[];
171
171
  /**
172
172
  * Names this directive exposes as context to descendants.
173
173
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gonia",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A lightweight, SSR-first reactive UI library with declarative directives",
5
5
  "type": "module",
6
6
  "license": "MIT",