@typed/template 0.1.4 → 0.3.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 (234) hide show
  1. package/Directive/package.json +6 -0
  2. package/ElementRef/package.json +6 -0
  3. package/ElementSource/package.json +6 -0
  4. package/Entry/package.json +6 -0
  5. package/EventHandler/package.json +6 -0
  6. package/Html/package.json +6 -0
  7. package/HtmlChunk/package.json +6 -0
  8. package/Hydrate/package.json +6 -0
  9. package/Many/package.json +6 -0
  10. package/Meta/package.json +6 -0
  11. package/Parser/package.json +6 -0
  12. package/Part/package.json +6 -0
  13. package/Placeholder/package.json +6 -0
  14. package/Platform/package.json +6 -0
  15. package/Render/package.json +6 -0
  16. package/RenderContext/package.json +6 -0
  17. package/RenderEvent/package.json +6 -0
  18. package/RenderTemplate/package.json +6 -0
  19. package/Renderable/package.json +6 -0
  20. package/Template/package.json +6 -0
  21. package/TemplateInstance/package.json +6 -0
  22. package/Test/package.json +6 -0
  23. package/Vitest/package.json +6 -0
  24. package/dist/cjs/Directive.js +1 -1
  25. package/dist/cjs/Directive.js.map +1 -1
  26. package/dist/cjs/ElementRef.js +23 -13
  27. package/dist/cjs/ElementRef.js.map +1 -1
  28. package/dist/cjs/ElementSource.js +16 -18
  29. package/dist/cjs/ElementSource.js.map +1 -1
  30. package/dist/cjs/EventHandler.js +1 -1
  31. package/dist/cjs/EventHandler.js.map +1 -1
  32. package/dist/cjs/Html.js +31 -32
  33. package/dist/cjs/Html.js.map +1 -1
  34. package/dist/cjs/HtmlChunk.js +4 -1
  35. package/dist/cjs/HtmlChunk.js.map +1 -1
  36. package/dist/cjs/Hydrate.js +1 -1
  37. package/dist/cjs/Hydrate.js.map +1 -1
  38. package/dist/cjs/Many.js +14 -13
  39. package/dist/cjs/Many.js.map +1 -1
  40. package/dist/cjs/Parser.js +11 -323
  41. package/dist/cjs/Parser.js.map +1 -1
  42. package/dist/cjs/Placeholder.js +3 -3
  43. package/dist/cjs/Placeholder.js.map +1 -1
  44. package/dist/cjs/Platform.js +4 -4
  45. package/dist/cjs/Platform.js.map +1 -1
  46. package/dist/cjs/Render.js +1 -1
  47. package/dist/cjs/Render.js.map +1 -1
  48. package/dist/cjs/RenderContext.js +48 -27
  49. package/dist/cjs/RenderContext.js.map +1 -1
  50. package/dist/cjs/RenderTemplate.js +2 -17
  51. package/dist/cjs/RenderTemplate.js.map +1 -1
  52. package/dist/cjs/Template.js +27 -1
  53. package/dist/cjs/Template.js.map +1 -1
  54. package/dist/cjs/TemplateInstance.js +2 -2
  55. package/dist/cjs/TemplateInstance.js.map +1 -1
  56. package/dist/cjs/Test.js +20 -7
  57. package/dist/cjs/Test.js.map +1 -1
  58. package/dist/cjs/index.js +0 -12
  59. package/dist/cjs/index.js.map +1 -1
  60. package/dist/cjs/internal/EventSource.js +95 -0
  61. package/dist/cjs/internal/EventSource.js.map +1 -0
  62. package/dist/cjs/internal/browser.js +11 -11
  63. package/dist/cjs/internal/browser.js.map +1 -1
  64. package/dist/cjs/internal/hydrate.js +49 -50
  65. package/dist/cjs/internal/hydrate.js.map +1 -1
  66. package/dist/cjs/internal/indexRefCounter.js +49 -2
  67. package/dist/cjs/internal/indexRefCounter.js.map +1 -1
  68. package/dist/cjs/internal/parser.js +60 -17
  69. package/dist/cjs/internal/parser.js.map +1 -1
  70. package/dist/cjs/internal/parts.js +128 -28
  71. package/dist/cjs/internal/parts.js.map +1 -1
  72. package/dist/cjs/internal/render.js +486 -53
  73. package/dist/cjs/internal/render.js.map +1 -1
  74. package/dist/cjs/internal/server.js +5 -2
  75. package/dist/cjs/internal/server.js.map +1 -1
  76. package/dist/dts/Directive.d.ts.map +1 -1
  77. package/dist/dts/ElementRef.d.ts +4 -2
  78. package/dist/dts/ElementRef.d.ts.map +1 -1
  79. package/dist/dts/ElementSource.d.ts +10 -5
  80. package/dist/dts/ElementSource.d.ts.map +1 -1
  81. package/dist/dts/EventHandler.d.ts.map +1 -1
  82. package/dist/dts/Html.d.ts +1 -1
  83. package/dist/dts/Html.d.ts.map +1 -1
  84. package/dist/dts/HtmlChunk.d.ts.map +1 -1
  85. package/dist/dts/Many.d.ts +13 -11
  86. package/dist/dts/Many.d.ts.map +1 -1
  87. package/dist/dts/Parser.d.ts +3 -6
  88. package/dist/dts/Parser.d.ts.map +1 -1
  89. package/dist/dts/Part.d.ts +13 -3
  90. package/dist/dts/Part.d.ts.map +1 -1
  91. package/dist/dts/Placeholder.d.ts +2 -2
  92. package/dist/dts/Placeholder.d.ts.map +1 -1
  93. package/dist/dts/Render.d.ts +2 -1
  94. package/dist/dts/Render.d.ts.map +1 -1
  95. package/dist/dts/RenderContext.d.ts +5 -4
  96. package/dist/dts/RenderContext.d.ts.map +1 -1
  97. package/dist/dts/RenderTemplate.d.ts +2 -16
  98. package/dist/dts/RenderTemplate.d.ts.map +1 -1
  99. package/dist/dts/Renderable.d.ts +2 -2
  100. package/dist/dts/Renderable.d.ts.map +1 -1
  101. package/dist/dts/Template.d.ts +21 -3
  102. package/dist/dts/Template.d.ts.map +1 -1
  103. package/dist/dts/TemplateInstance.d.ts +3 -2
  104. package/dist/dts/TemplateInstance.d.ts.map +1 -1
  105. package/dist/dts/Test.d.ts +4 -6
  106. package/dist/dts/Test.d.ts.map +1 -1
  107. package/dist/dts/index.d.ts +0 -4
  108. package/dist/dts/index.d.ts.map +1 -1
  109. package/dist/dts/internal/EventSource.d.ts +12 -0
  110. package/dist/dts/internal/EventSource.d.ts.map +1 -0
  111. package/dist/dts/internal/browser.d.ts.map +1 -1
  112. package/dist/dts/internal/hydrate.d.ts +5 -5
  113. package/dist/dts/internal/hydrate.d.ts.map +1 -1
  114. package/dist/dts/internal/indexRefCounter.d.ts +5 -0
  115. package/dist/dts/internal/indexRefCounter.d.ts.map +1 -1
  116. package/dist/dts/internal/module-augmentation.d.ts +0 -4
  117. package/dist/dts/internal/module-augmentation.d.ts.map +1 -1
  118. package/dist/dts/internal/parser.d.ts +8 -0
  119. package/dist/dts/internal/parser.d.ts.map +1 -1
  120. package/dist/dts/internal/parts.d.ts +66 -56
  121. package/dist/dts/internal/parts.d.ts.map +1 -1
  122. package/dist/dts/internal/render.d.ts +7 -7
  123. package/dist/dts/internal/render.d.ts.map +1 -1
  124. package/dist/dts/internal/server.d.ts.map +1 -1
  125. package/dist/esm/Directive.js +1 -1
  126. package/dist/esm/Directive.js.map +1 -1
  127. package/dist/esm/ElementRef.js +12 -7
  128. package/dist/esm/ElementRef.js.map +1 -1
  129. package/dist/esm/ElementSource.js +16 -13
  130. package/dist/esm/ElementSource.js.map +1 -1
  131. package/dist/esm/EventHandler.js +1 -1
  132. package/dist/esm/EventHandler.js.map +1 -1
  133. package/dist/esm/Html.js +29 -24
  134. package/dist/esm/Html.js.map +1 -1
  135. package/dist/esm/HtmlChunk.js +6 -1
  136. package/dist/esm/HtmlChunk.js.map +1 -1
  137. package/dist/esm/Hydrate.js +1 -1
  138. package/dist/esm/Hydrate.js.map +1 -1
  139. package/dist/esm/Many.js +14 -10
  140. package/dist/esm/Many.js.map +1 -1
  141. package/dist/esm/Parser.js +6 -335
  142. package/dist/esm/Parser.js.map +1 -1
  143. package/dist/esm/Placeholder.js +2 -2
  144. package/dist/esm/Placeholder.js.map +1 -1
  145. package/dist/esm/Platform.js +2 -2
  146. package/dist/esm/Platform.js.map +1 -1
  147. package/dist/esm/Render.js +1 -1
  148. package/dist/esm/Render.js.map +1 -1
  149. package/dist/esm/RenderContext.js +38 -17
  150. package/dist/esm/RenderContext.js.map +1 -1
  151. package/dist/esm/RenderTemplate.js +2 -12
  152. package/dist/esm/RenderTemplate.js.map +1 -1
  153. package/dist/esm/Template.js +24 -0
  154. package/dist/esm/Template.js.map +1 -1
  155. package/dist/esm/TemplateInstance.js +2 -2
  156. package/dist/esm/TemplateInstance.js.map +1 -1
  157. package/dist/esm/Test.js +20 -7
  158. package/dist/esm/Test.js.map +1 -1
  159. package/dist/esm/index.js +0 -4
  160. package/dist/esm/index.js.map +1 -1
  161. package/dist/esm/internal/EventSource.js +91 -0
  162. package/dist/esm/internal/EventSource.js.map +1 -0
  163. package/dist/esm/internal/browser.js +12 -12
  164. package/dist/esm/internal/browser.js.map +1 -1
  165. package/dist/esm/internal/hydrate.js +45 -46
  166. package/dist/esm/internal/hydrate.js.map +1 -1
  167. package/dist/esm/internal/indexRefCounter.js +50 -2
  168. package/dist/esm/internal/indexRefCounter.js.map +1 -1
  169. package/dist/esm/internal/parser.js +84 -17
  170. package/dist/esm/internal/parser.js.map +1 -1
  171. package/dist/esm/internal/parts.js +110 -27
  172. package/dist/esm/internal/parts.js.map +1 -1
  173. package/dist/esm/internal/render.js +476 -58
  174. package/dist/esm/internal/render.js.map +1 -1
  175. package/dist/esm/internal/server.js +5 -4
  176. package/dist/esm/internal/server.js.map +1 -1
  177. package/package.json +10 -26
  178. package/src/Directive.ts +1 -1
  179. package/src/ElementRef.ts +18 -14
  180. package/src/ElementSource.ts +62 -47
  181. package/src/EventHandler.ts +1 -1
  182. package/src/Html.ts +58 -57
  183. package/src/HtmlChunk.ts +15 -1
  184. package/src/Hydrate.ts +1 -1
  185. package/src/Many.ts +53 -43
  186. package/src/Parser.ts +10 -453
  187. package/src/Part.ts +15 -3
  188. package/src/Placeholder.ts +4 -4
  189. package/src/Platform.ts +2 -2
  190. package/src/Render.ts +7 -2
  191. package/src/RenderContext.ts +49 -21
  192. package/src/RenderTemplate.ts +9 -54
  193. package/src/Renderable.ts +2 -1
  194. package/src/Template.ts +26 -0
  195. package/src/TemplateInstance.ts +9 -9
  196. package/src/Test.ts +40 -21
  197. package/src/index.ts +0 -4
  198. package/src/internal/EventSource.ts +153 -0
  199. package/src/internal/browser.ts +26 -25
  200. package/src/internal/hydrate.ts +68 -61
  201. package/src/internal/indexRefCounter.ts +63 -2
  202. package/src/internal/module-augmentation.ts +0 -4
  203. package/src/internal/parser.ts +92 -19
  204. package/src/internal/parts.ts +158 -73
  205. package/src/internal/render.ts +701 -89
  206. package/src/internal/server.ts +5 -3
  207. package/dist/cjs/Token.js +0 -270
  208. package/dist/cjs/Token.js.map +0 -1
  209. package/dist/cjs/Tokenizer.js +0 -18
  210. package/dist/cjs/Tokenizer.js.map +0 -1
  211. package/dist/cjs/internal/readAttribute.js +0 -34
  212. package/dist/cjs/internal/readAttribute.js.map +0 -1
  213. package/dist/cjs/internal/tokenizer.js +0 -264
  214. package/dist/cjs/internal/tokenizer.js.map +0 -1
  215. package/dist/dts/Token.d.ts +0 -202
  216. package/dist/dts/Token.d.ts.map +0 -1
  217. package/dist/dts/Tokenizer.d.ts +0 -6
  218. package/dist/dts/Tokenizer.d.ts.map +0 -1
  219. package/dist/dts/internal/readAttribute.d.ts +0 -9
  220. package/dist/dts/internal/readAttribute.d.ts.map +0 -1
  221. package/dist/dts/internal/tokenizer.d.ts +0 -3
  222. package/dist/dts/internal/tokenizer.d.ts.map +0 -1
  223. package/dist/esm/Token.js +0 -264
  224. package/dist/esm/Token.js.map +0 -1
  225. package/dist/esm/Tokenizer.js +0 -9
  226. package/dist/esm/Tokenizer.js.map +0 -1
  227. package/dist/esm/internal/readAttribute.js +0 -24
  228. package/dist/esm/internal/readAttribute.js.map +0 -1
  229. package/dist/esm/internal/tokenizer.js +0 -296
  230. package/dist/esm/internal/tokenizer.js.map +0 -1
  231. package/src/Token.ts +0 -269
  232. package/src/Tokenizer.ts +0 -10
  233. package/src/internal/readAttribute.ts +0 -28
  234. package/src/internal/tokenizer.ts +0 -338
