gonia 0.3.3 → 0.3.4
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/bind-utils.d.ts +31 -0
- package/dist/bind-utils.js +56 -0
- package/dist/client/hydrate.d.ts +9 -4
- package/dist/client/hydrate.js +102 -109
- package/dist/resolver-config.d.ts +47 -0
- package/dist/resolver-config.js +62 -0
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +10 -1
- package/dist/server/render.d.ts +4 -7
- package/dist/server/render.js +160 -258
- package/dist/template-utils.d.ts +28 -0
- package/dist/template-utils.js +50 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared g-bind attribute processing utilities.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Expression, Context } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Apply a single g-bind attribute value to an element.
|
|
9
|
+
*
|
|
10
|
+
* @param el - The element to update
|
|
11
|
+
* @param targetAttr - The attribute name to set (without g-bind: prefix)
|
|
12
|
+
* @param value - The evaluated value
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyBindValue(el: Element, targetAttr: string, value: unknown): void;
|
|
15
|
+
/**
|
|
16
|
+
* Process all g-bind:* attributes on an element.
|
|
17
|
+
* For server-side rendering (one-time evaluation).
|
|
18
|
+
*
|
|
19
|
+
* @param el - The element to process
|
|
20
|
+
* @param ctx - The context for expression evaluation
|
|
21
|
+
* @param decode - Whether to decode HTML entities (for server-side)
|
|
22
|
+
*/
|
|
23
|
+
export declare function processBindAttributesOnce(el: Element, ctx: Context, decode?: boolean): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get all g-bind attributes from an element.
|
|
26
|
+
* Used by client to set up reactive bindings.
|
|
27
|
+
*
|
|
28
|
+
* @param el - The element to get bindings from
|
|
29
|
+
* @returns Array of [targetAttr, expression] pairs
|
|
30
|
+
*/
|
|
31
|
+
export declare function getBindAttributes(el: Element): Array<[string, Expression]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared g-bind attribute processing utilities.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { decodeHTMLEntities } from './template-utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Apply a single g-bind attribute value to an element.
|
|
9
|
+
*
|
|
10
|
+
* @param el - The element to update
|
|
11
|
+
* @param targetAttr - The attribute name to set (without g-bind: prefix)
|
|
12
|
+
* @param value - The evaluated value
|
|
13
|
+
*/
|
|
14
|
+
export function applyBindValue(el, targetAttr, value) {
|
|
15
|
+
if (value === null || value === undefined) {
|
|
16
|
+
el.removeAttribute(targetAttr);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
el.setAttribute(targetAttr, String(value));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Process all g-bind:* attributes on an element.
|
|
24
|
+
* For server-side rendering (one-time evaluation).
|
|
25
|
+
*
|
|
26
|
+
* @param el - The element to process
|
|
27
|
+
* @param ctx - The context for expression evaluation
|
|
28
|
+
* @param decode - Whether to decode HTML entities (for server-side)
|
|
29
|
+
*/
|
|
30
|
+
export function processBindAttributesOnce(el, ctx, decode = false) {
|
|
31
|
+
for (const attr of [...el.attributes]) {
|
|
32
|
+
if (attr.name.startsWith('g-bind:')) {
|
|
33
|
+
const targetAttr = attr.name.slice('g-bind:'.length);
|
|
34
|
+
const expr = decode ? decodeHTMLEntities(attr.value) : attr.value;
|
|
35
|
+
const value = ctx.eval(expr);
|
|
36
|
+
applyBindValue(el, targetAttr, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get all g-bind attributes from an element.
|
|
42
|
+
* Used by client to set up reactive bindings.
|
|
43
|
+
*
|
|
44
|
+
* @param el - The element to get bindings from
|
|
45
|
+
* @returns Array of [targetAttr, expression] pairs
|
|
46
|
+
*/
|
|
47
|
+
export function getBindAttributes(el) {
|
|
48
|
+
const bindings = [];
|
|
49
|
+
for (const attr of el.attributes) {
|
|
50
|
+
if (attr.name.startsWith('g-bind:')) {
|
|
51
|
+
const targetAttr = attr.name.slice('g-bind:'.length);
|
|
52
|
+
bindings.push([targetAttr, attr.value]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return bindings;
|
|
56
|
+
}
|
package/dist/client/hydrate.d.ts
CHANGED
|
@@ -8,10 +8,7 @@ import { Directive, Context } from '../types.js';
|
|
|
8
8
|
* Registry of directives by name.
|
|
9
9
|
*/
|
|
10
10
|
export type DirectiveRegistry = Map<string, Directive<any>>;
|
|
11
|
-
|
|
12
|
-
* Service registry for dependency injection.
|
|
13
|
-
*/
|
|
14
|
-
export type ServiceRegistry = Map<string, unknown>;
|
|
11
|
+
export type { ServiceRegistry } from '../resolver-config.js';
|
|
15
12
|
/**
|
|
16
13
|
* Set context for an element (used by directives that create child contexts).
|
|
17
14
|
*
|
|
@@ -52,3 +49,11 @@ export declare const hydrate: typeof init;
|
|
|
52
49
|
* Alias for {@link init}. Use for pure client-side rendering.
|
|
53
50
|
*/
|
|
54
51
|
export declare const mount: typeof init;
|
|
52
|
+
/**
|
|
53
|
+
* Reset hydration state for testing.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Clears cached selector, disconnects observer, and resets initialized flag.
|
|
57
|
+
* Primarily useful for testing.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resetHydration(): void;
|
package/dist/client/hydrate.js
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
import { Mode, DirectivePriority, getDirective, getDirectiveNames } from '../types.js';
|
|
7
7
|
import { createContext } from '../context.js';
|
|
8
8
|
import { processNativeSlot } from '../directives/slot.js';
|
|
9
|
-
import { getLocalState, registerProvider,
|
|
9
|
+
import { getLocalState, registerProvider, registerDIProviders } from '../providers.js';
|
|
10
10
|
import { FOR_PROCESSED_ATTR } from '../directives/for.js';
|
|
11
11
|
import { findParentScope, createElementScope, getElementScope } from '../scope.js';
|
|
12
12
|
import { resolveDependencies as resolveInjectables } from '../inject.js';
|
|
13
|
-
import { resolveContext } from '../context-registry.js';
|
|
14
13
|
import { effect } from '../reactivity.js';
|
|
15
14
|
import { applyAssigns, directiveNeedsScope } from '../directive-utils.js';
|
|
15
|
+
import { getTemplateAttrs } from '../template-utils.js';
|
|
16
|
+
import { applyBindValue, getBindAttributes } from '../bind-utils.js';
|
|
17
|
+
import { createClientResolverConfig } from '../resolver-config.js';
|
|
16
18
|
// Built-in directives
|
|
17
19
|
import { text } from '../directives/text.js';
|
|
18
20
|
import { show } from '../directives/show.js';
|
|
@@ -27,6 +29,8 @@ let cachedSelector = null;
|
|
|
27
29
|
let initialized = false;
|
|
28
30
|
/** Registered services */
|
|
29
31
|
let services = new Map();
|
|
32
|
+
/** Current MutationObserver (for cleanup) */
|
|
33
|
+
let observer = null;
|
|
30
34
|
/** Context cache by element */
|
|
31
35
|
const contextCache = new WeakMap();
|
|
32
36
|
/** Default registry with built-in directives */
|
|
@@ -162,30 +166,6 @@ function getContextForElement(el) {
|
|
|
162
166
|
export function setElementContext(el, ctx) {
|
|
163
167
|
contextCache.set(el, ctx);
|
|
164
168
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Create resolver config for client-side dependency resolution.
|
|
167
|
-
*
|
|
168
|
-
* @internal
|
|
169
|
-
*/
|
|
170
|
-
function createClientResolverConfig(el, ctx) {
|
|
171
|
-
return {
|
|
172
|
-
resolveContext: (key) => resolveContext(el, key),
|
|
173
|
-
resolveState: () => findParentScope(el, true) ?? getLocalState(el),
|
|
174
|
-
resolveCustom: (name) => {
|
|
175
|
-
// Look up in ancestor DI providers first (provide option)
|
|
176
|
-
const diProvided = resolveFromDIProviders(el, name);
|
|
177
|
-
if (diProvided !== undefined)
|
|
178
|
-
return diProvided;
|
|
179
|
-
// Look up in global services registry
|
|
180
|
-
const service = services.get(name);
|
|
181
|
-
if (service !== undefined)
|
|
182
|
-
return service;
|
|
183
|
-
// Look up in ancestor context providers ($context)
|
|
184
|
-
return resolveFromProviders(el, name);
|
|
185
|
-
},
|
|
186
|
-
mode: 'client'
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
169
|
/**
|
|
190
170
|
* Process directives on a single element.
|
|
191
171
|
* Returns a promise if any directive is async, otherwise void.
|
|
@@ -209,7 +189,7 @@ function processElement(el, registry) {
|
|
|
209
189
|
if (ifDirective) {
|
|
210
190
|
const expr = el.getAttribute('data-g-if') || '';
|
|
211
191
|
const ctx = getContextForElement(el);
|
|
212
|
-
const config = createClientResolverConfig(el,
|
|
192
|
+
const config = createClientResolverConfig(el, () => findParentScope(el, true) ?? getLocalState(el), services);
|
|
213
193
|
const registration = getDirective('g-if');
|
|
214
194
|
const args = resolveInjectables(ifDirective, expr, el, ctx.eval.bind(ctx), config, registration?.options.using);
|
|
215
195
|
const result = ifDirective(...args);
|
|
@@ -263,26 +243,17 @@ function processElement(el, registry) {
|
|
|
263
243
|
}
|
|
264
244
|
}
|
|
265
245
|
// Process g-bind:* attributes (dynamic attribute binding with reactivity)
|
|
266
|
-
for (const
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const value = ctx.eval(valueExpr);
|
|
272
|
-
if (value === null || value === undefined) {
|
|
273
|
-
el.removeAttribute(targetAttr);
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
el.setAttribute(targetAttr, String(value));
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
}
|
|
246
|
+
for (const [targetAttr, valueExpr] of getBindAttributes(el)) {
|
|
247
|
+
effect(() => {
|
|
248
|
+
const value = ctx.eval(valueExpr);
|
|
249
|
+
applyBindValue(el, targetAttr, value);
|
|
250
|
+
});
|
|
280
251
|
}
|
|
281
252
|
// Process directives sequentially, handling async ones properly
|
|
282
253
|
let chain;
|
|
283
254
|
for (const { directive, expr, using } of directives) {
|
|
284
255
|
const processDirective = () => {
|
|
285
|
-
const config = createClientResolverConfig(el,
|
|
256
|
+
const config = createClientResolverConfig(el, () => findParentScope(el, true) ?? getLocalState(el), services);
|
|
286
257
|
const args = resolveInjectables(directive, expr, el, ctx.eval.bind(ctx), config, using);
|
|
287
258
|
const result = directive(...args);
|
|
288
259
|
// Register as provider if directive declares $context
|
|
@@ -387,24 +358,32 @@ export function registerService(name, service) {
|
|
|
387
358
|
* ```
|
|
388
359
|
*/
|
|
389
360
|
/**
|
|
390
|
-
*
|
|
361
|
+
* Build a selector for custom element directives that need processing.
|
|
391
362
|
*
|
|
392
363
|
* @internal
|
|
393
364
|
*/
|
|
394
|
-
function
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
365
|
+
function getCustomElementSelector() {
|
|
366
|
+
const selectors = [];
|
|
367
|
+
for (const name of getDirectiveNames()) {
|
|
368
|
+
const registration = getDirective(name);
|
|
369
|
+
if (!registration)
|
|
370
|
+
continue;
|
|
371
|
+
const { options } = registration;
|
|
372
|
+
// Only include directives with templates, scope, provide, or using
|
|
373
|
+
if (options.template || options.scope || options.provide || options.using) {
|
|
374
|
+
selectors.push(name);
|
|
375
|
+
}
|
|
400
376
|
}
|
|
401
|
-
return
|
|
377
|
+
return selectors.join(',');
|
|
402
378
|
}
|
|
403
379
|
/**
|
|
404
380
|
* Process custom element directives (those with templates).
|
|
405
381
|
* Directives with templates are web components and must be processed before
|
|
406
382
|
* attribute directives so their content is rendered first.
|
|
407
383
|
*
|
|
384
|
+
* Elements are processed in document order (parents before children) to ensure
|
|
385
|
+
* parent scopes are initialized before child expressions are evaluated.
|
|
386
|
+
*
|
|
408
387
|
* Order for each element:
|
|
409
388
|
* 1. Create scope (if scope: true)
|
|
410
389
|
* 2. Call directive function (if fn exists) - initializes state
|
|
@@ -414,74 +393,68 @@ function getTemplateAttrs(el) {
|
|
|
414
393
|
* @internal
|
|
415
394
|
*/
|
|
416
395
|
async function processDirectiveElements() {
|
|
417
|
-
|
|
396
|
+
const selector = getCustomElementSelector();
|
|
397
|
+
if (!selector)
|
|
398
|
+
return;
|
|
399
|
+
// Get all custom elements in document order (parents before children)
|
|
400
|
+
const elements = document.querySelectorAll(selector);
|
|
401
|
+
for (const el of elements) {
|
|
402
|
+
// Skip if already processed
|
|
403
|
+
if (getElementScope(el)) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Find the directive registration for this element
|
|
407
|
+
const name = el.tagName.toLowerCase();
|
|
418
408
|
const registration = getDirective(name);
|
|
419
409
|
if (!registration) {
|
|
420
410
|
continue;
|
|
421
411
|
}
|
|
422
412
|
const { fn, options } = registration;
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
// 1. Create scope if needed
|
|
436
|
-
let scope = {};
|
|
437
|
-
if (options.scope) {
|
|
438
|
-
const parentScope = findParentScope(el);
|
|
439
|
-
scope = createElementScope(el, parentScope);
|
|
440
|
-
// Collect unique directive names on this element for conflict detection
|
|
441
|
-
const directiveNameSet = new Set([name]);
|
|
442
|
-
for (const attr of el.attributes) {
|
|
443
|
-
const attrReg = getDirective(attr.name);
|
|
444
|
-
if (attrReg) {
|
|
445
|
-
directiveNameSet.add(attr.name);
|
|
446
|
-
}
|
|
413
|
+
// 1. Create scope if needed
|
|
414
|
+
let scope = {};
|
|
415
|
+
if (options.scope) {
|
|
416
|
+
const parentScope = findParentScope(el);
|
|
417
|
+
scope = createElementScope(el, parentScope);
|
|
418
|
+
// Collect unique directive names on this element for conflict detection
|
|
419
|
+
const directiveNameSet = new Set([name]);
|
|
420
|
+
for (const attr of el.attributes) {
|
|
421
|
+
const attrReg = getDirective(attr.name);
|
|
422
|
+
if (attrReg) {
|
|
423
|
+
directiveNameSet.add(attr.name);
|
|
447
424
|
}
|
|
448
|
-
// Apply assigns with conflict detection
|
|
449
|
-
applyAssigns(scope, [...directiveNameSet]);
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
scope = findParentScope(el, true) ?? {};
|
|
453
425
|
}
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
426
|
+
// Apply assigns with conflict detection
|
|
427
|
+
applyAssigns(scope, [...directiveNameSet]);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
scope = findParentScope(el, true) ?? {};
|
|
431
|
+
}
|
|
432
|
+
// 2. Register DI providers if present (for descendants)
|
|
433
|
+
if (options.provide) {
|
|
434
|
+
registerDIProviders(el, options.provide);
|
|
435
|
+
}
|
|
436
|
+
// 3. Call directive function if present (initializes state)
|
|
437
|
+
if (fn) {
|
|
438
|
+
const ctx = createContext(Mode.CLIENT, scope);
|
|
439
|
+
const config = createClientResolverConfig(el, () => scope, services);
|
|
440
|
+
const args = resolveInjectables(fn, '', el, ctx.eval.bind(ctx), config, options.using);
|
|
441
|
+
const result = fn(...args);
|
|
442
|
+
if (result instanceof Promise) {
|
|
443
|
+
await result;
|
|
457
444
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
};
|
|
466
|
-
const args = resolveInjectables(fn, '', el, ctx.eval.bind(ctx), config, options.using);
|
|
467
|
-
const result = fn(...args);
|
|
468
|
-
if (result instanceof Promise) {
|
|
469
|
-
await result;
|
|
470
|
-
}
|
|
445
|
+
}
|
|
446
|
+
// 4. Render template if present (can query DOM for <template> elements etc)
|
|
447
|
+
if (options.template) {
|
|
448
|
+
const attrs = getTemplateAttrs(el);
|
|
449
|
+
let html;
|
|
450
|
+
if (typeof options.template === 'string') {
|
|
451
|
+
html = options.template;
|
|
471
452
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
let html;
|
|
476
|
-
if (typeof options.template === 'string') {
|
|
477
|
-
html = options.template;
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
const result = options.template(attrs, el);
|
|
481
|
-
html = result instanceof Promise ? await result : result;
|
|
482
|
-
}
|
|
483
|
-
el.innerHTML = html;
|
|
453
|
+
else {
|
|
454
|
+
const result = options.template(attrs, el);
|
|
455
|
+
html = result instanceof Promise ? await result : result;
|
|
484
456
|
}
|
|
457
|
+
el.innerHTML = html;
|
|
485
458
|
}
|
|
486
459
|
}
|
|
487
460
|
}
|
|
@@ -511,7 +484,11 @@ export async function init(registry) {
|
|
|
511
484
|
await Promise.all(promises);
|
|
512
485
|
}
|
|
513
486
|
// Set up MutationObserver for dynamic elements
|
|
514
|
-
|
|
487
|
+
// Clean up previous observer if it exists
|
|
488
|
+
if (observer) {
|
|
489
|
+
observer.disconnect();
|
|
490
|
+
}
|
|
491
|
+
observer = new MutationObserver((mutations) => {
|
|
515
492
|
for (const mutation of mutations) {
|
|
516
493
|
for (const node of mutation.addedNodes) {
|
|
517
494
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
@@ -537,3 +514,19 @@ export const hydrate = init;
|
|
|
537
514
|
* Alias for {@link init}. Use for pure client-side rendering.
|
|
538
515
|
*/
|
|
539
516
|
export const mount = init;
|
|
517
|
+
/**
|
|
518
|
+
* Reset hydration state for testing.
|
|
519
|
+
*
|
|
520
|
+
* @remarks
|
|
521
|
+
* Clears cached selector, disconnects observer, and resets initialized flag.
|
|
522
|
+
* Primarily useful for testing.
|
|
523
|
+
*/
|
|
524
|
+
export function resetHydration() {
|
|
525
|
+
cachedSelector = null;
|
|
526
|
+
initialized = false;
|
|
527
|
+
defaultRegistry = null;
|
|
528
|
+
if (observer) {
|
|
529
|
+
observer.disconnect();
|
|
530
|
+
observer = null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared resolver configuration factory for client and server.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { ContextKey } from './context-registry.js';
|
|
7
|
+
/**
|
|
8
|
+
* Service registry for dependency injection.
|
|
9
|
+
*/
|
|
10
|
+
export type ServiceRegistry = Map<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolver configuration for dependency injection.
|
|
13
|
+
*/
|
|
14
|
+
export interface ResolverConfig {
|
|
15
|
+
resolveContext: (key: ContextKey<unknown>) => unknown;
|
|
16
|
+
resolveState: () => Record<string, unknown>;
|
|
17
|
+
resolveRootState?: () => Record<string, unknown>;
|
|
18
|
+
resolveCustom: (name: string) => unknown;
|
|
19
|
+
mode: 'client' | 'server';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create the resolveCustom function shared by client and server.
|
|
23
|
+
*
|
|
24
|
+
* @param el - The element for provider lookup
|
|
25
|
+
* @param services - The global service registry
|
|
26
|
+
* @returns A function that resolves custom dependencies
|
|
27
|
+
*/
|
|
28
|
+
export declare function createCustomResolver(el: Element, services: ServiceRegistry): (name: string) => unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Create resolver config for client-side dependency resolution.
|
|
31
|
+
*
|
|
32
|
+
* @param el - The element being processed
|
|
33
|
+
* @param resolveState - Function to resolve the current state
|
|
34
|
+
* @param services - The global service registry
|
|
35
|
+
* @returns Resolver configuration for client mode
|
|
36
|
+
*/
|
|
37
|
+
export declare function createClientResolverConfig(el: Element, resolveState: () => Record<string, unknown>, services: ServiceRegistry): ResolverConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Create resolver config for server-side dependency resolution.
|
|
40
|
+
*
|
|
41
|
+
* @param el - The element being processed
|
|
42
|
+
* @param scopeState - The current scope state
|
|
43
|
+
* @param rootState - The root state object
|
|
44
|
+
* @param services - The global service registry
|
|
45
|
+
* @returns Resolver configuration for server mode
|
|
46
|
+
*/
|
|
47
|
+
export declare function createServerResolverConfig(el: Element, scopeState: Record<string, unknown>, rootState: Record<string, unknown>, services: ServiceRegistry): ResolverConfig;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared resolver configuration factory for client and server.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { resolveFromProviders, resolveFromDIProviders } from './providers.js';
|
|
7
|
+
import { resolveContext } from './context-registry.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create the resolveCustom function shared by client and server.
|
|
10
|
+
*
|
|
11
|
+
* @param el - The element for provider lookup
|
|
12
|
+
* @param services - The global service registry
|
|
13
|
+
* @returns A function that resolves custom dependencies
|
|
14
|
+
*/
|
|
15
|
+
export function createCustomResolver(el, services) {
|
|
16
|
+
return (name) => {
|
|
17
|
+
// Look up in ancestor DI providers first (provide option)
|
|
18
|
+
const diProvided = resolveFromDIProviders(el, name);
|
|
19
|
+
if (diProvided !== undefined)
|
|
20
|
+
return diProvided;
|
|
21
|
+
// Look up in global services registry
|
|
22
|
+
const service = services.get(name);
|
|
23
|
+
if (service !== undefined)
|
|
24
|
+
return service;
|
|
25
|
+
// Look up in ancestor context providers ($context)
|
|
26
|
+
return resolveFromProviders(el, name);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create resolver config for client-side dependency resolution.
|
|
31
|
+
*
|
|
32
|
+
* @param el - The element being processed
|
|
33
|
+
* @param resolveState - Function to resolve the current state
|
|
34
|
+
* @param services - The global service registry
|
|
35
|
+
* @returns Resolver configuration for client mode
|
|
36
|
+
*/
|
|
37
|
+
export function createClientResolverConfig(el, resolveState, services) {
|
|
38
|
+
return {
|
|
39
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
40
|
+
resolveState,
|
|
41
|
+
resolveCustom: createCustomResolver(el, services),
|
|
42
|
+
mode: 'client'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create resolver config for server-side dependency resolution.
|
|
47
|
+
*
|
|
48
|
+
* @param el - The element being processed
|
|
49
|
+
* @param scopeState - The current scope state
|
|
50
|
+
* @param rootState - The root state object
|
|
51
|
+
* @param services - The global service registry
|
|
52
|
+
* @returns Resolver configuration for server mode
|
|
53
|
+
*/
|
|
54
|
+
export function createServerResolverConfig(el, scopeState, rootState, services) {
|
|
55
|
+
return {
|
|
56
|
+
resolveContext: (key) => resolveContext(el, key),
|
|
57
|
+
resolveState: () => scopeState,
|
|
58
|
+
resolveRootState: () => rootState,
|
|
59
|
+
resolveCustom: createCustomResolver(el, services),
|
|
60
|
+
mode: 'server'
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/scope.d.ts
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import { Directive, DirectiveOptions } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Clear all element scopes.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Primarily useful for testing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function clearElementScopes(): void;
|
|
7
14
|
/**
|
|
8
15
|
* Get or create the root scope.
|
|
9
16
|
*
|
package/dist/scope.js
CHANGED
|
@@ -10,7 +10,16 @@ import { resolveDependencies } from './inject.js';
|
|
|
10
10
|
import { findAncestor } from './dom.js';
|
|
11
11
|
import { resolveContext } from './context-registry.js';
|
|
12
12
|
/** WeakMap to store element scopes */
|
|
13
|
-
|
|
13
|
+
let elementScopes = new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Clear all element scopes.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Primarily useful for testing.
|
|
19
|
+
*/
|
|
20
|
+
export function clearElementScopes() {
|
|
21
|
+
elementScopes = new WeakMap();
|
|
22
|
+
}
|
|
14
23
|
/** Root scope for top-level directives without explicit parent scope */
|
|
15
24
|
let rootScope = null;
|
|
16
25
|
/**
|
package/dist/server/render.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server-side rendering with
|
|
2
|
+
* Server-side rendering with direct tree walking for directive indexing.
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
@@ -8,10 +8,7 @@ import { Directive } from '../types.js';
|
|
|
8
8
|
* Registry of directives by name.
|
|
9
9
|
*/
|
|
10
10
|
export type DirectiveRegistry = Map<string, Directive>;
|
|
11
|
-
|
|
12
|
-
* Service registry for dependency injection.
|
|
13
|
-
*/
|
|
14
|
-
export type ServiceRegistry = Map<string, unknown>;
|
|
11
|
+
export type { ServiceRegistry } from '../resolver-config.js';
|
|
15
12
|
/**
|
|
16
13
|
* Register a directive in the registry.
|
|
17
14
|
*
|
|
@@ -33,8 +30,8 @@ export declare function registerService(name: string, service: unknown): void;
|
|
|
33
30
|
* Render HTML with directives on the server.
|
|
34
31
|
*
|
|
35
32
|
* @remarks
|
|
36
|
-
* Uses
|
|
37
|
-
*
|
|
33
|
+
* Uses direct tree walking to index elements with directive attributes,
|
|
34
|
+
* then executes directives to produce the final HTML.
|
|
38
35
|
* Directive attributes are preserved in output for client hydration.
|
|
39
36
|
* Directives are processed in tree order (parents before children),
|
|
40
37
|
* with priority used only for multiple directives on the same element.
|
package/dist/server/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server-side rendering with
|
|
2
|
+
* Server-side rendering with direct tree walking for directive indexing.
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
@@ -7,28 +7,16 @@ import { Window } from 'happy-dom';
|
|
|
7
7
|
import { Mode, DirectivePriority, getDirective, getDirectiveNames } from '../types.js';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { processNativeSlot } from '../directives/slot.js';
|
|
10
|
-
import { registerProvider,
|
|
10
|
+
import { registerProvider, registerDIProviders } from '../providers.js';
|
|
11
11
|
import { createElementScope, getElementScope } from '../scope.js';
|
|
12
12
|
import { FOR_PROCESSED_ATTR, FOR_TEMPLATE_ATTR } from '../directives/for.js';
|
|
13
13
|
import { IF_PROCESSED_ATTR } from '../directives/if.js';
|
|
14
14
|
import { PROCESSED_ATTR } from '../process.js';
|
|
15
15
|
import { resolveDependencies as resolveInjectables } from '../inject.js';
|
|
16
|
-
import { resolveContext } from '../context-registry.js';
|
|
17
16
|
import { applyAssigns, directiveNeedsScope } from '../directive-utils.js';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @internal
|
|
22
|
-
*/
|
|
23
|
-
function decodeHTMLEntities(str) {
|
|
24
|
-
return str
|
|
25
|
-
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
26
|
-
.replace(/"/g, '"')
|
|
27
|
-
.replace(/'/g, "'")
|
|
28
|
-
.replace(/</g, '<')
|
|
29
|
-
.replace(/>/g, '>')
|
|
30
|
-
.replace(/&/g, '&');
|
|
31
|
-
}
|
|
17
|
+
import { getTemplateAttrs, hasBindAttributes, decodeHTMLEntities } from '../template-utils.js';
|
|
18
|
+
import { processBindAttributesOnce } from '../bind-utils.js';
|
|
19
|
+
import { createServerResolverConfig } from '../resolver-config.js';
|
|
32
20
|
/** Registered services */
|
|
33
21
|
let services = new Map();
|
|
34
22
|
/**
|
|
@@ -68,49 +56,23 @@ function getSelector(localRegistry) {
|
|
|
68
56
|
selectors.push('[g-scope]');
|
|
69
57
|
// Match common g-bind:* attributes for dynamic binding
|
|
70
58
|
// These need to be indexed so their expressions can be evaluated with proper scope
|
|
71
|
-
|
|
72
|
-
selectors.push('[g-bind
|
|
73
|
-
selectors.push('[g-bind
|
|
74
|
-
selectors.push('[g-bind
|
|
75
|
-
selectors.push('[g-bind
|
|
76
|
-
selectors.push('[g-bind
|
|
77
|
-
selectors.push('[g-bind
|
|
78
|
-
selectors.push('[g-bind
|
|
79
|
-
selectors.push('[g-bind
|
|
80
|
-
selectors.push('[g-bind
|
|
81
|
-
selectors.push('[g-bind
|
|
82
|
-
selectors.push('[g-bind
|
|
83
|
-
selectors.push('[g-bind
|
|
59
|
+
// Note: happy-dom doesn't need colon escaping (and escaped colons don't work)
|
|
60
|
+
selectors.push('[g-bind:class]');
|
|
61
|
+
selectors.push('[g-bind:style]');
|
|
62
|
+
selectors.push('[g-bind:href]');
|
|
63
|
+
selectors.push('[g-bind:src]');
|
|
64
|
+
selectors.push('[g-bind:id]');
|
|
65
|
+
selectors.push('[g-bind:value]');
|
|
66
|
+
selectors.push('[g-bind:disabled]');
|
|
67
|
+
selectors.push('[g-bind:checked]');
|
|
68
|
+
selectors.push('[g-bind:placeholder]');
|
|
69
|
+
selectors.push('[g-bind:title]');
|
|
70
|
+
selectors.push('[g-bind:alt]');
|
|
71
|
+
selectors.push('[g-bind:name]');
|
|
72
|
+
selectors.push('[g-bind:type]');
|
|
84
73
|
// Note: Can't do wildcard for data-* attributes in CSS, but hasBindAttributes handles them
|
|
85
74
|
return selectors.join(',');
|
|
86
75
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Get template attributes from an element.
|
|
89
|
-
*
|
|
90
|
-
* @internal
|
|
91
|
-
*/
|
|
92
|
-
function getTemplateAttrs(el) {
|
|
93
|
-
const attrs = {
|
|
94
|
-
children: el.innerHTML
|
|
95
|
-
};
|
|
96
|
-
for (const attr of el.attributes) {
|
|
97
|
-
attrs[attr.name] = attr.value;
|
|
98
|
-
}
|
|
99
|
-
return attrs;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Check if element has any g-bind:* attributes.
|
|
103
|
-
*
|
|
104
|
-
* @internal
|
|
105
|
-
*/
|
|
106
|
-
function hasBindAttributes(el) {
|
|
107
|
-
for (const attr of el.attributes) {
|
|
108
|
-
if (attr.name.startsWith('g-bind:')) {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
76
|
/**
|
|
115
77
|
* Register a directive in the registry.
|
|
116
78
|
*
|
|
@@ -149,37 +111,12 @@ function findServerScope(el, rootState) {
|
|
|
149
111
|
}
|
|
150
112
|
return rootState;
|
|
151
113
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Create resolver config for server-side dependency resolution.
|
|
154
|
-
*
|
|
155
|
-
* @internal
|
|
156
|
-
*/
|
|
157
|
-
function createServerResolverConfig(el, scopeState, rootState) {
|
|
158
|
-
return {
|
|
159
|
-
resolveContext: (key) => resolveContext(el, key),
|
|
160
|
-
resolveState: () => scopeState,
|
|
161
|
-
resolveRootState: () => rootState,
|
|
162
|
-
resolveCustom: (name) => {
|
|
163
|
-
// Look up in ancestor DI providers first (provide option)
|
|
164
|
-
const diProvided = resolveFromDIProviders(el, name);
|
|
165
|
-
if (diProvided !== undefined)
|
|
166
|
-
return diProvided;
|
|
167
|
-
// Look up in global services registry
|
|
168
|
-
const service = services.get(name);
|
|
169
|
-
if (service !== undefined)
|
|
170
|
-
return service;
|
|
171
|
-
// Look up in ancestor context providers ($context)
|
|
172
|
-
return resolveFromProviders(el, name);
|
|
173
|
-
},
|
|
174
|
-
mode: 'server'
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
114
|
/**
|
|
178
115
|
* Render HTML with directives on the server.
|
|
179
116
|
*
|
|
180
117
|
* @remarks
|
|
181
|
-
* Uses
|
|
182
|
-
*
|
|
118
|
+
* Uses direct tree walking to index elements with directive attributes,
|
|
119
|
+
* then executes directives to produce the final HTML.
|
|
183
120
|
* Directive attributes are preserved in output for client hydration.
|
|
184
121
|
* Directives are processed in tree order (parents before children),
|
|
185
122
|
* with priority used only for multiple directives on the same element.
|
|
@@ -206,159 +143,133 @@ export async function render(html, state, registry) {
|
|
|
206
143
|
const window = new Window();
|
|
207
144
|
const document = window.document;
|
|
208
145
|
const index = [];
|
|
209
|
-
const
|
|
146
|
+
const indexed = new Set(); // Track which elements have been indexed
|
|
210
147
|
const selector = getSelector(registry);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Index all directive elements in a subtree.
|
|
150
|
+
* Called after innerHTML is set to discover new elements.
|
|
151
|
+
*/
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
function indexTree(root) {
|
|
154
|
+
// Get all matching elements in the subtree
|
|
155
|
+
const elements = root.querySelectorAll(selector);
|
|
156
|
+
for (const match of elements) {
|
|
157
|
+
// Skip if already indexed
|
|
158
|
+
if (indexed.has(match))
|
|
159
|
+
continue;
|
|
160
|
+
indexed.add(match);
|
|
161
|
+
// Skip elements inside template content (used as placeholders)
|
|
162
|
+
if (match.closest('template')) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// Handle native <slot> elements
|
|
166
|
+
if (match.tagName === 'SLOT') {
|
|
167
|
+
index.push({
|
|
168
|
+
el: match,
|
|
169
|
+
name: 'slot',
|
|
170
|
+
directive: null,
|
|
171
|
+
expr: '',
|
|
172
|
+
priority: DirectivePriority.NORMAL,
|
|
173
|
+
isNativeSlot: true
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Handle g-scope elements that don't have other directives
|
|
178
|
+
if (match.hasAttribute('g-scope')) {
|
|
179
|
+
let hasDirective = false;
|
|
180
|
+
for (const name of getDirectiveNames()) {
|
|
181
|
+
if (match.hasAttribute(name)) {
|
|
182
|
+
hasDirective = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!hasDirective) {
|
|
187
|
+
index.push({
|
|
188
|
+
el: match,
|
|
189
|
+
name: 'scope',
|
|
190
|
+
directive: null,
|
|
191
|
+
expr: '',
|
|
192
|
+
priority: DirectivePriority.STRUCTURAL,
|
|
193
|
+
isNativeSlot: false
|
|
194
|
+
});
|
|
233
195
|
}
|
|
234
196
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Filter out descendants that will be processed as direct addedNodes
|
|
243
|
-
const descendants = [...el.querySelectorAll(selector)].filter(desc => !directNodes.has(desc));
|
|
244
|
-
for (const match of [...matches, ...descendants]) {
|
|
245
|
-
// Skip elements inside template content (used as placeholders)
|
|
246
|
-
if (match.closest('template')) {
|
|
247
|
-
continue;
|
|
197
|
+
// Handle g-bind:* elements that don't have other directives
|
|
198
|
+
if (hasBindAttributes(match)) {
|
|
199
|
+
let hasDirective = false;
|
|
200
|
+
for (const name of getDirectiveNames()) {
|
|
201
|
+
if (match.hasAttribute(name)) {
|
|
202
|
+
hasDirective = true;
|
|
203
|
+
break;
|
|
248
204
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
205
|
+
}
|
|
206
|
+
if (!hasDirective && !match.hasAttribute('g-scope')) {
|
|
207
|
+
index.push({
|
|
208
|
+
el: match,
|
|
209
|
+
name: 'bind',
|
|
210
|
+
directive: null,
|
|
211
|
+
expr: '',
|
|
212
|
+
priority: DirectivePriority.NORMAL,
|
|
213
|
+
isNativeSlot: false
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Check all registered directives from global registry
|
|
218
|
+
const tagName = match.tagName.toLowerCase();
|
|
219
|
+
for (const name of getDirectiveNames()) {
|
|
220
|
+
const registration = getDirective(name);
|
|
221
|
+
if (!registration)
|
|
222
|
+
continue;
|
|
223
|
+
const { fn, options } = registration;
|
|
224
|
+
// Check if this is a custom element directive (tag name matches)
|
|
225
|
+
if (tagName === name) {
|
|
226
|
+
if (options.template || options.scope || options.provide || options.using) {
|
|
227
|
+
index.push({
|
|
252
228
|
el: match,
|
|
253
|
-
name
|
|
254
|
-
directive:
|
|
229
|
+
name,
|
|
230
|
+
directive: fn,
|
|
255
231
|
expr: '',
|
|
256
|
-
priority: DirectivePriority.
|
|
257
|
-
|
|
232
|
+
priority: fn?.priority ?? DirectivePriority.TEMPLATE,
|
|
233
|
+
isCustomElement: true,
|
|
234
|
+
using: options.using
|
|
258
235
|
});
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
// Handle g-scope elements that don't have other directives
|
|
262
|
-
// Add a placeholder entry so they get processed
|
|
263
|
-
if (match.hasAttribute('g-scope')) {
|
|
264
|
-
let hasDirective = false;
|
|
265
|
-
for (const name of getDirectiveNames()) {
|
|
266
|
-
if (match.hasAttribute(name)) {
|
|
267
|
-
hasDirective = true;
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (!hasDirective) {
|
|
272
|
-
addToIndex({
|
|
273
|
-
el: match,
|
|
274
|
-
name: 'scope',
|
|
275
|
-
directive: null,
|
|
276
|
-
expr: '',
|
|
277
|
-
priority: DirectivePriority.STRUCTURAL,
|
|
278
|
-
isNativeSlot: false
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// Handle g-bind:* elements that don't have other directives
|
|
283
|
-
// Add a placeholder so they get processed for dynamic attribute binding
|
|
284
|
-
if (hasBindAttributes(match)) {
|
|
285
|
-
let hasDirective = false;
|
|
286
|
-
for (const name of getDirectiveNames()) {
|
|
287
|
-
if (match.hasAttribute(name)) {
|
|
288
|
-
hasDirective = true;
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (!hasDirective && !match.hasAttribute('g-scope')) {
|
|
293
|
-
addToIndex({
|
|
294
|
-
el: match,
|
|
295
|
-
name: 'bind',
|
|
296
|
-
directive: null,
|
|
297
|
-
expr: '',
|
|
298
|
-
priority: DirectivePriority.NORMAL,
|
|
299
|
-
isNativeSlot: false
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// Check all registered directives from global registry
|
|
304
|
-
const tagName = match.tagName.toLowerCase();
|
|
305
|
-
for (const name of getDirectiveNames()) {
|
|
306
|
-
const registration = getDirective(name);
|
|
307
|
-
if (!registration)
|
|
308
|
-
continue;
|
|
309
|
-
const { fn, options } = registration;
|
|
310
|
-
// Check if this is a custom element directive (tag name matches)
|
|
311
|
-
if (tagName === name) {
|
|
312
|
-
if (options.template || options.scope || options.provide || options.using) {
|
|
313
|
-
addToIndex({
|
|
314
|
-
el: match,
|
|
315
|
-
name,
|
|
316
|
-
directive: fn,
|
|
317
|
-
expr: '',
|
|
318
|
-
priority: fn?.priority ?? DirectivePriority.TEMPLATE,
|
|
319
|
-
isCustomElement: true,
|
|
320
|
-
using: options.using
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Check if this is an attribute directive
|
|
325
|
-
const attr = match.getAttribute(name);
|
|
326
|
-
if (attr !== null) {
|
|
327
|
-
addToIndex({
|
|
328
|
-
el: match,
|
|
329
|
-
name,
|
|
330
|
-
directive: fn,
|
|
331
|
-
expr: decodeHTMLEntities(attr),
|
|
332
|
-
priority: fn?.priority ?? DirectivePriority.NORMAL,
|
|
333
|
-
using: options.using
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// Also check local registry for backward compatibility
|
|
338
|
-
// Local registry uses short names with g- prefix
|
|
339
|
-
for (const [name, directive] of registry) {
|
|
340
|
-
const attr = match.getAttribute(`g-${name}`);
|
|
341
|
-
if (attr !== null) {
|
|
342
|
-
// Skip if already added from global registry
|
|
343
|
-
const fullName = `g-${name}`;
|
|
344
|
-
if (getDirective(fullName))
|
|
345
|
-
continue;
|
|
346
|
-
addToIndex({
|
|
347
|
-
el: match,
|
|
348
|
-
name,
|
|
349
|
-
directive,
|
|
350
|
-
expr: decodeHTMLEntities(attr),
|
|
351
|
-
priority: directive.priority ?? DirectivePriority.NORMAL
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
236
|
}
|
|
355
237
|
}
|
|
238
|
+
// Check if this is an attribute directive
|
|
239
|
+
const attr = match.getAttribute(name);
|
|
240
|
+
if (attr !== null) {
|
|
241
|
+
index.push({
|
|
242
|
+
el: match,
|
|
243
|
+
name,
|
|
244
|
+
directive: fn,
|
|
245
|
+
expr: decodeHTMLEntities(attr),
|
|
246
|
+
priority: fn?.priority ?? DirectivePriority.NORMAL,
|
|
247
|
+
using: options.using
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Also check local registry for backward compatibility
|
|
252
|
+
for (const [name, directive] of registry) {
|
|
253
|
+
const attr = match.getAttribute(`g-${name}`);
|
|
254
|
+
if (attr !== null) {
|
|
255
|
+
// Skip if already added from global registry
|
|
256
|
+
const fullName = `g-${name}`;
|
|
257
|
+
if (getDirective(fullName))
|
|
258
|
+
continue;
|
|
259
|
+
index.push({
|
|
260
|
+
el: match,
|
|
261
|
+
name,
|
|
262
|
+
directive,
|
|
263
|
+
expr: decodeHTMLEntities(attr),
|
|
264
|
+
priority: directive.priority ?? DirectivePriority.NORMAL
|
|
265
|
+
});
|
|
266
|
+
}
|
|
356
267
|
}
|
|
357
268
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
269
|
+
}
|
|
270
|
+
// Set HTML and index initial tree
|
|
360
271
|
document.body.innerHTML = html;
|
|
361
|
-
|
|
272
|
+
indexTree(document.body);
|
|
362
273
|
const ctx = createContext(Mode.SERVER, state);
|
|
363
274
|
const processed = new Set();
|
|
364
275
|
// Process directives in rounds until no new elements are added
|
|
@@ -409,30 +320,6 @@ export async function render(html, state, registry) {
|
|
|
409
320
|
const directives = byElement.get(el);
|
|
410
321
|
// Sort directives on this element by priority (higher first)
|
|
411
322
|
directives.sort((a, b) => b.priority - a.priority);
|
|
412
|
-
// Process g-scope first (inline scope initialization)
|
|
413
|
-
const scopeAttr = el.getAttribute('g-scope');
|
|
414
|
-
if (scopeAttr) {
|
|
415
|
-
const scopeValues = ctx.eval(decodeHTMLEntities(scopeAttr));
|
|
416
|
-
if (scopeValues && typeof scopeValues === 'object') {
|
|
417
|
-
Object.assign(state, scopeValues);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// Process g-bind:* attributes (dynamic attribute binding)
|
|
421
|
-
// Use the nearest ancestor scope for evaluation
|
|
422
|
-
const bindScope = findServerScope(el, state);
|
|
423
|
-
const bindCtx = createContext(Mode.SERVER, bindScope);
|
|
424
|
-
for (const attr of [...el.attributes]) {
|
|
425
|
-
if (attr.name.startsWith('g-bind:')) {
|
|
426
|
-
const targetAttr = attr.name.slice('g-bind:'.length);
|
|
427
|
-
const value = bindCtx.eval(decodeHTMLEntities(attr.value));
|
|
428
|
-
if (value === null || value === undefined) {
|
|
429
|
-
el.removeAttribute(targetAttr);
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
el.setAttribute(targetAttr, String(value));
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
323
|
// Collect unique directive names for conflict detection
|
|
437
324
|
const directiveNameSet = new Set();
|
|
438
325
|
for (const item of directives) {
|
|
@@ -442,6 +329,7 @@ export async function render(html, state, registry) {
|
|
|
442
329
|
}
|
|
443
330
|
const directiveNames = [...directiveNameSet];
|
|
444
331
|
// Check if any directive needs scope - create once if so
|
|
332
|
+
// Must happen BEFORE g-scope and g-bind so assigns are available
|
|
445
333
|
let elementScope = null;
|
|
446
334
|
for (const name of directiveNames) {
|
|
447
335
|
if (directiveNeedsScope(name)) {
|
|
@@ -452,6 +340,19 @@ export async function render(html, state, registry) {
|
|
|
452
340
|
break;
|
|
453
341
|
}
|
|
454
342
|
}
|
|
343
|
+
// Use element scope if created, otherwise find nearest ancestor
|
|
344
|
+
const scopeState = elementScope ?? findServerScope(el, state);
|
|
345
|
+
const scopeCtx = createContext(Mode.SERVER, scopeState);
|
|
346
|
+
// Process g-scope (inline scope initialization)
|
|
347
|
+
const scopeAttr = el.getAttribute('g-scope');
|
|
348
|
+
if (scopeAttr) {
|
|
349
|
+
const scopeValues = scopeCtx.eval(decodeHTMLEntities(scopeAttr));
|
|
350
|
+
if (scopeValues && typeof scopeValues === 'object') {
|
|
351
|
+
Object.assign(scopeState, scopeValues);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Process g-bind:* attributes (dynamic attribute binding)
|
|
355
|
+
processBindAttributesOnce(el, scopeCtx, true);
|
|
455
356
|
for (const item of directives) {
|
|
456
357
|
// Check if element was disconnected by a previous directive (e.g., g-for replacing it)
|
|
457
358
|
if (!item.el.isConnected) {
|
|
@@ -476,7 +377,7 @@ export async function render(html, state, registry) {
|
|
|
476
377
|
}
|
|
477
378
|
// Call directive function if present (initializes state)
|
|
478
379
|
if (fn) {
|
|
479
|
-
const config = createServerResolverConfig(item.el, scopeState, state);
|
|
380
|
+
const config = createServerResolverConfig(item.el, scopeState, state, services);
|
|
480
381
|
const args = resolveInjectables(fn, item.expr, item.el, ctx.eval.bind(ctx), config, options.using);
|
|
481
382
|
await fn(...args);
|
|
482
383
|
// Register as context provider if directive declares $context
|
|
@@ -487,15 +388,17 @@ export async function render(html, state, registry) {
|
|
|
487
388
|
// Render template if present
|
|
488
389
|
if (options.template) {
|
|
489
390
|
const attrs = getTemplateAttrs(item.el);
|
|
490
|
-
let
|
|
391
|
+
let templateHtml;
|
|
491
392
|
if (typeof options.template === 'string') {
|
|
492
|
-
|
|
393
|
+
templateHtml = options.template;
|
|
493
394
|
}
|
|
494
395
|
else {
|
|
495
396
|
const result = options.template(attrs, item.el);
|
|
496
|
-
|
|
397
|
+
templateHtml = result instanceof Promise ? await result : result;
|
|
497
398
|
}
|
|
498
|
-
item.el.innerHTML =
|
|
399
|
+
item.el.innerHTML = templateHtml;
|
|
400
|
+
// Index new elements from the template
|
|
401
|
+
indexTree(item.el);
|
|
499
402
|
}
|
|
500
403
|
}
|
|
501
404
|
else if (item.directive === null) {
|
|
@@ -512,7 +415,7 @@ export async function render(html, state, registry) {
|
|
|
512
415
|
if (options.provide) {
|
|
513
416
|
registerDIProviders(item.el, options.provide);
|
|
514
417
|
}
|
|
515
|
-
const config = createServerResolverConfig(item.el, scopeState, state);
|
|
418
|
+
const config = createServerResolverConfig(item.el, scopeState, state, services);
|
|
516
419
|
const args = resolveInjectables(item.directive, item.expr, item.el, ctx.eval.bind(ctx), config, item.using);
|
|
517
420
|
await item.directive(...args);
|
|
518
421
|
// Register as context provider if directive declares $context
|
|
@@ -521,10 +424,9 @@ export async function render(html, state, registry) {
|
|
|
521
424
|
}
|
|
522
425
|
}
|
|
523
426
|
}
|
|
524
|
-
// Let observer catch new elements
|
|
525
|
-
await new Promise(r => setTimeout(r, 0));
|
|
526
427
|
}
|
|
428
|
+
// Re-index tree to catch elements added by directives (e.g., g-template)
|
|
429
|
+
indexTree(document.body);
|
|
527
430
|
}
|
|
528
|
-
observer.disconnect();
|
|
529
431
|
return document.body.innerHTML;
|
|
530
432
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared template utilities for client and server.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { TemplateAttrs } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Extract template attributes from an element.
|
|
9
|
+
*
|
|
10
|
+
* @param el - The element to extract attributes from
|
|
11
|
+
* @returns An object with all attributes and children innerHTML
|
|
12
|
+
*/
|
|
13
|
+
export declare function getTemplateAttrs(el: Element): TemplateAttrs;
|
|
14
|
+
/**
|
|
15
|
+
* Check if element has any g-bind:* attributes.
|
|
16
|
+
*
|
|
17
|
+
* @param el - The element to check
|
|
18
|
+
* @returns True if element has g-bind attributes
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasBindAttributes(el: Element): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Decode HTML entities that happy-dom doesn't decode.
|
|
23
|
+
* Only needed for server-side rendering.
|
|
24
|
+
*
|
|
25
|
+
* @param str - The string with HTML entities
|
|
26
|
+
* @returns The decoded string
|
|
27
|
+
*/
|
|
28
|
+
export declare function decodeHTMLEntities(str: string): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared template utilities for client and server.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Extract template attributes from an element.
|
|
8
|
+
*
|
|
9
|
+
* @param el - The element to extract attributes from
|
|
10
|
+
* @returns An object with all attributes and children innerHTML
|
|
11
|
+
*/
|
|
12
|
+
export function getTemplateAttrs(el) {
|
|
13
|
+
const attrs = {
|
|
14
|
+
children: el.innerHTML
|
|
15
|
+
};
|
|
16
|
+
for (const attr of el.attributes) {
|
|
17
|
+
attrs[attr.name] = attr.value;
|
|
18
|
+
}
|
|
19
|
+
return attrs;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if element has any g-bind:* attributes.
|
|
23
|
+
*
|
|
24
|
+
* @param el - The element to check
|
|
25
|
+
* @returns True if element has g-bind attributes
|
|
26
|
+
*/
|
|
27
|
+
export function hasBindAttributes(el) {
|
|
28
|
+
for (const attr of el.attributes) {
|
|
29
|
+
if (attr.name.startsWith('g-bind:')) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decode HTML entities that happy-dom doesn't decode.
|
|
37
|
+
* Only needed for server-side rendering.
|
|
38
|
+
*
|
|
39
|
+
* @param str - The string with HTML entities
|
|
40
|
+
* @returns The decoded string
|
|
41
|
+
*/
|
|
42
|
+
export function decodeHTMLEntities(str) {
|
|
43
|
+
return str
|
|
44
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
45
|
+
.replace(/"/g, '"')
|
|
46
|
+
.replace(/'/g, "'")
|
|
47
|
+
.replace(/</g, '<')
|
|
48
|
+
.replace(/>/g, '>')
|
|
49
|
+
.replace(/&/g, '&');
|
|
50
|
+
}
|