ilha 0.2.1 → 0.3.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 +36 -7
- package/dist/index.d.ts +33 -31
- package/dist/index.js +204 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,23 +289,51 @@ ilha.css(styles).render(() => `<div class="card">…</div>`);
|
|
|
289
289
|
|
|
290
290
|
---
|
|
291
291
|
|
|
292
|
-
###
|
|
292
|
+
### Composing Islands
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
Child islands are interpolated directly inside a parent's html`` template. During SSR the child's HTML is rendered inline; during client mount the child is activated independently inside its own host element.
|
|
295
295
|
|
|
296
296
|
```ts
|
|
297
297
|
const Icon = ilha.render(() => `<svg>…</svg>`);
|
|
298
298
|
|
|
299
|
-
const Card = ilha.
|
|
300
|
-
(
|
|
299
|
+
const Card = ilha.render(
|
|
300
|
+
() => html`
|
|
301
301
|
<div class="card">
|
|
302
|
-
${
|
|
302
|
+
${Icon}
|
|
303
303
|
<p>Card content</p>
|
|
304
304
|
</div>
|
|
305
305
|
`,
|
|
306
306
|
);
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
+
**Passing props** — call the child island with a props object:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const Badge = ilha
|
|
313
|
+
.input(z.object({ label: z.string(), color: z.string().default("teal") }))
|
|
314
|
+
.render(({ input }) => html`<span style="background:${input.color}">${input.label}</span>`);
|
|
315
|
+
|
|
316
|
+
const Card = ilha.render(
|
|
317
|
+
() => html`
|
|
318
|
+
<div>
|
|
319
|
+
${Badge({ label: "New", color: "coral" })}
|
|
320
|
+
<p>Content</p>
|
|
321
|
+
</div>
|
|
322
|
+
`,
|
|
323
|
+
);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const List = ilha.render(
|
|
330
|
+
() =>
|
|
331
|
+
html`<ul>
|
|
332
|
+
${items.map((item) => html`<li>${Item.key(item.id)({ name: item.name })}</li>`)}
|
|
333
|
+
</ul>`,
|
|
334
|
+
);
|
|
335
|
+
```
|
|
336
|
+
|
|
309
337
|
---
|
|
310
338
|
|
|
311
339
|
### `.transition(opts)`
|
|
@@ -331,7 +359,7 @@ The `leave` transition is awaited before cleanup runs.
|
|
|
331
359
|
|
|
332
360
|
### `.render(fn)`
|
|
333
361
|
|
|
334
|
-
Finalises the builder and returns an `Island`. The render function receives `{ state, derived, input
|
|
362
|
+
Finalises the builder and returns an `Island`. The render function receives `{ state, derived, input }` and must return a string or `RawHtml`.
|
|
335
363
|
|
|
336
364
|
```ts
|
|
337
365
|
const MyIsland = ilha.state("x", 1).render(({ state, input }) => html`<p>${state.x}</p>`);
|
|
@@ -471,6 +499,7 @@ Interpolation rules:
|
|
|
471
499
|
| `raw(str)` | Inserted as-is (no escaping) |
|
|
472
500
|
| `html\`…\`` | Inserted as-is (already safe) |
|
|
473
501
|
| Signal accessor | Called and escaped |
|
|
502
|
+
| Island / Island call | Emitted as `data-ilha-slot` host element |
|
|
474
503
|
| Array | Each item processed recursively (no commas) |
|
|
475
504
|
|
|
476
505
|
**List rendering pattern:**
|
|
@@ -581,7 +610,7 @@ import type {
|
|
|
581
610
|
IslandState,
|
|
582
611
|
IslandDerived,
|
|
583
612
|
DerivedValue,
|
|
584
|
-
|
|
613
|
+
KeyedIsland,
|
|
585
614
|
HydratableOptions,
|
|
586
615
|
OnMountContext,
|
|
587
616
|
HandlerContext,
|
package/dist/index.d.ts
CHANGED
|
@@ -31,16 +31,18 @@ declare namespace StandardSchemaV1 {
|
|
|
31
31
|
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
32
32
|
}
|
|
33
33
|
declare const RAW: unique symbol;
|
|
34
|
-
declare const SLOT_ACCESSOR: unique symbol;
|
|
35
34
|
declare const SIGNAL_ACCESSOR: unique symbol;
|
|
35
|
+
declare const ISLAND: unique symbol;
|
|
36
|
+
declare const ISLAND_CALL: unique symbol;
|
|
36
37
|
interface RawHtml {
|
|
37
38
|
[RAW]: true;
|
|
38
39
|
value: string;
|
|
39
40
|
}
|
|
40
|
-
interface
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
interface IslandCall {
|
|
42
|
+
[ISLAND_CALL]: true;
|
|
43
|
+
island: AnyIsland;
|
|
44
|
+
props: Record<string, unknown> | undefined;
|
|
45
|
+
key: string | undefined;
|
|
44
46
|
}
|
|
45
47
|
interface MarkedSignalAccessor<T> {
|
|
46
48
|
(): T;
|
|
@@ -96,16 +98,18 @@ interface Island<TInput = Record<string, unknown>, _TStateMap extends Record<str
|
|
|
96
98
|
toString(props?: Partial<TInput>): string;
|
|
97
99
|
mount(host: Element, props?: Partial<TInput>): () => void;
|
|
98
100
|
hydratable(props: Partial<TInput>, options: HydratableOptions): Promise<string>;
|
|
101
|
+
key(key: string): KeyedIsland<TInput>;
|
|
102
|
+
[ISLAND]: true;
|
|
103
|
+
}
|
|
104
|
+
interface KeyedIsland<TInput> {
|
|
105
|
+
(props?: Partial<TInput>): IslandCall;
|
|
106
|
+
[ISLAND_CALL]: true;
|
|
99
107
|
}
|
|
100
|
-
type InferIslandInput<T> = T extends Island<infer TInput, any> ? TInput : Record<string, unknown>;
|
|
101
108
|
type AnyIsland = Island<any, any>;
|
|
102
|
-
type
|
|
103
|
-
type SlotsProxy<TSlots extends SlotMap> = { readonly [K in keyof TSlots]: SlotAccessor<Partial<InferIslandInput<TSlots[K]>>> };
|
|
104
|
-
type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TSlots extends SlotMap> = {
|
|
109
|
+
type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> = {
|
|
105
110
|
state: IslandState<TStateMap>;
|
|
106
111
|
derived: IslandDerived<TDerivedMap>;
|
|
107
112
|
input: TInput;
|
|
108
|
-
slots: SlotsProxy<TSlots>;
|
|
109
113
|
};
|
|
110
114
|
type EffectContext<TInput, TStateMap extends Record<string, unknown>> = {
|
|
111
115
|
state: IslandState<TStateMap>;
|
|
@@ -167,40 +171,38 @@ interface MountOptions {
|
|
|
167
171
|
interface MountResult {
|
|
168
172
|
unmount: () => void;
|
|
169
173
|
}
|
|
170
|
-
interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown
|
|
174
|
+
interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
|
|
171
175
|
schema: StandardSchemaV1 | null;
|
|
172
176
|
states: StateEntry<TInput>[];
|
|
173
177
|
deriveds: DerivedEntry<TInput, TStateMap>[];
|
|
174
178
|
ons: OnEntry<TInput, TStateMap, TDerivedMap>[];
|
|
175
179
|
effects: EffectEntry<TInput, TStateMap>[];
|
|
176
180
|
onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap>[];
|
|
177
|
-
slots: Record<string, AnyIsland>;
|
|
178
181
|
transition: TransitionOptions | null;
|
|
179
182
|
binds: BindEntry<TStateMap>[];
|
|
180
183
|
css: string | null;
|
|
181
184
|
}
|
|
182
|
-
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<string, never
|
|
183
|
-
readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap
|
|
184
|
-
constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap
|
|
185
|
-
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<string, never>, Record<string, never
|
|
186
|
-
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<string, never>, Record<string, never
|
|
187
|
-
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap
|
|
188
|
-
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V
|
|
189
|
-
bind(selector: string, stateKey: keyof TStateMap & string): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
190
|
-
bind<T>(selector: string, externalSignal: ExternalSignal<T>): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
191
|
-
on<S extends OnSelectorString>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
192
|
-
on(selectorOrCombined: string, handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
193
|
-
effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
194
|
-
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap, TSlots>) => string | RawHtml): Island<TInput, TStateMap>;
|
|
185
|
+
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<string, never>> {
|
|
186
|
+
readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>;
|
|
187
|
+
constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>);
|
|
188
|
+
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<string, never>, Record<string, never>>;
|
|
189
|
+
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<string, never>, Record<string, never>>;
|
|
190
|
+
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap>;
|
|
191
|
+
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>>;
|
|
192
|
+
bind(selector: string, stateKey: keyof TStateMap & string): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
193
|
+
bind<T>(selector: string, externalSignal: ExternalSignal<T>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
194
|
+
on<S extends OnSelectorString>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
195
|
+
on(selectorOrCombined: string, handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
196
|
+
effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
197
|
+
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
198
|
+
transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
199
|
+
css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
200
|
+
render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap>) => string | RawHtml): Island<TInput, TStateMap>;
|
|
199
201
|
}
|
|
200
202
|
declare function ilhaFrom<TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>): (() => void) | null;
|
|
201
203
|
type IslandRegistry = Record<string, AnyIsland>;
|
|
202
204
|
declare function mountAll(registry: IslandRegistry, options?: MountOptions): MountResult;
|
|
203
|
-
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never
|
|
205
|
+
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
|
|
204
206
|
html: typeof ilhaHtml;
|
|
205
207
|
raw: typeof ilhaRaw;
|
|
206
208
|
mount: typeof mountAll;
|
|
@@ -214,4 +216,4 @@ declare const mount: typeof mountAll;
|
|
|
214
216
|
declare const from: typeof ilhaFrom;
|
|
215
217
|
declare const context: typeof ilhaContext;
|
|
216
218
|
//#endregion
|
|
217
|
-
export { DerivedValue, HandlerContext, HandlerContextFor, HydratableOptions, Island, IslandDerived, IslandState, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor,
|
|
219
|
+
export { DerivedValue, HandlerContext, HandlerContextFor, HydratableOptions, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, context, css, ilha as default, from, html, mount, raw };
|
package/dist/index.js
CHANGED
|
@@ -77,8 +77,9 @@ function dedentString(str) {
|
|
|
77
77
|
return lines.map((l) => l.slice(indent)).join("\n");
|
|
78
78
|
}
|
|
79
79
|
const RAW = Symbol("ilha.raw");
|
|
80
|
-
const SLOT_ACCESSOR = Symbol("ilha.slotAccessor");
|
|
81
80
|
const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
|
|
81
|
+
const ISLAND = Symbol("ilha.island");
|
|
82
|
+
const ISLAND_CALL = Symbol("ilha.islandCall");
|
|
82
83
|
const SLOT_ATTR = "data-ilha-slot";
|
|
83
84
|
const PROPS_ATTR = "data-ilha-props";
|
|
84
85
|
const STATE_ATTR = "data-ilha-state";
|
|
@@ -86,17 +87,70 @@ const CSS_ATTR = "data-ilha-css";
|
|
|
86
87
|
function buildScopedStyle(css) {
|
|
87
88
|
return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css}}</style>`;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const renderCtxStack = [];
|
|
91
|
+
function pushRenderCtx(liveHost, asyncChildren) {
|
|
92
|
+
const ctx = {
|
|
93
|
+
slots: /* @__PURE__ */ new Map(),
|
|
94
|
+
positional: 0,
|
|
95
|
+
liveHost,
|
|
96
|
+
pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0
|
|
97
|
+
};
|
|
98
|
+
renderCtxStack.push(ctx);
|
|
99
|
+
return ctx;
|
|
100
|
+
}
|
|
101
|
+
function popRenderCtx() {
|
|
102
|
+
renderCtxStack.pop();
|
|
103
|
+
}
|
|
104
|
+
function currentRenderCtx() {
|
|
105
|
+
return renderCtxStack[renderCtxStack.length - 1];
|
|
106
|
+
}
|
|
107
|
+
function isIsland(v) {
|
|
108
|
+
return typeof v === "function" && ISLAND in v;
|
|
109
|
+
}
|
|
110
|
+
function isIslandCall(v) {
|
|
111
|
+
return (typeof v === "object" || typeof v === "function") && v !== null && ISLAND_CALL in v;
|
|
112
|
+
}
|
|
113
|
+
function emitIslandSlot(island, props, key) {
|
|
114
|
+
const ctx = currentRenderCtx();
|
|
115
|
+
let id;
|
|
116
|
+
if (key !== void 0) {
|
|
117
|
+
id = `k:${key}`;
|
|
118
|
+
if (ctx && __DEV__ && ctx.slots.has(id)) warn(`Duplicate slot key "${key}" — two children with the same key in a single render will collide. Each .key() call must be unique.`);
|
|
119
|
+
} else id = ctx ? `p:${ctx.positional++}` : "p:0";
|
|
120
|
+
if (ctx) ctx.slots.set(id, {
|
|
121
|
+
island,
|
|
122
|
+
props
|
|
93
123
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
const propsAttr = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
|
|
125
|
+
if (ctx?.liveHost) return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}></div>`;
|
|
126
|
+
if (ctx?.pending) {
|
|
127
|
+
popRenderCtx();
|
|
128
|
+
try {
|
|
129
|
+
const result = island(props);
|
|
130
|
+
if (result instanceof Promise) {
|
|
131
|
+
ctx.pending.set(id, result.then(String));
|
|
132
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}></div>`;
|
|
133
|
+
}
|
|
134
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}>${result}</div>`;
|
|
135
|
+
} finally {
|
|
136
|
+
renderCtxStack.push(ctx);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const inner = island.toString(props);
|
|
140
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}>${inner}</div>`;
|
|
97
141
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
142
|
+
async function resolveAsyncChildren(html, ctx) {
|
|
143
|
+
for (const [id, promise] of ctx.pending) {
|
|
144
|
+
const inner = await promise;
|
|
145
|
+
const escaped = escapeHtml(id);
|
|
146
|
+
if (escaped.length > 500) throw new Error(`Slot id exceeds safe length for regex replacement (${escaped.length} > 500).`);
|
|
147
|
+
const attrPattern = escaped.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
const placeholder = new RegExp(`<div\\s[^>]*${SLOT_ATTR}="${attrPattern}"[^>]*></div>`, "g");
|
|
149
|
+
html = html.replace(placeholder, (match) => {
|
|
150
|
+
return match.slice(0, -6) + `>${inner}</div>`;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return html;
|
|
100
154
|
}
|
|
101
155
|
function markSignalAccessor(fn) {
|
|
102
156
|
fn[SIGNAL_ACCESSOR] = true;
|
|
@@ -124,7 +178,11 @@ function interpolateValue(v) {
|
|
|
124
178
|
if (v == null) return "";
|
|
125
179
|
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
126
180
|
if (typeof v === "object" && RAW in v) return v.value;
|
|
127
|
-
if (
|
|
181
|
+
if (isIslandCall(v)) {
|
|
182
|
+
const call = typeof v === "function" ? v() : v;
|
|
183
|
+
return emitIslandSlot(call.island, call.props, call.key);
|
|
184
|
+
}
|
|
185
|
+
if (isIsland(v)) return emitIslandSlot(v, void 0, void 0);
|
|
128
186
|
if (isSignalAccessor(v)) return escapeHtml(v());
|
|
129
187
|
if (typeof v === "function") return escapeHtml(v());
|
|
130
188
|
return escapeHtml(v);
|
|
@@ -167,10 +225,6 @@ function buildDerivedSignals(entries, state, input, derivedSnapshot) {
|
|
|
167
225
|
let ac = new AbortController();
|
|
168
226
|
let skipFirst = derivedSnapshot != null && entry.key in derivedSnapshot;
|
|
169
227
|
const stopEffect = effect(() => {
|
|
170
|
-
if (skipFirst) {
|
|
171
|
-
skipFirst = false;
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
228
|
ac.abort();
|
|
175
229
|
ac = new AbortController();
|
|
176
230
|
const currentAc = ac;
|
|
@@ -179,6 +233,11 @@ function buildDerivedSignals(entries, state, input, derivedSnapshot) {
|
|
|
179
233
|
input,
|
|
180
234
|
signal: currentAc.signal
|
|
181
235
|
});
|
|
236
|
+
if (skipFirst) {
|
|
237
|
+
skipFirst = false;
|
|
238
|
+
if (result instanceof Promise) result.catch(() => {});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
182
241
|
if (!(result instanceof Promise)) {
|
|
183
242
|
const prevSub = setActiveSub(void 0);
|
|
184
243
|
env({
|
|
@@ -321,7 +380,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
321
380
|
ons: [],
|
|
322
381
|
effects: [],
|
|
323
382
|
onMounts: [],
|
|
324
|
-
slots: {},
|
|
325
383
|
transition: null,
|
|
326
384
|
binds: [],
|
|
327
385
|
css: null
|
|
@@ -380,15 +438,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
380
438
|
onMounts: [...this._cfg.onMounts, { fn }]
|
|
381
439
|
});
|
|
382
440
|
}
|
|
383
|
-
slot(name, island) {
|
|
384
|
-
return new IlhaBuilder({
|
|
385
|
-
...this._cfg,
|
|
386
|
-
slots: {
|
|
387
|
-
...this._cfg.slots,
|
|
388
|
-
[name]: island
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
441
|
transition(opts) {
|
|
393
442
|
return new IlhaBuilder({
|
|
394
443
|
...this._cfg,
|
|
@@ -413,28 +462,38 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
413
462
|
});
|
|
414
463
|
}
|
|
415
464
|
render(fn) {
|
|
416
|
-
const { schema, states, deriveds, ons, effects, onMounts,
|
|
465
|
+
const { schema, states, deriveds, ons, effects, onMounts, transition, binds, css: cssSource } = this._cfg;
|
|
417
466
|
const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
|
|
418
467
|
function resolveInput(props) {
|
|
419
468
|
const value = props ?? {};
|
|
420
469
|
if (!schema) return value;
|
|
421
470
|
return validateSchema(schema, value);
|
|
422
471
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
472
|
+
function renderWithCtx(fn, liveHost, asyncChildren) {
|
|
473
|
+
const ctx = pushRenderCtx(liveHost, asyncChildren);
|
|
474
|
+
let isAsync = false;
|
|
475
|
+
try {
|
|
476
|
+
const html = unwrapHtml(fn());
|
|
477
|
+
if (ctx.pending && ctx.pending.size > 0) {
|
|
478
|
+
isAsync = true;
|
|
479
|
+
return (async () => {
|
|
480
|
+
try {
|
|
481
|
+
return {
|
|
482
|
+
html: await resolveAsyncChildren(html, ctx),
|
|
483
|
+
slots: ctx.slots
|
|
484
|
+
};
|
|
485
|
+
} finally {
|
|
486
|
+
popRenderCtx();
|
|
487
|
+
}
|
|
488
|
+
})();
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
html,
|
|
492
|
+
slots: ctx.slots
|
|
493
|
+
};
|
|
494
|
+
} finally {
|
|
495
|
+
if (!isAsync) popRenderCtx();
|
|
496
|
+
}
|
|
438
497
|
}
|
|
439
498
|
function buildPlainState(input) {
|
|
440
499
|
const state = {};
|
|
@@ -462,7 +521,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
462
521
|
function renderToString(props, sync = false) {
|
|
463
522
|
const input = resolveInput(props);
|
|
464
523
|
const state = buildPlainState(input);
|
|
465
|
-
const slots = makeSlotsProxy(true);
|
|
466
524
|
const results = deriveds.map((entry) => {
|
|
467
525
|
try {
|
|
468
526
|
return {
|
|
@@ -492,12 +550,12 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
492
550
|
value: r.result,
|
|
493
551
|
error: void 0
|
|
494
552
|
};
|
|
495
|
-
|
|
553
|
+
const { html } = renderWithCtx(() => fn({
|
|
496
554
|
state,
|
|
497
555
|
derived,
|
|
498
|
-
input
|
|
499
|
-
slots
|
|
556
|
+
input
|
|
500
557
|
}));
|
|
558
|
+
return stylePrefix + html;
|
|
501
559
|
}
|
|
502
560
|
return Promise.all(results.map(async (r) => {
|
|
503
561
|
try {
|
|
@@ -519,15 +577,15 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
519
577
|
}
|
|
520
578
|
};
|
|
521
579
|
}
|
|
522
|
-
})).then((resolved) => {
|
|
580
|
+
})).then(async (resolved) => {
|
|
523
581
|
const derived = {};
|
|
524
582
|
for (const r of resolved) derived[r.key] = r.envelope;
|
|
525
|
-
|
|
583
|
+
const { html } = await renderWithCtx(() => fn({
|
|
526
584
|
state,
|
|
527
585
|
derived,
|
|
528
|
-
input
|
|
529
|
-
|
|
530
|
-
|
|
586
|
+
input
|
|
587
|
+
}), void 0, true);
|
|
588
|
+
return stylePrefix + html;
|
|
531
589
|
});
|
|
532
590
|
}
|
|
533
591
|
function mountIsland(host, props) {
|
|
@@ -576,28 +634,49 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
576
634
|
}
|
|
577
635
|
const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input, derivedSnapshot);
|
|
578
636
|
cleanups.push(stopDerived);
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
637
|
+
const mountedSlots = /* @__PURE__ */ new Map();
|
|
638
|
+
function findSlot(id) {
|
|
639
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
|
|
640
|
+
const candidates = host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`);
|
|
641
|
+
for (const candidate of candidates) {
|
|
642
|
+
let el = candidate;
|
|
643
|
+
while (el && el !== host) {
|
|
644
|
+
if (el.hasAttribute("data-ilha")) break;
|
|
645
|
+
el = el.parentElement;
|
|
646
|
+
}
|
|
647
|
+
if (el === host) return candidate;
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
function mountSlots(slotMap) {
|
|
652
|
+
for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) {
|
|
653
|
+
entry.unmount();
|
|
654
|
+
mountedSlots.delete(id);
|
|
655
|
+
}
|
|
656
|
+
for (const [id, { island: childIsland, props }] of slotMap) {
|
|
657
|
+
if (mountedSlots.has(id)) continue;
|
|
658
|
+
const slotEl = findSlot(id);
|
|
584
659
|
if (!slotEl) continue;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
warn(`Failed to parse props on [${SLOT_ATTR}="${name}"] — invalid JSON ignored.`);
|
|
660
|
+
let slotProps = props;
|
|
661
|
+
if (slotProps === void 0) {
|
|
662
|
+
const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
|
|
663
|
+
if (rawProps) try {
|
|
664
|
+
slotProps = JSON.parse(rawProps);
|
|
665
|
+
} catch {
|
|
666
|
+
warn(`Failed to parse props on [${SLOT_ATTR}="${id}"] — invalid JSON ignored.`);
|
|
667
|
+
}
|
|
594
668
|
}
|
|
595
669
|
const prevSub = setActiveSub(void 0);
|
|
670
|
+
let unmount;
|
|
596
671
|
try {
|
|
597
|
-
|
|
672
|
+
unmount = childIsland.mount(slotEl, slotProps);
|
|
598
673
|
} finally {
|
|
599
674
|
setActiveSub(prevSub);
|
|
600
675
|
}
|
|
676
|
+
mountedSlots.set(id, {
|
|
677
|
+
el: slotEl,
|
|
678
|
+
unmount
|
|
679
|
+
});
|
|
601
680
|
}
|
|
602
681
|
}
|
|
603
682
|
const listeners = [];
|
|
@@ -644,18 +723,18 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
644
723
|
for (const l of listeners) l.target.removeEventListener(l.type, l.fn, l.options);
|
|
645
724
|
listeners.length = 0;
|
|
646
725
|
}
|
|
647
|
-
const
|
|
648
|
-
|
|
726
|
+
const hasExistingContent = hydrated && host.childNodes.length > 0;
|
|
727
|
+
const initial = renderWithCtx(() => fn({
|
|
649
728
|
state,
|
|
650
729
|
derived,
|
|
651
|
-
input
|
|
652
|
-
|
|
653
|
-
|
|
730
|
+
input
|
|
731
|
+
}), hasExistingContent ? host : void 0);
|
|
732
|
+
if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
|
|
654
733
|
attachListeners();
|
|
655
734
|
let stopBindings = applyBindings(host, binds, state);
|
|
656
735
|
cleanups.push(() => stopBindings());
|
|
657
|
-
mountSlots();
|
|
658
|
-
cleanups.push(() =>
|
|
736
|
+
mountSlots(initial.slots);
|
|
737
|
+
cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
|
|
659
738
|
if (!shouldSkipOnMount) for (const entry of onMounts) {
|
|
660
739
|
const prevSub = setActiveSub(void 0);
|
|
661
740
|
let userCleanup;
|
|
@@ -674,24 +753,42 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
674
753
|
}
|
|
675
754
|
let initialized = false;
|
|
676
755
|
const stopRender = effect(() => {
|
|
677
|
-
const html =
|
|
756
|
+
const { html: rendered, slots: newSlotMap } = renderWithCtx(() => fn({
|
|
678
757
|
state,
|
|
679
758
|
derived,
|
|
680
|
-
input
|
|
681
|
-
|
|
682
|
-
|
|
759
|
+
input
|
|
760
|
+
}), host);
|
|
761
|
+
const html = stylePrefix + rendered;
|
|
683
762
|
if (!initialized) {
|
|
684
763
|
initialized = true;
|
|
685
764
|
return;
|
|
686
765
|
}
|
|
687
766
|
detachListeners();
|
|
688
767
|
stopBindings();
|
|
768
|
+
for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
|
|
769
|
+
entry.unmount();
|
|
770
|
+
mountedSlots.delete(id);
|
|
771
|
+
}
|
|
772
|
+
const preserved = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const [id, entry] of mountedSlots) {
|
|
774
|
+
if (!newSlotMap.has(id)) continue;
|
|
775
|
+
if (!entry.el.isConnected) continue;
|
|
776
|
+
const stub = document.createElement("div");
|
|
777
|
+
stub.setAttribute(SLOT_ATTR, id);
|
|
778
|
+
entry.el.replaceWith(stub);
|
|
779
|
+
preserved.set(id, entry.el);
|
|
780
|
+
}
|
|
689
781
|
const tpl = document.createElement("template");
|
|
690
782
|
tpl.innerHTML = `<div>${html}</div>`;
|
|
691
783
|
morphInner(host, tpl.content.firstElementChild);
|
|
784
|
+
for (const [id, liveEl] of preserved) {
|
|
785
|
+
const stub = findSlot(id);
|
|
786
|
+
if (!stub) continue;
|
|
787
|
+
stub.replaceWith(liveEl);
|
|
788
|
+
}
|
|
692
789
|
attachListeners();
|
|
693
790
|
stopBindings = applyBindings(host, binds, state);
|
|
694
|
-
mountSlots();
|
|
791
|
+
mountSlots(newSlotMap);
|
|
695
792
|
});
|
|
696
793
|
cleanups.push(stopRender);
|
|
697
794
|
cleanups.push(detachListeners);
|
|
@@ -728,11 +825,41 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
728
825
|
cleanups.forEach((c) => c());
|
|
729
826
|
};
|
|
730
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Dual-mode island invocation:
|
|
830
|
+
* - When called inside an `html` interpolation (i.e. {@link currentRenderCtx}
|
|
831
|
+
* is active), returns an {@link IslandCall} carrying props to be emitted
|
|
832
|
+
* as a child slot marker; {@link interpolateValue} consumes this marker.
|
|
833
|
+
* - When called at the top level with no active render context, returns
|
|
834
|
+
* SSR HTML via {@link renderToString} (backward-compatible public API).
|
|
835
|
+
*
|
|
836
|
+
* The declared return type omits {@link IslandCall} because TypeScript
|
|
837
|
+
* cannot narrow based on runtime stack state.
|
|
838
|
+
*/
|
|
731
839
|
const island = function(props) {
|
|
840
|
+
if (currentRenderCtx()) return {
|
|
841
|
+
[ISLAND_CALL]: true,
|
|
842
|
+
island,
|
|
843
|
+
props,
|
|
844
|
+
key: void 0
|
|
845
|
+
};
|
|
732
846
|
return renderToString(props);
|
|
733
847
|
};
|
|
734
848
|
island.toString = (props) => renderToString(props, true);
|
|
735
849
|
island.mount = (host, props) => mountIsland(host, props);
|
|
850
|
+
island.key = (key) => {
|
|
851
|
+
if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
|
|
852
|
+
if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
|
|
853
|
+
const keyed = ((props) => ({
|
|
854
|
+
[ISLAND_CALL]: true,
|
|
855
|
+
island,
|
|
856
|
+
props,
|
|
857
|
+
key
|
|
858
|
+
}));
|
|
859
|
+
keyed[ISLAND_CALL] = true;
|
|
860
|
+
return keyed;
|
|
861
|
+
};
|
|
862
|
+
island[ISLAND] = true;
|
|
736
863
|
island.hydratable = async (props, opts) => {
|
|
737
864
|
const { name, as: tag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
|
|
738
865
|
const resolvedProps = props ?? {};
|
|
@@ -831,7 +958,6 @@ const rootBuilder = new IlhaBuilder({
|
|
|
831
958
|
ons: [],
|
|
832
959
|
effects: [],
|
|
833
960
|
onMounts: [],
|
|
834
|
-
slots: {},
|
|
835
961
|
transition: null,
|
|
836
962
|
binds: [],
|
|
837
963
|
css: null
|