@@ -11,10 +11,13 @@ import type { Environment } from "@typed/environment"
11
11
  import { CurrentEnvironment } from "@typed/environment"
12
12
  import * as Idle from "@typed/fx/Idle"
13
13
  import type { Rendered } from "@typed/wire"
14
- import { Effect, Layer, Option } from "effect"
14
+ import * as Effect from "effect/Effect"
15
+ import * as Layer from "effect/Layer"
16
+ import * as Option from "effect/Option"
15
17
  import * as Scope from "effect/Scope"
16
18
  import type { Entry } from "./Entry.js"
17
- import type { Part, SparsePart } from "./Part.js"
19
+
20
+ // TODO: We should probably have a more explicit environment type between DOM/HTML rendering
18
21
 
19
22
  /**
20
23
  * The context in which templates are rendered within
@@ -54,7 +57,7 @@ export const RenderContext: Context.Tagged<RenderContext, RenderContext> = Conte
54
57
  * @since 1.0.0
55
58
  */
56
59
  export interface RenderQueue {
57
- readonly add: (part: Part | SparsePart, task: () => void) => Effect.Effect<Scope.Scope, never, void>
60
+ readonly add: (part: unknown, task: () => void) => Effect.Effect<Scope.Scope, never, void>
58
61
  }
59
62
 
