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
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Gonia
|
|
2
|
+
|
|
3
|
+
A lightweight, SSR-first reactive UI library for building web applications with HTML-based templates and declarative directives.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **SSR-First Architecture** - Server-side rendering with seamless client hydration
|
|
8
|
+
- **Declarative Directives** - Vue-inspired template syntax (`c-text`, `c-for`, `c-if`, etc.)
|
|
9
|
+
- **Fine-Grained Reactivity** - Efficient updates without virtual DOM diffing
|
|
10
|
+
- **Zero Dependencies** - Core library has no runtime dependencies (linkedom for SSR only)
|
|
11
|
+
- **TypeScript Native** - Full type safety with excellent IDE support
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add gonia
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Server-Side Rendering
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { render, registerDirective } from 'gonia/server';
|
|
25
|
+
import { text, show, cfor, cif, cclass } from 'gonia';
|
|
26
|
+
|
|
27
|
+
// Create directive registry
|
|
28
|
+
const registry = new Map();
|
|
29
|
+
registerDirective(registry, 'text', text);
|
|
30
|
+
registerDirective(registry, 'show', show);
|
|
31
|
+
registerDirective(registry, 'for', cfor);
|
|
32
|
+
registerDirective(registry, 'if', cif);
|
|
33
|
+
registerDirective(registry, 'class', cclass);
|
|
34
|
+
|
|
35
|
+
// Render HTML with state
|
|
36
|
+
const html = await render(
|
|
37
|
+
'<ul><li c-for="item in items" c-text="item"></li></ul>',
|
|
38
|
+
{ items: ['Apple', 'Banana', 'Cherry'] },
|
|
39
|
+
registry
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Client-Side Hydration
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { hydrate } from 'gonia/client';
|
|
47
|
+
import { directive } from 'gonia';
|
|
48
|
+
|
|
49
|
+
// Import directives (registers globally)
|
|
50
|
+
import './directives/my-app.js';
|
|
51
|
+
|
|
52
|
+
// Hydrate when DOM is ready
|
|
53
|
+
hydrate();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Creating a Component Directive
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { directive, Directive } from 'gonia';
|
|
60
|
+
|
|
61
|
+
const myApp: Directive = ($element, $state) => {
|
|
62
|
+
// Initialize state
|
|
63
|
+
$state.count = 0;
|
|
64
|
+
|
|
65
|
+
// Define methods
|
|
66
|
+
$state.increment = () => {
|
|
67
|
+
$state.count++;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
myApp.$inject = ['$element', '$state'];
|
|
72
|
+
|
|
73
|
+
// Register with scope: true to create isolated state
|
|
74
|
+
directive('my-app', myApp, { scope: true });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<my-app>
|
|
79
|
+
<p c-text="count"></p>
|
|
80
|
+
<button c-on="click: increment">+1</button>
|
|
81
|
+
</my-app>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Directives
|
|
85
|
+
|
|
86
|
+
| Directive | Description | Example |
|
|
87
|
+
|-----------|-------------|---------|
|
|
88
|
+
| `c-text` | Set text content | `<span c-text="message"></span>` |
|
|
89
|
+
| `c-show` | Toggle visibility | `<div c-show="isVisible">...</div>` |
|
|
90
|
+
| `c-if` | Conditional render | `<p c-if="hasError">Error!</p>` |
|
|
91
|
+
| `c-for` | Loop iteration | `<li c-for="item in items">...</li>` |
|
|
92
|
+
| `c-class` | Dynamic classes | `<div c-class="{ active: isActive }">` |
|
|
93
|
+
| `c-model` | Two-way binding | `<input c-model="name">` |
|
|
94
|
+
| `c-on` | Event handling | `<button c-on="click: handleClick">` |
|
|
95
|
+
|
|
96
|
+
## Vite Integration
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// vite.config.ts
|
|
100
|
+
import { defineConfig } from 'vite';
|
|
101
|
+
import { gonia } from 'gonia/vite';
|
|
102
|
+
|
|
103
|
+
export default defineConfig({
|
|
104
|
+
plugins: [gonia()]
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Documentation
|
|
109
|
+
|
|
110
|
+
See the [docs](./docs) folder for detailed documentation:
|
|
111
|
+
|
|
112
|
+
- [Getting Started](./docs/getting-started.md)
|
|
113
|
+
- [Directives Reference](./docs/directives.md)
|
|
114
|
+
- [SSR Guide](./docs/ssr.md)
|
|
115
|
+
- [Reactivity](./docs/reactivity.md)
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side hydration and runtime directive binding.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Directive, Context } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Registry of directives by name.
|
|
9
|
+
*/
|
|
10
|
+
export type DirectiveRegistry = Map<string, Directive<any>>;
|
|
11
|
+
/**
|
|
12
|
+
* Service registry for dependency injection.
|
|
13
|
+
*/
|
|
14
|
+
export type ServiceRegistry = Map<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Set context for an element (used by directives that create child contexts).
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare function setElementContext(el: Element, ctx: Context): void;
|
|
21
|
+
/**
|
|
22
|
+
* Register a directive in the registry.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* If called after hydration, scans the DOM for any elements using
|
|
26
|
+
* this directive and processes them immediately.
|
|
27
|
+
*
|
|
28
|
+
* @param registry - The directive registry
|
|
29
|
+
* @param name - Directive name (without c- prefix)
|
|
30
|
+
* @param fn - The directive function
|
|
31
|
+
*/
|
|
32
|
+
export declare function registerDirective(registry: DirectiveRegistry, name: string, fn: Directive): void;
|
|
33
|
+
/**
|
|
34
|
+
* Register a service for dependency injection.
|
|
35
|
+
*
|
|
36
|
+
* @param name - Service name (used in $inject arrays)
|
|
37
|
+
* @param service - The service instance
|
|
38
|
+
*/
|
|
39
|
+
export declare function registerService(name: string, service: unknown): void;
|
|
40
|
+
export declare function init(registry?: DirectiveRegistry): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Hydrate server-rendered HTML.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Alias for {@link init}. Use when hydrating SSR output.
|
|
46
|
+
*/
|
|
47
|
+
export declare const hydrate: typeof init;
|
|
48
|
+
/**
|
|
49
|
+
* Mount the framework on client-rendered HTML.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* Alias for {@link init}. Use for pure client-side rendering.
|
|
53
|
+
*/
|
|
54
|
+
export declare const mount: typeof init;
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side hydration and runtime directive binding.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Mode, DirectivePriority, getDirective, getDirectiveNames } from '../types.js';
|
|
7
|
+
import { createContext } from '../context.js';
|
|
8
|
+
import { processNativeSlot } from '../directives/slot.js';
|
|
9
|
+
import { getLocalState, registerProvider, resolveFromProviders, registerDIProviders, resolveFromDIProviders } from '../providers.js';
|
|
10
|
+
import { FOR_PROCESSED_ATTR } from '../directives/for.js';
|
|
11
|
+
import { findParentScope, createElementScope, getElementScope } from '../scope.js';
|
|
12
|
+
import { getInjectables } from '../inject.js';
|
|
13
|
+
// Built-in directives
|
|
14
|
+
import { text } from '../directives/text.js';
|
|
15
|
+
import { show } from '../directives/show.js';
|
|
16
|
+
import { cclass } from '../directives/class.js';
|
|
17
|
+
import { model } from '../directives/model.js';
|
|
18
|
+
import { on } from '../directives/on.js';
|
|
19
|
+
import { cfor } from '../directives/for.js';
|
|
20
|
+
import { cif } from '../directives/if.js';
|
|
21
|
+
/** Cached selector string */
|
|
22
|
+
let cachedSelector = null;
|
|
23
|
+
/** Whether init() has been called */
|
|
24
|
+
let initialized = false;
|
|
25
|
+
/** Registered services */
|
|
26
|
+
let services = new Map();
|
|
27
|
+
/** Context cache by element */
|
|
28
|
+
const contextCache = new WeakMap();
|
|
29
|
+
/** Default registry with built-in directives */
|
|
30
|
+
let defaultRegistry = null;
|
|
31
|
+
/**
|
|
32
|
+
* Get the default directive registry with built-in directives.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
function getDefaultRegistry() {
|
|
37
|
+
if (!defaultRegistry) {
|
|
38
|
+
defaultRegistry = new Map();
|
|
39
|
+
defaultRegistry.set('text', text);
|
|
40
|
+
defaultRegistry.set('show', show);
|
|
41
|
+
defaultRegistry.set('class', cclass);
|
|
42
|
+
defaultRegistry.set('model', model);
|
|
43
|
+
defaultRegistry.set('on', on);
|
|
44
|
+
defaultRegistry.set('for', cfor);
|
|
45
|
+
defaultRegistry.set('if', cif);
|
|
46
|
+
}
|
|
47
|
+
return defaultRegistry;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build a CSS selector for all registered directives.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
function getSelector(registry) {
|
|
55
|
+
if (!cachedSelector) {
|
|
56
|
+
const directiveSelectors = [];
|
|
57
|
+
for (const name of registry.keys()) {
|
|
58
|
+
directiveSelectors.push(`[c-${name}]`);
|
|
59
|
+
}
|
|
60
|
+
// Also match native <slot> elements
|
|
61
|
+
directiveSelectors.push('slot');
|
|
62
|
+
cachedSelector = directiveSelectors.join(',');
|
|
63
|
+
}
|
|
64
|
+
return cachedSelector;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get directives for an element, sorted by priority (highest first).
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
function getDirectivesForElement(el, registry) {
|
|
72
|
+
const directives = [];
|
|
73
|
+
for (const [name, directive] of registry) {
|
|
74
|
+
const attr = el.getAttribute(`c-${name}`);
|
|
75
|
+
if (attr !== null) {
|
|
76
|
+
directives.push({ name, directive, expr: attr });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Sort by priority (higher first)
|
|
80
|
+
directives.sort((a, b) => {
|
|
81
|
+
const priorityA = a.directive.priority ?? DirectivePriority.NORMAL;
|
|
82
|
+
const priorityB = b.directive.priority ?? DirectivePriority.NORMAL;
|
|
83
|
+
return priorityB - priorityA;
|
|
84
|
+
});
|
|
85
|
+
return directives;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get or create context for an element.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* Walks up the DOM to find the nearest ancestor with a cached context,
|
|
92
|
+
* then creates a context using the nearest scope.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
function getContextForElement(el) {
|
|
97
|
+
// Check cache first
|
|
98
|
+
const cached = contextCache.get(el);
|
|
99
|
+
if (cached)
|
|
100
|
+
return cached;
|
|
101
|
+
// Walk up to find nearest context
|
|
102
|
+
let current = el.parentElement;
|
|
103
|
+
while (current) {
|
|
104
|
+
const parentCtx = contextCache.get(current);
|
|
105
|
+
if (parentCtx) {
|
|
106
|
+
const ctx = parentCtx.child({});
|
|
107
|
+
contextCache.set(el, ctx);
|
|
108
|
+
return ctx;
|
|
109
|
+
}
|
|
110
|
+
current = current.parentElement;
|
|
111
|
+
}
|
|
112
|
+
// Find nearest scope or create empty one
|
|
113
|
+
const scope = findParentScope(el, true) ?? createElementScope(el);
|
|
114
|
+
const ctx = createContext(Mode.CLIENT, scope);
|
|
115
|
+
contextCache.set(el, ctx);
|
|
116
|
+
return ctx;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Set context for an element (used by directives that create child contexts).
|
|
120
|
+
*
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export function setElementContext(el, ctx) {
|
|
124
|
+
contextCache.set(el, ctx);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve dependencies for a directive based on its $inject array.
|
|
128
|
+
*
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
function resolveDependencies(directive, expr, el, ctx) {
|
|
132
|
+
const inject = getInjectables(directive);
|
|
133
|
+
return inject.map(name => {
|
|
134
|
+
switch (name) {
|
|
135
|
+
case '$expr':
|
|
136
|
+
return expr;
|
|
137
|
+
case '$element':
|
|
138
|
+
return el;
|
|
139
|
+
case '$eval':
|
|
140
|
+
return ctx.eval.bind(ctx);
|
|
141
|
+
case '$state':
|
|
142
|
+
// Find nearest ancestor scope (including self)
|
|
143
|
+
return findParentScope(el, true) ?? getLocalState(el);
|
|
144
|
+
case '$rootState':
|
|
145
|
+
// Deprecated: same as $state now (scoped state)
|
|
146
|
+
return findParentScope(el, true) ?? getLocalState(el);
|
|
147
|
+
case '$mode':
|
|
148
|
+
return Mode.CLIENT;
|
|
149
|
+
default: {
|
|
150
|
+
// Look up in ancestor DI providers first (provide option)
|
|
151
|
+
const diProvided = resolveFromDIProviders(el, name);
|
|
152
|
+
if (diProvided !== undefined) {
|
|
153
|
+
return diProvided;
|
|
154
|
+
}
|
|
155
|
+
// Look up in global services registry
|
|
156
|
+
const service = services.get(name);
|
|
157
|
+
if (service !== undefined) {
|
|
158
|
+
return service;
|
|
159
|
+
}
|
|
160
|
+
// Look up in ancestor context providers ($context)
|
|
161
|
+
const contextProvided = resolveFromProviders(el, name);
|
|
162
|
+
if (contextProvided !== undefined) {
|
|
163
|
+
return contextProvided;
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Unknown injectable: ${name}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Process directives on a single element.
|
|
172
|
+
* Returns a promise if any directive is async, otherwise void.
|
|
173
|
+
* Directives on the same element are processed sequentially to handle dependencies.
|
|
174
|
+
*
|
|
175
|
+
* @internal
|
|
176
|
+
*/
|
|
177
|
+
function processElement(el, registry) {
|
|
178
|
+
// Skip elements already processed by c-for (they have their own child scope)
|
|
179
|
+
if (el.hasAttribute(FOR_PROCESSED_ATTR)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Handle native <slot> elements
|
|
183
|
+
if (el.tagName === 'SLOT') {
|
|
184
|
+
processNativeSlot(el);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const directives = getDirectivesForElement(el, registry);
|
|
188
|
+
if (directives.length === 0)
|
|
189
|
+
return;
|
|
190
|
+
const ctx = getContextForElement(el);
|
|
191
|
+
// Process directives sequentially, handling async ones properly
|
|
192
|
+
let chain;
|
|
193
|
+
for (const { directive, expr } of directives) {
|
|
194
|
+
const processDirective = () => {
|
|
195
|
+
const args = resolveDependencies(directive, expr, el, ctx);
|
|
196
|
+
const result = directive(...args);
|
|
197
|
+
// Register as provider if directive declares $context
|
|
198
|
+
if (directive.$context?.length) {
|
|
199
|
+
const state = getLocalState(el);
|
|
200
|
+
registerProvider(el, directive, state);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
};
|
|
204
|
+
// STRUCTURAL directives (like c-for) take ownership of the element.
|
|
205
|
+
// They remove the original and handle other directives on clones themselves.
|
|
206
|
+
const isStructural = directive.priority === DirectivePriority.STRUCTURAL;
|
|
207
|
+
if (chain instanceof Promise) {
|
|
208
|
+
// Previous directive was async, chain this one after it
|
|
209
|
+
chain = chain.then(() => {
|
|
210
|
+
const result = processDirective();
|
|
211
|
+
return result instanceof Promise ? result : undefined;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Previous directive was sync (or this is the first)
|
|
216
|
+
const result = processDirective();
|
|
217
|
+
if (result instanceof Promise) {
|
|
218
|
+
chain = result;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (isStructural) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return chain;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Process a node and its descendants for directives.
|
|
229
|
+
* Returns a promise that resolves when all async directives complete.
|
|
230
|
+
*
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
function processNode(node, selector, registry) {
|
|
234
|
+
const matches = node.matches?.(selector) ? [node] : [];
|
|
235
|
+
const descendants = [...(node.querySelectorAll?.(selector) ?? [])];
|
|
236
|
+
const promises = [];
|
|
237
|
+
for (const el of [...matches, ...descendants]) {
|
|
238
|
+
const result = processElement(el, registry);
|
|
239
|
+
if (result instanceof Promise) {
|
|
240
|
+
promises.push(result);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (promises.length > 0) {
|
|
244
|
+
return Promise.all(promises).then(() => { });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Register a directive in the registry.
|
|
249
|
+
*
|
|
250
|
+
* @remarks
|
|
251
|
+
* If called after hydration, scans the DOM for any elements using
|
|
252
|
+
* this directive and processes them immediately.
|
|
253
|
+
*
|
|
254
|
+
* @param registry - The directive registry
|
|
255
|
+
* @param name - Directive name (without c- prefix)
|
|
256
|
+
* @param fn - The directive function
|
|
257
|
+
*/
|
|
258
|
+
export function registerDirective(registry, name, fn) {
|
|
259
|
+
registry.set(name, fn);
|
|
260
|
+
cachedSelector = null;
|
|
261
|
+
if (document.body && initialized) {
|
|
262
|
+
const selector = `[c-${name}]`;
|
|
263
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
264
|
+
processElement(el, registry);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Register a service for dependency injection.
|
|
270
|
+
*
|
|
271
|
+
* @param name - Service name (used in $inject arrays)
|
|
272
|
+
* @param service - The service instance
|
|
273
|
+
*/
|
|
274
|
+
export function registerService(name, service) {
|
|
275
|
+
services.set(name, service);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Initialize the client-side framework.
|
|
279
|
+
*
|
|
280
|
+
* @remarks
|
|
281
|
+
* Processes all existing elements with directive attributes, then
|
|
282
|
+
* sets up a MutationObserver to handle dynamically added elements.
|
|
283
|
+
* Works for both SSR hydration and pure client-side rendering.
|
|
284
|
+
*
|
|
285
|
+
* State is now scoped per custom element. Each element with `scope: true`
|
|
286
|
+
* creates its own state that child elements inherit via prototype chain.
|
|
287
|
+
*
|
|
288
|
+
* @param registry - The directive registry
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* const registry = new Map();
|
|
293
|
+
* registry.set('text', textDirective);
|
|
294
|
+
*
|
|
295
|
+
* init(registry);
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
/**
|
|
299
|
+
* Extract template attributes from an element.
|
|
300
|
+
*
|
|
301
|
+
* @internal
|
|
302
|
+
*/
|
|
303
|
+
function getTemplateAttrs(el) {
|
|
304
|
+
const attrs = {
|
|
305
|
+
children: el.innerHTML
|
|
306
|
+
};
|
|
307
|
+
for (const attr of el.attributes) {
|
|
308
|
+
attrs[attr.name] = attr.value;
|
|
309
|
+
}
|
|
310
|
+
return attrs;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Process custom element directives (those with templates).
|
|
314
|
+
* Directives with templates are web components and must be processed before
|
|
315
|
+
* attribute directives so their content is rendered first.
|
|
316
|
+
*
|
|
317
|
+
* Order for each element:
|
|
318
|
+
* 1. Create scope (if scope: true)
|
|
319
|
+
* 2. Call directive function (if fn exists) - initializes state
|
|
320
|
+
* 3. Render template with (props, state) - can use initialized state
|
|
321
|
+
* 4. Child directives are processed later by main hydration
|
|
322
|
+
*
|
|
323
|
+
* @internal
|
|
324
|
+
*/
|
|
325
|
+
async function processDirectiveElements() {
|
|
326
|
+
for (const name of getDirectiveNames()) {
|
|
327
|
+
const registration = getDirective(name);
|
|
328
|
+
if (!registration) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const { fn, options } = registration;
|
|
332
|
+
// Only process directives with templates (web components),
|
|
333
|
+
// scope: true, or provide (DI overrides)
|
|
334
|
+
if (!options.template && !options.scope && !options.provide) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
// Find all elements matching this directive's tag name
|
|
338
|
+
const elements = document.querySelectorAll(name);
|
|
339
|
+
for (const el of elements) {
|
|
340
|
+
// Skip if already processed
|
|
341
|
+
if (getElementScope(el)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// 1. Create scope if needed
|
|
345
|
+
let scope = {};
|
|
346
|
+
if (options.scope) {
|
|
347
|
+
const parentScope = findParentScope(el);
|
|
348
|
+
scope = createElementScope(el, parentScope);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
scope = findParentScope(el, true) ?? {};
|
|
352
|
+
}
|
|
353
|
+
// 2. Register DI providers if present (for descendants)
|
|
354
|
+
if (options.provide) {
|
|
355
|
+
registerDIProviders(el, options.provide);
|
|
356
|
+
}
|
|
357
|
+
// 3. Call directive function if present (initializes state)
|
|
358
|
+
if (fn) {
|
|
359
|
+
const ctx = createContext(Mode.CLIENT, scope);
|
|
360
|
+
const inject = getInjectables(fn);
|
|
361
|
+
const args = inject.map((dep) => {
|
|
362
|
+
switch (dep) {
|
|
363
|
+
case '$element':
|
|
364
|
+
return el;
|
|
365
|
+
case '$state':
|
|
366
|
+
return scope;
|
|
367
|
+
case '$eval':
|
|
368
|
+
return ctx.eval.bind(ctx);
|
|
369
|
+
default:
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
const result = fn(...args);
|
|
374
|
+
if (result instanceof Promise) {
|
|
375
|
+
await result;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// 4. Render template if present (can query DOM for <template> elements etc)
|
|
379
|
+
if (options.template) {
|
|
380
|
+
const attrs = getTemplateAttrs(el);
|
|
381
|
+
let html;
|
|
382
|
+
if (typeof options.template === 'string') {
|
|
383
|
+
html = options.template;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const result = options.template(attrs, el);
|
|
387
|
+
html = result instanceof Promise ? await result : result;
|
|
388
|
+
}
|
|
389
|
+
el.innerHTML = html;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export async function init(registry) {
|
|
395
|
+
const reg = registry ?? getDefaultRegistry();
|
|
396
|
+
cachedSelector = null;
|
|
397
|
+
// Process custom element directives first (those with templates or scope)
|
|
398
|
+
// This ensures templates are rendered and scopes exist before child directives run
|
|
399
|
+
await processDirectiveElements();
|
|
400
|
+
const selector = getSelector(reg);
|
|
401
|
+
// Process existing elements synchronously, collecting promises from async directives.
|
|
402
|
+
// Note: We collect elements first, but some may be removed during processing
|
|
403
|
+
// (e.g., c-for removes its template). Check isConnected before processing.
|
|
404
|
+
const elements = document.querySelectorAll(selector);
|
|
405
|
+
const promises = [];
|
|
406
|
+
for (const el of elements) {
|
|
407
|
+
if (!el.isConnected) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const result = processElement(el, reg);
|
|
411
|
+
if (result instanceof Promise) {
|
|
412
|
+
promises.push(result);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Wait for any async directives to complete
|
|
416
|
+
if (promises.length > 0) {
|
|
417
|
+
await Promise.all(promises);
|
|
418
|
+
}
|
|
419
|
+
// Set up MutationObserver for dynamic elements
|
|
420
|
+
const observer = new MutationObserver((mutations) => {
|
|
421
|
+
for (const mutation of mutations) {
|
|
422
|
+
for (const node of mutation.addedNodes) {
|
|
423
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
424
|
+
continue;
|
|
425
|
+
processNode(node, selector, reg);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
430
|
+
initialized = true;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Hydrate server-rendered HTML.
|
|
434
|
+
*
|
|
435
|
+
* @remarks
|
|
436
|
+
* Alias for {@link init}. Use when hydrating SSR output.
|
|
437
|
+
*/
|
|
438
|
+
export const hydrate = init;
|
|
439
|
+
/**
|
|
440
|
+
* Mount the framework on client-rendered HTML.
|
|
441
|
+
*
|
|
442
|
+
* @remarks
|
|
443
|
+
* Alias for {@link init}. Use for pure client-side rendering.
|
|
444
|
+
*/
|
|
445
|
+
export const mount = init;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context creation and management.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
import { Mode, Context } from './types.js';
|
|
7
|
+
export type { Context };
|
|
8
|
+
/**
|
|
9
|
+
* Create an evaluation context for directives.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Uses {@link findRoots} to only access state keys that the expression
|
|
13
|
+
* actually references, enabling precise dependency tracking with the
|
|
14
|
+
* reactive system.
|
|
15
|
+
*
|
|
16
|
+
* Supports scoped values via `get()` for things like $component, $renderingChain.
|
|
17
|
+
* Create child contexts with `child()` for nested scopes.
|
|
18
|
+
*
|
|
19
|
+
* @param mode - Execution mode (server or client)
|
|
20
|
+
* @param state - The reactive state object
|
|
21
|
+
* @param scope - Optional scoped values (for context-specific data)
|
|
22
|
+
* @returns A new context
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const state = reactive({ user: { name: 'Alice' } });
|
|
27
|
+
* const ctx = createContext(Mode.CLIENT, state);
|
|
28
|
+
* ctx.eval('user.name' as Expression); // 'Alice'
|
|
29
|
+
*
|
|
30
|
+
* const childCtx = ctx.child({ $component: el });
|
|
31
|
+
* childCtx.get('$component'); // el
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function createContext(mode: Mode, state: Record<string, unknown>, scope?: Record<string, unknown>): Context;
|
|
35
|
+
/**
|
|
36
|
+
* Create a child context with additional bindings.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Use ctx.child() instead
|
|
39
|
+
*/
|
|
40
|
+
export declare function createChildContext(parent: Context, additions: Record<string, unknown>): Context;
|