@typed/template 0.3.2 → 0.3.4

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.
@@ -1,12 +1,14 @@
1
1
  import * as Fx from "@typed/fx/Fx"
2
+ import * as Sink from "@typed/fx/Sink"
2
3
  import { TypeId } from "@typed/fx/TypeId"
3
4
  import type { Rendered } from "@typed/wire"
4
5
  import { persistent } from "@typed/wire"
5
- import { Effect } from "effect"
6
+ import { Effect, ExecutionStrategy, Exit } from "effect"
6
7
  import type { Cause } from "effect/Cause"
7
8
  import type { Chunk } from "effect/Chunk"
8
9
  import * as Context from "effect/Context"
9
- import { Scope } from "effect/Scope"
10
+ import * as Scope from "effect/Scope"
11
+ import { uncapitalize } from "effect/String"
10
12
  import type { Directive } from "../Directive.js"
11
13
  import { isDirective } from "../Directive.js"
12
14
  import * as ElementRef from "../ElementRef.js"
@@ -49,7 +51,7 @@ import { findHoleComment, findPath } from "./utils.js"
49
51
  * @internal
50
52
  */
51
53
  export type RenderPartContext = {
52
- readonly context: Context.Context<Scope>
54
+ readonly context: Context.Context<Scope.Scope>
53
55
  readonly document: Document
54
56
  readonly eventSource: EventSource
55
57
  readonly refCounter: IndexRefCounter2
@@ -60,6 +62,7 @@ export type RenderPartContext = {
60
62
  readonly makeHydrateContext?: (index: number) => HydrateContext
61
63
 
62
64
  expected: number
65
+ spreadIndex: number
63
66
  }
64
67
 
65
68
  type RenderPartMap = {
@@ -101,15 +104,16 @@ const RenderPartMap: RenderPartMap = {
101
104
  "boolean-part": (templatePart, node, ctx) => {
102
105
  const { refCounter, renderContext, values } = ctx
103
106
  const element = node as HTMLElement | SVGElement
107
+ const name = templatePart.name
104
108
  const renderable = values[templatePart.index]
105
109
  const setValue = (value: boolean | null | undefined) => {
106
- element.toggleAttribute(templatePart.name, isNullOrUndefined(value) ? false : Boolean(value))
110
+ element.toggleAttribute(name, isNullOrUndefined(value) ? false : Boolean(value))
107
111
  }
108
112
 
109
113
  return matchSettablePart(
110
114
  renderable,
111
115
  setValue,
112
- () => BooleanPartImpl.browser(templatePart.index, element, templatePart.name, renderContext),
116
+ () => BooleanPartImpl.browser(templatePart.index, element, name, renderContext),
113
117
  (f) => Effect.zipRight(renderContext.queue.add(element, f), refCounter.release(templatePart.index)),
114
118
  () => ctx.expected++
115
119
  )
@@ -124,14 +128,13 @@ const RenderPartMap: RenderPartMap = {
124
128
  element.classList.remove(...classNames)
125
129
  classNames.clear()
126
130
  } else {
127
- const newClassNames = new Set(Array.isArray(value) ? value : [String(value)])
131
+ const newClassNames = new Set(
132
+ Array.isArray(value) ? value.flatMap((x) => splitClassNames(String(x))) : splitClassNames(String(value))
133
+ )
128
134
  const { added, removed } = diffClassNames(classNames, newClassNames)
129
135
 
130
- if (removed.length > 0) {
131
- element.classList.remove(...removed)
132
- }
133
- if (added.length > 0) element.classList.add(...added)
134
-
136
+ element.classList.remove(...removed)
137
+ element.classList.add(...added)
135
138
  classNames = newClassNames
136
139
  }
137
140
  }
@@ -219,7 +222,7 @@ const RenderPartMap: RenderPartMap = {
219
222
 
220
223
  const handle = handlePart(
221
224
  ctx.values[templatePart.index],
222
- (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
225
+ Sink.make(ctx.onCause, (value) => Effect.zipRight(part.update(value), ctx.refCounter.release(templatePart.index)))
223
226
  )
224
227
 
225
228
  if (makeHydrateContext) {
@@ -273,18 +276,21 @@ const RenderPartMap: RenderPartMap = {
273
276
  ;(element as any)[key] = value
274
277
  }
275
278
  }
279
+ const setClassNames = (previous: Set<string>, updated: Set<string>) => {
280
+ const { added, removed } = diffClassNames(previous, updated)
276
281
 
277
- const effects: Array<Effect.Effect<any, any, void>> = []
282
+ element.classList.remove(...removed)
283
+ element.classList.add(...added)
284
+ removed.forEach((r) => previous.delete(r))
285
+ added.forEach((a) => previous.add(a))
286
+ }
278
287
 
279
- // We need indexes to track async values that won't conflict
280
- // with any other Parts, we can start end of the current values.length
281
- // As there should only ever be exactly 1 properties part.
282
- let i = ctx.values.length
288
+ const effects: Array<Effect.Effect<any, any, void>> = []
289
+ const entries = Object.entries(renderable)
283
290
 
284
291
  loop:
285
- for (const [key, value] of Object.entries(renderable)) {
286
- const index = ++i
287
-
292
+ for (const [key, value] of entries) {
293
+ const index = ++ctx.spreadIndex
288
294
  switch (key[0]) {
289
295
  case "?": {
290
296
  const name = key.slice(1)
@@ -315,7 +321,7 @@ const RenderPartMap: RenderPartMap = {
315
321
  continue loop
316
322
  }
317
323
  case "@": {
318
- const name = key.slice(1)
324
+ const name = uncapitalize(key.slice(1))
319
325
  const handler = getEventHandler(value, ctx.context, ctx.onCause)
320
326
  if (handler) {
321
327
  ctx.eventSource.addEventListener(element, name, handler)
@@ -324,7 +330,7 @@ const RenderPartMap: RenderPartMap = {
324
330
  }
325
331
  case "o": {
326
332
  if (key[1] === "n") {
327
- const name = key.slice(2)
333
+ const name = uncapitalize(key.slice(2))
328
334
  const handler = getEventHandler(value, ctx.context, ctx.onCause)
329
335
  if (handler) {
330
336
  ctx.eventSource.addEventListener(element, name, handler)
@@ -334,15 +340,40 @@ const RenderPartMap: RenderPartMap = {
334
340
  }
335
341
  }
336
342
 
337
- const eff = matchSettablePart(
338
- value,
339
- (value) => setAttribute(key, value),
340
- () => AttributePartImpl.browser(index, element, key, ctx.renderContext),
341
- (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
342
- () => ctx.expected++
343
- )
344
- if (eff !== null) {
345
- effects.push(eff)
343
+ const lowerCaseName = key.toLowerCase()
344
+
345
+ const isClass = lowerCaseName === "class" || lowerCaseName === "classname"
346
+
347
+ if (isClass) {
348
+ const classNames: Set<string> = new Set()
349
+ const eff = matchSettablePart(
350
+ value,
351
+ (value) => {
352
+ if (isNullOrUndefined(value)) {
353
+ element.classList.remove(...classNames)
354
+ classNames.clear()
355
+ } else {
356
+ setClassNames(classNames, new Set(splitClassNames(String(value))))
357
+ }
358
+ },
359
+ () => ClassNamePartImpl.browser(index, element, ctx.renderContext),
360
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
361
+ () => ctx.expected++
362
+ )
363
+ if (eff !== null) {
364
+ effects.push(eff)
365
+ }
366
+ } else {
367
+ const eff = matchSettablePart(
368
+ value,
369
+ (value) => setAttribute(key, value),
370
+ () => AttributePartImpl.browser(index, element, key, ctx.renderContext),
371
+ (f) => Effect.zipRight(ctx.renderContext.queue.add(element, f), ctx.refCounter.release(index)),
372
+ () => ctx.expected++
373
+ )
374
+ if (eff !== null) {
375
+ effects.push(eff)
376
+ }
346
377
  }
347
378
  }
348
379
 
@@ -474,7 +505,10 @@ const RenderPartMap: RenderPartMap = {
474
505
 
475
506
  return handlePart(
476
507
  ctx.values[templatePart.index],
477
- (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
508
+ Sink.make(
509
+ ctx.onCause,
510
+ (value) => Effect.zipRight(part.update(value as any), ctx.refCounter.release(templatePart.index))
511
+ )
478
512
  )
479
513
  }
480
514
  }
@@ -538,13 +572,15 @@ export const renderTemplate: (document: Document, renderContext: RenderContext)
538
572
  return Fx.sync(() => DomRenderEvent(persistent(document.importNode(entry.content, true))))
539
573
  }
540
574
 
541
- return Fx.make<Scope | Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>, RenderEvent>((
575
+ return Fx.make<Scope.Scope | Placeholder.Context<Values[number]>, Placeholder.Error<Values[number]>, RenderEvent>((
542
576
  sink
543
577
  ) => {
544
578
  return Effect.gen(function*(_) {
545
- const content = document.importNode(entry.content, true)
546
- const context = yield* _(Effect.context<Scope>())
579
+ const context = yield* _(Effect.context<Scope.Scope | Placeholder.Context<Values[number]>>())
580
+ const parentScope = Context.get(context, Scope.Scope)
581
+ const scope = yield* _(Scope.fork(parentScope, ExecutionStrategy.sequential))
547
582
  const refCounter = yield* _(indexRefCounter2())
583
+ const content = document.importNode(entry.content, true)
548
584
  const ctx: RenderPartContext = {
549
585
  context,
550
586
  document,
@@ -553,17 +589,18 @@ export const renderTemplate: (document: Document, renderContext: RenderContext)
553
589
  refCounter,
554
590
  renderContext,
555
591
  onCause: sink.onFailure as any,
592
+ spreadIndex: values.length,
556
593
  values
557
594
  }
558
595
 
559
596
  // Connect our interpolated values to our template parts
560
- const effects: Array<Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>> = []
597
+ const effects: Array<Effect.Effect<Scope.Scope | Placeholder.Context<Values[number]>, never, void>> = []
561
598
  for (const [part, path] of entry.template.parts) {
562
599
  const eff = renderPart2(part, content, path, ctx)
563
600
  if (eff !== null) {
564
601
  effects.push(
565
602
  ...(Array.isArray(eff) ? eff : [eff]) as Array<
566
- Effect.Effect<Scope | Placeholder.Context<Values[number]>, never, void>
603
+ Effect.Effect<Scope.Scope | Placeholder.Context<Values[number]>, never, void>
567
604
  >
568
605
  )
569
606
  }
@@ -571,7 +608,11 @@ export const renderTemplate: (document: Document, renderContext: RenderContext)
571
608
 
572
609
  // Fork any effects necessary
573
610
  if (effects.length > 0) {
574
- yield* _(Effect.forkAll(effects))
611
+ for (const eff of effects) {
612
+ yield* _(
613
+ Effect.forkIn(Effect.catchAllCause(eff, ctx.onCause), scope)
614
+ )
615
+ }
575
616
  }
576
617
 
577
618
  // If there's anything to wait on and it's not already done, wait for an initial value
@@ -584,14 +625,17 @@ export const renderTemplate: (document: Document, renderContext: RenderContext)
584
625
  const wire = persistent(content) as T
585
626
 
586
627
  // Set the element when it is ready
587
- yield* _(ctx.eventSource.setup(wire, Context.get(context, Scope)))
628
+ yield* _(ctx.eventSource.setup(wire, scope))
588
629
 
589
630
  // Emit our DomRenderEvent
590
- yield* _(sink.onSuccess(DomRenderEvent(wire)))
591
-
592
- // Ensure our templates last forever in the DOM environment
593
- // so event listeners are kept attached to the current Scope.
594
- yield* _(Effect.never)
631
+ yield* _(
632
+ sink.onSuccess(DomRenderEvent(wire)),
633
+ // Ensure our templates last forever in the DOM environment
634
+ // so event listeners are kept attached to the current Scope.
635
+ Effect.zipRight(Effect.never),
636
+ // Close our scope whenever the current Fiber is interrupted
637
+ Effect.ensuring(Scope.close(scope, Exit.unit))
638
+ )
595
639
  })
596
640
  })
597
641
  }
@@ -619,26 +663,26 @@ function getEventHandler<R, E>(
619
663
  return null
620
664
  }
621
665
 
622
- function handlePart<R, E>(
666
+ function handlePart<R, E, R2>(
623
667
  renderable: unknown,
624
- update: (u: unknown) => Effect.Effect<Scope, never, unknown>
625
- ): Effect.Effect<R | Scope, E, any> {
668
+ sink: Sink.Sink<R2, any, any>
669
+ ): Effect.Effect<R | R2 | Scope.Scope, never, any> {
626
670
  switch (typeof renderable) {
627
671
  case "undefined":
628
672
  case "object": {
629
- if (renderable === null || renderable === undefined) return update(null)
673
+ if (renderable === null || renderable === undefined) return sink.onSuccess(null)
630
674
  else if (Array.isArray(renderable)) {
631
675
  return renderable.length === 0
632
- ? update(null)
633
- : Effect.forkScoped(Fx.observe(Fx.tuple(renderable.map(unwrapRenderable)) as any, update))
676
+ ? sink.onSuccess(null)
677
+ : Fx.tuple(renderable.map(unwrapRenderable)).run(sink) as any
634
678
  } else if (TypeId in renderable) {
635
- return Effect.forkScoped(Fx.observe(renderable as any, update))
679
+ return (renderable as Fx.Fx<R | R2, any, any>).run(sink)
636
680
  } else if (Effect.EffectTypeId in renderable) {
637
- return Effect.flatMap(renderable as Effect.Effect<R, E, any>, update)
638
- } else return update(renderable)
681
+ return Effect.matchCauseEffect(renderable as Effect.Effect<R, E, any>, sink)
682
+ } else return sink.onSuccess(renderable)
639
683
  }
640
684
  default:
641
- return update(renderable)
685
+ return sink.onSuccess(renderable)
642
686
  }
643
687
  }
644
688
 
@@ -648,7 +692,9 @@ function unwrapRenderable<R, E>(renderable: unknown): Fx.Fx<R, E, any> {
648
692
  case "object": {
649
693
  if (renderable === null || renderable === undefined) return Fx.succeed(null)
650
694
  else if (Array.isArray(renderable)) {
651
- return renderable.length === 0 ? Fx.succeed(null) : Fx.tuple(renderable.map(unwrapRenderable)) as any
695
+ return renderable.length === 0
696
+ ? Fx.succeed(null)
697
+ : Fx.map(Fx.tuple(renderable.map(unwrapRenderable)), (xs) => xs.flat()) as any
652
698
  } else if (TypeId in renderable) {
653
699
  return renderable as any
654
700
  } else if (Effect.EffectTypeId in renderable) {
@@ -837,7 +883,7 @@ function matchSettablePart(
837
883
  renderable: Renderable<any, any>,
838
884
  setValue: (value: any) => void,
839
885
  makePart: () => Part,
840
- schedule: (f: () => void) => Effect.Effect<Scope, never, void>,
886
+ schedule: (f: () => void) => Effect.Effect<Scope.Scope, never, void>,
841
887
  expect: () => void
842
888
  ) {
843
889
  return matchRenderable(renderable, {