gonia 0.0.1
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 +119 -0
- package/dist/client/hydrate.d.ts +54 -0
- package/dist/client/hydrate.js +445 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.js +6 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +69 -0
- package/dist/directives/class.d.ts +21 -0
- package/dist/directives/class.js +42 -0
- package/dist/directives/for.d.ts +29 -0
- package/dist/directives/for.js +265 -0
- package/dist/directives/html.d.ts +16 -0
- package/dist/directives/html.js +19 -0
- package/dist/directives/if.d.ts +25 -0
- package/dist/directives/if.js +133 -0
- package/dist/directives/index.d.ts +15 -0
- package/dist/directives/index.js +15 -0
- package/dist/directives/model.d.ts +27 -0
- package/dist/directives/model.js +134 -0
- package/dist/directives/on.d.ts +21 -0
- package/dist/directives/on.js +54 -0
- package/dist/directives/show.d.ts +15 -0
- package/dist/directives/show.js +19 -0
- package/dist/directives/slot.d.ts +48 -0
- package/dist/directives/slot.js +99 -0
- package/dist/directives/template.d.ts +55 -0
- package/dist/directives/template.js +147 -0
- package/dist/directives/text.d.ts +15 -0
- package/dist/directives/text.js +18 -0
- package/dist/expression.d.ts +60 -0
- package/dist/expression.js +96 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +16 -0
- package/dist/inject.d.ts +42 -0
- package/dist/inject.js +63 -0
- package/dist/providers.d.ts +96 -0
- package/dist/providers.js +146 -0
- package/dist/reactivity.d.ts +95 -0
- package/dist/reactivity.js +219 -0
- package/dist/scope.d.ts +43 -0
- package/dist/scope.js +112 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +6 -0
- package/dist/server/render.d.ts +61 -0
- package/dist/server/render.js +243 -0
- package/dist/templates.d.ts +92 -0
- package/dist/templates.js +124 -0
- package/dist/types.d.ts +362 -0
- package/dist/types.js +110 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.js +6 -0
- package/dist/vite/plugin.d.ts +30 -0
- package/dist/vite/plugin.js +127 -0
- package/package.json +67 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-way binding directive.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { directive } from '../types.js';
|
|
7
|
+
import { effect } from '../reactivity.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse a property path and return getter/setter functions.
|
|
10
|
+
*
|
|
11
|
+
* @param path - Dot-separated property path (e.g., 'user.name')
|
|
12
|
+
* @param state - The state object to operate on
|
|
13
|
+
* @returns Object with get and set functions
|
|
14
|
+
*/
|
|
15
|
+
function createAccessor(path, $eval, state) {
|
|
16
|
+
const parts = path.trim().split('.');
|
|
17
|
+
return {
|
|
18
|
+
get: () => $eval(path),
|
|
19
|
+
set: (value) => {
|
|
20
|
+
let target = state;
|
|
21
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
22
|
+
const part = parts[i];
|
|
23
|
+
if (target[part] === undefined || target[part] === null) {
|
|
24
|
+
target[part] = {};
|
|
25
|
+
}
|
|
26
|
+
target = target[part];
|
|
27
|
+
}
|
|
28
|
+
target[parts[parts.length - 1]] = value;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the element type for determining binding behavior.
|
|
34
|
+
*/
|
|
35
|
+
function getInputType(el) {
|
|
36
|
+
const tagName = el.tagName.toLowerCase();
|
|
37
|
+
if (tagName === 'select') {
|
|
38
|
+
return 'select';
|
|
39
|
+
}
|
|
40
|
+
if (tagName === 'textarea') {
|
|
41
|
+
return 'textarea';
|
|
42
|
+
}
|
|
43
|
+
if (tagName === 'input') {
|
|
44
|
+
const type = el.type?.toLowerCase() || 'text';
|
|
45
|
+
if (type === 'checkbox') {
|
|
46
|
+
return 'checkbox';
|
|
47
|
+
}
|
|
48
|
+
if (type === 'radio') {
|
|
49
|
+
return 'radio';
|
|
50
|
+
}
|
|
51
|
+
if (type === 'number' || type === 'range') {
|
|
52
|
+
return 'number';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return 'text';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Bind form element value to state with two-way data binding.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Updates the element value when state changes, and updates state
|
|
62
|
+
* when the user modifies the element. Handles different input types:
|
|
63
|
+
* - text/textarea: binds to value, uses input event
|
|
64
|
+
* - checkbox: binds to checked, uses change event
|
|
65
|
+
* - radio: binds to checked, uses change event
|
|
66
|
+
* - select: binds to value, uses change event
|
|
67
|
+
* - number: binds to value (as number), uses input event
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```html
|
|
71
|
+
* <input c-model="name">
|
|
72
|
+
* <input type="checkbox" c-model="isActive">
|
|
73
|
+
* <select c-model="selectedOption">
|
|
74
|
+
* <textarea c-model="description"></textarea>
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const model = function model($expr, $element, $eval, $rootState) {
|
|
78
|
+
const inputType = getInputType($element);
|
|
79
|
+
const accessor = createAccessor($expr, $eval, $rootState);
|
|
80
|
+
const el = $element;
|
|
81
|
+
if (inputType === 'checkbox') {
|
|
82
|
+
effect(() => {
|
|
83
|
+
el.checked = Boolean(accessor.get());
|
|
84
|
+
});
|
|
85
|
+
el.addEventListener('change', () => {
|
|
86
|
+
accessor.set(el.checked);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (inputType === 'radio') {
|
|
90
|
+
effect(() => {
|
|
91
|
+
const value = accessor.get();
|
|
92
|
+
el.checked = el.value === String(value);
|
|
93
|
+
});
|
|
94
|
+
el.addEventListener('change', () => {
|
|
95
|
+
if (el.checked) {
|
|
96
|
+
accessor.set(el.value);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (inputType === 'select') {
|
|
101
|
+
effect(() => {
|
|
102
|
+
el.value = String(accessor.get() ?? '');
|
|
103
|
+
});
|
|
104
|
+
el.addEventListener('change', () => {
|
|
105
|
+
accessor.set(el.value);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else if (inputType === 'number') {
|
|
109
|
+
effect(() => {
|
|
110
|
+
const value = accessor.get();
|
|
111
|
+
el.value = value === null || value === undefined ? '' : String(value);
|
|
112
|
+
});
|
|
113
|
+
el.addEventListener('input', () => {
|
|
114
|
+
const value = el.value;
|
|
115
|
+
if (value === '') {
|
|
116
|
+
accessor.set(null);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const num = Number(value);
|
|
120
|
+
accessor.set(isNaN(num) ? value : num);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
effect(() => {
|
|
126
|
+
el.value = String(accessor.get() ?? '');
|
|
127
|
+
});
|
|
128
|
+
el.addEventListener('input', () => {
|
|
129
|
+
accessor.set(el.value);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
model.$inject = ['$expr', '$element', '$eval', '$rootState'];
|
|
134
|
+
directive('c-model', model);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event handling directive.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Directive } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Bind event handler to element.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* The handler receives the event object and can call preventDefault(),
|
|
12
|
+
* stopPropagation(), etc. as needed.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```html
|
|
16
|
+
* <button c-on="click: handleClick">Click me</button>
|
|
17
|
+
* <form c-on="submit: save">
|
|
18
|
+
* <input c-on="keydown: onKey">
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const on: Directive<['$expr', '$element', '$eval', '$rootState']>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event handling directive.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { directive } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Parse c-on expression: "event: handler" or "event: handler()"
|
|
9
|
+
*/
|
|
10
|
+
function parseOnExpression(expr) {
|
|
11
|
+
const colonIdx = expr.indexOf(':');
|
|
12
|
+
if (colonIdx === -1) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const event = expr.slice(0, colonIdx).trim();
|
|
16
|
+
const handler = expr.slice(colonIdx + 1).trim();
|
|
17
|
+
if (!event || !handler) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return { event, handler };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Bind event handler to element.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* The handler receives the event object and can call preventDefault(),
|
|
27
|
+
* stopPropagation(), etc. as needed.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```html
|
|
31
|
+
* <button c-on="click: handleClick">Click me</button>
|
|
32
|
+
* <form c-on="submit: save">
|
|
33
|
+
* <input c-on="keydown: onKey">
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const on = function on($expr, $element, $eval, $rootState) {
|
|
37
|
+
const parsed = parseOnExpression($expr);
|
|
38
|
+
if (!parsed) {
|
|
39
|
+
console.error(`Invalid c-on expression: ${$expr}. Expected "event: handler"`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { event: eventName, handler: handlerExpr } = parsed;
|
|
43
|
+
const handler = (event) => {
|
|
44
|
+
// Evaluate the expression. If it returns a function (e.g., "addTodo" instead
|
|
45
|
+
// of "addTodo()"), call it with the state as 'this' context and event as arg.
|
|
46
|
+
const result = $eval(handlerExpr);
|
|
47
|
+
if (typeof result === 'function') {
|
|
48
|
+
result.call($rootState, event);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
$element.addEventListener(eventName, handler);
|
|
52
|
+
};
|
|
53
|
+
on.$inject = ['$expr', '$element', '$eval', '$rootState'];
|
|
54
|
+
directive('c-on', on);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Directive } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Toggle element visibility based on an expression.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Sets `display: none` when the expression is falsy,
|
|
7
|
+
* removes inline display style when truthy.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```html
|
|
11
|
+
* <div c-show="isVisible">Visible content</div>
|
|
12
|
+
* <div c-show="items.length > 0">Has items</div>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const show: Directive<['$expr', '$element', '$eval']>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { directive } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Toggle element visibility based on an expression.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Sets `display: none` when the expression is falsy,
|
|
7
|
+
* removes inline display style when truthy.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```html
|
|
11
|
+
* <div c-show="isVisible">Visible content</div>
|
|
12
|
+
* <div c-show="items.length > 0">Has items</div>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const show = function show($expr, $element, $eval) {
|
|
16
|
+
const value = $eval($expr);
|
|
17
|
+
$element.style.display = value ? '' : 'none';
|
|
18
|
+
};
|
|
19
|
+
directive('c-show', show);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot directive for content transclusion.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* A slot is a placeholder in a template that receives content
|
|
6
|
+
* from the element using the template. Slots enable composition
|
|
7
|
+
* by allowing parent content to be projected into child templates.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { Directive } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Slot directive for content transclusion.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Finds the nearest template ancestor and transcludes the
|
|
17
|
+
* matching slot content into itself.
|
|
18
|
+
*
|
|
19
|
+
* If the slot name is an expression, wraps in an effect
|
|
20
|
+
* for reactivity.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* Static slot name:
|
|
24
|
+
* ```html
|
|
25
|
+
* <slot name="header"></slot>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Dynamic slot name:
|
|
29
|
+
* ```html
|
|
30
|
+
* <slot c-slot="activeTab"></slot>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Default slot (no name):
|
|
34
|
+
* ```html
|
|
35
|
+
* <slot></slot>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const slot: Directive<['$expr', '$element', '$eval']>;
|
|
39
|
+
/**
|
|
40
|
+
* Process native <slot> elements.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* This handles native `<slot>` elements in templates,
|
|
44
|
+
* treating them the same as `c-slot` directives.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare function processNativeSlot(el: Element): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot directive for content transclusion.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* A slot is a placeholder in a template that receives content
|
|
6
|
+
* from the element using the template. Slots enable composition
|
|
7
|
+
* by allowing parent content to be projected into child templates.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { directive } from '../types.js';
|
|
12
|
+
import { effect } from '../reactivity.js';
|
|
13
|
+
import { findTemplateAncestor, getSavedContent } from './template.js';
|
|
14
|
+
/**
|
|
15
|
+
* Slot directive for content transclusion.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Finds the nearest template ancestor and transcludes the
|
|
19
|
+
* matching slot content into itself.
|
|
20
|
+
*
|
|
21
|
+
* If the slot name is an expression, wraps in an effect
|
|
22
|
+
* for reactivity.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* Static slot name:
|
|
26
|
+
* ```html
|
|
27
|
+
* <slot name="header"></slot>
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Dynamic slot name:
|
|
31
|
+
* ```html
|
|
32
|
+
* <slot c-slot="activeTab"></slot>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Default slot (no name):
|
|
36
|
+
* ```html
|
|
37
|
+
* <slot></slot>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const slot = function slot($expr, $element, $eval) {
|
|
41
|
+
// Determine slot name
|
|
42
|
+
// If expr is empty, check for name attribute, otherwise use 'default'
|
|
43
|
+
const getName = () => {
|
|
44
|
+
if ($expr && String($expr).trim()) {
|
|
45
|
+
// Dynamic slot name from expression
|
|
46
|
+
return String($eval($expr));
|
|
47
|
+
}
|
|
48
|
+
// Static slot name from attribute or default
|
|
49
|
+
return $element.getAttribute('name') ?? 'default';
|
|
50
|
+
};
|
|
51
|
+
const transclude = () => {
|
|
52
|
+
const name = getName();
|
|
53
|
+
const templateEl = findTemplateAncestor($element);
|
|
54
|
+
if (!templateEl) {
|
|
55
|
+
// No template ancestor - leave slot as-is or clear it
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const content = getSavedContent(templateEl);
|
|
59
|
+
const slotContent = content.slots.get(name);
|
|
60
|
+
if (slotContent) {
|
|
61
|
+
$element.innerHTML = slotContent;
|
|
62
|
+
// MutationObserver will process the new content
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// No content for this slot - could show fallback
|
|
66
|
+
// For now, leave any default content in the slot
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// If expression is provided, make it reactive
|
|
70
|
+
if ($expr && String($expr).trim()) {
|
|
71
|
+
effect(transclude);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Static slot name - just run once
|
|
75
|
+
transclude();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
directive('c-slot', slot);
|
|
79
|
+
/**
|
|
80
|
+
* Process native <slot> elements.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* This handles native `<slot>` elements in templates,
|
|
84
|
+
* treating them the same as `c-slot` directives.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
export function processNativeSlot(el) {
|
|
89
|
+
const name = el.getAttribute('name') ?? 'default';
|
|
90
|
+
const templateEl = findTemplateAncestor(el);
|
|
91
|
+
if (!templateEl) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const content = getSavedContent(templateEl);
|
|
95
|
+
const slotContent = content.slots.get(name);
|
|
96
|
+
if (slotContent) {
|
|
97
|
+
el.outerHTML = slotContent;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template directive for rendering reusable templates.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Saves the element's children for slot transclusion,
|
|
6
|
+
* fetches and renders the template, and sets up context
|
|
7
|
+
* for nested slots to access the saved content.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { Directive } from '../types.js';
|
|
12
|
+
import { EffectScope } from '../reactivity.js';
|
|
13
|
+
/**
|
|
14
|
+
* Saved slot content for an element.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface SlotContent {
|
|
19
|
+
/** Content by slot name. 'default' for unnamed content. */
|
|
20
|
+
slots: Map<string, string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get saved slot content for an element.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare function getSavedContent(el: Element): SlotContent | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Find the nearest ancestor with saved content (the template element).
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export declare function findTemplateAncestor(el: Element): Element | null;
|
|
34
|
+
/**
|
|
35
|
+
* Template directive for rendering reusable templates.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Fetches a template by name and replaces the element's content.
|
|
39
|
+
* Children are saved for slot transclusion before replacement.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```html
|
|
43
|
+
* <div c-template="dialog">
|
|
44
|
+
* <span slot="header">Title</span>
|
|
45
|
+
* <p>Body content</p>
|
|
46
|
+
* </div>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const template: Directive<['$expr', '$element', '$templates']>;
|
|
50
|
+
/**
|
|
51
|
+
* Get the effect scope for an element.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export declare function getElementScope(el: Element): EffectScope | undefined;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template directive for rendering reusable templates.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Saves the element's children for slot transclusion,
|
|
6
|
+
* fetches and renders the template, and sets up context
|
|
7
|
+
* for nested slots to access the saved content.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { directive, DirectivePriority } from '../types.js';
|
|
12
|
+
import { createEffectScope } from '../reactivity.js';
|
|
13
|
+
/** WeakMap storing saved children per element. */
|
|
14
|
+
const savedContent = new WeakMap();
|
|
15
|
+
/** WeakMap storing effect scopes per element for cleanup. */
|
|
16
|
+
const elementScopes = new WeakMap();
|
|
17
|
+
/** Set tracking which templates are currently rendering (cycle detection). */
|
|
18
|
+
const renderingChain = new WeakMap();
|
|
19
|
+
/**
|
|
20
|
+
* Get saved slot content for an element.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export function getSavedContent(el) {
|
|
25
|
+
return savedContent.get(el);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find the nearest ancestor with saved content (the template element).
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function findTemplateAncestor(el) {
|
|
33
|
+
let current = el.parentElement;
|
|
34
|
+
while (current) {
|
|
35
|
+
if (savedContent.has(current)) {
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
current = current.parentElement;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if a node is an Element.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
function isElement(node) {
|
|
48
|
+
return node.nodeType === 1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract slot content from an element's children.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
function extractSlotContent(el) {
|
|
56
|
+
const slots = new Map();
|
|
57
|
+
const defaultParts = [];
|
|
58
|
+
for (const child of Array.from(el.childNodes)) {
|
|
59
|
+
if (isElement(child) && child.hasAttribute('slot')) {
|
|
60
|
+
const slotName = child.getAttribute('slot');
|
|
61
|
+
const existing = slots.get(slotName) ?? '';
|
|
62
|
+
slots.set(slotName, existing + child.outerHTML);
|
|
63
|
+
}
|
|
64
|
+
else if (isElement(child)) {
|
|
65
|
+
defaultParts.push(child.outerHTML);
|
|
66
|
+
}
|
|
67
|
+
else if (child.nodeType === 3) { // TEXT_NODE
|
|
68
|
+
const text = child.textContent;
|
|
69
|
+
if (text.trim()) {
|
|
70
|
+
defaultParts.push(text);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (defaultParts.length > 0) {
|
|
75
|
+
slots.set('default', defaultParts.join(''));
|
|
76
|
+
}
|
|
77
|
+
return slots;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Template directive for rendering reusable templates.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* Fetches a template by name and replaces the element's content.
|
|
84
|
+
* Children are saved for slot transclusion before replacement.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```html
|
|
88
|
+
* <div c-template="dialog">
|
|
89
|
+
* <span slot="header">Title</span>
|
|
90
|
+
* <p>Body content</p>
|
|
91
|
+
* </div>
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export const template = async function template($expr, $element, $templates) {
|
|
95
|
+
const templateName = String($expr);
|
|
96
|
+
// Clean up previous render if re-rendering
|
|
97
|
+
const prevScope = elementScopes.get($element);
|
|
98
|
+
if (prevScope) {
|
|
99
|
+
prevScope.stop();
|
|
100
|
+
// Clear the element's own chain since we're starting fresh
|
|
101
|
+
renderingChain.delete($element);
|
|
102
|
+
}
|
|
103
|
+
// Cycle detection - only inherit from ancestors
|
|
104
|
+
let chain;
|
|
105
|
+
let parent = $element.parentElement;
|
|
106
|
+
while (parent) {
|
|
107
|
+
const parentChain = renderingChain.get(parent);
|
|
108
|
+
if (parentChain) {
|
|
109
|
+
chain = new Set(parentChain);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
parent = parent.parentElement;
|
|
113
|
+
}
|
|
114
|
+
chain = chain ?? new Set();
|
|
115
|
+
if (chain.has(templateName)) {
|
|
116
|
+
console.error(`Cycle detected: template "${templateName}" is already being rendered`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Save children for slots
|
|
120
|
+
const slotContent = {
|
|
121
|
+
slots: extractSlotContent($element)
|
|
122
|
+
};
|
|
123
|
+
savedContent.set($element, slotContent);
|
|
124
|
+
// Track this template in the chain
|
|
125
|
+
const newChain = new Set(chain);
|
|
126
|
+
newChain.add(templateName);
|
|
127
|
+
renderingChain.set($element, newChain);
|
|
128
|
+
// Create effect scope for this element's descendants
|
|
129
|
+
const scope = createEffectScope();
|
|
130
|
+
elementScopes.set($element, scope);
|
|
131
|
+
// Fetch and render template
|
|
132
|
+
const html = await $templates.get(templateName);
|
|
133
|
+
$element.innerHTML = html;
|
|
134
|
+
// Note: MutationObserver will process the new children
|
|
135
|
+
};
|
|
136
|
+
template.$inject = ['$expr', '$element', '$templates'];
|
|
137
|
+
template.transclude = true;
|
|
138
|
+
template.priority = DirectivePriority.TEMPLATE;
|
|
139
|
+
directive('c-template', template);
|
|
140
|
+
/**
|
|
141
|
+
* Get the effect scope for an element.
|
|
142
|
+
*
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
145
|
+
export function getElementScope(el) {
|
|
146
|
+
return elementScopes.get(el);
|
|
147
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Directive } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Set element's text content from an expression.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Evaluates the expression and sets the element's `textContent`.
|
|
7
|
+
* Safe from XSS as it doesn't interpret HTML.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```html
|
|
11
|
+
* <span c-text="user.name"></span>
|
|
12
|
+
* <span c-text="'Hello, ' + user.name + '!'"></span>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const text: Directive<['$expr', '$element', '$eval']>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { directive } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Set element's text content from an expression.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Evaluates the expression and sets the element's `textContent`.
|
|
7
|
+
* Safe from XSS as it doesn't interpret HTML.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```html
|
|
11
|
+
* <span c-text="user.name"></span>
|
|
12
|
+
* <span c-text="'Hello, ' + user.name + '!'"></span>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const text = function text($expr, $element, $eval) {
|
|
16
|
+
$element.textContent = String($eval($expr) ?? '');
|
|
17
|
+
};
|
|
18
|
+
directive('c-text', text);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression parsing utilities.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Parses expression strings (from HTML attributes) to find root identifiers.
|
|
6
|
+
* Used for reactive dependency tracking - only access state keys that
|
|
7
|
+
* the expression actually references.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Find root identifiers in an expression.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* These are the top-level variable references that need to come from state.
|
|
16
|
+
* Used to enable precise reactive dependency tracking.
|
|
17
|
+
*
|
|
18
|
+
* @param expr - The expression string to parse
|
|
19
|
+
* @returns Array of root identifier names
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* findRoots('user.name') // ['user']
|
|
24
|
+
* findRoots('user.name + item.count') // ['user', 'item']
|
|
25
|
+
* findRoots('"hello.world"') // []
|
|
26
|
+
* findRoots('items.filter(x => x.active)') // ['items', 'x']
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function findRoots(expr: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* A segment of parsed interpolation template.
|
|
32
|
+
*/
|
|
33
|
+
export interface Segment {
|
|
34
|
+
/** Segment type: static text or expression */
|
|
35
|
+
type: 'static' | 'expr';
|
|
36
|
+
/** The segment value */
|
|
37
|
+
value: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse interpolation syntax in a string.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Splits a template string with `{{ expr }}` markers into segments
|
|
44
|
+
* of static text and expressions. Used by directives like c-href
|
|
45
|
+
* that support inline interpolation.
|
|
46
|
+
*
|
|
47
|
+
* @param template - The template string with interpolation markers
|
|
48
|
+
* @returns Array of segments
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* parseInterpolation('/users/{{ user.id }}/profile')
|
|
53
|
+
* // [
|
|
54
|
+
* // { type: 'static', value: '/users/' },
|
|
55
|
+
* // { type: 'expr', value: 'user.id' },
|
|
56
|
+
* // { type: 'static', value: '/profile' }
|
|
57
|
+
* // ]
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseInterpolation(template: string): Segment[];
|