60
63
  /**
@@ -114,19 +117,19 @@ const buildWithCurrentEnvironment = (environment: Environment, skipRenderSchedul
114
117
  /**
115
118
  * @since 1.0.0
116
119
  */
117
- export const browser: (
120
+ export const dom: (
118
121
  window: Window & GlobalThis,
119
122
  options?: DomServicesElementParams & { readonly skipRenderScheduling?: boolean }
120
123
  ) => Layer.Layer<never, never, RenderContext | CurrentEnvironment | DomServices> = (window, options) =>
121
124
  Layer.provideMerge(
122
- Layer.mergeAll(Window.layer(window), GlobalThis.layer(window)),
123
125
  Layer.mergeAll(
124
126
  buildWithCurrentEnvironment(
125
- "browser",
127
+ "dom",
126
128
  options?.skipRenderScheduling
127
129
  ),
128
130
  domServices(options)
129
- )
131
+ ),
132
+ Layer.mergeAll(Window.layer(window), GlobalThis.layer(window))
130
133
  )
131
134
 
132
135
  /**
@@ -146,8 +149,9 @@ export {
146
149
  }
147
150
 
148
151
  class RenderQueueImpl implements RenderQueue {
149
- queue = new Map<Part | SparsePart, () => void>()
152
+ queue = new Map<unknown, () => void>()
150
153
  scheduled = false
154
+ run: Effect.Effect<Scope.Scope, never, void>
151
155
 
152
156
  constructor(
153
157
  readonly scope: Scope.Scope,
@@ -155,9 +159,11 @@ class RenderQueueImpl implements RenderQueue {
155
159
  readonly skipRenderScheduling: boolean = false
156
160
  ) {
157
161
  this.add.bind(this)
162
+
163
+ this.run = typeof requestAnimationFrame === "undefined" ? this.runIdle : this.runAnimationFrame
158
164
  }
159
165
 
160
- add(part: Part | SparsePart, task: () => void) {
166
+ add(part: unknown, task: () => void) {
161
167
  if (this.skipRenderScheduling) return Effect.sync(task)
162
168
 
163
169
  return Effect.suspend(() => {
@@ -190,26 +196,20 @@ class RenderQueueImpl implements RenderQueue {
190
196
  )
191
197
  })
192
198
 
193
- run: Effect.Effect<Scope.Scope, never, void> = Effect.suspend(() =>
194
- Effect.flatMap(
199
+ runIdle: Effect.Effect<Scope.Scope, never, void> = Effect.suspend(() => {
200
+ return Effect.flatMap(
195
201
  Idle.whenIdle(this.options),
196
202
  (deadline) =>
197
203
  Effect.suspend(() => {
198
204
  const iterator = this.queue.entries()
199
205
 
200
- while (Idle.shouldContinue(deadline)) {
201
- const result = iterator.next()
202
-
203
- if (result.done) break
204
- else {
205
- const [part, task] = result.value
206
- this.queue.delete(part)
207
- task()
208
- }
206
+ while (Idle.shouldContinue(deadline) && this.runTask(iterator)) {
207
+ // Continue
209
208
  }
210
209
 
210
+ // If we have more work to do, schedule another run
211
211
  if (this.queue.size > 0) {
212
- return this.run
212
+ return this.runIdle
213
213
  }
214
214
 
215
215
  this.scheduled = false
@@ -217,5 +217,33 @@ class RenderQueueImpl implements RenderQueue {
217
217
  return Effect.unit
218
218
  })
219
219
  )
220
+ })
221
+
222
+ runAnimationFrame: Effect.Effect<Scope.Scope, never, void> = Effect.zipRight(
223
+ Effect.asyncOption<never, never, void>((cb) => {
224
+ const id = requestAnimationFrame(() => cb(Effect.unit))
225
+ return Option.some(Effect.sync(() => cancelAnimationFrame(id)))
226
+ }),
227
+ Effect.sync(() => {
228
+ const iterator = this.queue.entries()
229
+
230
+ while (this.runTask(iterator)) {
231
+ // Continue
232
+ }
233
+
234
+ this.scheduled = false
235
+ })
220
236
  )
237
+
238
+ private runTask = (iterator: Iterator<[unknown, () => void]>) => {
239
+ const result = iterator.next()
240
+
241
+ if (result.done) return false
242
+ else {
243
+ const [part, task] = result.value
244
+ this.queue.delete(part)
245
+ task()
246
+ return true
247
+ }
248
+ }
221
249
  }
@@ -4,30 +4,22 @@
4
4
 
5
5
  import * as Context from "@typed/context"
6
6
  import * as Fx from "@typed/fx/Fx"
7
- import type { Rendered } from "@typed/wire"
8
- import type * as Effect from "effect/Effect"
9
7
  import type { Scope } from "effect/Scope"
10
- import type { ElementRef } from "./ElementRef.js"
11
8
  import type { Placeholder } from "./Placeholder.js"
12
9
  import type { Renderable } from "./Renderable.js"
13
10
  import type { RenderEvent } from "./RenderEvent.js"
14
- import type { TemplateInstance } from "./TemplateInstance.js"
15
11
 
16
12
  /**
17
13
  * @since 1.0.0
18
14
  */
