@zubra/runtime 5.7.0 → 5.8.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 -0
- package/dist/production/bridge.js +97 -0
- package/dist/production/context.js +101 -0
- package/dist/production/element.js +25 -1
- package/dist/production/forms.js +126 -0
- package/dist/production/navigation.js +53 -0
- package/dist/production/outlet.js +80 -0
- package/dist/production/react.js +136 -2
- package/dist/production/testing.js +154 -0
- package/dist/production/vue.js +67 -1
- package/package.json +43 -3
- package/src/bridge.js +96 -0
- package/src/context.js +100 -0
- package/src/element.js +25 -1
- package/src/forms.js +125 -0
- package/src/navigation.js +52 -0
- package/src/outlet.js +79 -0
- package/src/react.js +136 -2
- package/src/testing.js +153 -0
- package/src/vue.js +67 -1
- package/types/bridge.d.ts +44 -0
- package/types/context.d.ts +17 -0
- package/types/forms.d.ts +37 -0
- package/types/navigation.d.ts +22 -0
- package/types/outlet.d.ts +23 -0
- package/types/react.d.ts +17 -1
- package/types/testing.d.ts +32 -0
- package/types/vue.d.ts +15 -0
package/src/testing.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Testing helpers: mount a Zubra component the way each framework's consumers do, so a library
|
|
2
|
+
// author can run one test through plain HTML, React and Vue, in any browser test runner (Vitest
|
|
3
|
+
// browser mode, Web Test Runner, Playwright's own).
|
|
4
|
+
//
|
|
5
|
+
// import { frameworks, mount } from '@zubra/runtime/testing';
|
|
6
|
+
// import StarRating from './star-rating.zb';
|
|
7
|
+
//
|
|
8
|
+
// for (const framework of await frameworks()) {
|
|
9
|
+
// test(`${framework}: a click sets the value and fires valuechange`, async () => {
|
|
10
|
+
// const view = await mount(StarRating, { framework, props: { value: 2 } });
|
|
11
|
+
// view.element.querySelector('button:nth-of-type(4)').click();
|
|
12
|
+
// expect(view.events('valuechange')).toEqual([4]);
|
|
13
|
+
// await view.set({ value: 1 }); // through the framework, as an app would
|
|
14
|
+
// expect(view.element.value).toBe(1);
|
|
15
|
+
// view.unmount();
|
|
16
|
+
// });
|
|
17
|
+
// }
|
|
18
|
+
//
|
|
19
|
+
// A `<prop>change` event updates the prop the framework passes, as an app binding it two-way
|
|
20
|
+
// would (`v-model`, React's controlled pattern). React and Vue are imported only when asked for, through the same adapters apps use
|
|
21
|
+
// (`@zubra/runtime/react`, `@zubra/runtime/vue`), so events reach the framework's own handlers,
|
|
22
|
+
// props are set the framework's way, and slots are the framework's children.
|
|
23
|
+
|
|
24
|
+
const ALL = ['html', 'react', 'vue'];
|
|
25
|
+
|
|
26
|
+
/** The frameworks that can be mounted here: plain HTML always, React and Vue when installed. */
|
|
27
|
+
export async function frameworks() {
|
|
28
|
+
const found = ['html'];
|
|
29
|
+
for (const name of ['react', 'vue']) {
|
|
30
|
+
try {
|
|
31
|
+
await load(name);
|
|
32
|
+
found.push(name);
|
|
33
|
+
} catch {
|
|
34
|
+
// not installed: skipped
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const loaded = {};
|
|
41
|
+
function load(framework) {
|
|
42
|
+
if (framework === 'react') {
|
|
43
|
+
loaded.react ??= Promise.all([import('react'), import('react-dom'), import('react-dom/client'), import('./react.js')])
|
|
44
|
+
.then(([React, ReactDOM, client, adapter]) => ({ React, flushSync: ReactDOM.flushSync, createRoot: client.createRoot, wrap: adapter.wrap }));
|
|
45
|
+
return loaded.react;
|
|
46
|
+
}
|
|
47
|
+
if (framework === 'vue') {
|
|
48
|
+
loaded.vue ??= Promise.all([import('vue'), import('./vue.js')]).then(([Vue, adapter]) => ({ Vue, wrap: adapter.wrap }));
|
|
49
|
+
return loaded.vue;
|
|
50
|
+
}
|
|
51
|
+
return Promise.resolve(null);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// What the element is doing once a change has landed: its own render, and the framework's.
|
|
55
|
+
const settle = async (element) => {
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
57
|
+
await element?.updateComplete;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mount `Element` (a compiled component's default export) through `framework` (`html`, `react` or
|
|
62
|
+
* `vue`), with `props` and `slots` (`{ default: 'text', title: 'Heading' }`: text for each slot).
|
|
63
|
+
* Resolves once it has rendered, to:
|
|
64
|
+
* - `element`: the component's host element;
|
|
65
|
+
* - `events(name)`: the `detail` of each `name` event so far, as the framework's handler got it
|
|
66
|
+
* (`events()`: every event as `[name, detail]`);
|
|
67
|
+
* - `set(props)`: change props through the framework (merged with the current ones), and settle;
|
|
68
|
+
* - `unmount()`: remove it the framework's way.
|
|
69
|
+
*/
|
|
70
|
+
export async function mount(Element, { framework = 'html', props = {}, slots = {}, container } = {}) {
|
|
71
|
+
const meta = Element?.zubra;
|
|
72
|
+
if (!meta) throw new TypeError('zubra: mount() needs a compiled zubra element (the default export of a .zb module)');
|
|
73
|
+
if (!ALL.includes(framework)) throw new TypeError(`zubra: mount() knows ${ALL.join(', ')}, not "${framework}"`);
|
|
74
|
+
if (!customElements.get(meta.tag)) customElements.define(meta.tag, Element);
|
|
75
|
+
const host = container ?? document.body.appendChild(document.createElement('div'));
|
|
76
|
+
const log = [];
|
|
77
|
+
// Like an app that binds the component two-way (`v-model`, React's controlled pattern): a
|
|
78
|
+
// `<prop>change` event updates the prop the framework passes back, so its next render doesn't
|
|
79
|
+
// put the old value back.
|
|
80
|
+
const models = Object.fromEntries(Object.keys(meta.props).filter((name) => meta.events.includes(`${name.toLowerCase()}change`)).map((name) => [`${name.toLowerCase()}change`, name]));
|
|
81
|
+
let model = () => {};
|
|
82
|
+
const record = (name) => (event) => {
|
|
83
|
+
log.push([name, event.detail]);
|
|
84
|
+
if (models[name]) model(models[name], event.detail);
|
|
85
|
+
};
|
|
86
|
+
let current = { ...props };
|
|
87
|
+
let view;
|
|
88
|
+
|
|
89
|
+
if (framework === 'html') {
|
|
90
|
+
const element = document.createElement(meta.tag);
|
|
91
|
+
for (const [name, value] of Object.entries(current)) element[name] = value;
|
|
92
|
+
for (const name of meta.events) element.addEventListener(name, record(name));
|
|
93
|
+
for (const [name, text] of Object.entries(slots)) {
|
|
94
|
+
if (name === 'default') element.append(text);
|
|
95
|
+
else element.append(Object.assign(document.createElement('span'), { slot: name, textContent: text }));
|
|
96
|
+
}
|
|
97
|
+
host.append(element);
|
|
98
|
+
view = {
|
|
99
|
+
element,
|
|
100
|
+
set: async (next) => { Object.assign(current, next); for (const [name, value] of Object.entries(next)) element[name] = value; },
|
|
101
|
+
unmount: () => element.remove(),
|
|
102
|
+
};
|
|
103
|
+
} else if (framework === 'react') {
|
|
104
|
+
const { React, flushSync, createRoot, wrap } = await load('react');
|
|
105
|
+
const Component = wrap(Element);
|
|
106
|
+
const handlers = Object.fromEntries(meta.events.map((name) => [`on${name}`, record(name)]));
|
|
107
|
+
const children = Object.entries(slots).map(([name, text]) =>
|
|
108
|
+
name === 'default' ? text : React.createElement('span', { key: name, slot: name }, text));
|
|
109
|
+
let element = null;
|
|
110
|
+
const ref = (node) => { if (node) element = node; };
|
|
111
|
+
const root = createRoot(host);
|
|
112
|
+
const render = () => flushSync(() => root.render(React.createElement(Component, { ...current, ...handlers, ref }, ...children)));
|
|
113
|
+
model = (name, value) => { current[name] = value; queueMicrotask(render); };
|
|
114
|
+
render();
|
|
115
|
+
view = {
|
|
116
|
+
get element() { return element; },
|
|
117
|
+
set: async (next) => { Object.assign(current, next); render(); },
|
|
118
|
+
unmount: () => root.unmount(),
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
const { Vue, wrap } = await load('vue');
|
|
122
|
+
const Component = wrap(Element);
|
|
123
|
+
const state = Vue.reactive({ ...current });
|
|
124
|
+
model = (name, value) => { current[name] = value; state[name] = value; };
|
|
125
|
+
const handlers = Object.fromEntries(meta.events.map((name) => [`on${name[0].toUpperCase()}${name.slice(1)}`, record(name)]));
|
|
126
|
+
const slotFunctions = Object.fromEntries(Object.entries(slots).map(([name, text]) => [name, () => (name === 'default' ? text : Vue.h('span', text))]));
|
|
127
|
+
const component = Vue.ref(null);
|
|
128
|
+
const app = Vue.createApp({ render: () => Vue.h(Component, { ...state, ...handlers, ref: component }, slotFunctions) });
|
|
129
|
+
app.mount(host);
|
|
130
|
+
view = {
|
|
131
|
+
get element() { return component.value?.element ?? null; },
|
|
132
|
+
set: async (next) => { Object.assign(current, next); Object.assign(state, next); await Vue.nextTick(); },
|
|
133
|
+
unmount: () => app.unmount(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await settle(view.element);
|
|
138
|
+
const element = () => view.element;
|
|
139
|
+
return {
|
|
140
|
+
framework,
|
|
141
|
+
host,
|
|
142
|
+
get element() { return element(); },
|
|
143
|
+
events: (name) => (name === undefined ? log.slice() : log.filter(([event]) => event === name).map(([, detail]) => detail)),
|
|
144
|
+
async set(next) {
|
|
145
|
+
await view.set(next);
|
|
146
|
+
await settle(element());
|
|
147
|
+
},
|
|
148
|
+
unmount() {
|
|
149
|
+
view.unmount();
|
|
150
|
+
if (!container) host.remove();
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
package/src/vue.js
CHANGED
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
// `v-model:<prop>`, and `v-model` binds the `value` pair (or the only pair, if there's one).
|
|
13
13
|
// - Other attributes and listeners (class, style, @click, …) go to the element; slots become its
|
|
14
14
|
// children, named slots with `slot="name"`.
|
|
15
|
-
import { defineComponent, h, withDirectives, Text, Comment, Fragment } from 'vue';
|
|
15
|
+
import { defineComponent, h, withDirectives, Text, Comment, Fragment, customRef, getCurrentScope, onScopeDispose } from 'vue';
|
|
16
16
|
import { serverContent } from './server.js';
|
|
17
|
+
import { routeLinks } from './navigation.js';
|
|
18
|
+
import './context.js';
|
|
19
|
+
import { subscribe, read, writable } from './bridge.js';
|
|
17
20
|
|
|
18
21
|
const clone = (value) => (value && typeof value === 'object' ? structuredClone(value) : value);
|
|
19
22
|
const pascal = (tag) => tag.split(/[-.]/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join('');
|
|
@@ -123,3 +126,66 @@ export function wrap(Element) {
|
|
|
123
126
|
},
|
|
124
127
|
});
|
|
125
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Route links inside Zubra components through Vue Router (or any `push`) until the current effect
|
|
132
|
+
* scope ends: `useRouteLinks((to) => router.push(to))` in a component's `setup`. Returns the
|
|
133
|
+
* function that stops it.
|
|
134
|
+
*/
|
|
135
|
+
export function useRouteLinks(navigate, options) {
|
|
136
|
+
const stop = routeLinks(navigate, options);
|
|
137
|
+
if (getCurrentScope()) onScopeDispose(stop);
|
|
138
|
+
return stop;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Provide a Context Protocol value to every element inside: an app's theme, locale or current user,
|
|
143
|
+
* read by components with `inject(context)` (or Lit's `@consume`).
|
|
144
|
+
*
|
|
145
|
+
* <ZubraContext :context="theme" :value="dark ? 'dark' : 'light'">…</ZubraContext>
|
|
146
|
+
*
|
|
147
|
+
* `value` can also be a signal. It renders a `<zb-context>` element, which is `display: contents`.
|
|
148
|
+
*/
|
|
149
|
+
export const ZubraContext = defineComponent({
|
|
150
|
+
name: 'ZubraContext',
|
|
151
|
+
props: { context: { default: undefined }, value: { default: undefined } },
|
|
152
|
+
setup(given, { slots }) {
|
|
153
|
+
// Set as DOM properties when the element is created, before its children are inserted.
|
|
154
|
+
return () => h('zb-context', { '.context': given.context, '.value': given.value, style: 'display: contents' }, slots.default?.());
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A Zubra signal (or a function of signals) as a Vue ref: reading `.value` tracks it in
|
|
160
|
+
* templates, computeds and watchers, and assigning `.value` writes the signal when it's writable
|
|
161
|
+
* (a `Signal.State`, `state()`, `from()`), so `v-model="cart"` works on one.
|
|
162
|
+
*
|
|
163
|
+
* const items = useSignal(cart, { deep: true })
|
|
164
|
+
*
|
|
165
|
+
* The subscription ends with the effect scope it was created in (the component's `setup`); outside
|
|
166
|
+
* one, call `stop()` on the ref when done. With `deep: true` a mutation inside an array or object
|
|
167
|
+
* triggers too.
|
|
168
|
+
*/
|
|
169
|
+
export function useSignal(source, { deep = false } = {}) {
|
|
170
|
+
let stop = () => {};
|
|
171
|
+
const ref = customRef((track, trigger) => {
|
|
172
|
+
let first = true;
|
|
173
|
+
stop = subscribe(source, () => {
|
|
174
|
+
if (first) first = false;
|
|
175
|
+
else trigger();
|
|
176
|
+
}, { deep });
|
|
177
|
+
return {
|
|
178
|
+
get() {
|
|
179
|
+
track();
|
|
180
|
+
return read(source);
|
|
181
|
+
},
|
|
182
|
+
set(value) {
|
|
183
|
+
if (!writable(source)) throw new TypeError('zubra: useSignal(): this signal is read-only (a computed); write the state it reads');
|
|
184
|
+
source.set(value);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
Object.defineProperty(ref, 'stop', { value: () => stop() });
|
|
189
|
+
if (getCurrentScope()) onScopeDispose(() => stop());
|
|
190
|
+
return ref;
|
|
191
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Signal } from './signal.js';
|
|
2
|
+
|
|
3
|
+
/** A signal, or a function that reads signals (tracked like a computed). */
|
|
4
|
+
export type SignalSource<T> = Signal.State<T> | Signal.Computed<T> | (() => T);
|
|
5
|
+
|
|
6
|
+
export interface BridgeOptions {
|
|
7
|
+
/** Also notify when something inside an array or plain object changes (`items.push(x)`). */
|
|
8
|
+
deep?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** An unsubscribe function that is also an RxJS-style `Unsubscribable`. */
|
|
12
|
+
export interface Unsubscribe {
|
|
13
|
+
(): void;
|
|
14
|
+
unsubscribe(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The Svelte store contract and an RxJS-style Subscribable at once: Svelte's `$store`, Solid's
|
|
19
|
+
* `from`, Angular's `toSignal` and RxJS's `from` take it as it is.
|
|
20
|
+
*/
|
|
21
|
+
export interface SignalStore<T> {
|
|
22
|
+
/** The current value. */
|
|
23
|
+
get(): T;
|
|
24
|
+
/** Call `run` now and on each change. */
|
|
25
|
+
subscribe(run: ((value: T) => void) | { next?(value: T): void }): Unsubscribe;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A store over a writable signal: also a Svelte writable store. */
|
|
29
|
+
export interface WritableSignalStore<T> extends SignalStore<T> {
|
|
30
|
+
set(value: T): void;
|
|
31
|
+
update(fn: (value: T) => T): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Call `callback(value)` now and on each change (batched per microtask) until unsubscribed. */
|
|
35
|
+
export declare function subscribe<T>(source: SignalSource<T>, callback: (value: T) => void, options?: BridgeOptions): () => void;
|
|
36
|
+
|
|
37
|
+
/** The source as a store. Writable (`set`, `update`) when the source is a `Signal.State` or has `set`. */
|
|
38
|
+
export declare function toStore<T>(source: Signal.State<T>, options?: BridgeOptions): WritableSignalStore<T>;
|
|
39
|
+
export declare function toStore<T>(source: SignalSource<T>, options?: BridgeOptions): SignalStore<T>;
|
|
40
|
+
|
|
41
|
+
/** The source's current value. */
|
|
42
|
+
export declare function read<T>(source: SignalSource<T>): T;
|
|
43
|
+
/** Whether the source can be written. */
|
|
44
|
+
export declare function writable(source: unknown): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<zb-context>`: answers the Context Protocol for the elements inside it. Set `context` (a key
|
|
3
|
+
* from `createContext()`, or any protocol key) and `value` (a value or a signal).
|
|
4
|
+
*/
|
|
5
|
+
export declare class ZubraContextElement extends HTMLElement {
|
|
6
|
+
context: unknown;
|
|
7
|
+
value: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Define `<zb-context>` (done on import; a no-op without `customElements`). */
|
|
11
|
+
export declare function defineContextElement(): void;
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLElementTagNameMap {
|
|
15
|
+
'zb-context': ZubraContextElement;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/types/forms.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The model prop of a Zubra control (`value` by default), with its `<prop>change` event. */
|
|
2
|
+
export interface FieldOptions {
|
|
3
|
+
prop?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** React Hook Form's `register(name)` result (the parts used). */
|
|
7
|
+
export interface Registration {
|
|
8
|
+
name: string;
|
|
9
|
+
ref: unknown;
|
|
10
|
+
onChange(event: { type: string; target: { name: string; value: unknown } }): unknown;
|
|
11
|
+
onBlur(event: { type: string; target: { name: string; value: unknown } }): unknown;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** React Hook Form's `register(name)` as props for a wrapped Zubra control (model: `value`). */
|
|
16
|
+
export declare function registerProps(registration: Registration): Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
/** A React Hook Form `<Controller>` field, or a TanStack Form field API. */
|
|
19
|
+
export type ReactField =
|
|
20
|
+
| { name: string; value: unknown; onChange(value: unknown): void; onBlur(): void; ref?: unknown }
|
|
21
|
+
| { name: string; state: { value: unknown }; handleChange(value: any): void; handleBlur(): void };
|
|
22
|
+
|
|
23
|
+
/** A field's props for a wrapped Zubra control in React. */
|
|
24
|
+
export declare function fieldProps(field: ReactField, options?: FieldOptions): Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
/** A field's props for a wrapped Zubra control in Vue, from VeeValidate's `useField()` or a `<Field>` slot's `field`. */
|
|
27
|
+
export declare function vueFieldProps(field: object, options?: FieldOptions): Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
/** Felte's `createField(name)` result (the parts used). */
|
|
30
|
+
export interface FelteField {
|
|
31
|
+
field(node: HTMLElement): { destroy?(): void } | void;
|
|
32
|
+
onInput(value: unknown): void;
|
|
33
|
+
onBlur(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Connect a Zubra control to a Felte field: a Svelte action, or call it with the element. */
|
|
37
|
+
export declare function felteControl(node: HTMLElement, field: FelteField, options?: FieldOptions): { destroy(): void };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** What a router is told about a routed link. */
|
|
2
|
+
export interface RoutedLink {
|
|
3
|
+
event: MouseEvent;
|
|
4
|
+
anchor: HTMLAnchorElement;
|
|
5
|
+
url: URL;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RouteLinksOptions {
|
|
9
|
+
/** Which ancestors make a link routed (default: a Zubra component's host). */
|
|
10
|
+
within?(node: Element): boolean;
|
|
11
|
+
/** Where to listen (default: the document). */
|
|
12
|
+
root?: Document | Element;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Whether `node` is a Zubra component's host. */
|
|
16
|
+
export declare function isComponent(node: unknown): boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Route clicks on links inside Zubra components through `navigate(to)`, where `to` is the path,
|
|
20
|
+
* query and hash. Returns a function that stops it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function routeLinks(navigate: (to: string, link: RoutedLink) => unknown, options?: RouteLinksOptions): () => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<zb-outlet name="…" item={…}>` (experimental): a place in a component where the app's framework
|
|
3
|
+
* renders its own content for `item`, through a portal. The React adapter fills it from a
|
|
4
|
+
* `render<Name>` prop.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ZubraOutletElement extends HTMLElement {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
item: unknown;
|
|
9
|
+
/** Who renders into this outlet, once claimed. */
|
|
10
|
+
owner: { change(outlet: ZubraOutletElement): void; disconnect(outlet: ZubraOutletElement): void } | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The unclaimed outlets inside `host` (light DOM and shadow root). */
|
|
14
|
+
export declare function outletsOf(host: Element): ZubraOutletElement[];
|
|
15
|
+
|
|
16
|
+
/** Define `<zb-outlet>` (done on import; a no-op without `customElements`). */
|
|
17
|
+
export declare function defineOutletElement(): void;
|
|
18
|
+
|
|
19
|
+
declare global {
|
|
20
|
+
interface HTMLElementTagNameMap {
|
|
21
|
+
'zb-outlet': ZubraOutletElement;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/types/react.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { ZubraElementClass } from './index.js';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A zubra element as a React component (React 18 and 19): props as properties, `on<Event>`
|
|
6
|
-
* handlers for its events, other props and children passed to the element
|
|
6
|
+
* handlers for its events, other props and children passed to the element, and `render<Name>`
|
|
7
|
+
* props that fill the element's `<zb-outlet name>`s (experimental). The build plugin's
|
|
7
8
|
* generated typings give exact prop types; this one is general.
|
|
8
9
|
*/
|
|
9
10
|
export declare function wrap<E extends HTMLElement>(
|
|
@@ -11,3 +12,18 @@ export declare function wrap<E extends HTMLElement>(
|
|
|
11
12
|
): React.ForwardRefExoticComponent<
|
|
12
13
|
React.HTMLAttributes<E> & Record<string, unknown> & { children?: React.ReactNode } & React.RefAttributes<E>
|
|
13
14
|
> & { readonly element: ZubraElementClass<E> };
|
|
15
|
+
|
|
16
|
+
/** Route links inside Zubra components through the app's router while mounted. */
|
|
17
|
+
export declare function useRouteLinks(
|
|
18
|
+
navigate: (to: string, link: import('./navigation.js').RoutedLink) => unknown,
|
|
19
|
+
options?: import('./navigation.js').RouteLinksOptions,
|
|
20
|
+
): void;
|
|
21
|
+
|
|
22
|
+
/** Provide a Context Protocol value (or a signal) to every element inside; renders `<zb-context>`. */
|
|
23
|
+
export declare function ZubraContext(props: { context: unknown; value: unknown; children?: React.ReactNode }): React.ReactElement;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read a Zubra signal (or a function of signals) in a React component, re-rendering when it
|
|
27
|
+
* changes. `deep: true` re-renders on mutations inside an array or object too.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useSignal<T>(source: import('./bridge.js').SignalSource<T>, options?: import('./bridge.js').BridgeOptions): T;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ZubraElementClass } from './index.js';
|
|
2
|
+
|
|
3
|
+
export type Framework = 'html' | 'react' | 'vue';
|
|
4
|
+
|
|
5
|
+
/** The frameworks that can be mounted here: `html` always, `react` and `vue` when installed. */
|
|
6
|
+
export declare function frameworks(): Promise<Framework[]>;
|
|
7
|
+
|
|
8
|
+
export interface MountOptions {
|
|
9
|
+
framework?: Framework;
|
|
10
|
+
props?: Record<string, unknown>;
|
|
11
|
+
/** Text for each slot: `{ default: 'Body', title: 'Heading' }`. */
|
|
12
|
+
slots?: Record<string, string>;
|
|
13
|
+
/** Where to mount (default: a new <div> in <body>, removed on unmount). */
|
|
14
|
+
container?: HTMLElement;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Mounted<E extends HTMLElement = HTMLElement> {
|
|
18
|
+
readonly framework: Framework;
|
|
19
|
+
readonly host: HTMLElement;
|
|
20
|
+
/** The component's host element. */
|
|
21
|
+
readonly element: E;
|
|
22
|
+
/** The `detail` of each `name` event so far, as the framework's handler got it; with no name, every event as `[name, detail]`. */
|
|
23
|
+
events(name: string): unknown[];
|
|
24
|
+
events(): [string, unknown][];
|
|
25
|
+
/** Change props through the framework (merged with the current ones), and settle. */
|
|
26
|
+
set(props: Record<string, unknown>): Promise<void>;
|
|
27
|
+
/** Remove it the framework's way. */
|
|
28
|
+
unmount(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Mount a compiled component through plain HTML, the React adapter or the Vue adapter. */
|
|
32
|
+
export declare function mount<E extends HTMLElement>(Element: ZubraElementClass<E>, options?: MountOptions): Promise<Mounted<E>>;
|
package/types/vue.d.ts
CHANGED
|
@@ -6,3 +6,18 @@ export declare function wrap(Element: ZubraElementClass): DefineComponent<Record
|
|
|
6
6
|
|
|
7
7
|
/** The component's two-way bindings: `<prop>` + `<prop>change` pairs. */
|
|
8
8
|
export declare function models(meta: Pick<ElementMeta, 'props' | 'events'>): { prop: string; event: string }[];
|
|
9
|
+
|
|
10
|
+
/** Route links inside Zubra components through Vue Router (or any push) until the scope ends. */
|
|
11
|
+
export declare function useRouteLinks(
|
|
12
|
+
navigate: (to: string, link: import('./navigation.js').RoutedLink) => unknown,
|
|
13
|
+
options?: import('./navigation.js').RouteLinksOptions,
|
|
14
|
+
): () => void;
|
|
15
|
+
|
|
16
|
+
/** Provide a Context Protocol value (or a signal) to every element inside; renders `<zb-context>`. */
|
|
17
|
+
export declare const ZubraContext: DefineComponent<{ context: unknown; value: unknown }>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A Zubra signal (or a function of signals) as a Vue ref; assigning `.value` writes a writable
|
|
21
|
+
* signal. Stops with the current effect scope, or with `stop()`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useSignal<T>(source: import('./bridge.js').SignalSource<T>, options?: import('./bridge.js').BridgeOptions): import('vue').Ref<T> & { stop(): void };
|