sinho 0.1.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/.github/workflows/ci.yml +24 -0
- package/.github/workflows/deploy-docs.yml +47 -0
- package/.prettierrc +3 -0
- package/LICENSE.md +21 -0
- package/README.md +33 -0
- package/ci/check-size.js +8 -0
- package/dist/array_mutation.d.ts +16 -0
- package/dist/array_mutation.js +75 -0
- package/dist/array_mutation.js.map +1 -0
- package/dist/bundle.d.ts +1126 -0
- package/dist/bundle.js +1074 -0
- package/dist/bundle.min.js +1 -0
- package/dist/component.d.ts +253 -0
- package/dist/component.js +256 -0
- package/dist/component.js.map +1 -0
- package/dist/context.d.ts +21 -0
- package/dist/context.js +34 -0
- package/dist/context.js.map +1 -0
- package/dist/create_element.d.ts +43 -0
- package/dist/create_element.js +43 -0
- package/dist/create_element.js.map +1 -0
- package/dist/dom.d.ts +602 -0
- package/dist/dom.js +97 -0
- package/dist/dom.js.map +1 -0
- package/dist/intrinsic/ClassComponent.d.ts +2 -0
- package/dist/intrinsic/ClassComponent.js +10 -0
- package/dist/intrinsic/ClassComponent.js.map +1 -0
- package/dist/intrinsic/Dynamic.d.ts +33 -0
- package/dist/intrinsic/Dynamic.js +53 -0
- package/dist/intrinsic/Dynamic.js.map +1 -0
- package/dist/intrinsic/ErrorBoundary.d.ts +14 -0
- package/dist/intrinsic/ErrorBoundary.js +36 -0
- package/dist/intrinsic/ErrorBoundary.js.map +1 -0
- package/dist/intrinsic/For.d.ts +10 -0
- package/dist/intrinsic/For.js +81 -0
- package/dist/intrinsic/For.js.map +1 -0
- package/dist/intrinsic/Fragment.d.ts +23 -0
- package/dist/intrinsic/Fragment.js +28 -0
- package/dist/intrinsic/Fragment.js.map +1 -0
- package/dist/intrinsic/If.d.ts +24 -0
- package/dist/intrinsic/If.js +47 -0
- package/dist/intrinsic/If.js.map +1 -0
- package/dist/intrinsic/Portal.d.ts +6 -0
- package/dist/intrinsic/Portal.js +15 -0
- package/dist/intrinsic/Portal.js.map +1 -0
- package/dist/intrinsic/Style.d.ts +7 -0
- package/dist/intrinsic/Style.js +70 -0
- package/dist/intrinsic/Style.js.map +1 -0
- package/dist/intrinsic/TagComponent.d.ts +4 -0
- package/dist/intrinsic/TagComponent.js +67 -0
- package/dist/intrinsic/TagComponent.js.map +1 -0
- package/dist/intrinsic/Text.d.ts +6 -0
- package/dist/intrinsic/Text.js +16 -0
- package/dist/intrinsic/Text.js.map +1 -0
- package/dist/intrinsic/mod.d.ts +5 -0
- package/dist/intrinsic/mod.js +6 -0
- package/dist/intrinsic/mod.js.map +1 -0
- package/dist/jsx-runtime/mod.d.ts +23 -0
- package/dist/jsx-runtime/mod.js +11 -0
- package/dist/jsx-runtime/mod.js.map +1 -0
- package/dist/mod.d.ts +8 -0
- package/dist/mod.js +7 -0
- package/dist/mod.js.map +1 -0
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.js +25 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scope.d.ts +138 -0
- package/dist/scope.js +228 -0
- package/dist/scope.js.map +1 -0
- package/dist/template.d.ts +10 -0
- package/dist/template.js +7 -0
- package/dist/template.js.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/package.json +71 -0
- package/src/array_mutation.ts +118 -0
- package/src/component.ts +624 -0
- package/src/context.ts +70 -0
- package/src/create_element.ts +89 -0
- package/src/dom.ts +819 -0
- package/src/intrinsic/ClassComponent.ts +17 -0
- package/src/intrinsic/For.ts +122 -0
- package/src/intrinsic/Fragment.ts +38 -0
- package/src/intrinsic/If.ts +73 -0
- package/src/intrinsic/Portal.ts +25 -0
- package/src/intrinsic/Style.ts +120 -0
- package/src/intrinsic/TagComponent.ts +102 -0
- package/src/intrinsic/Text.ts +24 -0
- package/src/intrinsic/mod.ts +5 -0
- package/src/jsx-runtime/mod.ts +41 -0
- package/src/mod.ts +37 -0
- package/src/renderer.ts +45 -0
- package/src/scope.ts +404 -0
- package/src/template.ts +16 -0
- package/src/utils.ts +29 -0
- package/terser.config.json +16 -0
- package/tsconfig.json +18 -0
- package/web/README.md +41 -0
- package/web/babel.config.js +3 -0
- package/web/dist/shingo.min.d.ts +1131 -0
- package/web/dist/shingo.min.js +1 -0
- package/web/docusaurus.config.ts +151 -0
- package/web/package-lock.json +14850 -0
- package/web/package.json +54 -0
- package/web/sidebars.ts +31 -0
- package/web/src/components/monacoEditor.tsx +72 -0
- package/web/src/components/playground.tsx +89 -0
- package/web/src/components/playgroundComponent.tsx +168 -0
- package/web/src/css/custom.css +37 -0
- package/web/src/pages/index.module.css +31 -0
- package/web/src/pages/index.tsx +73 -0
- package/web/src/pages/playground.tsx +64 -0
- package/web/static/.nojekyll +0 -0
- package/web/static/dist/bundle.d.ts +1126 -0
- package/web/static/dist/bundle.min.js +1 -0
- package/web/tsconfig.json +8 -0
package/src/component.ts
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cleanup,
|
|
3
|
+
MaybeSignal,
|
|
4
|
+
Signal,
|
|
5
|
+
useEffect,
|
|
6
|
+
useSubscope,
|
|
7
|
+
useSignal,
|
|
8
|
+
} from "./scope.js";
|
|
9
|
+
import type { DomEventProps, DomProps } from "./dom.js";
|
|
10
|
+
import { runWithRenderer } from "./renderer.js";
|
|
11
|
+
import {
|
|
12
|
+
camelCaseToKebabCase,
|
|
13
|
+
JsxPropNameToEventName,
|
|
14
|
+
jsxPropNameToEventName,
|
|
15
|
+
} from "./utils.js";
|
|
16
|
+
import { useScope } from "./scope.js";
|
|
17
|
+
import { Context, isContext, provideContext } from "./context.js";
|
|
18
|
+
import { Template } from "./template.js";
|
|
19
|
+
|
|
20
|
+
interface Tagged<in out T> {
|
|
21
|
+
_tag: T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type OmitNever<T> = Omit<
|
|
25
|
+
T,
|
|
26
|
+
{ [K in keyof T]: T[K] extends never ? K : never }[keyof T]
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
type PartialRequire<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
30
|
+
|
|
31
|
+
/** @ignore */
|
|
32
|
+
export interface PropMeta<T> extends PropOptions<T>, Tagged<"p"> {
|
|
33
|
+
_type?: [T];
|
|
34
|
+
_defaultOrContext: T;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AttributeOptions<T> {
|
|
38
|
+
/**
|
|
39
|
+
* The name of the attribute to observe.
|
|
40
|
+
*
|
|
41
|
+
* Defaults to the kebab-case version of the prop.
|
|
42
|
+
*/
|
|
43
|
+
name?: string;
|
|
44
|
+
/**
|
|
45
|
+
* A function to transform the attribute value to the prop value.
|
|
46
|
+
*/
|
|
47
|
+
transform: (value: string) => T;
|
|
48
|
+
/**
|
|
49
|
+
* Set to `true` to not observe the attribute for changes.
|
|
50
|
+
*
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
static?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type PartialPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
57
|
+
|
|
58
|
+
export interface PropOptions<T> {
|
|
59
|
+
attribute?:
|
|
60
|
+
| ((value: string) => T)
|
|
61
|
+
| (string extends T
|
|
62
|
+
? PartialPartial<AttributeOptions<T>, "transform">
|
|
63
|
+
: AttributeOptions<T>);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Props<M> = OmitNever<{
|
|
67
|
+
readonly [K in keyof M]: M[K] extends PropMeta<infer T> ? Signal<T> : never;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
export type EventConstructor<T = any, E = Event> = new (
|
|
71
|
+
name: string,
|
|
72
|
+
arg: T,
|
|
73
|
+
) => E;
|
|
74
|
+
|
|
75
|
+
/** @ignore */
|
|
76
|
+
export interface EventMeta<out E extends EventConstructor> extends Tagged<"e"> {
|
|
77
|
+
_event: E;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Events<M> = OmitNever<
|
|
81
|
+
Omit<
|
|
82
|
+
{
|
|
83
|
+
readonly [K in keyof M]: K extends `on${string}`
|
|
84
|
+
? M[K] extends EventMeta<infer E>
|
|
85
|
+
? E
|
|
86
|
+
: never
|
|
87
|
+
: never;
|
|
88
|
+
},
|
|
89
|
+
`on${Lowercase<keyof HTMLElementEventMap>}`
|
|
90
|
+
>
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
type GeneralJsxProps<T> = Partial<
|
|
94
|
+
OmitNever<{
|
|
95
|
+
[K in keyof T]: K extends
|
|
96
|
+
| typeof jsxPropsSym
|
|
97
|
+
| keyof DomProps<any>
|
|
98
|
+
| `on${string}`
|
|
99
|
+
// Readonly HTMLElement properties
|
|
100
|
+
| `${Uppercase<infer _>}${string}`
|
|
101
|
+
| "accessKeyLabel"
|
|
102
|
+
| "offsetHeight"
|
|
103
|
+
| "offsetLeft"
|
|
104
|
+
| "offsetParent"
|
|
105
|
+
| "offsetTop"
|
|
106
|
+
| "offsetWidth"
|
|
107
|
+
| "attributes"
|
|
108
|
+
| "classList"
|
|
109
|
+
| "clientHeight"
|
|
110
|
+
| "clientLeft"
|
|
111
|
+
| "clientTop"
|
|
112
|
+
| "clientWidth"
|
|
113
|
+
| "localName"
|
|
114
|
+
| "namespaceURI"
|
|
115
|
+
| "ownerDocument"
|
|
116
|
+
| "part"
|
|
117
|
+
| "prefix"
|
|
118
|
+
| "scrollHeight"
|
|
119
|
+
| "scrollWidth"
|
|
120
|
+
| "shadowRoot"
|
|
121
|
+
| "tagName"
|
|
122
|
+
| "baseURI"
|
|
123
|
+
| "childNodes"
|
|
124
|
+
| "firstChild"
|
|
125
|
+
| "isConnected"
|
|
126
|
+
| "lastChild"
|
|
127
|
+
| "nextSibling"
|
|
128
|
+
| "nodeName"
|
|
129
|
+
| "nodeType"
|
|
130
|
+
| "parentElement"
|
|
131
|
+
| "parentNode"
|
|
132
|
+
| "previousSibling"
|
|
133
|
+
| "nextElementSibling"
|
|
134
|
+
| "previousElementSibling"
|
|
135
|
+
| "childElementCount"
|
|
136
|
+
| "firstElementChild"
|
|
137
|
+
| "lastElementChild"
|
|
138
|
+
| "assignedSlot"
|
|
139
|
+
| "attributeStyleMap"
|
|
140
|
+
| "isContentEditable"
|
|
141
|
+
| "dataset"
|
|
142
|
+
? never
|
|
143
|
+
: T[K] extends Function
|
|
144
|
+
? never
|
|
145
|
+
: MaybeSignal<T[K]>;
|
|
146
|
+
}>
|
|
147
|
+
> &
|
|
148
|
+
DomProps<any> &
|
|
149
|
+
DomEventProps<never> &
|
|
150
|
+
// Allow other HTMLElement attributes
|
|
151
|
+
Record<string, any>;
|
|
152
|
+
|
|
153
|
+
export type JsxProps<T extends HTMLElement> = typeof jsxPropsSym extends keyof T
|
|
154
|
+
? NonNullable<T[typeof jsxPropsSym]>
|
|
155
|
+
: any;
|
|
156
|
+
|
|
157
|
+
type ComponentJsxProps<M> = Partial<
|
|
158
|
+
OmitNever<{
|
|
159
|
+
[K in keyof Props<M>]: Props<M>[K] extends Signal<infer T>
|
|
160
|
+
? MaybeSignal<T>
|
|
161
|
+
: never;
|
|
162
|
+
}> & {
|
|
163
|
+
[K in keyof Events<M>]: (evt: InstanceType<Events<M>[K]>) => void;
|
|
164
|
+
}
|
|
165
|
+
> &
|
|
166
|
+
GeneralJsxProps<HTMLElement>;
|
|
167
|
+
|
|
168
|
+
type EventEmitters<M> = OmitNever<
|
|
169
|
+
Omit<
|
|
170
|
+
{
|
|
171
|
+
[K in keyof Events<M>]: Events<M>[K] extends EventConstructor<infer E>
|
|
172
|
+
? undefined extends E
|
|
173
|
+
? (arg?: E) => boolean
|
|
174
|
+
: (arg: E) => boolean
|
|
175
|
+
: never;
|
|
176
|
+
},
|
|
177
|
+
`on${Lowercase<keyof HTMLElementEventMap>}`
|
|
178
|
+
>
|
|
179
|
+
>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Defines a property in your component metadata that can be set from outside
|
|
183
|
+
* of the component.
|
|
184
|
+
*
|
|
185
|
+
* Make sure to avoid conflicts with native `HTMLElement` properties.
|
|
186
|
+
*
|
|
187
|
+
* You can get properties by accessing the {@link Signal} in `this.props`.
|
|
188
|
+
* It's also possible to set the properties directly on the component instance.
|
|
189
|
+
*
|
|
190
|
+
* It's also possible to define an attribute for the property by setting the
|
|
191
|
+
* `attribute` option. By default, the attribute name is the kebab-case version
|
|
192
|
+
* of the property name. The attribute will be observed and the signal updated
|
|
193
|
+
* on changes. You can also provide a custom name and a transform function to
|
|
194
|
+
* convert the attribute value to the property value.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```tsx
|
|
198
|
+
* class App extends Component("x-app", {
|
|
199
|
+
* greetingMessage: prop<string>("Hello, world!", {
|
|
200
|
+
* attribute: {
|
|
201
|
+
* name: "greeting",
|
|
202
|
+
* }
|
|
203
|
+
* }),
|
|
204
|
+
* }) {
|
|
205
|
+
* render() {
|
|
206
|
+
* return <h1>{this.props.greetingMessage}</h1>;
|
|
207
|
+
* }
|
|
208
|
+
* }
|
|
209
|
+
*
|
|
210
|
+
* defineComponents(App);
|
|
211
|
+
*
|
|
212
|
+
* const app = new App();
|
|
213
|
+
* app.greetingMessage = "Hello, universe!";
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export const prop: (<T>(
|
|
217
|
+
context: Context<T>,
|
|
218
|
+
opts?: PropOptions<T>,
|
|
219
|
+
) => PropMeta<T | undefined>) &
|
|
220
|
+
(<T>(defaultValue: T, opts?: PropOptions<T>) => PropMeta<T>) &
|
|
221
|
+
(<T>(
|
|
222
|
+
defaultValue?: T,
|
|
223
|
+
opts?: PropOptions<T | undefined>,
|
|
224
|
+
) => PropMeta<T | undefined>) = <T>(
|
|
225
|
+
defaultOrContext?: Context<T> | T,
|
|
226
|
+
opts?: PropOptions<T>,
|
|
227
|
+
): PropMeta<any> => ({
|
|
228
|
+
_tag: "p",
|
|
229
|
+
_defaultOrContext: defaultOrContext,
|
|
230
|
+
...opts,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// CustomEvent<T> has a flaw in its constructor signature since it allows
|
|
234
|
+
// `detail` to be optional. This is a workaround to make it required unless
|
|
235
|
+
// `undefined` can be assigned to `T`.
|
|
236
|
+
|
|
237
|
+
type _CustomEventContructor<T> = undefined extends T
|
|
238
|
+
? typeof CustomEvent<T>
|
|
239
|
+
: EventConstructor<
|
|
240
|
+
PartialRequire<CustomEventInit<T>, "detail">,
|
|
241
|
+
CustomEvent<T>
|
|
242
|
+
>;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Defines an event in your component metadata that can be dispatched by
|
|
246
|
+
* the component.
|
|
247
|
+
*
|
|
248
|
+
* Make sure your event name starts with `on` and to avoid conflicts with
|
|
249
|
+
* native `HTMLElement` events. The event name will be converted to kebab-case.
|
|
250
|
+
*
|
|
251
|
+
* You can dispatch events either using `HTMLElement.dispatchEvent` or by
|
|
252
|
+
* calling the event emitter function in `this.events` inside the `render`
|
|
253
|
+
* function of a component.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```tsx
|
|
257
|
+
* class App extends Component("x-app", {
|
|
258
|
+
* onSomethingHappen: event<string>(),
|
|
259
|
+
* // Event name will be `something-happen`
|
|
260
|
+
* }) {
|
|
261
|
+
* render() {
|
|
262
|
+
* // …
|
|
263
|
+
* this.events.onSomethingHappen({ detail: "Something happened! "});
|
|
264
|
+
* }
|
|
265
|
+
* }
|
|
266
|
+
*
|
|
267
|
+
* const app = new App();
|
|
268
|
+
* app.addEventListener("something-happen", (evt) => {
|
|
269
|
+
* // `evt` is `CustomEvent<string>`
|
|
270
|
+
* console.log(evt.detail);
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
273
|
+
*
|
|
274
|
+
* You can also provide a custom event constructor:
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```tsx
|
|
278
|
+
* class App extends Component("x-app", {
|
|
279
|
+
* onSomethingClick: event(() => MouseEvent),
|
|
280
|
+
* }) {
|
|
281
|
+
* render() {
|
|
282
|
+
* return (
|
|
283
|
+
* <button onclick={evt => this.events.onSomethingClick(evt)}>
|
|
284
|
+
* Click me!
|
|
285
|
+
* </button>
|
|
286
|
+
* );
|
|
287
|
+
* }
|
|
288
|
+
* }
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export const event: (() => EventMeta<_CustomEventContructor<undefined>>) &
|
|
292
|
+
(<T>() => EventMeta<_CustomEventContructor<T>>) &
|
|
293
|
+
(<E extends EventConstructor>(eventConstructor: E) => EventMeta<E>) = ((
|
|
294
|
+
eventConstructor: EventConstructor = CustomEvent,
|
|
295
|
+
): EventMeta<EventConstructor> => ({
|
|
296
|
+
_tag: "e",
|
|
297
|
+
_event: eventConstructor,
|
|
298
|
+
})) as any;
|
|
299
|
+
|
|
300
|
+
export type Metadata = {
|
|
301
|
+
// Forbid all library properties
|
|
302
|
+
[K in keyof ComponentInner<any> | "props" | "events"]?: never;
|
|
303
|
+
} & {
|
|
304
|
+
// Forbid all dom props
|
|
305
|
+
[K in keyof DomProps<any>]?: never;
|
|
306
|
+
} & {
|
|
307
|
+
// Forbid all HTMLElement props
|
|
308
|
+
[K in keyof HTMLElement]?: never;
|
|
309
|
+
} & {
|
|
310
|
+
[name: string]: PropMeta<any> | EventMeta<any>;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const componentSym = Symbol("Component");
|
|
314
|
+
export declare const jsxPropsSym: unique symbol;
|
|
315
|
+
|
|
316
|
+
declare abstract class ComponentInner<M extends Metadata> {
|
|
317
|
+
protected props: Props<M>;
|
|
318
|
+
protected events: EventEmitters<M>;
|
|
319
|
+
|
|
320
|
+
readonly [jsxPropsSym]?: ComponentJsxProps<M>;
|
|
321
|
+
readonly [componentSym]: {
|
|
322
|
+
_scope?: ReturnType<typeof useScope>;
|
|
323
|
+
_destroy?: (() => void) | void;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
connectedCallback(): void;
|
|
327
|
+
disconnectedCallback(): void;
|
|
328
|
+
attributeChangedCallback(
|
|
329
|
+
name: string,
|
|
330
|
+
oldValue: string | null,
|
|
331
|
+
value: string | null,
|
|
332
|
+
): void;
|
|
333
|
+
|
|
334
|
+
addEventListener<K extends keyof Events<M> & string>(
|
|
335
|
+
type: JsxPropNameToEventName<K>,
|
|
336
|
+
listener: (event: InstanceType<Events<M>[K]>) => void,
|
|
337
|
+
options?: boolean | AddEventListenerOptions,
|
|
338
|
+
): void;
|
|
339
|
+
removeEventListener<K extends keyof Events<M> & string>(
|
|
340
|
+
type: JsxPropNameToEventName<K>,
|
|
341
|
+
listener: (event: InstanceType<Events<M>[K]>) => void,
|
|
342
|
+
options?: boolean | EventListenerOptions,
|
|
343
|
+
): void;
|
|
344
|
+
|
|
345
|
+
abstract render(): Template;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export type Component<M extends Metadata = {}> = {
|
|
349
|
+
-readonly [K in keyof Props<M>]: Props<M>[K] extends Signal<infer T>
|
|
350
|
+
? T
|
|
351
|
+
: never;
|
|
352
|
+
} & ComponentInner<M> &
|
|
353
|
+
HTMLElement;
|
|
354
|
+
|
|
355
|
+
export interface ComponentConstructor<M extends Metadata = {}> {
|
|
356
|
+
/** @ignore */
|
|
357
|
+
readonly [componentSym]: {
|
|
358
|
+
readonly _tagName: string;
|
|
359
|
+
};
|
|
360
|
+
readonly observedAttributes: readonly string[];
|
|
361
|
+
|
|
362
|
+
new (): Component<M>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export interface ComponentOptions {
|
|
366
|
+
/**
|
|
367
|
+
* Shadow DOM options. Set to `false` to disable shadow DOM.
|
|
368
|
+
*
|
|
369
|
+
* @default { mode: "open" }
|
|
370
|
+
*/
|
|
371
|
+
shadow?: ShadowRootInit | false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let mountEffects: [fn: () => Cleanup, deps?: Signal<unknown>[]][] | undefined;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Creates an effect which will rerun when any accessed signal changes.
|
|
378
|
+
*
|
|
379
|
+
* If used inside of a component and the component is not yet mounted, the
|
|
380
|
+
* effect will run only after the component is mounted. Otherwise, the effect
|
|
381
|
+
* will run immediately.
|
|
382
|
+
*
|
|
383
|
+
* @param fn The function to run; it may return a cleanup function.
|
|
384
|
+
*/
|
|
385
|
+
export const useMountEffect = (
|
|
386
|
+
fn: () => Cleanup,
|
|
387
|
+
deps?: Signal<unknown>[],
|
|
388
|
+
): void => {
|
|
389
|
+
if (mountEffects) {
|
|
390
|
+
mountEffects.push([fn, deps]);
|
|
391
|
+
} else {
|
|
392
|
+
useEffect(fn, deps);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Creates a new web component class.
|
|
398
|
+
*
|
|
399
|
+
* Specify props and events using the `metadata` parameter.
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```tsx
|
|
403
|
+
* class MyComponent extends Component("my-component", {
|
|
404
|
+
* myProp: prop<string>("Hello, world!"),
|
|
405
|
+
* onMyEvent: event(),
|
|
406
|
+
* }) {
|
|
407
|
+
* render() {
|
|
408
|
+
* return (
|
|
409
|
+
* <>
|
|
410
|
+
* <h1>{this.props.myProp}</h1>
|
|
411
|
+
* <button onclick={() => this.events.onMyEvent()}>Click me</button>
|
|
412
|
+
* </>
|
|
413
|
+
* );
|
|
414
|
+
* },
|
|
415
|
+
* }
|
|
416
|
+
*
|
|
417
|
+
* customElements.define("my-component", MyComponent);
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
export const Component: ((tagName: string) => ComponentConstructor<{}>) &
|
|
421
|
+
(<const M extends Metadata>(
|
|
422
|
+
tagName: string,
|
|
423
|
+
metadata: M,
|
|
424
|
+
opts?: ComponentOptions,
|
|
425
|
+
) => ComponentConstructor<M>) = ((
|
|
426
|
+
tagName: string,
|
|
427
|
+
metadata: Metadata = {},
|
|
428
|
+
opts: ComponentOptions = {},
|
|
429
|
+
): ComponentConstructor => {
|
|
430
|
+
// Extract attribute information
|
|
431
|
+
|
|
432
|
+
const observedAttributes: string[] = [];
|
|
433
|
+
const attributePropMap = new Map<
|
|
434
|
+
string,
|
|
435
|
+
{
|
|
436
|
+
name: string;
|
|
437
|
+
meta: PropMeta<any> & {
|
|
438
|
+
attribute: Required<
|
|
439
|
+
NonNullable<Exclude<PropMeta<any>["attribute"], boolean | Function>>
|
|
440
|
+
>;
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
>();
|
|
444
|
+
|
|
445
|
+
for (const name in metadata) {
|
|
446
|
+
const meta = metadata[name] as PropMeta<any> | EventMeta<any>;
|
|
447
|
+
|
|
448
|
+
if (meta._tag == "p" && meta.attribute) {
|
|
449
|
+
if (typeof meta.attribute == "function") {
|
|
450
|
+
meta.attribute = { transform: meta.attribute };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const attribute: AttributeOptions<any> = (meta.attribute = {
|
|
454
|
+
name: camelCaseToKebabCase(name),
|
|
455
|
+
static: false,
|
|
456
|
+
transform: (x) => x,
|
|
457
|
+
...meta.attribute,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
attributePropMap.set(attribute.name!, {
|
|
461
|
+
name,
|
|
462
|
+
meta: meta as any,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (!attribute.static) {
|
|
466
|
+
observedAttributes.push(attribute.name!);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Create base class
|
|
472
|
+
|
|
473
|
+
opts.shadow ??= { mode: "open" };
|
|
474
|
+
|
|
475
|
+
const getRenderParent = (component: _Component) =>
|
|
476
|
+
opts.shadow
|
|
477
|
+
? component.shadowRoot ?? component.attachShadow(opts.shadow)
|
|
478
|
+
: component;
|
|
479
|
+
abstract class _Component extends HTMLElement {
|
|
480
|
+
static readonly [componentSym]: ComponentConstructor[typeof componentSym] =
|
|
481
|
+
{
|
|
482
|
+
_tagName: tagName,
|
|
483
|
+
};
|
|
484
|
+
static readonly observedAttributes: readonly string[] = observedAttributes;
|
|
485
|
+
|
|
486
|
+
protected props: Record<string, Signal<any>> = {};
|
|
487
|
+
protected events: Record<string, (arg: unknown) => any> = {};
|
|
488
|
+
|
|
489
|
+
readonly [componentSym]: ComponentInner<any>[typeof componentSym] = {};
|
|
490
|
+
|
|
491
|
+
constructor() {
|
|
492
|
+
super();
|
|
493
|
+
|
|
494
|
+
for (const name in metadata) {
|
|
495
|
+
const meta = metadata[name];
|
|
496
|
+
|
|
497
|
+
if (meta._tag == "p") {
|
|
498
|
+
const context = isContext(meta._defaultOrContext)
|
|
499
|
+
? meta._defaultOrContext
|
|
500
|
+
: null;
|
|
501
|
+
const [getter, setter] = useSignal<unknown>(
|
|
502
|
+
context ? undefined : meta._defaultOrContext,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
this.props[name] = getter;
|
|
506
|
+
|
|
507
|
+
if (context) {
|
|
508
|
+
provideContext(context, this, getter);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
Object.defineProperty(this, name, {
|
|
512
|
+
get: getter.peek,
|
|
513
|
+
set: (value) => setter(() => value, { force: true }),
|
|
514
|
+
});
|
|
515
|
+
} else if (meta._tag == "e" && name.startsWith("on")) {
|
|
516
|
+
const eventName = jsxPropNameToEventName(name as `on${string}`);
|
|
517
|
+
|
|
518
|
+
this.events[name] = (arg: unknown) =>
|
|
519
|
+
this.dispatchEvent(new meta._event(eventName, arg));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
connectedCallback(): void {
|
|
525
|
+
const renderParent = getRenderParent(this);
|
|
526
|
+
|
|
527
|
+
this[componentSym]._destroy = useSubscope(() =>
|
|
528
|
+
runWithRenderer(
|
|
529
|
+
{
|
|
530
|
+
_svg: false,
|
|
531
|
+
_component: this as any,
|
|
532
|
+
_nodes: renderParent.childNodes.values(),
|
|
533
|
+
},
|
|
534
|
+
() => {
|
|
535
|
+
this[componentSym]._scope = useScope();
|
|
536
|
+
|
|
537
|
+
// Render
|
|
538
|
+
|
|
539
|
+
const prevMountEffects = mountEffects;
|
|
540
|
+
mountEffects = [];
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
renderParent?.append(...this.render().build());
|
|
544
|
+
|
|
545
|
+
// Run mount effects
|
|
546
|
+
|
|
547
|
+
mountEffects.forEach(([fn, opts]) => useEffect(fn, opts));
|
|
548
|
+
} finally {
|
|
549
|
+
mountEffects = prevMountEffects;
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
),
|
|
553
|
+
)[1];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
disconnectedCallback(): void {
|
|
557
|
+
this[componentSym]._destroy?.();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
attributeChangedCallback(
|
|
561
|
+
name: string,
|
|
562
|
+
_: string | null,
|
|
563
|
+
value: string | null,
|
|
564
|
+
): void {
|
|
565
|
+
const prop = attributePropMap.get(name);
|
|
566
|
+
|
|
567
|
+
if (prop) {
|
|
568
|
+
this[prop.name as keyof this] =
|
|
569
|
+
value != null
|
|
570
|
+
? prop.meta.attribute.transform.call(this, value)
|
|
571
|
+
: isContext(prop.meta._defaultOrContext)
|
|
572
|
+
? undefined
|
|
573
|
+
: prop.meta._defaultOrContext;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
abstract render(): Template;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return _Component as any;
|
|
581
|
+
}) as any;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Determines whether the given value is a component created by
|
|
585
|
+
* extending {@link ComponentConstructor}.
|
|
586
|
+
*/
|
|
587
|
+
export const isComponent = (
|
|
588
|
+
value: any,
|
|
589
|
+
): value is ComponentConstructor | Component => !!value?.[componentSym];
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Represents a functional component.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```tsx
|
|
596
|
+
* const MyComponent: FunctionalComponent<{
|
|
597
|
+
* name: MaybeSignal<string>;
|
|
598
|
+
* }> = ({ name }) => {
|
|
599
|
+
* return <h1>Hello, {name}!</h1>;
|
|
600
|
+
* };
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
export interface FunctionalComponent<in P extends object = {}> {
|
|
604
|
+
(props: P): Template;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Defines a set of components with the given prefix.
|
|
609
|
+
*/
|
|
610
|
+
export const defineComponents: ((
|
|
611
|
+
...components: ComponentConstructor[]
|
|
612
|
+
) => void) &
|
|
613
|
+
((prefix: string, ...components: ComponentConstructor[]) => void) = (
|
|
614
|
+
...args: [string | ComponentConstructor, ...ComponentConstructor[]]
|
|
615
|
+
) => {
|
|
616
|
+
const [prefix, components] =
|
|
617
|
+
typeof args[0] == "string"
|
|
618
|
+
? [args[0], args.slice(1) as ComponentConstructor[]]
|
|
619
|
+
: ["", args as ComponentConstructor[]];
|
|
620
|
+
|
|
621
|
+
for (const component of components) {
|
|
622
|
+
customElements.define(prefix + component[componentSym]._tagName, component);
|
|
623
|
+
}
|
|
624
|
+
};
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useRenderer } from "./renderer.js";
|
|
2
|
+
import { useMemo, Signal, SetSignalOptions, MaybeSignal } from "./scope.js";
|
|
3
|
+
|
|
4
|
+
const contextSym = Symbol("Context");
|
|
5
|
+
|
|
6
|
+
type ContextEvent<T> = CustomEvent<(value: T) => void>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A value that can be passed through the component tree without having to be
|
|
10
|
+
* explicitly passed as a prop.
|
|
11
|
+
*/
|
|
12
|
+
export interface Context<in out T> {
|
|
13
|
+
readonly [contextSym]: string;
|
|
14
|
+
/** @ignore */
|
|
15
|
+
readonly _init: T;
|
|
16
|
+
/** @ignore */
|
|
17
|
+
readonly _opts?: SetSignalOptions;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new context with the given value.
|
|
22
|
+
*/
|
|
23
|
+
export const createContext: (<T>(
|
|
24
|
+
value: T,
|
|
25
|
+
opts?: SetSignalOptions,
|
|
26
|
+
) => Context<T>) &
|
|
27
|
+
(<T>(value?: T, opts?: SetSignalOptions) => Context<T | undefined>) = (<T>(
|
|
28
|
+
value?: T,
|
|
29
|
+
opts?: SetSignalOptions,
|
|
30
|
+
): Context<T | undefined> => ({
|
|
31
|
+
[contextSym]: Math.random().toString(36).slice(2),
|
|
32
|
+
_init: value,
|
|
33
|
+
_opts: opts,
|
|
34
|
+
})) as any;
|
|
35
|
+
|
|
36
|
+
export const isContext = (value: any): value is Context<unknown> =>
|
|
37
|
+
!!value?.[contextSym];
|
|
38
|
+
|
|
39
|
+
export const provideContext = <T>(
|
|
40
|
+
context: Context<T>,
|
|
41
|
+
element: Element,
|
|
42
|
+
value: MaybeSignal<T | undefined>,
|
|
43
|
+
) => {
|
|
44
|
+
element.addEventListener(context[contextSym], (evt) => {
|
|
45
|
+
const innerValue = MaybeSignal.get(value);
|
|
46
|
+
|
|
47
|
+
if (innerValue !== undefined) {
|
|
48
|
+
evt.stopPropagation();
|
|
49
|
+
(evt as ContextEvent<T>).detail(innerValue);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const useContext = <T>(context: Context<T>): Signal<T> => {
|
|
55
|
+
const renderer = useRenderer();
|
|
56
|
+
|
|
57
|
+
return useMemo(() => {
|
|
58
|
+
let result = context._init;
|
|
59
|
+
|
|
60
|
+
renderer._component?.dispatchEvent(
|
|
61
|
+
new CustomEvent(context[contextSym], {
|
|
62
|
+
detail: (value) => (result = value),
|
|
63
|
+
bubbles: true,
|
|
64
|
+
composed: true,
|
|
65
|
+
}) as ContextEvent<T>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
};
|