19
15
  export interface RenderTemplate {
20
- <Values extends ReadonlyArray<Renderable<any, any>>, T extends Rendered = Rendered>(
16
+ <Values extends ReadonlyArray<Renderable<any, any>>>(
21
17
  templateStrings: TemplateStringsArray,
22
- values: Values,
23
- ref?: ElementRef<T>
24
- ): Effect.Effect<
25
- Scope | Placeholder.Context<readonly [] extends Values ? never : Values[number]>,
26
- never,
27
- TemplateInstance<
28
- Placeholder.Error<Values[number]>,
29
- T
30
- >
18
+ values: Values
19
+ ): Fx.Fx<
20
+ Scope | Placeholder.Context<Values[number]>,
21
+ Placeholder.Error<Values[number]>,
22
+ RenderEvent
31
23
  >
32
24
  }
33
25
 
@@ -38,51 +30,14 @@ export const RenderTemplate: Context.Tagged<RenderTemplate, RenderTemplate> = Co
38
30
  RenderTemplate,
39
31
  RenderTemplate
40
32
  >(
41
- "./RenderTemplate.js"
33
+ "@typed/template/RenderTemplate"
42
34
  )
43
-
44
- /**
45
- * @since 1.0.0
46
- */
47
- export interface TemplateFx<R, E, T extends Rendered = Rendered> extends
48
- Fx.Fx<
49
- RenderTemplate | Scope | R,
50
- E,
51
- RenderEvent
52
- >
53
- {
54
- readonly instance: Effect.Effect<
55
- RenderTemplate | Scope | R,
56
- never,
57
- TemplateInstance<
58
- E,
59
- T
60
- >
61
- >
62
- }
63
-
64
35
  /**
65
36
  * @since 1.0.0
66
37
  */
