@seahax/elemental 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/LICENSE +24 -0
- package/README.md +166 -0
- package/dist/component.d.ts +53 -0
- package/dist/hooks/core.d.ts +7 -0
- package/dist/hooks/loading.d.ts +11 -0
- package/dist/hooks/route.d.ts +10 -0
- package/dist/hooks/store.d.ts +5 -0
- package/dist/html.d.ts +33 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +257 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/callbacks.d.ts +8 -0
- package/dist/internal/constants.d.ts +2 -0
- package/dist/router.d.ts +13 -0
- package/dist/store.d.ts +6 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @seahax/elemental
|
|
2
|
+
|
|
3
|
+
Small building blocks with a lot of potential.
|
|
4
|
+
|
|
5
|
+
Extremely small reactive web components library, containing everything you really need to build anything up to a full reactive application, with only about 3KB overhead (depending on compression, minification, and tree-shaking).
|
|
6
|
+
|
|
7
|
+
- No Build Tooling
|
|
8
|
+
- No Dependencies
|
|
9
|
+
- Portable Web Components
|
|
10
|
+
- Global & Local Reactivity
|
|
11
|
+
- Efficient DOM Rendering
|
|
12
|
+
- Functional Hooks
|
|
13
|
+
- Routing
|
|
14
|
+
|
|
15
|
+
## Define A Web Component
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
defineComponent,
|
|
20
|
+
useRef,
|
|
21
|
+
useStore,
|
|
22
|
+
useAttributes,
|
|
23
|
+
useRoute,
|
|
24
|
+
useLoading,
|
|
25
|
+
useEffect,
|
|
26
|
+
h,
|
|
27
|
+
} from '@seahax/elemental';
|
|
28
|
+
|
|
29
|
+
export const MyComponent = defineComponent((shadow) => {
|
|
30
|
+
// This function is run once after the component is created, when it is
|
|
31
|
+
// first connected to the document.
|
|
32
|
+
|
|
33
|
+
// Create HTML elements and save references to them.
|
|
34
|
+
const myInput = h('input');
|
|
35
|
+
|
|
36
|
+
// Render content to the shadow DOM.
|
|
37
|
+
h(shadow, [
|
|
38
|
+
h('style', [/* css */]),
|
|
39
|
+
h('p', { class: 'hello' }, ['Hello, World!']),
|
|
40
|
+
h('div', { class: 'inputs' }, [
|
|
41
|
+
myInput,
|
|
42
|
+
]),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Use reference (reactive state) hooks.
|
|
46
|
+
const localStateRef = useRef('initial value', (newValue) => {
|
|
47
|
+
// Handle
|
|
48
|
+
});
|
|
49
|
+
const globalStateRef = useStore(myStore, select, mutate);
|
|
50
|
+
const [dataValueRef, ...] = useAttributes('data-value', ...);
|
|
51
|
+
const [routeMatchRef, routeStateRef] = useRoute('/path/', {
|
|
52
|
+
match: 'prefix', // 'exact' | 'prefix' | RegExp
|
|
53
|
+
source: 'pathname', // 'pathname' | 'hash'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Reactively load async data.
|
|
57
|
+
const loadingStateRef = useLoading([
|
|
58
|
+
// dependency references
|
|
59
|
+
], async (signal, ...dependencyValues) => {
|
|
60
|
+
// Reactive async code runs when the component is connected to the
|
|
61
|
+
// document, and when any of the dependencies change. The signal is
|
|
62
|
+
// aborted if the dependencies change before the promise returned by
|
|
63
|
+
// this function is resolved.
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// React to reference changes.
|
|
67
|
+
useEffect([
|
|
68
|
+
// dependency references
|
|
69
|
+
localStateRef,
|
|
70
|
+
globalStateRef,
|
|
71
|
+
dataValueRef,
|
|
72
|
+
routeMatchRef,
|
|
73
|
+
routeStateRef,
|
|
74
|
+
loadingStateRef,
|
|
75
|
+
], (...dependencyValues) => {
|
|
76
|
+
// Reactive code runs when the component is connected to the document,
|
|
77
|
+
// and when any of the dependencies change.
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
// Cleanup before the next effect callback and after the component
|
|
81
|
+
// is disconnected from the document.
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Customize The Shadow Root
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const MyComponent = defineComponent(
|
|
91
|
+
(shadow) => {
|
|
92
|
+
// Renderer...
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
// Shadow root options.
|
|
96
|
+
shadow: {
|
|
97
|
+
mode: 'closed',
|
|
98
|
+
...
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Add Web Component Properties
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface Props {
|
|
108
|
+
checked: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const MyComponent = defineComponent<Props>(
|
|
112
|
+
(shadow, propRefs) => {
|
|
113
|
+
// Get properties (the ref value is initially undefined).
|
|
114
|
+
const isChecked = propRefs.checked.value ?? shadow.host.hasAttribute('checked');
|
|
115
|
+
|
|
116
|
+
// Set properties.
|
|
117
|
+
propRefs.checked.value = true;
|
|
118
|
+
|
|
119
|
+
// Alternatively, access the property on the host element.
|
|
120
|
+
const isChecked = shadow.host.checked;
|
|
121
|
+
shadow.host.checked = true;
|
|
122
|
+
|
|
123
|
+
// React to property changes.
|
|
124
|
+
useEffect([propRefs.checked], (checked) => {
|
|
125
|
+
...
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
props: {
|
|
130
|
+
// Return a property descriptor that uses a pre-defined ref and the
|
|
131
|
+
// host element. The property descriptor must have a `get` function,
|
|
132
|
+
// `value` is not allowed, and all other properties are optional.
|
|
133
|
+
// The ref value is initially undefined.
|
|
134
|
+
checked: (ref, host) => {
|
|
135
|
+
return {
|
|
136
|
+
get: () => ref.value ?? host.hasAttribute('checked'),
|
|
137
|
+
set: (value) => (ref.value = value),
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const element = new MyComponent();
|
|
145
|
+
|
|
146
|
+
// Properties are defined publicly on component (`HTMLElement`) instances.
|
|
147
|
+
element.checked = true;
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Render Lists With Keys
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// Create a reusable root element.
|
|
154
|
+
const parent = h('div');
|
|
155
|
+
|
|
156
|
+
h(parent, items.map((item) => {
|
|
157
|
+
// No previous children, so all children will be created.
|
|
158
|
+
return h('p', { 'data-key': item.id }, [item.text]);
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
// Update children with matching keys.
|
|
162
|
+
h(parent, items.map((item) => {
|
|
163
|
+
// Second render, so reuse (and update) children with matching keys.
|
|
164
|
+
return h('p', { 'data-key': item.id }, [item.text]);
|
|
165
|
+
}));
|
|
166
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Callbacks } from './internal/callbacks.ts';
|
|
2
|
+
import { $$ref, $$renderContextStack } from './internal/constants.ts';
|
|
3
|
+
type SafeProps<TProps> = any extends any ? {
|
|
4
|
+
[P in keyof TProps as P extends keyof HTMLElement ? never : P]: TProps[P];
|
|
5
|
+
} : never;
|
|
6
|
+
export interface ComponentConstructor<TProps extends object> {
|
|
7
|
+
new (): ComponentWithProps<TProps>;
|
|
8
|
+
}
|
|
9
|
+
export interface ComponentOptions<TProps extends object> {
|
|
10
|
+
readonly shadow?: Partial<ShadowRootInit>;
|
|
11
|
+
readonly props?: ComponentPropDescriptors<TProps>;
|
|
12
|
+
}
|
|
13
|
+
export type ComponentPropDescriptors<TProps extends object> = {
|
|
14
|
+
readonly [P in keyof SafeProps<TProps>]: ComponentPropDescriptorFactory<TProps[P]>;
|
|
15
|
+
};
|
|
16
|
+
export type ComponentPropDescriptorFactory<TType> = (ref: Ref<TType | undefined>, host: HTMLElement) => ComponentPropDescriptor<TType>;
|
|
17
|
+
export interface ComponentPropDescriptor<T> extends Omit<PropertyDescriptor, 'value' | 'get' | 'set'> {
|
|
18
|
+
get(): T;
|
|
19
|
+
set?(value: T): void;
|
|
20
|
+
}
|
|
21
|
+
export type ComponentWithProps<TProps extends object> = HTMLElement & {
|
|
22
|
+
-readonly [P in keyof SafeProps<TProps>]: TProps[P];
|
|
23
|
+
};
|
|
24
|
+
export type ComponentShadowRoot<TProps extends object> = Omit<ShadowRoot, 'host'> & {
|
|
25
|
+
readonly host: ComponentWithProps<TProps>;
|
|
26
|
+
};
|
|
27
|
+
export type ComponentPropRefs<TProps extends object> = {
|
|
28
|
+
readonly [P in keyof SafeProps<TProps>]: Ref<TProps[P] | undefined>;
|
|
29
|
+
};
|
|
30
|
+
export interface Ref<T> extends ReadonlyRef<T> {
|
|
31
|
+
value: T;
|
|
32
|
+
}
|
|
33
|
+
export interface ReadonlyRef<T> {
|
|
34
|
+
/** @hidden */
|
|
35
|
+
[$$ref]: unknown;
|
|
36
|
+
readonly value: T;
|
|
37
|
+
}
|
|
38
|
+
export type RefValues<T> = T extends readonly any[] ? {
|
|
39
|
+
[K in keyof T]: T[K] extends ReadonlyRef<infer V> ? V : never;
|
|
40
|
+
} : never;
|
|
41
|
+
declare global {
|
|
42
|
+
interface Window {
|
|
43
|
+
readonly [$$renderContextStack]: {
|
|
44
|
+
readonly host: HTMLElement;
|
|
45
|
+
readonly onDisconnect: Callbacks;
|
|
46
|
+
readonly onSetRef: Callbacks;
|
|
47
|
+
readonly useRef: <T>(initialValue: T, onChange?: (value: T) => void) => Ref<T>;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Define a custom `HTMLElement` that is functional and reactive. */
|
|
52
|
+
export declare function defineComponent<TProps extends object = {}>(render: (shadow: ComponentShadowRoot<TProps>, props: ComponentPropRefs<TProps>) => void, options?: ComponentOptions<TProps>): ComponentConstructor<TProps>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReadonlyRef, type Ref, type RefValues } from '../component.ts';
|
|
2
|
+
/** Create an observable value. */
|
|
3
|
+
export declare function useRef<T>(initialValue: T, onChange?: (value: T) => void): Ref<T>;
|
|
4
|
+
/** React to observable (reference) changes. */
|
|
5
|
+
export declare function useEffect<const TDeps extends readonly ReadonlyRef<any>[]>(deps: TDeps, callback: (...values: RefValues<TDeps>) => (() => void) | void): void;
|
|
6
|
+
/** Observe attribute changes. */
|
|
7
|
+
export declare function useAttributes<TName extends string>(...names: TName[]): Readonly<Record<TName, Ref<string | null>>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReadonlyRef, Ref, RefValues } from '../component.ts';
|
|
2
|
+
export interface LoadingValue<TValue> {
|
|
3
|
+
readonly loading: boolean;
|
|
4
|
+
readonly value: TValue | undefined;
|
|
5
|
+
readonly error: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface LoadingOptions {
|
|
8
|
+
readonly debounceMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Load data asynchronously. */
|
|
11
|
+
export declare function useLoading<const TDeps extends readonly ReadonlyRef<any>[], TValue>(deps: TDeps, callback: (signal: AbortSignal, ...values: RefValues<TDeps>) => Promise<TValue>, { debounceMs }?: LoadingOptions): Ref<LoadingValue<TValue>>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Ref } from '../component.ts';
|
|
2
|
+
export interface RouteOptions {
|
|
3
|
+
readonly match?: 'prefix' | 'exact' | RegExp;
|
|
4
|
+
readonly source?: 'pathname' | 'hash';
|
|
5
|
+
}
|
|
6
|
+
export type RouteMatchArray = readonly [string, ...string[]] & {
|
|
7
|
+
readonly groups: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
/** Observe route (window.history) changes. */
|
|
10
|
+
export declare function useRoute(path: string | readonly string[], { match, source }?: RouteOptions): [match: Ref<RouteMatchArray | null>, state: Ref<unknown>];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReadonlyRef, Ref } from '../component.ts';
|
|
2
|
+
import type { Store } from '../store.ts';
|
|
3
|
+
/** Observe store (external state) changes. */
|
|
4
|
+
export declare function useStore<TState, TValue = TState>(store: Store<TState>, select: (state: TState) => TValue, mutate: (store: Store<TState>, value: TValue) => void): Ref<TValue>;
|
|
5
|
+
export declare function useStore<TState, TValue = TState>(store: Store<TState>, select?: (state: TState) => TValue): ReadonlyRef<TValue>;
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap;
|
|
2
|
+
type TagType<TTag extends string> = TTag extends keyof TagMap ? TagMap[TTag] : HTMLElement;
|
|
3
|
+
type ChildValue = Node | HtmlDeferred | string | number | bigint | false | null | undefined;
|
|
4
|
+
type AttrValue = string | number | bigint | boolean | null | undefined;
|
|
5
|
+
export interface HtmlDeferred<TTag extends keyof TagMap | (string & {}) = string> {
|
|
6
|
+
readonly tag: TTag;
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly attrs: Readonly<Record<string, AttrValue>>;
|
|
9
|
+
readonly children: readonly ChildValue[] | undefined;
|
|
10
|
+
resolve: () => TagType<TTag>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a deferred HTML element with a key. Final element creation is
|
|
14
|
+
* deferred until the element is used as a child, when it can be determined if
|
|
15
|
+
* a new element should be created, or an existing element with the same key
|
|
16
|
+
* should be updated.
|
|
17
|
+
*/
|
|
18
|
+
export declare function html<const TTag extends keyof TagMap | (string & {})>(tag: TTag | CustomElementConstructor, attrs: {
|
|
19
|
+
readonly $key: string;
|
|
20
|
+
} & Readonly<Record<string, AttrValue>>, children?: readonly ChildValue[]): HtmlDeferred<TTag>;
|
|
21
|
+
/** Create an HTML element with attributes and children. */
|
|
22
|
+
export declare function html<const TTag extends keyof TagMap | (string & {})>(tag: TTag | CustomElementConstructor, attrs?: Readonly<Record<string, AttrValue>>, children?: readonly ChildValue[]): TagType<TTag>;
|
|
23
|
+
/** Create an HTML element with children. */
|
|
24
|
+
export declare function html<const TTag extends keyof TagMap | (string & {})>(tag: TTag | CustomElementConstructor, children?: readonly ChildValue[]): TagType<TTag>;
|
|
25
|
+
/** Update element attributes and replace children. */
|
|
26
|
+
export declare function html<TElement extends Element | Document | ShadowRoot>(element: TElement, attrs?: TElement extends {
|
|
27
|
+
setAttribute: (...args: any[]) => any;
|
|
28
|
+
} ? Readonly<Record<string, AttrValue>> : undefined, children?: readonly ChildValue[]): TElement;
|
|
29
|
+
/** Replace element children. */
|
|
30
|
+
export declare function html<TElement extends Element | Document | ShadowRoot>(element: TElement, children?: readonly ChildValue[]): TElement;
|
|
31
|
+
/** Parse a raw HTML string into a `DocumentFragment`. */
|
|
32
|
+
export declare function parseHTML(htmlString: string): DocumentFragment;
|
|
33
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './component.ts';
|
|
2
|
+
export * from './hooks/core.ts';
|
|
3
|
+
export * from './hooks/loading.ts';
|
|
4
|
+
export * from './hooks/route.ts';
|
|
5
|
+
export * from './hooks/store.ts';
|
|
6
|
+
export * from './html.ts';
|
|
7
|
+
export { html as h } from './html.ts';
|
|
8
|
+
export * from './router.ts';
|
|
9
|
+
export * from './store.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
//#region src/internal/callbacks.ts
|
|
2
|
+
function e() {
|
|
3
|
+
let e = /* @__PURE__ */ new Set(), t = {
|
|
4
|
+
push: (t) => (e.add(t), () => e.delete(t)),
|
|
5
|
+
run: ({ clear: n = !1 } = {}) => {
|
|
6
|
+
let r = [], i = [...e];
|
|
7
|
+
for (let e of i) try {
|
|
8
|
+
e();
|
|
9
|
+
} catch (e) {
|
|
10
|
+
r.push(e);
|
|
11
|
+
}
|
|
12
|
+
if (n && e.clear(), r.length > 0) throw AggregateError(r);
|
|
13
|
+
return t;
|
|
14
|
+
},
|
|
15
|
+
clear: () => e.clear()
|
|
16
|
+
};
|
|
17
|
+
return t;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/internal/constants.ts
|
|
21
|
+
var t = Symbol(), n = Symbol();
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/component.ts
|
|
24
|
+
Object.assign(window, { [n]: [] });
|
|
25
|
+
function r(r, i = {}) {
|
|
26
|
+
return class extends HTMLElement {
|
|
27
|
+
#e = e();
|
|
28
|
+
#t = e();
|
|
29
|
+
#n = i;
|
|
30
|
+
#r;
|
|
31
|
+
#i = !1;
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
let e = this.#r = {};
|
|
35
|
+
if (this.#n.props) {
|
|
36
|
+
let t = this.#n.props;
|
|
37
|
+
for (let [n, r] of Object.entries(t)) {
|
|
38
|
+
if (n in this) continue;
|
|
39
|
+
let t = r(e[n] = this.#a(void 0), this);
|
|
40
|
+
Object.defineProperty(this, n, t);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
connectedCallback() {
|
|
45
|
+
let e = this.attachShadow({
|
|
46
|
+
...this.#n.shadow,
|
|
47
|
+
mode: this.#n.shadow?.mode ?? "open"
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
window[n].push({
|
|
51
|
+
host: this,
|
|
52
|
+
onDisconnect: this.#e,
|
|
53
|
+
onSetRef: this.#t,
|
|
54
|
+
useRef: this.#a
|
|
55
|
+
}), r(e, this.#r);
|
|
56
|
+
} finally {
|
|
57
|
+
window[n].pop();
|
|
58
|
+
}
|
|
59
|
+
this.#t.run();
|
|
60
|
+
}
|
|
61
|
+
disconnectedCallback() {
|
|
62
|
+
this.#t.clear(), this.#e.run({ clear: !0 });
|
|
63
|
+
}
|
|
64
|
+
#a = (e, n) => {
|
|
65
|
+
let r = this.#o, i = e;
|
|
66
|
+
return {
|
|
67
|
+
[t]: !0,
|
|
68
|
+
get value() {
|
|
69
|
+
return i;
|
|
70
|
+
},
|
|
71
|
+
set value(e) {
|
|
72
|
+
e !== i && (i = e, n?.(i), r());
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
#o = () => {
|
|
77
|
+
this.#i || (this.#i = !0, queueMicrotask(() => {
|
|
78
|
+
this.#i = !1, this.#t.run();
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/hooks/core.ts
|
|
85
|
+
function i(e, t) {
|
|
86
|
+
return s().useRef(e, t);
|
|
87
|
+
}
|
|
88
|
+
function a(t, n) {
|
|
89
|
+
let { onSetRef: r, onDisconnect: i } = s(), a = e(), o = () => a.run({ clear: !0 }), c;
|
|
90
|
+
r.push(() => {
|
|
91
|
+
let e = t.map((e) => e.value);
|
|
92
|
+
if (c?.length === e.length && c?.every((t, n) => t === e[n])) return;
|
|
93
|
+
c = e, o();
|
|
94
|
+
let r = n(...c);
|
|
95
|
+
r && a.push(() => r());
|
|
96
|
+
}), i.push(o);
|
|
97
|
+
}
|
|
98
|
+
function o(...e) {
|
|
99
|
+
if (e.length === 0) return {};
|
|
100
|
+
let { host: t } = s(), n = Object.fromEntries(e.map((e) => [e, i(t.getAttribute(e), (n) => {
|
|
101
|
+
n == null ? t.removeAttribute(e) : t.setAttribute(e, n);
|
|
102
|
+
})])), r = new MutationObserver((e) => {
|
|
103
|
+
for (let { attributeName: r } of e) r != null && Object.hasOwn(n, r) && (n[r].value = t.getAttribute(r));
|
|
104
|
+
});
|
|
105
|
+
return a([], () => (r.observe(t, {
|
|
106
|
+
attributeFilter: e,
|
|
107
|
+
attributes: !0
|
|
108
|
+
}), () => r.disconnect())), n;
|
|
109
|
+
}
|
|
110
|
+
function s() {
|
|
111
|
+
let e = window[n].at(-1);
|
|
112
|
+
if (!e) throw Error("hooks must be called inside a render function");
|
|
113
|
+
return e;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/hooks/loading.ts
|
|
117
|
+
function c(e, t, { debounceMs: n } = {}) {
|
|
118
|
+
let r = i({
|
|
119
|
+
loading: !0,
|
|
120
|
+
value: void 0,
|
|
121
|
+
error: void 0
|
|
122
|
+
}), o = !0;
|
|
123
|
+
return a(e, () => (...e) => {
|
|
124
|
+
let i = new AbortController();
|
|
125
|
+
return Promise.race(o ? [Promise.resolve()] : [new Promise((e) => setTimeout(e, n)), new Promise((e) => i.signal.addEventListener("abort", e, { once: !0 }))]).then(() => {
|
|
126
|
+
if (!i.signal.aborted) return t(i.signal, ...e);
|
|
127
|
+
}).then((e) => {
|
|
128
|
+
i.signal.aborted || (r.value = {
|
|
129
|
+
loading: !1,
|
|
130
|
+
value: e,
|
|
131
|
+
error: void 0
|
|
132
|
+
});
|
|
133
|
+
}).catch((e) => {
|
|
134
|
+
i.signal.aborted || (r.value = {
|
|
135
|
+
loading: !1,
|
|
136
|
+
value: void 0,
|
|
137
|
+
error: e
|
|
138
|
+
});
|
|
139
|
+
}), o = !1, () => i.abort();
|
|
140
|
+
}), a([], () => () => o = !0), r;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/store.ts
|
|
144
|
+
function l(t) {
|
|
145
|
+
let n = e(), r = t;
|
|
146
|
+
return {
|
|
147
|
+
get state() {
|
|
148
|
+
return r;
|
|
149
|
+
},
|
|
150
|
+
set state(e) {
|
|
151
|
+
e !== r && (r = e, n.run());
|
|
152
|
+
},
|
|
153
|
+
subscribe: (e) => n.push(() => e(r))
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/router.ts
|
|
158
|
+
function u() {
|
|
159
|
+
return d ??= f();
|
|
160
|
+
}
|
|
161
|
+
var d;
|
|
162
|
+
function f() {
|
|
163
|
+
let e = l({
|
|
164
|
+
url: window.location.href,
|
|
165
|
+
pathname: window.location.pathname,
|
|
166
|
+
hash: window.location.hash,
|
|
167
|
+
state: window.history.state
|
|
168
|
+
}), t = () => {
|
|
169
|
+
let t = e.state;
|
|
170
|
+
t.url !== window.location.href && (t = {
|
|
171
|
+
...t,
|
|
172
|
+
url: window.location.href,
|
|
173
|
+
pathname: window.location.pathname,
|
|
174
|
+
hash: window.location.hash
|
|
175
|
+
}), t.state !== window.history.state && (t = {
|
|
176
|
+
...t,
|
|
177
|
+
state: window.history.state
|
|
178
|
+
}), e.state = t;
|
|
179
|
+
};
|
|
180
|
+
return Object.defineProperties(history, Object.fromEntries(["pushState", "replaceState"].map((e) => {
|
|
181
|
+
let n = history[e].bind(history);
|
|
182
|
+
return [e, {
|
|
183
|
+
value: (...e) => {
|
|
184
|
+
n(...e), t();
|
|
185
|
+
},
|
|
186
|
+
enumerable: !0,
|
|
187
|
+
configurable: !0
|
|
188
|
+
}];
|
|
189
|
+
}))), window.addEventListener("popstate", t), e;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/hooks/route.ts
|
|
193
|
+
function p(e, { match: t = "prefix", source: n = "pathname" } = {}) {
|
|
194
|
+
let r = t === "exact" ? /^$/u : t === "prefix" ? /^.*$/u : t, o = Array.isArray(e) ? e : [e], s = (e) => {
|
|
195
|
+
let t = new URL(e)[n], i = o.find((e) => t.endsWith(e)) ?? null;
|
|
196
|
+
return i == null ? null : (t = t.slice(i.length), t.match(r));
|
|
197
|
+
}, c = i(window.location.href), l = i(s(window.location.href)), d = i(window.history.state);
|
|
198
|
+
return a([], () => u().subscribe(({ url: e, state: t }) => {
|
|
199
|
+
c.value = e, d.value = t;
|
|
200
|
+
})), a([c], (e) => {
|
|
201
|
+
l.value = s(e);
|
|
202
|
+
}), [l, d];
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/hooks/store.ts
|
|
206
|
+
function m(e, t = (e) => e, n) {
|
|
207
|
+
let r = i(t(e.state), n && ((t) => n(e, t)));
|
|
208
|
+
return a([], () => e.subscribe((e) => r.value = t(e))), r;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/html.ts
|
|
212
|
+
var h = "data-key";
|
|
213
|
+
function g(e, t = {}, n = []) {
|
|
214
|
+
if (typeof e == "function") {
|
|
215
|
+
let t = customElements.getName(e);
|
|
216
|
+
t ?? (t = `ce-${crypto.randomUUID()}`, customElements.define(t, e)), e = t;
|
|
217
|
+
}
|
|
218
|
+
let r;
|
|
219
|
+
if ([r, n] = Array.isArray(t) ? [{}, t] : [t, n], typeof e == "string") {
|
|
220
|
+
let { [h]: t, ...i } = r;
|
|
221
|
+
return typeof t == "string" ? {
|
|
222
|
+
tag: e,
|
|
223
|
+
key: t,
|
|
224
|
+
attrs: i,
|
|
225
|
+
children: n,
|
|
226
|
+
resolve: () => g(document.createElement(e), i, n)
|
|
227
|
+
} : g(document.createElement(e), r, n);
|
|
228
|
+
}
|
|
229
|
+
if ("setAttribute" in e) {
|
|
230
|
+
let t = new Set(e.getAttributeNames());
|
|
231
|
+
for (let [n, i] of Object.entries(r)) {
|
|
232
|
+
if (t?.delete(n), i == null || i == 0) continue;
|
|
233
|
+
let r = i == 1 ? "" : String(i);
|
|
234
|
+
e.getAttribute(n) !== r && e.setAttribute(n, r);
|
|
235
|
+
}
|
|
236
|
+
for (let n of t) e.removeAttribute(n);
|
|
237
|
+
}
|
|
238
|
+
let i;
|
|
239
|
+
return e.replaceChildren(...n.filter((e) => e != null && e !== !1).map((t) => {
|
|
240
|
+
if (typeof t != "object") return document.createTextNode(String(t));
|
|
241
|
+
if (t instanceof Node) return t;
|
|
242
|
+
i ??= new Map([...e.children].flatMap((e) => {
|
|
243
|
+
let t = e.getAttribute(h);
|
|
244
|
+
return t == null ? [] : [[t, e]];
|
|
245
|
+
}));
|
|
246
|
+
let n = i.get(t.key);
|
|
247
|
+
return n && n.tagName.toLowerCase() === t.tag ? (i.delete(t.key), g(n, t.attrs, t.children)) : t.resolve();
|
|
248
|
+
})), e;
|
|
249
|
+
}
|
|
250
|
+
function _(e) {
|
|
251
|
+
let t = g("template");
|
|
252
|
+
return t.innerHTML = e, t.content;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
export { l as createStore, r as defineComponent, u as getRouter, g as h, g as html, _ as parseHTML, o as useAttributes, a as useEffect, c as useLoading, i as useRef, p as useRoute, m as useStore };
|
|
256
|
+
|
|
257
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#onDisconnect","#onSetRef","#options","#refs","#useRef","#notify","#notifying"],"sources":["../src/internal/callbacks.ts","../src/internal/constants.ts","../src/component.ts","../src/hooks/core.ts","../src/hooks/loading.ts","../src/store.ts","../src/router.ts","../src/hooks/route.ts","../src/hooks/store.ts","../src/html.ts"],"sourcesContent":["export interface Callbacks {\n readonly push: (callback: () => void) => () => void;\n readonly run: (options?: { readonly clear?: boolean }) => void;\n readonly clear: () => void;\n}\n\nexport function createCallbacks(): Callbacks {\n const callbacks = new Set<() => void>();\n\n const self: Callbacks = {\n push: (callback) => {\n callbacks.add(callback);\n return () => callbacks.delete(callback);\n },\n run: ({ clear = false } = {}) => {\n const errors: unknown[] = [];\n const callbacksCopy = [...callbacks];\n\n for (const callback of callbacksCopy) {\n try {\n callback();\n } catch (error: unknown) {\n errors.push(error);\n }\n }\n\n if (clear) callbacks.clear();\n if (errors.length > 0) throw new AggregateError(errors);\n return self;\n },\n clear: () => callbacks.clear(),\n };\n\n return self;\n}\n","export const $$ref = Symbol();\nexport const $$renderContextStack = Symbol();\n","import { type Callbacks, createCallbacks } from './internal/callbacks.ts';\nimport { $$ref, $$renderContextStack } from './internal/constants.ts';\n\ntype SafeProps<TProps> = any extends any\n ? { [P in keyof TProps as P extends keyof HTMLElement ? never : P]: TProps[P] }\n : never;\n\nexport interface ComponentConstructor<TProps extends object> {\n new (): ComponentWithProps<TProps>;\n}\n\nexport interface ComponentOptions<TProps extends object> {\n readonly shadow?: Partial<ShadowRootInit>;\n readonly props?: ComponentPropDescriptors<TProps>;\n}\n\nexport type ComponentPropDescriptors<TProps extends object> = {\n readonly [P in keyof SafeProps<TProps>]: ComponentPropDescriptorFactory<TProps[P]>;\n};\n\nexport type ComponentPropDescriptorFactory<TType> = (\n ref: Ref<TType | undefined>,\n host: HTMLElement,\n) => ComponentPropDescriptor<TType>;\n\nexport interface ComponentPropDescriptor<T> extends Omit<PropertyDescriptor, 'value' | 'get' | 'set'> {\n get(): T;\n set?(value: T): void;\n}\n\nexport type ComponentWithProps<TProps extends object> = HTMLElement & {\n -readonly [P in keyof SafeProps<TProps>]: TProps[P];\n};\n\nexport type ComponentShadowRoot<TProps extends object> = Omit<ShadowRoot, 'host'> & {\n readonly host: ComponentWithProps<TProps>;\n};\n\nexport type ComponentPropRefs<TProps extends object> = {\n readonly [P in keyof SafeProps<TProps>]: Ref<TProps[P] | undefined>;\n};\n\nexport interface Ref<T> extends ReadonlyRef<T> {\n value: T;\n}\n\nexport interface ReadonlyRef<T> {\n /** @hidden */\n [$$ref]: unknown;\n readonly value: T;\n}\n\nexport type RefValues<T> = T extends readonly any[]\n ? { [K in keyof T]: T[K] extends ReadonlyRef<infer V> ? V : never }\n : never;\n\ndeclare global {\n interface Window {\n readonly [$$renderContextStack]: {\n readonly host: HTMLElement;\n readonly onDisconnect: Callbacks;\n readonly onSetRef: Callbacks;\n readonly useRef: <T>(initialValue: T, onChange?: (value: T) => void) => Ref<T>;\n }[];\n }\n}\n\nObject.assign(window, { [$$renderContextStack]: [] });\n\n/** Define a custom `HTMLElement` that is functional and reactive. */\nexport function defineComponent<TProps extends object = {}>(\n render: (shadow: ComponentShadowRoot<TProps>, props: ComponentPropRefs<TProps>) => void,\n options?: ComponentOptions<TProps>,\n): ComponentConstructor<TProps>;\nexport function defineComponent(\n render: (\n shadow: ComponentShadowRoot<Record<string, unknown>>,\n props: ComponentPropRefs<Record<string, unknown>>,\n ) => void,\n options: ComponentOptions<Record<string, unknown>> = {},\n): new () => HTMLElement {\n return class extends HTMLElement {\n readonly #onDisconnect = createCallbacks();\n readonly #onSetRef = createCallbacks();\n readonly #options = options;\n readonly #refs: ComponentPropRefs<Record<string, unknown>>;\n #notifying = false;\n\n constructor() {\n super();\n const refs: Record<string, Ref<unknown>> = (this.#refs = {});\n\n if (this.#options.props) {\n const descriptors = this.#options.props;\n\n for (const [key, getDescriptor] of Object.entries(descriptors)) {\n if (key in this) continue;\n const ref = (refs[key] = this.#useRef<any>(undefined));\n const descriptor = getDescriptor(ref, this);\n Object.defineProperty(this, key, descriptor);\n }\n }\n }\n\n protected connectedCallback(): void {\n const shadow = this.attachShadow({ ...this.#options.shadow, mode: this.#options.shadow?.mode ?? 'open' });\n\n try {\n window[$$renderContextStack].push({\n host: this,\n onDisconnect: this.#onDisconnect,\n onSetRef: this.#onSetRef,\n useRef: this.#useRef,\n });\n\n render(shadow as ComponentShadowRoot<Record<string, unknown>>, this.#refs);\n } finally {\n window[$$renderContextStack].pop();\n }\n\n this.#onSetRef.run();\n }\n\n protected disconnectedCallback(): void {\n this.#onSetRef.clear();\n this.#onDisconnect.run({ clear: true });\n }\n\n readonly #useRef = <T>(initialValue: T, onChange?: (value: T) => void): Ref<T> => {\n const notify = this.#notify;\n let value = initialValue;\n\n return {\n [$$ref]: true,\n get value() {\n return value;\n },\n set value(newValue) {\n if (newValue === value) return;\n value = newValue;\n onChange?.(value);\n notify();\n },\n };\n };\n\n readonly #notify = (): void => {\n if (this.#notifying) return;\n this.#notifying = true;\n\n queueMicrotask(() => {\n this.#notifying = false;\n this.#onSetRef.run();\n });\n };\n };\n}\n","import { type ReadonlyRef, type Ref, type RefValues } from '../component.ts';\nimport { createCallbacks } from '../internal/callbacks.ts';\nimport { $$renderContextStack } from '../internal/constants.ts';\n\n/** Create an observable value. */\nexport function useRef<T>(initialValue: T, onChange?: (value: T) => void): Ref<T> {\n return getHookContext().useRef(initialValue, onChange);\n}\n\n/** React to observable (reference) changes. */\nexport function useEffect<const TDeps extends readonly ReadonlyRef<any>[]>(\n deps: TDeps,\n callback: (...values: RefValues<TDeps>) => (() => void) | void,\n): void {\n const { onSetRef, onDisconnect } = getHookContext();\n const cleanupCallback = createCallbacks();\n const cleanup = (): void => cleanupCallback.run({ clear: true });\n let values: any[] | undefined;\n\n onSetRef.push((): void => {\n const newValues = deps.map((dep) => dep.value);\n if (values?.length === newValues.length && values?.every((value, i) => value === newValues[i])) return;\n values = newValues;\n cleanup();\n const maybeCleanup = callback(...(values as any));\n if (maybeCleanup) cleanupCallback.push(() => maybeCleanup());\n });\n\n onDisconnect.push(cleanup);\n}\n\n/** Observe attribute changes. */\nexport function useAttributes<TName extends string>(...names: TName[]): Readonly<Record<TName, Ref<string | null>>> {\n if (names.length === 0) return {} as any;\n const { host } = getHookContext();\n\n const refs = Object.fromEntries(\n names.map((name) => [\n name,\n useRef(host.getAttribute(name), (value) => {\n if (value == null) host.removeAttribute(name);\n else host.setAttribute(name, value);\n }),\n ]),\n );\n\n const observer = new MutationObserver((mutation) => {\n for (const { attributeName } of mutation) {\n if (attributeName != null && Object.hasOwn(refs, attributeName)) {\n refs[attributeName]!.value = host.getAttribute(attributeName);\n }\n }\n });\n\n useEffect([], () => {\n observer.observe(host, { attributeFilter: names, attributes: true });\n return () => observer.disconnect();\n });\n\n return refs as Readonly<Record<TName, Ref<string | null>>>;\n}\n\nfunction getHookContext(): (typeof window)[typeof $$renderContextStack][0] {\n const context = window[$$renderContextStack].at(-1);\n if (!context) throw new Error('hooks must be called inside a render function');\n return context;\n}\n","import type { ReadonlyRef, Ref, RefValues } from '../component.ts';\nimport { useEffect, useRef } from './core.ts';\n\nexport interface LoadingValue<TValue> {\n readonly loading: boolean;\n readonly value: TValue | undefined;\n readonly error: unknown;\n}\n\nexport interface LoadingOptions {\n readonly debounceMs?: number;\n}\n\n/** Load data asynchronously. */\nexport function useLoading<const TDeps extends readonly ReadonlyRef<any>[], TValue>(\n deps: TDeps,\n callback: (signal: AbortSignal, ...values: RefValues<TDeps>) => Promise<TValue>,\n { debounceMs }: LoadingOptions = {},\n): Ref<LoadingValue<TValue>> {\n const ref = useRef<LoadingValue<TValue>>({ loading: true, value: undefined, error: undefined });\n let skipDebounce = true;\n\n useEffect(deps, () => (...values) => {\n const ac = new AbortController();\n\n Promise.race(\n skipDebounce\n ? [Promise.resolve()]\n : [\n new Promise((resolve) => setTimeout(resolve, debounceMs)),\n new Promise((resolve) => ac.signal.addEventListener('abort', resolve, { once: true })),\n ],\n )\n .then(() => {\n if (ac.signal.aborted) return;\n return callback(ac.signal, ...(values as any));\n })\n .then((value) => {\n if (ac.signal.aborted) return;\n ref.value = { loading: false, value, error: undefined };\n })\n .catch((error: unknown) => {\n if (ac.signal.aborted) return;\n ref.value = { loading: false, value: undefined, error };\n });\n\n skipDebounce = false;\n return () => ac.abort();\n });\n\n // Skip the debounce again if the component unmounts.\n useEffect([], () => () => (skipDebounce = true));\n\n return ref;\n}\n","import { createCallbacks } from './internal/callbacks.ts';\n\nexport interface Store<TState> {\n state: TState;\n subscribe: (callback: (state: TState) => void) => () => void;\n}\n\n/** Create a new store containing an observable state. */\nexport function createStore<TState>(initialState: TState): Store<TState> {\n const callbacks = createCallbacks();\n let state = initialState;\n\n return {\n get state() {\n return state;\n },\n set state(newState) {\n if (newState === state) return;\n state = newState;\n callbacks.run();\n },\n subscribe: (callback) => callbacks.push(() => callback(state)),\n };\n}\n","import { createStore, type Store } from './store.ts';\n\nexport interface RouterState {\n readonly url: string;\n readonly pathname: string;\n readonly hash: string;\n readonly state: unknown;\n}\n\n/**\n * Get a router {@link Store} that is updated when the client side route (aka:\n * `History`) changes. This method returns the same store every time it is\n * called (ie. a singleton).\n */\nexport function getRouter(): Store<RouterState> {\n return (singleton ??= createRouter());\n}\n\nlet singleton: Store<RouterState> | undefined;\n\nfunction createRouter(): Store<RouterState> {\n const store = createStore<RouterState>({\n url: window.location.href,\n pathname: window.location.pathname,\n hash: window.location.hash,\n state: window.history.state,\n });\n\n const onUpdate = (): void => {\n let state = store.state;\n\n if (state.url !== window.location.href) {\n state = {\n ...state,\n url: window.location.href,\n pathname: window.location.pathname,\n hash: window.location.hash,\n };\n }\n\n if (state.state !== window.history.state) {\n state = { ...state, state: window.history.state };\n }\n\n store.state = state;\n };\n\n Object.defineProperties(\n history,\n Object.fromEntries(\n (['pushState', 'replaceState'] as const).map((method: 'pushState' | 'replaceState') => {\n const original = history[method].bind(history) as History['pushState'] & History['replaceState'];\n const descriptor: PropertyDescriptor = {\n value: (...args: Parameters<typeof original>) => {\n original(...args);\n onUpdate();\n },\n enumerable: true,\n configurable: true,\n };\n\n return [method, descriptor];\n }),\n ),\n );\n\n window.addEventListener('popstate', onUpdate);\n\n return store;\n}\n","import type { Ref } from '../component.ts';\nimport { getRouter } from '../router.ts';\nimport { useEffect, useRef } from './core.ts';\n\nexport interface RouteOptions {\n readonly match?: 'prefix' | 'exact' | RegExp;\n readonly source?: 'pathname' | 'hash';\n}\n\nexport type RouteMatchArray = readonly [string, ...string[]] & { readonly groups: Record<string, string> };\n\n/** Observe route (window.history) changes. */\nexport function useRoute(\n path: string | readonly string[],\n { match = 'prefix', source = 'pathname' }: RouteOptions = {},\n): [match: Ref<RouteMatchArray | null>, state: Ref<unknown>] {\n const matchRx = match === 'exact' ? /^$/u : match === 'prefix' ? /^.*$/u : match;\n const paths = Array.isArray(path) ? (path as readonly string[]) : [path as string];\n\n const getMatch = (url: string): RouteMatchArray | null => {\n let value = new URL(url)[source];\n const prefix = paths.find((path) => value.endsWith(path)) ?? null;\n if (prefix == null) return null;\n value = value.slice(prefix.length);\n return value.match(matchRx) as RouteMatchArray | null;\n };\n\n const refUrl = useRef<string>(window.location.href);\n const refMatch = useRef<RouteMatchArray | null>(getMatch(window.location.href));\n const refState = useRef<unknown>(window.history.state);\n\n useEffect([], () => {\n return getRouter().subscribe(({ url, state }) => {\n refUrl.value = url;\n refState.value = state;\n });\n });\n\n useEffect([refUrl], (url) => {\n refMatch.value = getMatch(url);\n });\n\n return [refMatch, refState];\n}\n","import type { ReadonlyRef, Ref } from '../component.ts';\nimport type { Store } from '../store.ts';\nimport { useEffect, useRef } from './core.ts';\n\n/** Observe store (external state) changes. */\nexport function useStore<TState, TValue = TState>(\n store: Store<TState>,\n select: (state: TState) => TValue,\n mutate: (store: Store<TState>, value: TValue) => void,\n): Ref<TValue>;\nexport function useStore<TState, TValue = TState>(\n store: Store<TState>,\n select?: (state: TState) => TValue,\n): ReadonlyRef<TValue>;\nexport function useStore<TState, TValue = TState>(\n store: Store<TState>,\n select: (state: TState) => TValue = (state) => state as unknown as TValue,\n mutate?: (store: Store<TState>, value: TValue) => void,\n): Ref<TValue> {\n const ref = useRef(select(store.state), mutate && ((value) => mutate(store, value)));\n useEffect([], () => store.subscribe((state) => (ref.value = select(state))));\n return ref;\n}\n","type TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap;\ntype TagType<TTag extends string> = TTag extends keyof TagMap ? TagMap[TTag] : HTMLElement;\ntype ChildValue = Node | HtmlDeferred | string | number | bigint | false | null | undefined;\ntype AttrValue = string | number | bigint | boolean | null | undefined;\n\nexport interface HtmlDeferred<TTag extends keyof TagMap | (string & {}) = string> {\n readonly tag: TTag;\n readonly key: string;\n readonly attrs: Readonly<Record<string, AttrValue>>;\n readonly children: readonly ChildValue[] | undefined;\n resolve: () => TagType<TTag>;\n}\n\nconst DATA_KEY = 'data-key';\n\n/**\n * Create a deferred HTML element with a key. Final element creation is\n * deferred until the element is used as a child, when it can be determined if\n * a new element should be created, or an existing element with the same key\n * should be updated.\n */\nexport function html<const TTag extends keyof TagMap | (string & {})>(\n tag: TTag | CustomElementConstructor,\n attrs: { readonly $key: string } & Readonly<Record<string, AttrValue>>,\n children?: readonly ChildValue[],\n): HtmlDeferred<TTag>;\n\n/** Create an HTML element with attributes and children. */\nexport function html<const TTag extends keyof TagMap | (string & {})>(\n tag: TTag | CustomElementConstructor,\n attrs?: Readonly<Record<string, AttrValue>>,\n children?: readonly ChildValue[],\n): TagType<TTag>;\n\n/** Create an HTML element with children. */\nexport function html<const TTag extends keyof TagMap | (string & {})>(\n tag: TTag | CustomElementConstructor,\n children?: readonly ChildValue[],\n): TagType<TTag>;\n\n/** Update element attributes and replace children. */\nexport function html<TElement extends Element | Document | ShadowRoot>(\n element: TElement,\n attrs?: TElement extends { setAttribute: (...args: any[]) => any } ? Readonly<Record<string, AttrValue>> : undefined,\n children?: readonly ChildValue[],\n): TElement;\n\n/** Replace element children. */\nexport function html<TElement extends Element | Document | ShadowRoot>(\n element: TElement,\n children?: readonly ChildValue[],\n): TElement;\n\nexport function html(\n el: string | CustomElementConstructor | Element | Document | ShadowRoot,\n attrsOrChildren: Readonly<Record<string, AttrValue>> | readonly ChildValue[] = {},\n children: readonly ChildValue[] = [],\n): Node | HtmlDeferred {\n if (typeof el === 'function') {\n let name = customElements.getName(el);\n\n if (name == null) {\n name = `ce-${crypto.randomUUID()}`;\n customElements.define(name, el);\n }\n\n el = name;\n }\n\n let attrs: Readonly<Record<string, AttrValue>>;\n [attrs, children] = Array.isArray(attrsOrChildren)\n ? [{}, attrsOrChildren as readonly ChildValue[]]\n : [attrsOrChildren as Readonly<Record<string, AttrValue>>, children];\n\n if (typeof el === 'string') {\n const { [DATA_KEY]: key, ...otherAttrs } = attrs;\n\n return typeof key === 'string'\n ? {\n tag: el,\n key,\n attrs: otherAttrs,\n children,\n resolve: () => html(document.createElement(el), otherAttrs, children),\n }\n : html(document.createElement(el), attrs, children);\n }\n\n if ('setAttribute' in el) {\n const oldAttrs = new Set(el.getAttributeNames());\n\n for (const [name, rawValue] of Object.entries(attrs)) {\n oldAttrs?.delete(name);\n if (rawValue == null || rawValue == false) continue;\n const value = rawValue == true ? '' : String(rawValue);\n if (el.getAttribute(name) === value) continue;\n el.setAttribute(name, value);\n }\n\n for (const name of oldAttrs) {\n el.removeAttribute(name);\n }\n }\n\n let keyElements: Map<string, Element> | undefined;\n\n el.replaceChildren(\n ...children\n .filter((child) => child != null && child !== false)\n .map((child) => {\n if (typeof child !== 'object') return document.createTextNode(String(child));\n if (child instanceof Node) return child;\n\n keyElements ??= new Map(\n [...el.children].flatMap((child) => {\n const key = child.getAttribute(DATA_KEY);\n return key == null ? [] : [[key, child] as const];\n }),\n );\n\n const reused = keyElements.get(child.key);\n\n if (reused && reused.tagName.toLowerCase() === child.tag) {\n keyElements.delete(child.key);\n return html(reused, child.attrs, child.children);\n }\n\n return child.resolve();\n }),\n );\n\n return el;\n}\n\n/** Parse a raw HTML string into a `DocumentFragment`. */\nexport function parseHTML(htmlString: string): DocumentFragment {\n const template = html('template');\n template.innerHTML = htmlString;\n return template.content;\n}\n"],"mappings":"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"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Store } from './store.ts';
|
|
2
|
+
export interface RouterState {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly pathname: string;
|
|
5
|
+
readonly hash: string;
|
|
6
|
+
readonly state: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get a router {@link Store} that is updated when the client side route (aka:
|
|
10
|
+
* `History`) changes. This method returns the same store every time it is
|
|
11
|
+
* called (ie. a singleton).
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRouter(): Store<RouterState>;
|
package/dist/store.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seahax/elemental",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Extremely small reactive web components library.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/seahax/workshop.git",
|
|
9
|
+
"directory": "packages/elemental"
|
|
10
|
+
},
|
|
11
|
+
"license": "Unlicense",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "vite build && tsc -b -f tsconfig.lib.json",
|
|
25
|
+
"clean": "rm -rf dist",
|
|
26
|
+
"diff": "npm run build >/dev/null && npm diff 'dist/**.mjs' 'dist/**.cjs' 'dist/**.js' 'dist/**.d.ts'",
|
|
27
|
+
"start": "vite dev",
|
|
28
|
+
"test": "eslint src && vitest run --coverage"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@seahax/eslint": "^0.2.27",
|
|
32
|
+
"@seahax/vitest": "^0.1.8",
|
|
33
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
34
|
+
"eslint": "^9.39.4",
|
|
35
|
+
"vite": "^8.0.8",
|
|
36
|
+
"vite-bundle-analyzer": "^1.3.7",
|
|
37
|
+
"vitest": "^4.1.4"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|