@e280/sly 0.2.0-3 → 0.2.0-30

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 (304) hide show
  1. package/README.md +624 -97
  2. package/package.json +13 -6
  3. package/s/base/element.ts +76 -0
  4. package/s/base/index.ts +6 -0
  5. package/s/{views → base}/use.ts +25 -16
  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 +9 -5
  11. package/s/demo/views/counter.ts +21 -24
  12. package/s/demo/views/demo.ts +10 -6
  13. package/s/demo/views/fastcount.ts +29 -0
  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 +22 -38
  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 -7
  29. package/s/dom/types.ts +42 -0
  30. package/s/index.html.ts +4 -2
  31. package/s/index.ts +7 -19
  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/drag-and-drops.ts +82 -0
  41. package/s/loot/{drop.ts → drops.ts} +8 -17
  42. package/s/loot/helpers.ts +3 -3
  43. package/s/loot/index.barrel.ts +5 -0
  44. package/s/loot/index.ts +2 -3
  45. package/s/ops/index.ts +5 -0
  46. package/s/ops/op.ts +1 -0
  47. package/s/spa/index.barrel.ts +6 -0
  48. package/s/spa/index.ts +4 -0
  49. package/s/spa/plumbing/braces.ts +76 -0
  50. package/s/spa/plumbing/primitives.ts +85 -0
  51. package/s/spa/plumbing/router-core.ts +49 -0
  52. package/s/spa/plumbing/types.ts +45 -0
  53. package/s/spa/router.ts +49 -0
  54. package/s/spa/spa.test.ts +91 -0
  55. package/s/tests.test.ts +4 -1
  56. package/s/view/index.ts +7 -0
  57. package/s/view/types.ts +39 -0
  58. package/s/view/utils/contextualize.ts +45 -0
  59. package/s/view/utils/make-component.ts +34 -0
  60. package/s/view/utils/make-view.ts +48 -0
  61. package/s/view/utils/parts/capsule.ts +67 -0
  62. package/s/view/utils/parts/chain.ts +40 -0
  63. package/s/view/utils/parts/context.ts +11 -0
  64. package/s/view/utils/parts/directive.ts +29 -0
  65. package/s/view/utils/parts/sly-view.ts +15 -0
  66. package/s/view/view.ts +24 -0
  67. package/x/base/css-reset.js.map +1 -0
  68. package/x/base/element.d.ts +19 -0
  69. package/x/base/element.js +52 -0
  70. package/x/base/element.js.map +1 -0
  71. package/x/base/index.d.ts +4 -0
  72. package/x/base/index.js +5 -0
  73. package/x/base/index.js.map +1 -0
  74. package/x/{views → base}/use.d.ts +8 -4
  75. package/x/{views → base}/use.js +15 -9
  76. package/x/base/use.js.map +1 -0
  77. package/x/base/utils/apply-styles.js.map +1 -0
  78. package/x/base/utils/attr-watcher.d.ts +8 -0
  79. package/x/base/utils/attr-watcher.js +20 -0
  80. package/x/base/utils/attr-watcher.js.map +1 -0
  81. package/x/base/utils/mounts.js.map +1 -0
  82. package/x/base/utils/reactor.d.ts +5 -0
  83. package/x/base/utils/reactor.js +25 -0
  84. package/x/base/utils/reactor.js.map +1 -0
  85. package/x/base/utils/states.d.ts +13 -0
  86. package/x/base/utils/states.js +41 -0
  87. package/x/base/utils/states.js.map +1 -0
  88. package/x/base/utils/use-attrs.d.ts +11 -0
  89. package/x/base/utils/use-attrs.js +18 -0
  90. package/x/base/utils/use-attrs.js.map +1 -0
  91. package/x/demo/demo.bundle.js +8 -4
  92. package/x/demo/demo.bundle.js.map +1 -1
  93. package/x/demo/demo.bundle.min.js +19 -22
  94. package/x/demo/demo.bundle.min.js.map +4 -4
  95. package/x/demo/views/counter.d.ts +374 -1
  96. package/x/demo/views/counter.js +19 -22
  97. package/x/demo/views/counter.js.map +1 -1
  98. package/x/demo/views/demo.d.ts +4 -1
  99. package/x/demo/views/demo.js +10 -5
  100. package/x/demo/views/demo.js.map +1 -1
  101. package/x/demo/views/fastcount.d.ts +12 -0
  102. package/x/demo/views/fastcount.js +21 -0
  103. package/x/demo/views/fastcount.js.map +1 -0
  104. package/x/demo/views/loaders.js +6 -6
  105. package/x/demo/views/loaders.js.map +1 -1
  106. package/x/dom/attrs/attrs.d.ts +23 -0
  107. package/x/dom/attrs/attrs.js +17 -0
  108. package/x/dom/attrs/attrs.js.map +1 -0
  109. package/x/dom/attrs/parts/attr-fns.d.ts +16 -0
  110. package/x/dom/attrs/parts/attr-fns.js +64 -0
  111. package/x/dom/attrs/parts/attr-fns.js.map +1 -0
  112. package/x/dom/attrs/parts/attr-proxies.d.ts +8 -0
  113. package/x/dom/attrs/parts/attr-proxies.js +21 -0
  114. package/x/dom/attrs/parts/attr-proxies.js.map +1 -0
  115. package/x/dom/attrs/parts/attr-spec.d.ts +3 -0
  116. package/x/dom/attrs/parts/attr-spec.js +21 -0
  117. package/x/dom/attrs/parts/attr-spec.js.map +1 -0
  118. package/x/dom/attrs/parts/on-attrs.d.ts +2 -0
  119. package/x/dom/attrs/parts/on-attrs.js +7 -0
  120. package/x/dom/attrs/parts/on-attrs.js.map +1 -0
  121. package/x/dom/dom.d.ts +15 -16
  122. package/x/dom/dom.js +21 -34
  123. package/x/dom/dom.js.map +1 -1
  124. package/x/dom/index.d.ts +2 -0
  125. package/x/dom/index.js +3 -0
  126. package/x/dom/index.js.map +1 -0
  127. package/x/dom/parts/dashify.js.map +1 -0
  128. package/x/dom/parts/dom-scope.d.ts +15 -0
  129. package/x/dom/parts/dom-scope.js +35 -0
  130. package/x/dom/parts/dom-scope.js.map +1 -0
  131. package/x/dom/parts/el.d.ts +2 -0
  132. package/x/dom/parts/el.js +7 -0
  133. package/x/dom/parts/el.js.map +1 -0
  134. package/x/dom/parts/elmer.d.ts +11 -0
  135. package/x/dom/parts/elmer.js +32 -0
  136. package/x/dom/parts/elmer.js.map +1 -0
  137. package/x/dom/parts/eve.d.ts +7 -0
  138. package/x/dom/parts/eve.js +16 -0
  139. package/x/dom/parts/eve.js.map +1 -0
  140. package/x/dom/parts/mk.d.ts +2 -0
  141. package/x/dom/parts/mk.js +7 -0
  142. package/x/dom/parts/mk.js.map +1 -0
  143. package/x/dom/parts/queries.d.ts +4 -0
  144. package/x/dom/parts/queries.js +13 -0
  145. package/x/dom/parts/queries.js.map +1 -0
  146. package/x/dom/{register.d.ts → parts/register.d.ts} +2 -6
  147. package/x/dom/parts/register.js.map +1 -0
  148. package/x/dom/types.d.ts +15 -0
  149. package/x/index.d.ts +7 -16
  150. package/x/index.html +6 -4
  151. package/x/index.html.js +4 -2
  152. package/x/index.html.js.map +1 -1
  153. package/x/index.js +7 -16
  154. package/x/index.js.map +1 -1
  155. package/x/loaders/index.barrel.d.ts +7 -0
  156. package/x/loaders/index.barrel.js +7 -0
  157. package/x/loaders/index.barrel.js.map +1 -0
  158. package/x/loaders/index.d.ts +2 -0
  159. package/x/loaders/index.js +2 -0
  160. package/x/loaders/index.js.map +1 -0
  161. package/x/loaders/make.d.ts +3 -0
  162. package/x/loaders/make.js +6 -0
  163. package/x/loaders/make.js.map +1 -0
  164. package/x/loaders/mock.d.ts +2 -0
  165. package/x/loaders/mock.js +8 -0
  166. package/x/loaders/mock.js.map +1 -0
  167. package/x/{ops/loaders → loaders}/parts/anims.d.ts +1 -1
  168. package/x/loaders/parts/anims.js.map +1 -0
  169. package/x/{ops/loaders → loaders}/parts/ascii-anim.d.ts +2 -2
  170. package/x/{ops/loaders → loaders}/parts/ascii-anim.js +4 -4
  171. package/x/loaders/parts/ascii-anim.js.map +1 -0
  172. package/x/loaders/parts/error-display.d.ts +1 -0
  173. package/x/{ops/loaders → loaders}/parts/error-display.js +2 -2
  174. package/x/loaders/parts/error-display.js.map +1 -0
  175. package/x/loaders/types.d.ts +3 -0
  176. package/x/loaders/types.js.map +1 -0
  177. package/x/loot/drag-and-drops.d.ts +30 -0
  178. package/x/loot/drag-and-drops.js +63 -0
  179. package/x/loot/drag-and-drops.js.map +1 -0
  180. package/x/loot/{drop.d.ts → drops.d.ts} +3 -5
  181. package/x/loot/drops.js +25 -0
  182. package/x/loot/drops.js.map +1 -0
  183. package/x/loot/helpers.d.ts +3 -3
  184. package/x/loot/helpers.js +3 -3
  185. package/x/loot/helpers.js.map +1 -1
  186. package/x/loot/index.barrel.d.ts +3 -0
  187. package/x/loot/index.barrel.js +4 -0
  188. package/x/loot/index.barrel.js.map +1 -0
  189. package/x/loot/index.d.ts +2 -3
  190. package/x/loot/index.js +1 -3
  191. package/x/loot/index.js.map +1 -1
  192. package/x/ops/index.d.ts +3 -0
  193. package/x/ops/index.js +4 -0
  194. package/x/ops/index.js.map +1 -0
  195. package/x/ops/op.js +1 -0
  196. package/x/ops/op.js.map +1 -1
  197. package/x/spa/index.barrel.d.ts +4 -0
  198. package/x/spa/index.barrel.js +3 -0
  199. package/x/spa/index.barrel.js.map +1 -0
  200. package/x/spa/index.d.ts +2 -0
  201. package/x/spa/index.js +2 -0
  202. package/x/spa/index.js.map +1 -0
  203. package/x/spa/plumbing/braces.d.ts +12 -0
  204. package/x/spa/plumbing/braces.js +55 -0
  205. package/x/spa/plumbing/braces.js.map +1 -0
  206. package/x/spa/plumbing/primitives.d.ts +22 -0
  207. package/x/spa/plumbing/primitives.js +65 -0
  208. package/x/spa/plumbing/primitives.js.map +1 -0
  209. package/x/spa/plumbing/router-core.d.ts +13 -0
  210. package/x/spa/plumbing/router-core.js +38 -0
  211. package/x/spa/plumbing/router-core.js.map +1 -0
  212. package/x/spa/plumbing/types.d.ts +35 -0
  213. package/x/spa/plumbing/types.js +2 -0
  214. package/x/spa/plumbing/types.js.map +1 -0
  215. package/x/spa/router.d.ts +16 -0
  216. package/x/spa/router.js +39 -0
  217. package/x/spa/router.js.map +1 -0
  218. package/x/spa/spa.test.d.ts +15 -0
  219. package/x/spa/spa.test.js +78 -0
  220. package/x/spa/spa.test.js.map +1 -0
  221. package/x/tests.test.js +4 -1
  222. package/x/tests.test.js.map +1 -1
  223. package/x/view/index.d.ts +5 -0
  224. package/x/view/index.js +6 -0
  225. package/x/view/index.js.map +1 -0
  226. package/x/view/types.d.ts +21 -0
  227. package/x/view/types.js +2 -0
  228. package/x/{views → view}/types.js.map +1 -1
  229. package/x/view/utils/contextualize.d.ts +13 -0
  230. package/x/view/utils/contextualize.js +18 -0
  231. package/x/view/utils/contextualize.js.map +1 -0
  232. package/x/view/utils/make-component.d.ts +5 -0
  233. package/x/view/utils/make-component.js +17 -0
  234. package/x/view/utils/make-component.js.map +1 -0
  235. package/x/view/utils/make-view.d.ts +2 -0
  236. package/x/view/utils/make-view.js +24 -0
  237. package/x/view/utils/make-view.js.map +1 -0
  238. package/x/view/utils/parts/capsule.d.ts +13 -0
  239. package/x/view/utils/parts/capsule.js +49 -0
  240. package/x/view/utils/parts/capsule.js.map +1 -0
  241. package/x/view/utils/parts/chain.d.ts +13 -0
  242. package/x/view/utils/parts/chain.js +26 -0
  243. package/x/view/utils/parts/chain.js.map +1 -0
  244. package/x/view/utils/parts/context.d.ts +9 -0
  245. package/x/view/utils/parts/context.js +10 -0
  246. package/x/view/utils/parts/context.js.map +1 -0
  247. package/x/view/utils/parts/directive.d.ts +5 -0
  248. package/x/view/utils/parts/directive.js +18 -0
  249. package/x/view/utils/parts/directive.js.map +1 -0
  250. package/x/view/utils/parts/sly-view.d.ts +5 -0
  251. package/x/view/utils/parts/sly-view.js +13 -0
  252. package/x/view/utils/parts/sly-view.js.map +1 -0
  253. package/x/view/view.d.ts +11 -0
  254. package/x/view/view.js +15 -0
  255. package/x/view/view.js.map +1 -0
  256. package/s/loot/drag-drop.ts +0 -76
  257. package/s/ops/loaders/make-loader.ts +0 -18
  258. package/s/views/attributes.ts +0 -89
  259. package/s/views/types.ts +0 -40
  260. package/s/views/utils/apply-attrs.ts +0 -33
  261. package/s/views/view.ts +0 -150
  262. package/x/dom/dashify.js.map +0 -1
  263. package/x/dom/register.js.map +0 -1
  264. package/x/loot/drag-drop.d.ts +0 -29
  265. package/x/loot/drag-drop.js +0 -54
  266. package/x/loot/drag-drop.js.map +0 -1
  267. package/x/loot/drop.js +0 -32
  268. package/x/loot/drop.js.map +0 -1
  269. package/x/ops/loaders/make-loader.d.ts +0 -5
  270. package/x/ops/loaders/make-loader.js +0 -7
  271. package/x/ops/loaders/make-loader.js.map +0 -1
  272. package/x/ops/loaders/parts/anims.js.map +0 -1
  273. package/x/ops/loaders/parts/ascii-anim.js.map +0 -1
  274. package/x/ops/loaders/parts/error-display.d.ts +0 -1
  275. package/x/ops/loaders/parts/error-display.js.map +0 -1
  276. package/x/views/attributes.d.ts +0 -10
  277. package/x/views/attributes.js +0 -46
  278. package/x/views/attributes.js.map +0 -1
  279. package/x/views/css-reset.js.map +0 -1
  280. package/x/views/types.d.ts +0 -31
  281. package/x/views/use.js.map +0 -1
  282. package/x/views/utils/apply-attrs.d.ts +0 -2
  283. package/x/views/utils/apply-attrs.js +0 -21
  284. package/x/views/utils/apply-attrs.js.map +0 -1
  285. package/x/views/utils/apply-styles.js.map +0 -1
  286. package/x/views/utils/mounts.js.map +0 -1
  287. package/x/views/view.d.ts +0 -9
  288. package/x/views/view.js +0 -116
  289. package/x/views/view.js.map +0 -1
  290. /package/s/{views → base}/css-reset.ts +0 -0
  291. /package/s/{views → base}/utils/apply-styles.ts +0 -0
  292. /package/s/{views → base}/utils/mounts.ts +0 -0
  293. /package/s/dom/{dashify.ts → parts/dashify.ts} +0 -0
  294. /package/x/{views → base}/css-reset.d.ts +0 -0
  295. /package/x/{views → base}/css-reset.js +0 -0
  296. /package/x/{views → base}/utils/apply-styles.d.ts +0 -0
  297. /package/x/{views → base}/utils/apply-styles.js +0 -0
  298. /package/x/{views → base}/utils/mounts.d.ts +0 -0
  299. /package/x/{views → base}/utils/mounts.js +0 -0
  300. /package/x/dom/{dashify.d.ts → parts/dashify.d.ts} +0 -0
  301. /package/x/dom/{dashify.js → parts/dashify.js} +0 -0
  302. /package/x/dom/{register.js → parts/register.js} +0 -0
  303. /package/x/{ops/loaders → loaders}/parts/anims.js +0 -0
  304. /package/x/{views → loaders}/types.js +0 -0
