gonia 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -7
- package/dist/client/hydrate.js +94 -59
- package/dist/context-registry.d.ts +130 -0
- package/dist/context-registry.js +153 -0
- package/dist/directives/for.d.ts +1 -1
- package/dist/directives/for.js +12 -76
- package/dist/directives/if.d.ts +4 -1
- package/dist/directives/if.js +69 -66
- package/dist/directives/slot.d.ts +4 -2
- package/dist/directives/slot.js +11 -16
- package/dist/directives/template.d.ts +9 -2
- package/dist/directives/template.js +14 -13
- package/dist/dom.d.ts +29 -0
- package/dist/dom.js +39 -0
- package/dist/expression.js +17 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -2
- package/dist/inject.d.ts +47 -6
- package/dist/inject.js +66 -4
- package/dist/process.d.ts +61 -0
- package/dist/process.js +215 -0
- package/dist/providers.js +9 -12
- package/dist/scope.js +17 -22
- package/dist/server/render.js +70 -41
- package/dist/types.d.ts +130 -20
- package/dist/types.js +48 -2
- package/dist/vite/plugin.d.ts +27 -0
- package/dist/vite/plugin.js +56 -3
- package/package.json +9 -2
package/dist/directives/for.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import { directive, DirectivePriority, Mode } from '../types.js';
|
|
7
|
-
import { effect, createEffectScope
|
|
8
|
-
import {
|
|
7
|
+
import { effect, createEffectScope } from '../reactivity.js';
|
|
8
|
+
import { processElementTree } from '../process.js';
|
|
9
9
|
/**
|
|
10
10
|
* Parse a g-for expression.
|
|
11
11
|
*
|
|
@@ -31,77 +31,10 @@ function parseForExpression(expr) {
|
|
|
31
31
|
export const FOR_PROCESSED_ATTR = 'data-g-for-processed';
|
|
32
32
|
/** Attribute used to mark template content that should be skipped during SSR */
|
|
33
33
|
export const FOR_TEMPLATE_ATTR = 'data-g-for-template';
|
|
34
|
-
/**
|
|
35
|
-
* Process directives on a cloned element within a child scope.
|
|
36
|
-
*/
|
|
37
|
-
function processClonedElement(el, parentState, scopeAdditions, mode) {
|
|
38
|
-
// Mark this element as processed by g-for so hydrate skips it
|
|
39
|
-
el.setAttribute(FOR_PROCESSED_ATTR, '');
|
|
40
|
-
const childScope = createScope(parentState, scopeAdditions);
|
|
41
|
-
const childCtx = createContext(mode, childScope);
|
|
42
|
-
// Process g-text directives
|
|
43
|
-
const textAttr = el.getAttribute('g-text');
|
|
44
|
-
if (textAttr) {
|
|
45
|
-
const value = childCtx.eval(textAttr);
|
|
46
|
-
el.textContent = String(value ?? '');
|
|
47
|
-
}
|
|
48
|
-
// Process g-class directives
|
|
49
|
-
const classAttr = el.getAttribute('g-class');
|
|
50
|
-
if (classAttr) {
|
|
51
|
-
const classObj = childCtx.eval(classAttr);
|
|
52
|
-
if (classObj && typeof classObj === 'object') {
|
|
53
|
-
for (const [className, shouldAdd] of Object.entries(classObj)) {
|
|
54
|
-
if (shouldAdd) {
|
|
55
|
-
el.classList.add(className);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
el.classList.remove(className);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Process g-show directives
|
|
64
|
-
const showAttr = el.getAttribute('g-show');
|
|
65
|
-
if (showAttr) {
|
|
66
|
-
const value = childCtx.eval(showAttr);
|
|
67
|
-
el.style.display = value ? '' : 'none';
|
|
68
|
-
}
|
|
69
|
-
// Process g-on directives (format: "event: handler") - client only
|
|
70
|
-
if (mode === Mode.CLIENT) {
|
|
71
|
-
const onAttr = el.getAttribute('g-on');
|
|
72
|
-
if (onAttr) {
|
|
73
|
-
setupEventHandler(el, onAttr, childCtx, childScope);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Process children recursively
|
|
77
|
-
for (const child of el.children) {
|
|
78
|
-
processClonedElement(child, childScope, {}, mode);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Set up an event handler on an element.
|
|
83
|
-
* Expression format: "event: handler"
|
|
84
|
-
*/
|
|
85
|
-
function setupEventHandler(el, expr, ctx, state) {
|
|
86
|
-
const colonIdx = expr.indexOf(':');
|
|
87
|
-
if (colonIdx === -1) {
|
|
88
|
-
console.error(`Invalid g-on expression: ${expr}. Expected "event: handler"`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const eventName = expr.slice(0, colonIdx).trim();
|
|
92
|
-
const handlerExpr = expr.slice(colonIdx + 1).trim();
|
|
93
|
-
const handler = (event) => {
|
|
94
|
-
const result = ctx.eval(handlerExpr);
|
|
95
|
-
if (typeof result === 'function') {
|
|
96
|
-
result.call(state, event);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
el.addEventListener(eventName, handler);
|
|
100
|
-
}
|
|
101
34
|
/**
|
|
102
35
|
* Render loop items (used by both server and client).
|
|
103
36
|
*/
|
|
104
|
-
function renderItems(template, parent, insertAfterNode, parsed, $eval, $
|
|
37
|
+
function renderItems(template, parent, insertAfterNode, parsed, $eval, $scope, mode) {
|
|
105
38
|
const { itemName, indexName, iterableName } = parsed;
|
|
106
39
|
const iterable = $eval(iterableName);
|
|
107
40
|
const renderedElements = [];
|
|
@@ -136,7 +69,10 @@ function renderItems(template, parent, insertAfterNode, parsed, $eval, $state, m
|
|
|
136
69
|
if (indexName) {
|
|
137
70
|
scopeAdditions[indexName] = key;
|
|
138
71
|
}
|
|
139
|
-
|
|
72
|
+
// Mark as processed
|
|
73
|
+
clone.setAttribute(FOR_PROCESSED_ATTR, '');
|
|
74
|
+
// Process with shared utility
|
|
75
|
+
processElementTree(clone, $scope, mode, { scopeAdditions });
|
|
140
76
|
if (insertAfter.nextSibling) {
|
|
141
77
|
parent.insertBefore(clone, insertAfter.nextSibling);
|
|
142
78
|
}
|
|
@@ -177,7 +113,7 @@ function removeSSRItems(templateEl) {
|
|
|
177
113
|
* <div g-for="(value, key) in object" g-text="key + ': ' + value"></div>
|
|
178
114
|
* ```
|
|
179
115
|
*/
|
|
180
|
-
export const cfor = function cfor($expr, $element, $eval, $
|
|
116
|
+
export const cfor = function cfor($expr, $element, $eval, $scope, $mode) {
|
|
181
117
|
const parsed = parseForExpression($expr);
|
|
182
118
|
if (!parsed) {
|
|
183
119
|
console.error(`Invalid g-for expression: ${$expr}`);
|
|
@@ -204,7 +140,7 @@ export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
|
|
|
204
140
|
// Replace original with template wrapper
|
|
205
141
|
parent.replaceChild(templateWrapper, $element);
|
|
206
142
|
// Render items after the template
|
|
207
|
-
renderItems(templateContent, parent, templateWrapper, parsed, $eval, $
|
|
143
|
+
renderItems(templateContent, parent, templateWrapper, parsed, $eval, $scope, $mode);
|
|
208
144
|
return;
|
|
209
145
|
}
|
|
210
146
|
// Client-side: check if hydrating from SSR or fresh render
|
|
@@ -230,7 +166,7 @@ export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
|
|
|
230
166
|
}
|
|
231
167
|
scope = createEffectScope();
|
|
232
168
|
scope.run(() => {
|
|
233
|
-
renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $
|
|
169
|
+
renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $scope, Mode.CLIENT);
|
|
234
170
|
});
|
|
235
171
|
});
|
|
236
172
|
}
|
|
@@ -255,11 +191,11 @@ export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
|
|
|
255
191
|
}
|
|
256
192
|
scope = createEffectScope();
|
|
257
193
|
scope.run(() => {
|
|
258
|
-
renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $
|
|
194
|
+
renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $scope, Mode.CLIENT);
|
|
259
195
|
});
|
|
260
196
|
});
|
|
261
197
|
}
|
|
262
198
|
};
|
|
263
|
-
cfor.$inject = ['$expr', '$element', '$eval', '$
|
|
199
|
+
cfor.$inject = ['$expr', '$element', '$eval', '$scope', '$mode'];
|
|
264
200
|
cfor.priority = DirectivePriority.STRUCTURAL;
|
|
265
201
|
directive('g-for', cfor);
|
package/dist/directives/if.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare const IF_PROCESSED_ATTR = "data-g-if-processed";
|
|
|
13
13
|
* Unlike g-show which uses display:none, g-if completely removes
|
|
14
14
|
* the element from the DOM when the condition is falsy.
|
|
15
15
|
*
|
|
16
|
+
* State within the conditional block is preserved across toggles.
|
|
17
|
+
* The scope is anchored to the placeholder, not the rendered element.
|
|
18
|
+
*
|
|
16
19
|
* On server: evaluates once and removes element if false.
|
|
17
20
|
* On client: sets up reactive effect to toggle element.
|
|
18
21
|
*
|
|
@@ -22,4 +25,4 @@ export declare const IF_PROCESSED_ATTR = "data-g-if-processed";
|
|
|
22
25
|
* <div g-if="items.length > 0">Has items</div>
|
|
23
26
|
* ```
|
|
24
27
|
*/
|
|
25
|
-
export declare const cif: Directive<['$expr', '$element', '$eval', '$
|
|
28
|
+
export declare const cif: Directive<['$expr', '$element', '$eval', '$scope', '$mode']>;
|
package/dist/directives/if.js
CHANGED
|
@@ -4,66 +4,30 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import { directive, DirectivePriority, Mode } from '../types.js';
|
|
7
|
-
import { effect } from '../reactivity.js';
|
|
8
|
-
import {
|
|
9
|
-
import { createScope } from '../reactivity.js';
|
|
7
|
+
import { effect, createScope } from '../reactivity.js';
|
|
8
|
+
import { processElementTree } from '../process.js';
|
|
10
9
|
/** Attribute used to mark elements processed by g-if */
|
|
11
10
|
export const IF_PROCESSED_ATTR = 'data-g-if-processed';
|
|
11
|
+
/** WeakMap to store persistent scopes for g-if placeholders */
|
|
12
|
+
const placeholderScopes = new WeakMap();
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Get or create a persistent scope for a g-if placeholder.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* The scope is anchored to the placeholder element, not the rendered content.
|
|
18
|
+
* This allows state to persist across condition toggles.
|
|
19
|
+
*
|
|
20
|
+
* @param placeholder - The template placeholder element
|
|
21
|
+
* @param parentState - The parent scope to inherit from
|
|
22
|
+
* @returns The persistent scope for this g-if block
|
|
14
23
|
*/
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const textAttr = el.getAttribute('g-text');
|
|
21
|
-
if (textAttr) {
|
|
22
|
-
const value = childCtx.eval(textAttr);
|
|
23
|
-
el.textContent = String(value ?? '');
|
|
24
|
-
}
|
|
25
|
-
// Process g-class directives
|
|
26
|
-
const classAttr = el.getAttribute('g-class');
|
|
27
|
-
if (classAttr) {
|
|
28
|
-
const classObj = childCtx.eval(classAttr);
|
|
29
|
-
if (classObj && typeof classObj === 'object') {
|
|
30
|
-
for (const [className, shouldAdd] of Object.entries(classObj)) {
|
|
31
|
-
if (shouldAdd) {
|
|
32
|
-
el.classList.add(className);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
el.classList.remove(className);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Process g-show directives
|
|
41
|
-
const showAttr = el.getAttribute('g-show');
|
|
42
|
-
if (showAttr) {
|
|
43
|
-
const value = childCtx.eval(showAttr);
|
|
44
|
-
el.style.display = value ? '' : 'none';
|
|
45
|
-
}
|
|
46
|
-
// Process g-on directives (format: "event: handler") - client only
|
|
47
|
-
if (mode === Mode.CLIENT) {
|
|
48
|
-
const onAttr = el.getAttribute('g-on');
|
|
49
|
-
if (onAttr) {
|
|
50
|
-
const colonIdx = onAttr.indexOf(':');
|
|
51
|
-
if (colonIdx !== -1) {
|
|
52
|
-
const eventName = onAttr.slice(0, colonIdx).trim();
|
|
53
|
-
const handlerExpr = onAttr.slice(colonIdx + 1).trim();
|
|
54
|
-
el.addEventListener(eventName, (event) => {
|
|
55
|
-
const result = childCtx.eval(handlerExpr);
|
|
56
|
-
if (typeof result === 'function') {
|
|
57
|
-
result.call(childScope, event);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Process children recursively
|
|
64
|
-
for (const child of el.children) {
|
|
65
|
-
processConditionalElement(child, childScope, mode);
|
|
24
|
+
function getOrCreateScope(placeholder, parentState) {
|
|
25
|
+
let scope = placeholderScopes.get(placeholder);
|
|
26
|
+
if (!scope) {
|
|
27
|
+
scope = createScope(parentState, {});
|
|
28
|
+
placeholderScopes.set(placeholder, scope);
|
|
66
29
|
}
|
|
30
|
+
return scope;
|
|
67
31
|
}
|
|
68
32
|
/**
|
|
69
33
|
* Conditionally render an element.
|
|
@@ -72,6 +36,9 @@ function processConditionalElement(el, parentState, mode) {
|
|
|
72
36
|
* Unlike g-show which uses display:none, g-if completely removes
|
|
73
37
|
* the element from the DOM when the condition is falsy.
|
|
74
38
|
*
|
|
39
|
+
* State within the conditional block is preserved across toggles.
|
|
40
|
+
* The scope is anchored to the placeholder, not the rendered element.
|
|
41
|
+
*
|
|
75
42
|
* On server: evaluates once and removes element if false.
|
|
76
43
|
* On client: sets up reactive effect to toggle element.
|
|
77
44
|
*
|
|
@@ -81,37 +48,72 @@ function processConditionalElement(el, parentState, mode) {
|
|
|
81
48
|
* <div g-if="items.length > 0">Has items</div>
|
|
82
49
|
* ```
|
|
83
50
|
*/
|
|
84
|
-
export const cif = function cif($expr, $element, $eval, $
|
|
51
|
+
export const cif = function cif($expr, $element, $eval, $scope, $mode) {
|
|
85
52
|
const parent = $element.parentNode;
|
|
86
53
|
if (!parent) {
|
|
87
54
|
return;
|
|
88
55
|
}
|
|
89
|
-
// Server-side: evaluate once and
|
|
56
|
+
// Server-side: evaluate once and leave template placeholder if false
|
|
90
57
|
if ($mode === Mode.SERVER) {
|
|
91
58
|
const condition = $eval($expr);
|
|
92
59
|
if (!condition) {
|
|
60
|
+
// Leave a template marker so client hydration knows where to insert
|
|
61
|
+
const placeholder = $element.ownerDocument.createElement('template');
|
|
62
|
+
placeholder.setAttribute('data-g-if', String($expr));
|
|
63
|
+
// Store original element inside template for hydration
|
|
64
|
+
placeholder.innerHTML = $element.outerHTML;
|
|
65
|
+
parent.insertBefore(placeholder, $element);
|
|
93
66
|
$element.remove();
|
|
94
67
|
}
|
|
95
68
|
else {
|
|
96
69
|
// Process child directives
|
|
97
70
|
$element.removeAttribute('g-if');
|
|
98
|
-
|
|
71
|
+
$element.setAttribute(IF_PROCESSED_ATTR, '');
|
|
72
|
+
processElementTree($element, $scope, $mode);
|
|
99
73
|
}
|
|
100
74
|
return;
|
|
101
75
|
}
|
|
102
|
-
// Client-side:
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
76
|
+
// Client-side: check if this is a template placeholder (from SSR)
|
|
77
|
+
const isTemplatePlaceholder = $element.tagName === 'TEMPLATE' && $element.hasAttribute('data-g-if');
|
|
78
|
+
let placeholder;
|
|
79
|
+
let template;
|
|
80
|
+
if (isTemplatePlaceholder) {
|
|
81
|
+
// Hydrating SSR output - template is the placeholder, content is inside
|
|
82
|
+
placeholder = $element;
|
|
83
|
+
const content = $element.content.firstElementChild
|
|
84
|
+
|| $element.innerHTML;
|
|
85
|
+
if (typeof content === 'string') {
|
|
86
|
+
const temp = $element.ownerDocument.createElement('div');
|
|
87
|
+
temp.innerHTML = content;
|
|
88
|
+
template = temp.firstElementChild;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
template = content.cloneNode(true);
|
|
92
|
+
}
|
|
93
|
+
template.removeAttribute('g-if');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Pure client-side - create template placeholder
|
|
97
|
+
placeholder = $element.ownerDocument.createElement('template');
|
|
98
|
+
placeholder.setAttribute('data-g-if', String($expr));
|
|
99
|
+
parent.insertBefore(placeholder, $element);
|
|
100
|
+
template = $element.cloneNode(true);
|
|
101
|
+
template.removeAttribute('g-if');
|
|
102
|
+
$element.remove();
|
|
103
|
+
}
|
|
104
|
+
// Create persistent scope anchored to the placeholder
|
|
105
|
+
const persistentScope = getOrCreateScope(placeholder, $scope);
|
|
108
106
|
let renderedElement = null;
|
|
109
107
|
effect(() => {
|
|
110
108
|
const condition = $eval($expr);
|
|
111
109
|
if (condition) {
|
|
112
110
|
if (!renderedElement) {
|
|
113
111
|
renderedElement = template.cloneNode(true);
|
|
114
|
-
|
|
112
|
+
renderedElement.setAttribute(IF_PROCESSED_ATTR, '');
|
|
113
|
+
// Process with the persistent scope - state survives across toggles
|
|
114
|
+
processElementTree(renderedElement, $scope, Mode.CLIENT, {
|
|
115
|
+
existingScope: persistentScope
|
|
116
|
+
});
|
|
115
117
|
if (placeholder.nextSibling) {
|
|
116
118
|
parent.insertBefore(renderedElement, placeholder.nextSibling);
|
|
117
119
|
}
|
|
@@ -124,10 +126,11 @@ export const cif = function cif($expr, $element, $eval, $state, $mode) {
|
|
|
124
126
|
if (renderedElement) {
|
|
125
127
|
renderedElement.remove();
|
|
126
128
|
renderedElement = null;
|
|
129
|
+
// Scope survives in persistentScope - ready for next render
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
});
|
|
130
133
|
};
|
|
131
|
-
cif.$inject = ['$expr', '$element', '$eval', '$
|
|
134
|
+
cif.$inject = ['$expr', '$element', '$eval', '$scope', '$mode'];
|
|
132
135
|
cif.priority = DirectivePriority.STRUCTURAL;
|
|
133
136
|
directive('g-if', cif);
|
|
@@ -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
|
+
}
|
package/dist/expression.js
CHANGED
|
@@ -43,8 +43,23 @@ export function findRoots(expr) {
|
|
|
43
43
|
.replace(/'(?:[^'\\]|\\.)*'/g, '""')
|
|
44
44
|
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
45
45
|
.replace(/`(?:[^`\\$]|\\.|\$(?!\{))*`/g, '""');
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Match identifiers that are not preceded by a dot
|
|
47
|
+
// Use simpler approach: match all identifiers, then filter out property accesses
|
|
48
|
+
const allIdentifiers = cleaned.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [];
|
|
49
|
+
// Filter to keep only root identifiers (not after a dot)
|
|
50
|
+
const roots = [];
|
|
51
|
+
let lastEnd = 0;
|
|
52
|
+
for (const id of allIdentifiers) {
|
|
53
|
+
const pos = cleaned.indexOf(id, lastEnd);
|
|
54
|
+
// Check if preceded by a dot (with optional whitespace)
|
|
55
|
+
const before = cleaned.slice(0, pos).trimEnd();
|
|
56
|
+
if (!before.endsWith('.')) {
|
|
57
|
+
if (!JS_KEYWORDS.has(id) && !roots.includes(id)) {
|
|
58
|
+
roots.push(id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lastEnd = pos + id.length;
|
|
62
|
+
}
|
|
48
63
|
return roots;
|
|
49
64
|
}
|
|
50
65
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
|
-
export { Mode, Expression, Context, Directive, directive, getDirective, getDirectiveNames, clearDirectives } from './types.js';
|
|
10
|
+
export { Mode, Expression, Context, Directive, directive, getDirective, getDirectiveNames, clearDirectives, configureDirective } from './types.js';
|
|
11
11
|
export type { DirectiveMeta } from './types.js';
|
|
12
12
|
export { createContext, createChildContext } from './context.js';
|
|
13
13
|
export { reactive, effect, createScope, createEffectScope } from './reactivity.js';
|
|
@@ -15,6 +15,12 @@ export type { EffectScope } from './reactivity.js';
|
|
|
15
15
|
export { createTemplateRegistry, createMemoryRegistry, createServerRegistry } from './templates.js';
|
|
16
16
|
export type { TemplateRegistry } from './templates.js';
|
|
17
17
|
export { findRoots, parseInterpolation } from './expression.js';
|
|
18
|
-
export { getInjectables } from './inject.js';
|
|
18
|
+
export { getInjectables, isContextKey } from './inject.js';
|
|
19
|
+
export type { Injectable } from './inject.js';
|
|
19
20
|
export { getRootScope, clearRootScope } from './scope.js';
|
|
21
|
+
export { findAncestor } from './dom.js';
|
|
22
|
+
export { processElementDirectives, processElementTree, PROCESSED_ATTR } from './process.js';
|
|
23
|
+
export type { ProcessOptions } from './process.js';
|
|
24
|
+
export { createContextKey, registerContext, resolveContext, hasContext, removeContext, clearContexts } from './context-registry.js';
|
|
25
|
+
export type { ContextKey } from './context-registry.js';
|
|
20
26
|
export * as directives from './directives/index.js';
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
|
-
export { Mode, directive, getDirective, getDirectiveNames, clearDirectives } from './types.js';
|
|
10
|
+
export { Mode, directive, getDirective, getDirectiveNames, clearDirectives, configureDirective } from './types.js';
|
|
11
11
|
export { createContext, createChildContext } from './context.js';
|
|
12
12
|
export { reactive, effect, createScope, createEffectScope } from './reactivity.js';
|
|
13
13
|
export { createTemplateRegistry, createMemoryRegistry, createServerRegistry } from './templates.js';
|
|
14
14
|
export { findRoots, parseInterpolation } from './expression.js';
|
|
15
|
-
export { getInjectables } from './inject.js';
|
|
15
|
+
export { getInjectables, isContextKey } from './inject.js';
|
|
16
16
|
export { getRootScope, clearRootScope } from './scope.js';
|
|
17
|
+
export { findAncestor } from './dom.js';
|
|
18
|
+
export { processElementDirectives, processElementTree, PROCESSED_ATTR } from './process.js';
|
|
19
|
+
export { createContextKey, registerContext, resolveContext, hasContext, removeContext, clearContexts } from './context-registry.js';
|
|
17
20
|
export * as directives from './directives/index.js';
|