67
38
  export function html<const Values extends ReadonlyArray<Renderable<any, any>>>(
68
39
  template: TemplateStringsArray,
69
40
  ...values: Values
70
- ): TemplateFx<Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>> {
71
- const instance = RenderTemplate.withEffect((render) => render(template, values))
72
-
73
- return Object.assign(Fx.fromFxEffect(instance), { instance })
74
- }
75
-
76
- /**
77
- * @since 1.0.0
78
- */
79
- export function as<T extends Rendered = Rendered>(ref: ElementRef<T>) {
80
- return function html<const Values extends ReadonlyArray<Renderable<any, any>>>(
81
- template: TemplateStringsArray,
82
- ...values: Values
83
- ): TemplateFx<Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>, T> {
84
- const instance = RenderTemplate.withEffect((render) => render(template, values, ref))
85
-
86
- return Object.assign(Fx.fromFxEffect(instance), { instance })
87
- }
41
+ ): Fx.Fx<RenderTemplate | Scope | Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>, RenderEvent> {
42
+ return Fx.fromFxEffect(RenderTemplate.with((render) => render(template, values)))
88
43
  }
package/src/Renderable.ts CHANGED
@@ -12,7 +12,8 @@ import type { RenderEvent } from "./RenderEvent.js"
12
12
  */
13
13
  export type Renderable<R = never, E = never> =
14
14
  | Renderable.Value
15
- | { readonly [key: string]: Renderable.Value } // TODO: Should we manage data attributes this way?
15
+ | Placeholder<R, E, any>
16
+ | { readonly [key: string]: Renderable<R, E> | Placeholder<R, E, any> | unknown } // TODO: Should we manage data attributes this way?
16
17
  | Placeholder<R, E, any>
17
18
  | Effect<R, E, any>
18
19
  | Fx<R, E, any>
package/src/Template.ts CHANGED
@@ -32,6 +32,7 @@ export type Node =
32
32
  | TextNode
33
33
  | NodePart
34
34
  | Comment
35
+ | DocType
35
36
 
36
37
  /**
37
38
  * @since 1.0.0
@@ -44,6 +45,7 @@ export type PartNode =
44
45
  | EventPartNode
45
46
  | NodePart
46
47
  | PropertyPartNode
48
+ | PropertiesPartNode
47
49
  | RefPartNode
48
50
  | TextPartNode
49
51
  | CommentPartNode
@@ -96,6 +98,18 @@ export class TextOnlyElement {
96
98
  ) {}
97
99
  }
98
100
 
101
+ /**
102
+ * @since 1.0.0
103
+ */
104
+ export class DocType {
105
+ readonly _tag = "doctype"
106
+ constructor(
107
+ readonly name: string,
108
+ readonly publicType?: string,
109
+ readonly systemId?: string
110
+ ) {}
111
+ }
112
+
99
113
  /**
100
114
  * @since 1.0.0
101
115
  */
@@ -110,6 +124,7 @@ export type Attribute =
110
124
  | DataPartNode
111
125
  | EventPartNode
112
126
  | PropertyPartNode
127
+ | PropertiesPartNode
113
128
  | RefPartNode
114
129
 
115
130
  /**
@@ -219,6 +234,17 @@ export class PropertyPartNode {
219
234
  ) {}
220
235
  }
221
236
 
237
+ /**
238
+ * @since 1.0.0
239
+ */
240
+ export class PropertiesPartNode {
241
+ readonly _tag = "properties" as const
242
+
243
+ constructor(
244
+ readonly index: number
245
+ ) {}
246
+ }
247
+
222
248
  /**
223
249
  * @since 1.0.0
224
250
  */