package/s/ops/index.ts ADDED
@@ -0,0 +1,5 @@
1
+
2
+ export * from "./op.js"
3
+ export * from "./podium.js"
4
+ export * from "./types.js"
5
+
package/s/ops/op.ts CHANGED
@@ -69,6 +69,7 @@ export class Op<V> {
69
69
  return value
70
70
  }
71
71
  catch (error) {
72
+ console.error(error)
72
73
  if (count === this.#count)
73
74
  await this.setError(error)
74
75
  }
@@ -0,0 +1,6 @@
1
+
2
+ export {route} from "./plumbing/braces.js"
3
+ export type {Navigable} from "./plumbing/primitives.js"
4
+ export {RouterOptions, Hasher, Route, Routes, Navigables, Params} from "./plumbing/types.js"
5
+ export {Router} from "./router.js"
6
+
package/s/spa/index.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export * as spa from "./index.barrel.js"
3
+ export type * from "./index.barrel.js"
4
+
@@ -0,0 +1,76 @@
1
+
2
+ import {Hasher, Route} from "./types.js"
3
+ import type {Content} from "../../view/types.js"
4
+
5
+ type ParamKeys<S extends string> =
6
+ S extends `${string}{${infer P}}${infer R}` ? (string & P) | ParamKeys<R> : never
7
+
8
+ type ParamsOf<S extends string> =
9
+ [ParamKeys<S>] extends [never] ? {} : { [K in ParamKeys<S>]: string }
10
+
11
+ type ParamsTuple<S extends string> =
12
+ keyof ParamsOf<S> extends never ? [] : [ParamsOf<S>]
13
+
14
+ export function hasher<S extends string>(spec: S): Hasher<ParamsTuple<S>> {
15
+ if (!spec.startsWith("#/"))
16
+ throw new Error(`hash route spec must start with "#/"`)
17
+
18
+ const specparts = spec.split("/")
19
+ const braceregex = /\{([^\}\/]+)\}/
20
+
21
+ function parse(hash: string): ParamsTuple<S> | null {
22
+ if (!hash.startsWith("#/"))
23
+ throw new Error(`hash must start with "#/"`)
24
+
25
+ const hashparts = hash.split("/")
26
+ const params: Record<string, string> = {}
27
+
28
+ if (hashparts.length !== specparts.length)
29
+ return null
30
+
31
+ for (const [index, specpart] of specparts.entries()) {
32
+ const hashpart = hashparts.at(index)
33
+ if (hashpart === undefined) return null
34
+ const bracematch = specpart.match(braceregex)
35
+ try {
36
+ if (bracematch) params[bracematch[1]] = decodeURIComponent(hashpart)
37
+ else if (hashpart !== specpart) return null
38
+ }
39
+ catch {
40
+ return null
41
+ }
42
+ }
43
+
44
+ return (Object.keys(params).length === 0)
45
+ ? ([] as ParamsTuple<S>)
46
+ : ([params as ParamsOf<S>] as ParamsTuple<S>)
47
+ }
48
+
49
+ function make(...[braces]: any[]): string {
50
+ const get = (param: string) => {
51
+ const p = param as any
52
+ if (p in braces) return braces[p]
53
+ else throw new Error(`missing param "${p}"`)
54
+ }
55
+ return specparts.map(specpart => {
56
+ const bracematch = specpart.match(braceregex)
57
+ return bracematch
58
+ ? encodeURIComponent(get(bracematch[1]))
59
+ : specpart
60
+ }).join("/")
61
+ }
62
+
63
+ return {parse, make}
64
+ }
65
+
66
+ export function route<S extends string>(
67
+ spec: S,
68
+ fn: (...params: ParamsTuple<S>) => Promise<Content>,
69
+ ): Route<ParamsTuple<S>> {
70
+
71
+ return {
72
+ hasher: hasher(spec),
73
+ fn,
74
+ }
75
+ }
76
+
@@ -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
+