@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.
Files changed (293) hide show
  1. package/README.md +400 -98
  2. package/package.json +14 -7
  3. package/s/base/element.ts +76 -0
  4. package/s/base/index.ts +6 -0
  5. package/s/{views → base}/use.ts +22 -14
  6. package/s/base/utils/attr-watcher.ts +22 -0
  7. package/s/base/utils/reactor.ts +32 -0
  8. package/s/base/utils/states.ts +49 -0
  9. package/s/base/utils/use-attrs.ts +36 -0
  10. package/s/demo/demo.bundle.ts +6 -7
  11. package/s/demo/views/counter.ts +21 -24
  12. package/s/demo/views/demo.ts +9 -6
  13. package/s/demo/views/{incredi.ts → fastcount.ts} +7 -6
  14. package/s/demo/views/loaders.ts +7 -7
  15. package/s/dom/attrs/attrs.ts +21 -0
  16. package/s/dom/attrs/parts/attr-fns.ts +68 -0
  17. package/s/dom/attrs/parts/attr-proxies.ts +35 -0
  18. package/s/dom/attrs/parts/attr-spec.ts +29 -0
  19. package/s/dom/attrs/parts/on-attrs.ts +8 -0
  20. package/s/dom/dom.ts +23 -60
  21. package/s/dom/index.ts +4 -0
  22. package/s/dom/parts/dom-scope.ts +46 -0
  23. package/s/dom/parts/el.ts +14 -0
  24. package/s/dom/parts/elmer.ts +38 -0
  25. package/s/dom/parts/eve.ts +24 -0
  26. package/s/dom/parts/mk.ts +9 -0
  27. package/s/dom/parts/queries.ts +26 -0
  28. package/s/dom/{register.ts → parts/register.ts} +2 -10
  29. package/s/dom/types.ts +50 -0
  30. package/s/index.html.ts +4 -3
  31. package/s/index.ts +7 -20
  32. package/s/loaders/index.barrel.ts +10 -0
  33. package/s/loaders/index.ts +4 -0
  34. package/s/loaders/make.ts +14 -0
  35. package/s/loaders/mock.ts +11 -0
  36. package/s/{ops/loaders → loaders}/parts/anims.ts +1 -1
  37. package/s/{ops/loaders → loaders}/parts/ascii-anim.ts +6 -5
  38. package/s/{ops/loaders → loaders}/parts/error-display.ts +2 -2
  39. package/s/loaders/types.ts +6 -0
  40. package/s/loot/index.barrel.ts +5 -0
  41. package/s/loot/index.ts +2 -3
  42. package/s/ops/index.ts +5 -0
  43. package/s/ops/op.ts +1 -0
  44. package/s/spa/index.barrel.ts +6 -0
  45. package/s/spa/index.ts +4 -0
  46. package/s/spa/plumbing/braces.ts +76 -0
  47. package/s/spa/plumbing/primitives.ts +85 -0
  48. package/s/spa/plumbing/router-core.ts +49 -0
  49. package/s/spa/plumbing/types.ts +45 -0
  50. package/s/spa/router.ts +49 -0
  51. package/s/spa/spa.test.ts +91 -0
  52. package/s/tests.test.ts +4 -1
  53. package/s/view/index.ts +7 -0
  54. package/s/view/types.ts +39 -0
  55. package/s/view/utils/contextualize.ts +45 -0
  56. package/s/view/utils/make-component.ts +34 -0
  57. package/s/view/utils/make-view.ts +48 -0
  58. package/s/view/utils/parts/capsule.ts +67 -0
  59. package/s/view/utils/parts/chain.ts +40 -0
  60. package/s/view/utils/parts/context.ts +11 -0
  61. package/s/view/utils/parts/directive.ts +29 -0
  62. package/s/view/utils/parts/sly-view.ts +15 -0
  63. package/s/view/view.ts +24 -0
  64. package/x/base/css-reset.js.map +1 -0
  65. package/x/base/element.d.ts +19 -0
  66. package/x/base/element.js +52 -0
  67. package/x/base/element.js.map +1 -0
  68. package/x/base/index.d.ts +4 -0
  69. package/x/base/index.js +5 -0
  70. package/x/base/index.js.map +1 -0
  71. package/x/{views → base}/use.d.ts +6 -2
  72. package/x/{views → base}/use.js +13 -8
  73. package/x/base/use.js.map +1 -0
  74. package/x/base/utils/apply-styles.js.map +1 -0
  75. package/x/base/utils/attr-watcher.d.ts +8 -0
  76. package/x/base/utils/attr-watcher.js +20 -0
  77. package/x/base/utils/attr-watcher.js.map +1 -0
  78. package/x/base/utils/mounts.js.map +1 -0
  79. package/x/base/utils/reactor.d.ts +5 -0
  80. package/x/base/utils/reactor.js +25 -0
  81. package/x/base/utils/reactor.js.map +1 -0
  82. package/x/base/utils/states.d.ts +13 -0
  83. package/x/base/utils/states.js +41 -0
  84. package/x/base/utils/states.js.map +1 -0
  85. package/x/base/utils/use-attrs.d.ts +11 -0
  86. package/x/base/utils/use-attrs.js +18 -0
  87. package/x/base/utils/use-attrs.js.map +1 -0
  88. package/x/demo/demo.bundle.js +6 -6
  89. package/x/demo/demo.bundle.js.map +1 -1
  90. package/x/demo/demo.bundle.min.js +17 -23
  91. package/x/demo/demo.bundle.min.js.map +4 -4
  92. package/x/demo/views/counter.d.ts +374 -1
  93. package/x/demo/views/counter.js +19 -22
  94. package/x/demo/views/counter.js.map +1 -1
  95. package/x/demo/views/demo.d.ts +4 -1
  96. package/x/demo/views/demo.js +9 -5
  97. package/x/demo/views/demo.js.map +1 -1
  98. package/x/demo/views/{incredi.d.ts → fastcount.d.ts} +3 -3
  99. package/x/demo/views/{incredi.js → fastcount.js} +6 -6
  100. package/x/demo/views/fastcount.js.map +1 -0
  101. package/x/demo/views/loaders.js +6 -6
  102. package/x/demo/views/loaders.js.map +1 -1
  103. package/x/dom/attrs/attrs.d.ts +23 -0
  104. package/x/dom/attrs/attrs.js +17 -0
  105. package/x/dom/attrs/attrs.js.map +1 -0
  106. package/x/dom/attrs/parts/attr-fns.d.ts +16 -0
  107. package/x/dom/attrs/parts/attr-fns.js +64 -0
  108. package/x/dom/attrs/parts/attr-fns.js.map +1 -0
  109. package/x/dom/attrs/parts/attr-proxies.d.ts +8 -0
  110. package/x/dom/attrs/parts/attr-proxies.js +21 -0
  111. package/x/dom/attrs/parts/attr-proxies.js.map +1 -0
  112. package/x/dom/attrs/parts/attr-spec.d.ts +3 -0
  113. package/x/dom/attrs/parts/attr-spec.js +21 -0
  114. package/x/dom/attrs/parts/attr-spec.js.map +1 -0
  115. package/x/dom/attrs/parts/on-attrs.d.ts +2 -0
  116. package/x/dom/attrs/parts/on-attrs.js +7 -0
  117. package/x/dom/attrs/parts/on-attrs.js.map +1 -0
  118. package/x/dom/dom.d.ts +16 -22
  119. package/x/dom/dom.js +21 -47
  120. package/x/dom/dom.js.map +1 -1
  121. package/x/dom/index.d.ts +2 -0
  122. package/x/dom/index.js +3 -0
  123. package/x/dom/index.js.map +1 -0
  124. package/x/dom/parts/dashify.js.map +1 -0
  125. package/x/dom/parts/dom-scope.d.ts +15 -0
  126. package/x/dom/parts/dom-scope.js +35 -0
  127. package/x/dom/parts/dom-scope.js.map +1 -0
  128. package/x/dom/parts/el.d.ts +2 -0
  129. package/x/dom/parts/el.js +7 -0
  130. package/x/dom/parts/el.js.map +1 -0
  131. package/x/dom/parts/elmer.d.ts +11 -0
  132. package/x/dom/parts/elmer.js +32 -0
  133. package/x/dom/parts/elmer.js.map +1 -0
  134. package/x/dom/parts/eve.d.ts +7 -0
  135. package/x/dom/parts/eve.js +16 -0
  136. package/x/dom/parts/eve.js.map +1 -0
  137. package/x/dom/parts/mk.d.ts +2 -0
  138. package/x/dom/parts/mk.js +7 -0
  139. package/x/dom/parts/mk.js.map +1 -0
  140. package/x/dom/parts/queries.d.ts +4 -0
  141. package/x/dom/parts/queries.js +13 -0
  142. package/x/dom/parts/queries.js.map +1 -0
  143. package/x/dom/{register.d.ts → parts/register.d.ts} +2 -10
  144. package/x/dom/parts/register.js.map +1 -0
  145. package/x/dom/types.d.ts +22 -0
  146. package/x/{views → dom}/types.js.map +1 -1
  147. package/x/index.d.ts +7 -17
  148. package/x/index.html +6 -5
  149. package/x/index.html.js +4 -3
  150. package/x/index.html.js.map +1 -1
  151. package/x/index.js +7 -17
  152. package/x/index.js.map +1 -1
  153. package/x/loaders/index.barrel.d.ts +7 -0
  154. package/x/loaders/index.barrel.js +7 -0
  155. package/x/loaders/index.barrel.js.map +1 -0
  156. package/x/loaders/index.d.ts +2 -0
  157. package/x/loaders/index.js +2 -0
  158. package/x/loaders/index.js.map +1 -0
  159. package/x/loaders/make.d.ts +3 -0
  160. package/x/loaders/make.js +6 -0
  161. package/x/loaders/make.js.map +1 -0
  162. package/x/loaders/mock.d.ts +2 -0
  163. package/x/loaders/mock.js +8 -0
  164. package/x/loaders/mock.js.map +1 -0
  165. package/x/{ops/loaders → loaders}/parts/anims.d.ts +1 -1
  166. package/x/loaders/parts/anims.js.map +1 -0
  167. package/x/{ops/loaders → loaders}/parts/ascii-anim.d.ts +2 -2
  168. package/x/{ops/loaders → loaders}/parts/ascii-anim.js +4 -4
  169. package/x/loaders/parts/ascii-anim.js.map +1 -0
  170. package/x/loaders/parts/error-display.d.ts +1 -0
  171. package/x/{ops/loaders → loaders}/parts/error-display.js +2 -2
  172. package/x/loaders/parts/error-display.js.map +1 -0
  173. package/x/loaders/types.d.ts +3 -0
  174. package/x/loaders/types.js +2 -0
  175. package/x/loaders/types.js.map +1 -0
  176. package/x/loot/index.barrel.d.ts +3 -0
  177. package/x/loot/index.barrel.js +4 -0
  178. package/x/loot/index.barrel.js.map +1 -0
  179. package/x/loot/index.d.ts +2 -3
  180. package/x/loot/index.js +1 -3
  181. package/x/loot/index.js.map +1 -1
  182. package/x/ops/index.d.ts +3 -0
  183. package/x/ops/index.js +4 -0
  184. package/x/ops/index.js.map +1 -0
  185. package/x/ops/op.js +1 -0
  186. package/x/ops/op.js.map +1 -1
  187. package/x/spa/index.barrel.d.ts +4 -0
  188. package/x/spa/index.barrel.js +3 -0
  189. package/x/spa/index.barrel.js.map +1 -0
  190. package/x/spa/index.d.ts +2 -0
  191. package/x/spa/index.js +2 -0
  192. package/x/spa/index.js.map +1 -0
  193. package/x/spa/plumbing/braces.d.ts +12 -0
  194. package/x/spa/plumbing/braces.js +55 -0
  195. package/x/spa/plumbing/braces.js.map +1 -0
  196. package/x/spa/plumbing/primitives.d.ts +22 -0
  197. package/x/spa/plumbing/primitives.js +65 -0
  198. package/x/spa/plumbing/primitives.js.map +1 -0
  199. package/x/spa/plumbing/router-core.d.ts +13 -0
  200. package/x/spa/plumbing/router-core.js +38 -0
  201. package/x/spa/plumbing/router-core.js.map +1 -0
  202. package/x/spa/plumbing/types.d.ts +35 -0
  203. package/x/spa/plumbing/types.js +2 -0
  204. package/x/spa/plumbing/types.js.map +1 -0
  205. package/x/spa/router.d.ts +13 -0
  206. package/x/spa/router.js +39 -0
  207. package/x/spa/router.js.map +1 -0
  208. package/x/spa/spa.test.d.ts +15 -0
  209. package/x/spa/spa.test.js +78 -0
  210. package/x/spa/spa.test.js.map +1 -0
  211. package/x/tests.test.js +4 -1
  212. package/x/tests.test.js.map +1 -1
  213. package/x/view/index.d.ts +5 -0
  214. package/x/view/index.js +6 -0
  215. package/x/view/index.js.map +1 -0
  216. package/x/view/types.d.ts +21 -0
  217. package/x/view/types.js +2 -0
  218. package/x/view/types.js.map +1 -0
  219. package/x/view/utils/contextualize.d.ts +13 -0
  220. package/x/view/utils/contextualize.js +18 -0
  221. package/x/view/utils/contextualize.js.map +1 -0
  222. package/x/view/utils/make-component.d.ts +5 -0
  223. package/x/view/utils/make-component.js +17 -0
  224. package/x/view/utils/make-component.js.map +1 -0
  225. package/x/view/utils/make-view.d.ts +2 -0
  226. package/x/view/utils/make-view.js +24 -0
  227. package/x/view/utils/make-view.js.map +1 -0
  228. package/x/view/utils/parts/capsule.d.ts +13 -0
  229. package/x/view/utils/parts/capsule.js +49 -0
  230. package/x/view/utils/parts/capsule.js.map +1 -0
  231. package/x/view/utils/parts/chain.d.ts +13 -0
  232. package/x/view/utils/parts/chain.js +26 -0
  233. package/x/view/utils/parts/chain.js.map +1 -0
  234. package/x/view/utils/parts/context.d.ts +9 -0
  235. package/x/view/utils/parts/context.js +10 -0
  236. package/x/view/utils/parts/context.js.map +1 -0
  237. package/x/view/utils/parts/directive.d.ts +5 -0
  238. package/x/view/utils/parts/directive.js +18 -0
  239. package/x/view/utils/parts/directive.js.map +1 -0
  240. package/x/view/utils/parts/sly-view.d.ts +5 -0
  241. package/x/view/utils/parts/sly-view.js +13 -0
  242. package/x/view/utils/parts/sly-view.js.map +1 -0
  243. package/x/view/view.d.ts +11 -0
  244. package/x/view/view.js +15 -0
  245. package/x/view/view.js.map +1 -0
  246. package/s/dom/attributes.ts +0 -89
  247. package/s/ops/loaders/make-loader.ts +0 -18
  248. package/s/views/base-element.ts +0 -84
  249. package/s/views/types.ts +0 -40
  250. package/s/views/utils/apply-attrs.ts +0 -33
  251. package/s/views/view.ts +0 -150
  252. package/x/demo/views/incredi.js.map +0 -1
  253. package/x/dom/attributes.d.ts +0 -10
  254. package/x/dom/attributes.js +0 -46
  255. package/x/dom/attributes.js.map +0 -1
  256. package/x/dom/dashify.js.map +0 -1
  257. package/x/dom/register.js.map +0 -1
  258. package/x/ops/loaders/make-loader.d.ts +0 -5
  259. package/x/ops/loaders/make-loader.js +0 -7
  260. package/x/ops/loaders/make-loader.js.map +0 -1
  261. package/x/ops/loaders/parts/anims.js.map +0 -1
  262. package/x/ops/loaders/parts/ascii-anim.js.map +0 -1
  263. package/x/ops/loaders/parts/error-display.d.ts +0 -1
  264. package/x/ops/loaders/parts/error-display.js.map +0 -1
  265. package/x/views/base-element.d.ts +0 -14
  266. package/x/views/base-element.js +0 -62
  267. package/x/views/base-element.js.map +0 -1
  268. package/x/views/css-reset.js.map +0 -1
  269. package/x/views/types.d.ts +0 -31
  270. package/x/views/use.js.map +0 -1
  271. package/x/views/utils/apply-attrs.d.ts +0 -2
  272. package/x/views/utils/apply-attrs.js +0 -21
  273. package/x/views/utils/apply-attrs.js.map +0 -1
  274. package/x/views/utils/apply-styles.js.map +0 -1
  275. package/x/views/utils/mounts.js.map +0 -1
  276. package/x/views/view.d.ts +0 -9
  277. package/x/views/view.js +0 -116
  278. package/x/views/view.js.map +0 -1
  279. /package/s/{views → base}/css-reset.ts +0 -0
  280. /package/s/{views → base}/utils/apply-styles.ts +0 -0
  281. /package/s/{views → base}/utils/mounts.ts +0 -0
  282. /package/s/dom/{dashify.ts → parts/dashify.ts} +0 -0
  283. /package/x/{views → base}/css-reset.d.ts +0 -0
  284. /package/x/{views → base}/css-reset.js +0 -0
  285. /package/x/{views → base}/utils/apply-styles.d.ts +0 -0
  286. /package/x/{views → base}/utils/apply-styles.js +0 -0
  287. /package/x/{views → base}/utils/mounts.d.ts +0 -0
  288. /package/x/{views → base}/utils/mounts.js +0 -0
  289. /package/x/dom/{dashify.d.ts → parts/dashify.d.ts} +0 -0
  290. /package/x/dom/{dashify.js → parts/dashify.js} +0 -0
  291. /package/x/dom/{register.js → parts/register.js} +0 -0
  292. /package/x/{views → dom}/types.js +0 -0
  293. /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
+
@@ -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
@@ -1,5 +1,8 @@
1
1
 
2
2
  import {Science} from "@e280/science"
3
+ import spa from "./spa/spa.test.js"
3
4
 
4
- await Science.run({})
5
+ await Science.run({
6
+ spa,
7
+ })
5
8
 
@@ -0,0 +1,7 @@
1
+
2
+ export * from "./utils/parts/chain.js"
3
+ export * from "./utils/parts/sly-view.js"
4
+ export * from "./utils/contextualize.js"
5
+ export * from "./types.js"
6
+ export * from "./view.js"
7
+
@@ -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
+