@@ -8,14 +8,14 @@ import type * as Versioned from "@typed/fx/Versioned"
8
8
  import type { Rendered } from "@typed/wire"
9
9
  import type { NoSuchElementException } from "effect/Cause"
10
10
  import type * as Effect from "effect/Effect"
11
+ import type { Scope } from "effect/Scope"
11
12
  import { type ElementRef, ElementRefTypeId } from "./ElementRef.js"
12
- import type { Placeholder } from "./Placeholder.js"
13
13
  import type { RenderEvent } from "./RenderEvent.js"
14
14
 
15
15
  /**
16
16
  * @since 1.0.0
17
17
  */
18
- export const TemplateInstanceTypeId = Symbol.for("./TemplateInstance.js")
18
+ export const TemplateInstanceTypeId = Symbol.for("@typed/template/TemplateInstance")
19
19
  /**
20
20
  * @since 1.0.0
21
21
  */
@@ -25,7 +25,7 @@ export type TemplateInstanceTypeId = typeof TemplateInstanceTypeId
25
25
  * @since 1.0.0
26
26
  */
27
27
  export interface TemplateInstance<E, T extends Rendered = Rendered>
28
- extends Versioned.Versioned<never, never, never, E, RenderEvent, never, E | NoSuchElementException, T>
28
+ extends Versioned.Versioned<never, never, Scope, E, RenderEvent, never, E | NoSuchElementException, T>
29
29
  {
30
30
  readonly [TemplateInstanceTypeId]: TemplateInstanceTypeId
31
31
 
@@ -42,16 +42,16 @@ export interface TemplateInstance<E, T extends Rendered = Rendered>
42
42
  * @since 1.0.0
43
43
  */
44
44
  export function TemplateInstance<T extends Rendered = Rendered, E = never>(
45
- events: Fx.Fx<never, E, RenderEvent>,
45
+ events: Fx.Fx<Scope, E, RenderEvent>,
46
46
  ref: ElementRef<T>
47
47
  ): TemplateInstance<E, T> {
48
48
  return new TemplateInstanceImpl(events, ref) as any
49
49
  }
50
50
 
51
- // @ts-expect-error placeholder issues
51
+ // @ts-expect-error does not implement Placeholder
52
52
  class TemplateInstanceImpl<E, T extends Rendered>
53
- extends FxEffectBase<never, E, RenderEvent, never, E | NoSuchElementException, T>
54
- implements Omit<TemplateInstance<E, T>, keyof Placeholder<never, E, RenderEvent>>
53
+ extends FxEffectBase<Scope, E, RenderEvent, never, E | NoSuchElementException, T>
54
+ implements TemplateInstance<E, T>
55
55
  {
56
56
  readonly [TemplateInstanceTypeId]: TemplateInstanceTypeId = TemplateInstanceTypeId
57
57
  query: TemplateInstance<E, T>["query"]
@@ -61,7 +61,7 @@ class TemplateInstanceImpl<E, T extends Rendered>
61
61
  version: Effect.Effect<never, never, number>
62
62
 
63
63
  constructor(
64
- readonly i0: Fx.Fx<never, E, RenderEvent>,
64
+ readonly i0: Fx.Fx<Scope, E, RenderEvent>,
65
65
  readonly i1: ElementRef<T>
66
66
  ) {
67
67
  super()
@@ -73,7 +73,7 @@ class TemplateInstanceImpl<E, T extends Rendered>
73
73
  this.version = this.i1[ElementRefTypeId].version
74
74
  }
75
75
 
76
- toFx(): Fx.Fx<never, E, RenderEvent> {
76
+ toFx(): Fx.Fx<Scope, E, RenderEvent> {
77
77
  return this.i0
78
78
  }
79
79
 
package/src/Test.ts CHANGED
@@ -6,17 +6,15 @@ import type { DomServices, DomServicesElementParams } from "@typed/dom/DomServic
6
6
  import type { GlobalThis } from "@typed/dom/GlobalThis"
7
7
  import type { Window } from "@typed/dom/Window"
8
8
  import type { CurrentEnvironment } from "@typed/environment"
9
- import type { Computed } from "@typed/fx/Computed"
10
- import type { Filtered } from "@typed/fx/Filtered"
11
9
  import * as Fx from "@typed/fx/Fx"
12
10
  import * as RefArray from "@typed/fx/RefArray"
11
+ import * as RefSubject from "@typed/fx/RefSubject"
13
12
  import * as Sink from "@typed/fx/Sink"
14
13
  import * as Cause from "effect/Cause"
15
14
  import * as Effect from "effect/Effect"
16
15
  import * as Either from "effect/Either"
17
16
  import * as Fiber from "effect/Fiber"
18
17
  import type * as Scope from "effect/Scope"
19
- import * as happyDOM from "happy-dom"
20
18
  import type IHappyDOMOptions from "happy-dom/lib/window/IHappyDOMOptions.js"
21
19
  import * as ElementRef from "./ElementRef.js"
22
20
  import { ROOT_CSS_SELECTOR } from "./ElementSource.js"
@@ -36,11 +34,11 @@ import type { RenderTemplate } from "./RenderTemplate.js"
36
34
  * @since 1.0.0
37
35
  */
38
36
  export interface TestRender<E> {
39
- readonly window: Window & GlobalThis & Pick<happyDOM.Window, "happyDOM">
37
+ readonly window: Window & GlobalThis
40
38
  readonly document: Document
41
39
  readonly elementRef: ElementRef.ElementRef
42
- readonly errors: Computed<never, never, ReadonlyArray<E>>
43
- readonly lastError: Filtered<never, never, E>
40
+ readonly errors: RefSubject.Computed<never, never, ReadonlyArray<E>>
41
+ readonly lastError: RefSubject.Filtered<never, never, E>
44
42
  readonly interrupt: Effect.Effect<never, never, void>
45
43
  readonly makeEvent: (type: string, eventInitDict?: EventInit) => Event
46
44
  readonly makeCustomEvent: <A>(type: string, eventInitDict?: CustomEventInit<A>) => CustomEvent<A>
@@ -62,24 +60,25 @@ export function testRender<R, E>(
62
60
  TestRender<E>
63
61
  > {
64
62
  return Effect.gen(function*(_) {
65
- const window = makeWindow(options)
63
+ const window = yield* _(getOrMakeWindow(options))
66
64
  const elementRef = yield* _(ElementRef.make())
67
- const errors = yield* _(RefArray.make<never, never, E>(Effect.succeed([])))
65
+ const errors = yield* _(RefSubject.make<never, never, ReadonlyArray<E>>(Effect.succeed([])))
68
66
  const fiber = yield* _(
69
67
  fx,
70
68
  render,
71
- Fx.run(Sink.Sink(
72
- (cause) =>
73
- Cause.failureOrCause(cause).pipe(
74
- Either.match({
75
- onLeft: (error) => RefArray.append(errors, error),
76
- onRight: (cause) => errors.onFailure(cause)
77
- })
78
- ),
79
- (rendered) => ElementRef.set(elementRef, rendered)
80
- )),
69
+ (x) =>
70
+ x.run(Sink.make(
71
+ (cause) =>
72
+ Cause.failureOrCause(cause).pipe(
73
+ Either.match({
74
+ onLeft: (error) => RefArray.append(errors, error),
75
+ onRight: (cause) => errors.onFailure(cause)
76
+ })
77
+ ),
78
+ (rendered) => ElementRef.set(elementRef, rendered)
79
+ )),
81
80
  Effect.forkScoped,
82
- Effect.provide(RenderContext.browser(window, { skipRenderScheduling: true }))
81
+ Effect.provide(RenderContext.dom(window, { skipRenderScheduling: true }))
83
82
  )
84
83
 
85
84
  const test: TestRender<E> = {
@@ -99,6 +98,9 @@ export function testRender<R, E>(
99
98
  yield* _(adjustTime(1))
100
99
  yield* _(adjustTime(1))
101
100
 
101
+ // Await the first render
102
+ yield* _(Fx.first(elementRef), Effect.race(Effect.delay(Effect.dieMessage(`Rendering taking too long`), 1000)))
103
+
102
104
  return test
103
105
  })
104
106
  }
@@ -146,6 +148,23 @@ export function click<E>(
146
148
 
147
149
  // internals
148
150
 
149
- function makeWindow(options?: IHappyDOMOptions) {
150
- return new happyDOM.Window(options) as any as Window & GlobalThis & Pick<happyDOM.Window, "happyDOM">
151
+ function getOrMakeWindow(options?: IHappyDOMOptions) {
152
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
153
+ return Effect.gen(function*(_) {
154
+ window.document.head.innerHTML = ""
155
+ window.document.body.innerHTML = ""
156
+ yield* _(Effect.addFinalizer(() =>
157
+ Effect.sync(() => {
158
+ window.document.head.innerHTML = ""
159
+ window.document.body.innerHTML = ""
160
+ })
161
+ ))
162
+
163
+ return window
164
+ })
165
+ }
166
+
167
+ return Effect.promise(() =>
168
+ import("happy-dom").then((happyDOM) => new happyDOM.Window(options) as any as Window & GlobalThis)
169
+ )
151
170
  }
package/src/index.ts CHANGED
@@ -60,7 +60,3 @@ export * from "./RenderEvent.js"
60
60
  * @since 1.0.0
61
61
  */
62
62
  export * from "./RenderTemplate.js"
63
- /**
64
- * @since 1.0.0
65
- */
66
- export * from "./TemplateInstance.js"
@@ -0,0 +1,153 @@
1
+ import type { Rendered } from "@typed/wire"
2
+ import { Effect, Scope } from "effect"
3
+ import type * as Fiber from "effect/Fiber"
4
+ import * as Runtime from "effect/Runtime"
5
+ import { getElements } from "../ElementSource"
6
+ import type { EventHandler } from "../EventHandler"
7
+
8
+ type EventName = string
9
+
10
+ type Handler<Ev extends Event> = EventHandler<never, never, Ev>
11
+
12
+ export interface EventSource {
13
+ readonly addEventListener: <Ev extends Event>(
14
+ element: Element,
15
+ event: EventName,
16
+ handler: Handler<Ev>
17
+ ) => void
18
+
19
+ readonly setup: (rendered: Rendered, scope: Scope.Scope) => Effect.Effect<never, never, void>
20
+ }
21
+
22
+ type Entry = readonly [Element, Handler<any>]
23
+ type Run = <E, A>(effect: Effect.Effect<never, E, A>) => Fiber.RuntimeFiber<E, A>
24
+
25
+ const disposable = (f: () => void): Disposable => ({
26
+ [Symbol.dispose]: f
27
+ })
28
+
29
+ export function makeEventSource(): EventSource {
30
+ const bubbleListeners = new Map<
31
+ EventName,
32
+ Set<Entry>
33
+ >()
34
+ const captureListeners = new Map<
35
+ EventName,
36
+ Set<Entry>
37
+ >()
38
+
39
+ function addListener(
40
+ listeners: Map<
41
+ EventName,
42
+ Set<Entry>
43
+ >,
44
+ event: EventName,
45
+ entry: Entry
46
+ ): void {
47
+ const set = listeners.get(event)
48
+ if (set === undefined) {
49
+ const set = new Set<Entry>()
50
+ set.add(entry)
51
+ listeners.set(event, set)
52
+ } else {
53
+ set.add(entry)
54
+ }
55
+ }
56
+
57
+ function addEventListener<Ev extends Event>(
58
+ element: Element,
59
+ event: EventName,
60
+ handler: Handler<Ev>
61
+ ): void {
62
+ if (handler.options?.capture === true) {
63
+ return addListener(captureListeners, event, [element, handler])
64
+ } else {
65
+ return addListener(bubbleListeners, event, [element, handler])
66
+ }
67
+ }
68
+
69
+ function setupBubbleListeners(element: Element, run: Run) {
70
+ const disposables: Array<Disposable> = []
71
+
72
+ for (const [event, handlers] of bubbleListeners) {
73
+ const listener = (ev: Event) =>
74
+ run(
75
+ Effect.forEach(handlers, ([el, handler]) =>
76
+ ev.target === el || el.contains(ev.target as Node) ? handler.handler(ev) : Effect.unit)
77
+ )
78
+ element.addEventListener(event, listener, getDerivedAddEventListenerOptions(handlers))
79
+ disposables.push(disposable(() => element.removeEventListener(event, listener)))
80
+ }
81
+
82
+ return disposables
83
+ }
84
+
85
+ function setupCaptureListeners(element: Element, run: Run) {
86
+ const disposables: Array<Disposable> = []
87
+
88
+ for (const [event, handlers] of captureListeners) {
89
+ const listener = (ev: Event) =>
90
+ run(
91
+ Effect.forEach(handlers, ([el, handler]) =>
92
+ ev.target === el || el.contains(ev.target as Node)
93
+ ? handler.handler(proxyCurrentTargetForCaptureEvents(ev, el))
94
+ : Effect.unit)
95
+ )
96
+ element.addEventListener(event, listener, getDerivedAddEventListenerOptions(handlers))
97
+ disposables.push(disposable(() => element.removeEventListener(event, listener)))
98
+ }
99
+
100
+ return disposables
101
+ }
102
+
103
+ function setup(rendered: Rendered, scope: Scope.Scope) {
104
+ const hasBubbleListeners = bubbleListeners.size > 0
105
+ const hasCaptureListeners = captureListeners.size > 0
106
+
107
+ if (!hasBubbleListeners && !hasCaptureListeners) {
108
+ return Effect.unit
109
+ }
110
+
111
+ return Effect.flatMap(Effect.runtime<never>(), (runtime) => {
112
+ const elements = getElements(rendered)
113
+ const disposables: Array<Disposable> = []
114
+ const runFork = Runtime.runFork(runtime)
115
+ const run: Run = <E, A>(effect: Effect.Effect<never, E, A>) =>
116
+ runFork(Effect.fromFiberEffect(Effect.forkIn(effect, scope)))
117
+
118
+ for (const element of elements) {
119
+ if (hasBubbleListeners) {
120
+ disposables.push(...setupBubbleListeners(element, run))
121
+ }
122
+ if (hasCaptureListeners) {
123
+ disposables.push(...setupCaptureListeners(element, run))
124
+ }
125
+ }
126
+
127
+ return Scope.addFinalizer(scope, Effect.sync(() => disposables.forEach((d) => d[Symbol.dispose]())))
128
+ })
129
+ }
130
+
131
+ return {
132
+ addEventListener,
133
+ setup
134
+ }
135
+ }
136
+
137
+ const EVENT_PROPERTY_TO_REPLACE = "currentTarget"
138
+
139
+ function proxyCurrentTargetForCaptureEvents<E extends Event>(event: E, currentTarget: Element): E {
140
+ return new Proxy(event, {
141
+ get(target: E, property: string | symbol) {
142
+ return property === EVENT_PROPERTY_TO_REPLACE ? currentTarget : target[property as keyof E]
143
+ }
144
+ })
145
+ }
146
+
147
+ function getDerivedAddEventListenerOptions(entries: Set<Entry>): AddEventListenerOptions {
148
+ const hs = Array.from(entries)
149
+ return {
150
+ once: hs.some((h) => h[1].options?.once === true),
151
+ passive: hs.every((h) => h[1].options?.passive === true)
152
+ }
153
+ }