@e280/sly 0.2.0-8 → 0.2.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 +400 -98
- package/package.json +14 -7
- package/s/base/element.ts +76 -0
- package/s/base/index.ts +6 -0
- package/s/{views → base}/use.ts +22 -14
- package/s/base/utils/attr-watcher.ts +22 -0
- package/s/base/utils/reactor.ts +32 -0
- package/s/base/utils/states.ts +49 -0
- package/s/base/utils/use-attrs.ts +36 -0
- package/s/demo/demo.bundle.ts +6 -7
- package/s/demo/views/counter.ts +21 -24
- package/s/demo/views/demo.ts +9 -6
- package/s/demo/views/{incredi.ts → fastcount.ts} +7 -6
- package/s/demo/views/loaders.ts +7 -7
- package/s/dom/attrs/attrs.ts +21 -0
- package/s/dom/attrs/parts/attr-fns.ts +68 -0
- package/s/dom/attrs/parts/attr-proxies.ts +35 -0
- package/s/dom/attrs/parts/attr-spec.ts +29 -0
- package/s/dom/attrs/parts/on-attrs.ts +8 -0
- package/s/dom/dom.ts +23 -60
- package/s/dom/index.ts +4 -0
- package/s/dom/parts/dom-scope.ts +46 -0
- package/s/dom/parts/el.ts +14 -0
- package/s/dom/parts/elmer.ts +38 -0
- package/s/dom/parts/eve.ts +24 -0
- package/s/dom/parts/mk.ts +9 -0
- package/s/dom/parts/queries.ts +26 -0
- package/s/dom/{register.ts → parts/register.ts} +2 -10
- package/s/dom/types.ts +50 -0
- package/s/index.html.ts +4 -3
- package/s/index.ts +7 -20
- package/s/loaders/index.barrel.ts +10 -0
- package/s/loaders/index.ts +4 -0
- package/s/loaders/make.ts +14 -0
- package/s/loaders/mock.ts +11 -0
- package/s/{ops/loaders → loaders}/parts/anims.ts +1 -1
- package/s/{ops/loaders → loaders}/parts/ascii-anim.ts +6 -5
- package/s/{ops/loaders → loaders}/parts/error-display.ts +2 -2
- package/s/loaders/types.ts +6 -0
- package/s/loot/index.barrel.ts +5 -0
- package/s/loot/index.ts +2 -3
- package/s/ops/index.ts +5 -0
- package/s/ops/op.ts +1 -0
- package/s/spa/index.barrel.ts +6 -0
- package/s/spa/index.ts +4 -0
- package/s/spa/plumbing/braces.ts +76 -0
- package/s/spa/plumbing/primitives.ts +85 -0
- package/s/spa/plumbing/router-core.ts +49 -0
- package/s/spa/plumbing/types.ts +45 -0
- package/s/spa/router.ts +49 -0
- package/s/spa/spa.test.ts +91 -0
- package/s/tests.test.ts +4 -1
- package/s/view/index.ts +7 -0
- package/s/view/types.ts +39 -0
- package/s/view/utils/contextualize.ts +45 -0
- package/s/view/utils/make-component.ts +34 -0
- package/s/view/utils/make-view.ts +48 -0
- package/s/view/utils/parts/capsule.ts +67 -0
- package/s/view/utils/parts/chain.ts +40 -0
- package/s/view/utils/parts/context.ts +11 -0
- package/s/view/utils/parts/directive.ts +29 -0
- package/s/view/utils/parts/sly-view.ts +15 -0
- package/s/view/view.ts +24 -0
- package/x/base/css-reset.js.map +1 -0
- package/x/base/element.d.ts +19 -0
- package/x/base/element.js +52 -0
- package/x/base/element.js.map +1 -0
- package/x/base/index.d.ts +4 -0
- package/x/base/index.js +5 -0
- package/x/base/index.js.map +1 -0
- package/x/{views → base}/use.d.ts +6 -2
- package/x/{views → base}/use.js +13 -8
- package/x/base/use.js.map +1 -0
- package/x/base/utils/apply-styles.js.map +1 -0
- package/x/base/utils/attr-watcher.d.ts +8 -0
- package/x/base/utils/attr-watcher.js +20 -0
- package/x/base/utils/attr-watcher.js.map +1 -0
- package/x/base/utils/mounts.js.map +1 -0
- package/x/base/utils/reactor.d.ts +5 -0
- package/x/base/utils/reactor.js +25 -0
- package/x/base/utils/reactor.js.map +1 -0
- package/x/base/utils/states.d.ts +13 -0
- package/x/base/utils/states.js +41 -0
- package/x/base/utils/states.js.map +1 -0
- package/x/base/utils/use-attrs.d.ts +11 -0
- package/x/base/utils/use-attrs.js +18 -0
- package/x/base/utils/use-attrs.js.map +1 -0
- package/x/demo/demo.bundle.js +6 -6
- package/x/demo/demo.bundle.js.map +1 -1
- package/x/demo/demo.bundle.min.js +17 -23
- package/x/demo/demo.bundle.min.js.map +4 -4
- package/x/demo/views/counter.d.ts +374 -1
- package/x/demo/views/counter.js +19 -22
- package/x/demo/views/counter.js.map +1 -1
- package/x/demo/views/demo.d.ts +4 -1
- package/x/demo/views/demo.js +9 -5
- package/x/demo/views/demo.js.map +1 -1
- package/x/demo/views/{incredi.d.ts → fastcount.d.ts} +3 -3
- package/x/demo/views/{incredi.js → fastcount.js} +6 -6
- package/x/demo/views/fastcount.js.map +1 -0
- package/x/demo/views/loaders.js +6 -6
- package/x/demo/views/loaders.js.map +1 -1
- package/x/dom/attrs/attrs.d.ts +23 -0
- package/x/dom/attrs/attrs.js +17 -0
- package/x/dom/attrs/attrs.js.map +1 -0
- package/x/dom/attrs/parts/attr-fns.d.ts +16 -0
- package/x/dom/attrs/parts/attr-fns.js +64 -0
- package/x/dom/attrs/parts/attr-fns.js.map +1 -0
- package/x/dom/attrs/parts/attr-proxies.d.ts +8 -0
- package/x/dom/attrs/parts/attr-proxies.js +21 -0
- package/x/dom/attrs/parts/attr-proxies.js.map +1 -0
- package/x/dom/attrs/parts/attr-spec.d.ts +3 -0
- package/x/dom/attrs/parts/attr-spec.js +21 -0
- package/x/dom/attrs/parts/attr-spec.js.map +1 -0
- package/x/dom/attrs/parts/on-attrs.d.ts +2 -0
- package/x/dom/attrs/parts/on-attrs.js +7 -0
- package/x/dom/attrs/parts/on-attrs.js.map +1 -0
- package/x/dom/dom.d.ts +16 -22
- package/x/dom/dom.js +21 -47
- package/x/dom/dom.js.map +1 -1
- package/x/dom/index.d.ts +2 -0
- package/x/dom/index.js +3 -0
- package/x/dom/index.js.map +1 -0
- package/x/dom/parts/dashify.js.map +1 -0
- package/x/dom/parts/dom-scope.d.ts +15 -0
- package/x/dom/parts/dom-scope.js +35 -0
- package/x/dom/parts/dom-scope.js.map +1 -0
- package/x/dom/parts/el.d.ts +2 -0
- package/x/dom/parts/el.js +7 -0
- package/x/dom/parts/el.js.map +1 -0
- package/x/dom/parts/elmer.d.ts +11 -0
- package/x/dom/parts/elmer.js +32 -0
- package/x/dom/parts/elmer.js.map +1 -0
- package/x/dom/parts/eve.d.ts +7 -0
- package/x/dom/parts/eve.js +16 -0
- package/x/dom/parts/eve.js.map +1 -0
- package/x/dom/parts/mk.d.ts +2 -0
- package/x/dom/parts/mk.js +7 -0
- package/x/dom/parts/mk.js.map +1 -0
- package/x/dom/parts/queries.d.ts +4 -0
- package/x/dom/parts/queries.js +13 -0
- package/x/dom/parts/queries.js.map +1 -0
- package/x/dom/{register.d.ts → parts/register.d.ts} +2 -10
- package/x/dom/parts/register.js.map +1 -0
- package/x/dom/types.d.ts +22 -0
- package/x/{views → dom}/types.js.map +1 -1
- package/x/index.d.ts +7 -17
- package/x/index.html +6 -5
- package/x/index.html.js +4 -3
- package/x/index.html.js.map +1 -1
- package/x/index.js +7 -17
- package/x/index.js.map +1 -1
- package/x/loaders/index.barrel.d.ts +7 -0
- package/x/loaders/index.barrel.js +7 -0
- package/x/loaders/index.barrel.js.map +1 -0
- package/x/loaders/index.d.ts +2 -0
- package/x/loaders/index.js +2 -0
- package/x/loaders/index.js.map +1 -0
- package/x/loaders/make.d.ts +3 -0
- package/x/loaders/make.js +6 -0
- package/x/loaders/make.js.map +1 -0
- package/x/loaders/mock.d.ts +2 -0
- package/x/loaders/mock.js +8 -0
- package/x/loaders/mock.js.map +1 -0
- package/x/{ops/loaders → loaders}/parts/anims.d.ts +1 -1
- package/x/loaders/parts/anims.js.map +1 -0
- package/x/{ops/loaders → loaders}/parts/ascii-anim.d.ts +2 -2
- package/x/{ops/loaders → loaders}/parts/ascii-anim.js +4 -4
- package/x/loaders/parts/ascii-anim.js.map +1 -0
- package/x/loaders/parts/error-display.d.ts +1 -0
- package/x/{ops/loaders → loaders}/parts/error-display.js +2 -2
- package/x/loaders/parts/error-display.js.map +1 -0
- package/x/loaders/types.d.ts +3 -0
- package/x/loaders/types.js +2 -0
- package/x/loaders/types.js.map +1 -0
- package/x/loot/index.barrel.d.ts +3 -0
- package/x/loot/index.barrel.js +4 -0
- package/x/loot/index.barrel.js.map +1 -0
- package/x/loot/index.d.ts +2 -3
- package/x/loot/index.js +1 -3
- package/x/loot/index.js.map +1 -1
- package/x/ops/index.d.ts +3 -0
- package/x/ops/index.js +4 -0
- package/x/ops/index.js.map +1 -0
- package/x/ops/op.js +1 -0
- package/x/ops/op.js.map +1 -1
- package/x/spa/index.barrel.d.ts +4 -0
- package/x/spa/index.barrel.js +3 -0
- package/x/spa/index.barrel.js.map +1 -0
- package/x/spa/index.d.ts +2 -0
- package/x/spa/index.js +2 -0
- package/x/spa/index.js.map +1 -0
- package/x/spa/plumbing/braces.d.ts +12 -0
- package/x/spa/plumbing/braces.js +55 -0
- package/x/spa/plumbing/braces.js.map +1 -0
- package/x/spa/plumbing/primitives.d.ts +22 -0
- package/x/spa/plumbing/primitives.js +65 -0
- package/x/spa/plumbing/primitives.js.map +1 -0
- package/x/spa/plumbing/router-core.d.ts +13 -0
- package/x/spa/plumbing/router-core.js +38 -0
- package/x/spa/plumbing/router-core.js.map +1 -0
- package/x/spa/plumbing/types.d.ts +35 -0
- package/x/spa/plumbing/types.js +2 -0
- package/x/spa/plumbing/types.js.map +1 -0
- package/x/spa/router.d.ts +13 -0
- package/x/spa/router.js +39 -0
- package/x/spa/router.js.map +1 -0
- package/x/spa/spa.test.d.ts +15 -0
- package/x/spa/spa.test.js +78 -0
- package/x/spa/spa.test.js.map +1 -0
- package/x/tests.test.js +4 -1
- package/x/tests.test.js.map +1 -1
- package/x/view/index.d.ts +5 -0
- package/x/view/index.js +6 -0
- package/x/view/index.js.map +1 -0
- package/x/view/types.d.ts +21 -0
- package/x/view/types.js +2 -0
- package/x/view/types.js.map +1 -0
- package/x/view/utils/contextualize.d.ts +13 -0
- package/x/view/utils/contextualize.js +18 -0
- package/x/view/utils/contextualize.js.map +1 -0
- package/x/view/utils/make-component.d.ts +5 -0
- package/x/view/utils/make-component.js +17 -0
- package/x/view/utils/make-component.js.map +1 -0
- package/x/view/utils/make-view.d.ts +2 -0
- package/x/view/utils/make-view.js +24 -0
- package/x/view/utils/make-view.js.map +1 -0
- package/x/view/utils/parts/capsule.d.ts +13 -0
- package/x/view/utils/parts/capsule.js +49 -0
- package/x/view/utils/parts/capsule.js.map +1 -0
- package/x/view/utils/parts/chain.d.ts +13 -0
- package/x/view/utils/parts/chain.js +26 -0
- package/x/view/utils/parts/chain.js.map +1 -0
- package/x/view/utils/parts/context.d.ts +9 -0
- package/x/view/utils/parts/context.js +10 -0
- package/x/view/utils/parts/context.js.map +1 -0
- package/x/view/utils/parts/directive.d.ts +5 -0
- package/x/view/utils/parts/directive.js +18 -0
- package/x/view/utils/parts/directive.js.map +1 -0
- package/x/view/utils/parts/sly-view.d.ts +5 -0
- package/x/view/utils/parts/sly-view.js +13 -0
- package/x/view/utils/parts/sly-view.js.map +1 -0
- package/x/view/view.d.ts +11 -0
- package/x/view/view.js +15 -0
- package/x/view/view.js.map +1 -0
- package/s/dom/attributes.ts +0 -89
- package/s/ops/loaders/make-loader.ts +0 -18
- package/s/views/base-element.ts +0 -84
- package/s/views/types.ts +0 -40
- package/s/views/utils/apply-attrs.ts +0 -33
- package/s/views/view.ts +0 -150
- package/x/demo/views/incredi.js.map +0 -1
- package/x/dom/attributes.d.ts +0 -10
- package/x/dom/attributes.js +0 -46
- package/x/dom/attributes.js.map +0 -1
- package/x/dom/dashify.js.map +0 -1
- package/x/dom/register.js.map +0 -1
- package/x/ops/loaders/make-loader.d.ts +0 -5
- package/x/ops/loaders/make-loader.js +0 -7
- package/x/ops/loaders/make-loader.js.map +0 -1
- package/x/ops/loaders/parts/anims.js.map +0 -1
- package/x/ops/loaders/parts/ascii-anim.js.map +0 -1
- package/x/ops/loaders/parts/error-display.d.ts +0 -1
- package/x/ops/loaders/parts/error-display.js.map +0 -1
- package/x/views/base-element.d.ts +0 -14
- package/x/views/base-element.js +0 -62
- package/x/views/base-element.js.map +0 -1
- package/x/views/css-reset.js.map +0 -1
- package/x/views/types.d.ts +0 -31
- package/x/views/use.js.map +0 -1
- package/x/views/utils/apply-attrs.d.ts +0 -2
- package/x/views/utils/apply-attrs.js +0 -21
- package/x/views/utils/apply-attrs.js.map +0 -1
- package/x/views/utils/apply-styles.js.map +0 -1
- package/x/views/utils/mounts.js.map +0 -1
- package/x/views/view.d.ts +0 -9
- package/x/views/view.js +0 -116
- package/x/views/view.js.map +0 -1
- /package/s/{views → base}/css-reset.ts +0 -0
- /package/s/{views → base}/utils/apply-styles.ts +0 -0
- /package/s/{views → base}/utils/mounts.ts +0 -0
- /package/s/dom/{dashify.ts → parts/dashify.ts} +0 -0
- /package/x/{views → base}/css-reset.d.ts +0 -0
- /package/x/{views → base}/css-reset.js +0 -0
- /package/x/{views → base}/utils/apply-styles.d.ts +0 -0
- /package/x/{views → base}/utils/apply-styles.js +0 -0
- /package/x/{views → base}/utils/mounts.d.ts +0 -0
- /package/x/{views → base}/utils/mounts.js +0 -0
- /package/x/dom/{dashify.d.ts → parts/dashify.d.ts} +0 -0
- /package/x/dom/{dashify.js → parts/dashify.js} +0 -0
- /package/x/dom/{register.js → parts/register.js} +0 -0
- /package/x/{views → dom}/types.js +0 -0
- /package/x/{ops/loaders → loaders}/parts/anims.js +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
import {ev, ob} from "@e280/stz"
|
|
3
|
+
import {Op} from "../../ops/op.js"
|
|
4
|
+
import {ResolvedRoute, Route, Params, Routes} from "./types.js"
|
|
5
|
+
|
|
6
|
+
export function eraseWindowHash() {
|
|
7
|
+
const {pathname, search} = window.location
|
|
8
|
+
history.replaceState(null, "", pathname + search)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeHash(hash: string) {
|
|
12
|
+
const homeEquivalents = [/^$/, /^#$/, /^#\/$/]
|
|
13
|
+
return (homeEquivalents.some(regex => regex.test(hash)))
|
|
14
|
+
? "#/"
|
|
15
|
+
: hash
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class HashNormalizer {
|
|
19
|
+
constructor(public location: Location) {}
|
|
20
|
+
|
|
21
|
+
get hash() {
|
|
22
|
+
const hash = normalizeHash(this.location.hash)
|
|
23
|
+
if (hash === "#/") eraseWindowHash()
|
|
24
|
+
return hash
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set hash(hash: string) {
|
|
28
|
+
this.location.hash = hash
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class Navigable<P extends any[] = any[]> {
|
|
33
|
+
static all<R extends Routes>(
|
|
34
|
+
routes: R,
|
|
35
|
+
getRoute: () => Route | null,
|
|
36
|
+
navigate: (hash: string) => Promise<ResolvedRoute>,
|
|
37
|
+
): {[K in keyof R]: Navigable<Params<R[K]>>} {
|
|
38
|
+
|
|
39
|
+
return ob(routes).map(route => new this(
|
|
40
|
+
route,
|
|
41
|
+
() => (getRoute() === route),
|
|
42
|
+
async(...params: any[]) => navigate(route.hasher.make(...params)),
|
|
43
|
+
)) as any
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
public route: Route<P>,
|
|
48
|
+
private isActive: () => boolean,
|
|
49
|
+
public go: (...params: P) => Promise<ResolvedRoute<P>>,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
get active() {
|
|
53
|
+
return this.isActive()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
hash(...params: P) {
|
|
57
|
+
return this.route.hasher.make(...params)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveRoute<R extends Routes>(
|
|
62
|
+
hash: string,
|
|
63
|
+
routes: R,
|
|
64
|
+
): ResolvedRoute | null {
|
|
65
|
+
|
|
66
|
+
for (const key in routes) {
|
|
67
|
+
const route = routes[key]
|
|
68
|
+
const params = route.hasher.parse(hash)
|
|
69
|
+
if (params) {
|
|
70
|
+
return {
|
|
71
|
+
key,
|
|
72
|
+
route,
|
|
73
|
+
params,
|
|
74
|
+
op: Op.promise(route.fn(...params))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function onHashChange(fn: (event: HashChangeEvent) => void) {
|
|
83
|
+
return ev(window, {hashchange: fn})
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import {signal} from "@e280/strata"
|
|
3
|
+
import type {Content} from "../../view/types.js"
|
|
4
|
+
import {Navigable, normalizeHash, resolveRoute} from "./primitives.js"
|
|
5
|
+
import {Hashbearer, Navigables, ResolvedRoute, Routes} from "./types.js"
|
|
6
|
+
|
|
7
|
+
export class RouterCore<R extends Routes> {
|
|
8
|
+
readonly nav: Navigables<R>
|
|
9
|
+
readonly $resolved = signal<ResolvedRoute | null>(null)
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
public readonly routes: R,
|
|
13
|
+
public readonly location: Hashbearer,
|
|
14
|
+
) {
|
|
15
|
+
|
|
16
|
+
this.nav = Navigable.all(
|
|
17
|
+
routes,
|
|
18
|
+
() => this.route,
|
|
19
|
+
async hash => {
|
|
20
|
+
this.location.hash = hash
|
|
21
|
+
const resolved = await this.refresh()
|
|
22
|
+
if (!resolved) throw new Error(`route failed "${hash}"`)
|
|
23
|
+
return resolved
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get hash() {
|
|
29
|
+
return normalizeHash(this.location.hash)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get content(): Content | null {
|
|
33
|
+
return this.$resolved.get()?.op.value ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get route() {
|
|
37
|
+
return this.$resolved.get()?.route ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async refresh(hash?: string) {
|
|
41
|
+
if (hash !== undefined) this.location.hash = hash
|
|
42
|
+
hash = this.hash
|
|
43
|
+
const resolved = resolveRoute(hash, this.routes)
|
|
44
|
+
await this.$resolved.set(resolved)
|
|
45
|
+
await resolved?.op
|
|
46
|
+
return resolved
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
import type {Op} from "../../ops/op.js"
|
|
3
|
+
import type {Navigable} from "./primitives.js"
|
|
4
|
+
import type {Content} from "../../view/types.js"
|
|
5
|
+
import type {Loader} from "../../loaders/types.js"
|
|
6
|
+
|
|
7
|
+
export type RouterOptions<R extends Routes> = {
|
|
8
|
+
routes: R
|
|
9
|
+
auto?: boolean
|
|
10
|
+
location?: Hashbearer
|
|
11
|
+
loader?: Loader
|
|
12
|
+
notFound?: () => Content
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Hashbearer = {hash: string}
|
|
16
|
+
|
|
17
|
+
export type Hasher<Params extends any[]> = {
|
|
18
|
+
parse: (hash: string) => (Params | null)
|
|
19
|
+
make: (...params: Params) => string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Route<P extends any[] = any[]> = {
|
|
23
|
+
hasher: Hasher<P>
|
|
24
|
+
fn: (...params: P) => Promise<Content>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Routes = {[key: string]: Route}
|
|
28
|
+
|
|
29
|
+
export type Params<X extends (Route | Navigable)> = (
|
|
30
|
+
X extends Route<infer P> ? P :
|
|
31
|
+
X extends Navigable<infer P> ? P :
|
|
32
|
+
never
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export type ResolvedRoute<P extends any[] = any[]> = {
|
|
36
|
+
key: string
|
|
37
|
+
route: Route<P>
|
|
38
|
+
params: P
|
|
39
|
+
op: Op<Content>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Navigables<R extends Routes> = {
|
|
43
|
+
[K in keyof R]: Navigable<Params<R[K]>>
|
|
44
|
+
}
|
|
45
|
+
|
package/s/spa/router.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import {disposer} from "@e280/stz"
|
|
3
|
+
import {Content} from "../view/types.js"
|
|
4
|
+
import {Loader} from "../loaders/types.js"
|
|
5
|
+
import {loaders} from "../loaders/index.js"
|
|
6
|
+
import {RouterCore} from "./plumbing/router-core.js"
|
|
7
|
+
import {RouterOptions, Routes} from "./plumbing/types.js"
|
|
8
|
+
import {HashNormalizer, onHashChange} from "./plumbing/primitives.js"
|
|
9
|
+
|
|
10
|
+
export class Router<R extends Routes> extends RouterCore<R> {
|
|
11
|
+
loader: Loader
|
|
12
|
+
notFound: () => Content
|
|
13
|
+
readonly dispose = disposer()
|
|
14
|
+
#lastHash: string
|
|
15
|
+
|
|
16
|
+
constructor(options: RouterOptions<R>) {
|
|
17
|
+
super(
|
|
18
|
+
options.routes,
|
|
19
|
+
options.location ?? new HashNormalizer(window.location),
|
|
20
|
+
)
|
|
21
|
+
const {auto = true} = options
|
|
22
|
+
this.loader = options.loader ?? loaders.make()
|
|
23
|
+
this.notFound = options.notFound ?? (() => null)
|
|
24
|
+
this.#lastHash = this.hash
|
|
25
|
+
if (auto) {
|
|
26
|
+
this.listen()
|
|
27
|
+
this.refresh()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render() {
|
|
32
|
+
const resolved = this.$resolved.get()
|
|
33
|
+
return resolved === null
|
|
34
|
+
? this.notFound()
|
|
35
|
+
: this.loader(resolved.op, content => content)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
listen() {
|
|
39
|
+
const dispose = onHashChange(() => {
|
|
40
|
+
const hash = this.hash
|
|
41
|
+
const isChanged = hash !== this.#lastHash
|
|
42
|
+
this.#lastHash = hash
|
|
43
|
+
if (isChanged) this.refresh()
|
|
44
|
+
})
|
|
45
|
+
this.dispose.schedule(dispose)
|
|
46
|
+
return dispose
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
import {Science, test, expect} from "@e280/science"
|
|
3
|
+
import {route} from "./plumbing/braces.js"
|
|
4
|
+
import {Routes} from "./plumbing/types.js"
|
|
5
|
+
import {RouterCore} from "./plumbing/router-core.js"
|
|
6
|
+
|
|
7
|
+
async function setup<R extends Routes>(routes: R) {
|
|
8
|
+
const location = {hash: ""}
|
|
9
|
+
const router = new RouterCore(routes, location)
|
|
10
|
+
return {location, router}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default Science.suite({
|
|
14
|
+
inits: Science.suite({
|
|
15
|
+
"#/": test(async() => {
|
|
16
|
+
const {location, router} = await setup({
|
|
17
|
+
home: route("#/", async() => "123"),
|
|
18
|
+
})
|
|
19
|
+
expect(router.content).is(null)
|
|
20
|
+
location.hash = "#/"
|
|
21
|
+
await router.refresh()
|
|
22
|
+
expect(router.content).is("123")
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
"#/hello/world": test(async() => {
|
|
26
|
+
const {location, router} = await setup({
|
|
27
|
+
helloWorld: route("#/hello/world", async() => "123"),
|
|
28
|
+
})
|
|
29
|
+
expect(router.content).is(null)
|
|
30
|
+
location.hash = "#/hello/world"
|
|
31
|
+
await router.refresh()
|
|
32
|
+
expect(router.content).is("123")
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
"#/item/a123": test(async() => {
|
|
36
|
+
const {location, router} = await setup({
|
|
37
|
+
item: route("#/item/{id}", async({id}) => `content ${id}`),
|
|
38
|
+
})
|
|
39
|
+
location.hash = "#/item/a123"
|
|
40
|
+
await router.refresh()
|
|
41
|
+
expect(router.content).is("content a123")
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
"#/item/a123/lol should miss": test(async() => {
|
|
45
|
+
const {location, router} = await setup({
|
|
46
|
+
item: route("#/item/{id}", async({id}) => `content ${id}`),
|
|
47
|
+
})
|
|
48
|
+
location.hash = "#/item/a123/lol"
|
|
49
|
+
await router.refresh()
|
|
50
|
+
expect(router.content).is(null)
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
"#/left/{mid}/right extraction": test(async() => {
|
|
54
|
+
const {location, router} = await setup({
|
|
55
|
+
item: route("#/left/{mid}/right", async({mid}) => `content ${mid}`),
|
|
56
|
+
})
|
|
57
|
+
location.hash = "#/left/middle/right"
|
|
58
|
+
await router.refresh()
|
|
59
|
+
expect(router.content).is("content middle")
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
"#/not-found-lol": test(async() => {
|
|
63
|
+
const {location, router} = await setup({
|
|
64
|
+
helloWorld: route("#/hello/world", async() => "123"),
|
|
65
|
+
})
|
|
66
|
+
location.hash = "#/not-found-lol"
|
|
67
|
+
await router.refresh()
|
|
68
|
+
expect(router.content).is(null)
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
nav: Science.suite({
|
|
73
|
+
"home to item and back": test(async() => {
|
|
74
|
+
const {location, router} = await setup({
|
|
75
|
+
home: route("#/", async() => `home`),
|
|
76
|
+
item: route("#/item/{id}", async({id}) => `item ${id}`),
|
|
77
|
+
})
|
|
78
|
+
location.hash = "#/"
|
|
79
|
+
|
|
80
|
+
await router.refresh()
|
|
81
|
+
expect(router.content).is("home")
|
|
82
|
+
|
|
83
|
+
await router.nav.item.go({id: "x234"})
|
|
84
|
+
expect(router.content).is("item x234")
|
|
85
|
+
|
|
86
|
+
await router.nav.home.go()
|
|
87
|
+
expect(router.content).is("home")
|
|
88
|
+
}),
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
|
package/s/tests.test.ts
CHANGED
package/s/view/index.ts
ADDED
package/s/view/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
import {TemplateResult} from "lit"
|
|
3
|
+
import {Constructor} from "@e280/stz"
|
|
4
|
+
import {DirectiveResult} from "lit/directive.js"
|
|
5
|
+
|
|
6
|
+
import {Use} from "../base/use.js"
|
|
7
|
+
import {BaseElement} from "../base/element.js"
|
|
8
|
+
import {ViewChain} from "./utils/parts/chain.js"
|
|
9
|
+
|
|
10
|
+
export type Content = TemplateResult | DirectiveResult | HTMLElement | string | null | undefined | void | Content[]
|
|
11
|
+
|
|
12
|
+
export type ViewFn<Props extends any[]> = (
|
|
13
|
+
(use: Use) =>
|
|
14
|
+
(...props: Props) =>
|
|
15
|
+
Content
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export type View<Props extends any[]> = {
|
|
19
|
+
(...props: Props): DirectiveResult
|
|
20
|
+
props: (...props: Props) => ViewChain<Props>
|
|
21
|
+
transmute: <PropsB extends any[]>(convert: (...propsB: PropsB) => Props) => View<PropsB>
|
|
22
|
+
component: <B extends Constructor<BaseElement>>(Base?: B) => {
|
|
23
|
+
props: (propFn: (component: InstanceType<B>) => Props) => (
|
|
24
|
+
ComponentClass<B, Props>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ViewProps<V extends View<any>> = (
|
|
30
|
+
V extends View<infer Props>
|
|
31
|
+
? Props
|
|
32
|
+
: never
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export type ComponentClass<B extends Constructor<BaseElement>, Props extends any[]> = {
|
|
36
|
+
view: View<Props>
|
|
37
|
+
new(): InstanceType<B>
|
|
38
|
+
} & B
|
|
39
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
import {DropFirst, ob} from "@e280/stz"
|
|
3
|
+
import {ComponentClass, View, ViewProps} from "../types.js"
|
|
4
|
+
|
|
5
|
+
export function contextualizeView<C, V extends View<any>>(
|
|
6
|
+
context: C,
|
|
7
|
+
view: V,
|
|
8
|
+
): View<DropFirst<ViewProps<V>>> {
|
|
9
|
+
return view.transmute((...p: any[]) => [context, ...p]) as any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function contextualizeViews<C, Vs extends Record<string, View<any>>>(
|
|
13
|
+
context: C,
|
|
14
|
+
views: Vs,
|
|
15
|
+
): {[K in keyof Vs]: View<DropFirst<ViewProps<Vs[K]>>>} {
|
|
16
|
+
|
|
17
|
+
return ob(views)
|
|
18
|
+
.map(view => contextualizeView(context, view))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getViews<
|
|
22
|
+
Cs extends Record<string, ComponentClass<any, any>>
|
|
23
|
+
>(components: Cs) {
|
|
24
|
+
|
|
25
|
+
return ob(components).map(C => C.view as any) as {
|
|
26
|
+
[K in keyof Cs]: Cs[K]["view"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function contextualizeComponents<
|
|
31
|
+
C,
|
|
32
|
+
Cs extends Record<string, ComponentClass<any, any>>
|
|
33
|
+
>(context: C, originalComponents: Cs) {
|
|
34
|
+
|
|
35
|
+
return ob(originalComponents).map((Cons: any) => class extends Cons {
|
|
36
|
+
context = context
|
|
37
|
+
static view = contextualizeView(context, super.view) as any
|
|
38
|
+
}) as any as {
|
|
39
|
+
[K in keyof Cs]: {
|
|
40
|
+
view: View<DropFirst<ViewProps<Cs[K]["view"]>>>
|
|
41
|
+
new(): InstanceType<{context: C} & Cs[K]>
|
|
42
|
+
} & Cs[K]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
import {Constructor} from "@e280/stz"
|
|
3
|
+
import {Use} from "../../base/use.js"
|
|
4
|
+
import {makeView} from "./make-view.js"
|
|
5
|
+
import {BaseElement} from "../../base/element.js"
|
|
6
|
+
import {Reactor} from "../../base/utils/reactor.js"
|
|
7
|
+
import {ComponentClass, ViewFn} from "../types.js"
|
|
8
|
+
|
|
9
|
+
/** make a component from a BaseElement and a view. */
|
|
10
|
+
export function makeComponent<B extends Constructor<BaseElement>, Props extends any[]>(
|
|
11
|
+
settings: ShadowRootInit,
|
|
12
|
+
Base: B,
|
|
13
|
+
propFn: (component: InstanceType<B>) => Props,
|
|
14
|
+
viewFn: ViewFn<Props>,
|
|
15
|
+
) {
|
|
16
|
+
|
|
17
|
+
return class Component extends Base {
|
|
18
|
+
static view = makeView(viewFn, settings)
|
|
19
|
+
#reactor = new Reactor()
|
|
20
|
+
|
|
21
|
+
createShadow() {
|
|
22
|
+
return this.attachShadow(settings)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render(use: Use) {
|
|
26
|
+
// reactor is tracking the propFn
|
|
27
|
+
return viewFn(use)(...this.#reactor.effect(
|
|
28
|
+
() => propFn(this as any),
|
|
29
|
+
() => this.update(),
|
|
30
|
+
))
|
|
31
|
+
}
|
|
32
|
+
} as any as ComponentClass<B, Props>
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
import {Constructor} from "@e280/stz"
|
|
3
|
+
import {DirectiveResult} from "lit/async-directive.js"
|
|
4
|
+
import {View, ViewFn} from "../types.js"
|
|
5
|
+
import {ViewChain} from "./parts/chain.js"
|
|
6
|
+
import {BaseElement} from "../../base/element.js"
|
|
7
|
+
import {ViewContext} from "./parts/context.js"
|
|
8
|
+
import {makeComponent} from "./make-component.js"
|
|
9
|
+
import {makeViewDirective} from "./parts/directive.js"
|
|
10
|
+
|
|
11
|
+
export function makeView<Props extends any[]>(
|
|
12
|
+
viewFn: ViewFn<Props>,
|
|
13
|
+
settings: ShadowRootInit,
|
|
14
|
+
): View<Props> {
|
|
15
|
+
|
|
16
|
+
const renderDirective = makeViewDirective(viewFn, settings)
|
|
17
|
+
|
|
18
|
+
function v(...props: Props): DirectiveResult {
|
|
19
|
+
return renderDirective(new ViewContext(props))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
v.props = (...props: Props) => new ViewChain(
|
|
23
|
+
new ViewContext(props),
|
|
24
|
+
renderDirective,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
v.transmute = <PropsB extends any[]>(convert: (...propsB: PropsB) => Props) => {
|
|
28
|
+
const viewFnB: ViewFn<PropsB> = use => {
|
|
29
|
+
const viewFnA2 = viewFn(use)
|
|
30
|
+
return (...propsB) => viewFnA2(...convert(...propsB))
|
|
31
|
+
}
|
|
32
|
+
return makeView<PropsB>(viewFnB, settings)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
v.component = <B extends Constructor<BaseElement>>(Base: B = BaseElement as any) => ({
|
|
36
|
+
props: (propFn: (component: InstanceType<B>) => Props) => (
|
|
37
|
+
makeComponent<B, Props>(
|
|
38
|
+
settings,
|
|
39
|
+
Base,
|
|
40
|
+
propFn,
|
|
41
|
+
viewFn,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return v
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
import {debounce} from "@e280/stz"
|
|
3
|
+
import {ViewFn} from "../../types.js"
|
|
4
|
+
import {SlyView} from "./sly-view.js"
|
|
5
|
+
import {dom} from "../../../dom/dom.js"
|
|
6
|
+
import {ViewContext} from "./context.js"
|
|
7
|
+
import {Reactor} from "../../../base/utils/reactor.js"
|
|
8
|
+
import {attrSet} from "../../../dom/attrs/parts/attr-fns.js"
|
|
9
|
+
import {AttrWatcher} from "../../../base/utils/attr-watcher.js"
|
|
10
|
+
import {_disconnect, _reconnect, _wrap, Use} from "../../../base/use.js"
|
|
11
|
+
|
|
12
|
+
/** controls the rendering of view context into an element. */
|
|
13
|
+
export class ViewCapsule<Props extends any[]> {
|
|
14
|
+
#element = SlyView.make()
|
|
15
|
+
#reactor = new Reactor()
|
|
16
|
+
|
|
17
|
+
#use: Use
|
|
18
|
+
#shadow: ShadowRoot
|
|
19
|
+
#context!: ViewContext<Props>
|
|
20
|
+
#attrWatcher = new AttrWatcher(this.#element, () => this.#renderDebounced())
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private viewFn: ViewFn<Props>,
|
|
24
|
+
private settings: ShadowRootInit,
|
|
25
|
+
) {
|
|
26
|
+
this.#shadow = this.#element.attachShadow(this.settings)
|
|
27
|
+
this.#use = new Use(
|
|
28
|
+
this.#element,
|
|
29
|
+
this.#shadow,
|
|
30
|
+
this.#renderNow,
|
|
31
|
+
this.#renderDebounced,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
update(context: ViewContext<Props>) {
|
|
36
|
+
this.#context = context
|
|
37
|
+
this.#renderNow()
|
|
38
|
+
return this.#element
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#renderNow = () => {
|
|
42
|
+
this.#use[_wrap](() => {
|
|
43
|
+
const content = this.#reactor.effect(
|
|
44
|
+
() => this.viewFn(this.#use)(...this.#context.props),
|
|
45
|
+
() => this.#renderDebounced(),
|
|
46
|
+
)
|
|
47
|
+
attrSet.entries(this.#element, this.#context.attrs)
|
|
48
|
+
dom.render(this.#shadow, content)
|
|
49
|
+
dom.render(this.#element, this.#context.children)
|
|
50
|
+
this.#attrWatcher.start()
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#renderDebounced = debounce(0, this.#renderNow)
|
|
55
|
+
|
|
56
|
+
disconnected() {
|
|
57
|
+
this.#use[_disconnect]()
|
|
58
|
+
this.#reactor.clear()
|
|
59
|
+
this.#attrWatcher.stop()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
reconnected() {
|
|
63
|
+
this.#use[_reconnect]()
|
|
64
|
+
this.#attrWatcher.start()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import {Content} from "../../types.js"
|
|
3
|
+
import {ViewContext} from "./context.js"
|
|
4
|
+
import {AttrValue} from "../../../dom/types.js"
|
|
5
|
+
import {DirectiveResult} from "lit/async-directive.js"
|
|
6
|
+
|
|
7
|
+
/** provides fluent chaining interface for adding context to rendering a view, think view.props().attr().children().render() */
|
|
8
|
+
export class ViewChain<Props extends any[]> {
|
|
9
|
+
#render: (context: ViewContext<Props>) => DirectiveResult
|
|
10
|
+
#context: ViewContext<Props>
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
context: ViewContext<Props>,
|
|
14
|
+
renderDirective: (context: ViewContext<Props>) => DirectiveResult,
|
|
15
|
+
) {
|
|
16
|
+
this.#context = context
|
|
17
|
+
this.#render = renderDirective
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
attr(key: string, value: AttrValue = true) {
|
|
21
|
+
this.#context.attrs.set(key, value)
|
|
22
|
+
return this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attrs(record: Record<string, AttrValue>) {
|
|
26
|
+
for (const [key, value] of Object.entries(record))
|
|
27
|
+
this.#context.attrs.set(key, value)
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
children(...contents: Content[]) {
|
|
32
|
+
this.#context.children.push(...contents)
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render() {
|
|
37
|
+
return this.#render(this.#context)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
import {Content} from "../../types.js"
|
|
3
|
+
import {AttrValue} from "../../../dom/types.js"
|
|
4
|
+
|
|
5
|
+
/** the information we need to render a view. */
|
|
6
|
+
export class ViewContext<Props extends any[]> {
|
|
7
|
+
attrs = new Map<string, AttrValue>()
|
|
8
|
+
children: Content[] = []
|
|
9
|
+
constructor(public props: Props) {}
|
|
10
|
+
}
|
|
11
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import {AsyncDirective, directive, DirectiveResult} from "lit/async-directive.js"
|
|
3
|
+
import {ViewFn} from "../../types.js"
|
|
4
|
+
import {ViewCapsule} from "./capsule.js"
|
|
5
|
+
import {ViewContext} from "./context.js"
|
|
6
|
+
|
|
7
|
+
/** creates a lit directive fn, which when called, emits a funky lit thing to inject in your html templates. */
|
|
8
|
+
export function makeViewDirective<Props extends any[]>(
|
|
9
|
+
viewFn: ViewFn<Props>,
|
|
10
|
+
settings: ShadowRootInit,
|
|
11
|
+
) {
|
|
12
|
+
|
|
13
|
+
return directive(class ViewDirective extends AsyncDirective {
|
|
14
|
+
#capsule = new ViewCapsule(viewFn, settings)
|
|
15
|
+
|
|
16
|
+
render(context: ViewContext<Props>) {
|
|
17
|
+
return this.#capsule.update(context)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
disconnected() {
|
|
21
|
+
this.#capsule.disconnected()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
reconnected() {
|
|
25
|
+
this.#capsule.reconnected()
|
|
26
|
+
}
|
|
27
|
+
}) as (context: ViewContext<Props>) => DirectiveResult
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import {dom} from "../../../dom/dom.js"
|
|
3
|
+
|
|
4
|
+
/** <sly-view> element that views are rendered into. */
|
|
5
|
+
export class SlyView extends HTMLElement {
|
|
6
|
+
static #registered = false
|
|
7
|
+
static make() {
|
|
8
|
+
if (!this.#registered) {
|
|
9
|
+
dom.register({SlyView}, {soft: true, upgrade: true})
|
|
10
|
+
this.#registered = true
|
|
11
|
+
}
|
|
12
|
+
return document.createElement("sly-view") as SlyView
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|