diff --git a/packages/nanoviews/src/flow/if.spec.ts b/packages/nanoviews/src/flow/if.spec.ts
index 2acbfb16..57db06e5 100644
--- a/packages/nanoviews/src/flow/if.spec.ts
+++ b/packages/nanoviews/src/flow/if.spec.ts
@@ -9,8 +9,13 @@ import { render } from '@nanoviews/testing-library'
import {
type WritableSignal,
type ReadableSignal,
- signal
+ signal,
+ effect
} from 'kida'
+import {
+ b,
+ i
+} from '../elements/elements.js'
import * as Stories from './if.stories.js'
import { if_ } from './if.js'
@@ -55,6 +60,100 @@ describe('nanoviews', () => {
expect(container.innerHTML).toBe('
')
})
+ it('should render a write to the condition made by the branch it selected', () => {
+ const $open = signal(false)
+ const $allowed = signal(false)
+ const $text = signal('closed')
+ const { container } = render(() => if_($open)(
+ () => {
+ // the branch refuses to be shown, so the write reaches the
+ // condition from an effect the swap itself started
+ effect(() => {
+ if (!$allowed()) {
+ $open(false)
+ }
+ })
+
+ return b()('open')
+ },
+ () => i()($text)
+ ))
+
+ $open(true)
+
+ expect(container.innerHTML).toBe('closed
')
+
+ // the branch the write brought back is live, not merely rendered
+ $text('shut')
+
+ expect(container.innerHTML).toBe('shut
')
+ })
+
+ it('should render a write to the condition made while the branch renders', () => {
+ const $open = signal(false)
+ const $tick = signal(0)
+ const runs: number[] = []
+ const { container } = render(() => if_($open)(
+ () => {
+ $open(false)
+
+ return b()('open')
+ },
+ () => {
+ // an effect of the branch the write brought back: unlike a
+ // binding it runs only if that branch was started
+ effect(() => {
+ runs.push($tick())
+ })
+
+ return i()('closed')
+ }
+ ))
+
+ $open(true)
+
+ expect(container.innerHTML).toBe('closed
')
+ expect(runs).toEqual([0, 0])
+
+ $tick(1)
+
+ expect(runs).toEqual([0, 0, 1])
+ })
+
+ it('should start the branch brought up by a write made on mount', () => {
+ const $open = signal(true)
+ const $allowed = signal(false)
+ const $tick = signal(0)
+ const runs: number[] = []
+ const { container } = render(() => if_($open)(
+ () => {
+ effect(() => {
+ if (!$allowed()) {
+ $open(false)
+ }
+ })
+
+ return b()('open')
+ },
+ () => {
+ // an effect of the branch the mount-time write brought up:
+ // unlike a binding it runs only if that branch was started
+ effect(() => {
+ runs.push($tick())
+ })
+
+ return i()('closed')
+ }
+ ))
+
+ expect(container.innerHTML).toBe('closed
')
+ expect(runs).toEqual([0])
+
+ $tick(1)
+
+ expect(runs).toEqual([0, 1])
+ })
+
it('should keep signal type in branches', () => {
const $value = signal('truthy')
diff --git a/packages/nanoviews/src/internals/effects.ts b/packages/nanoviews/src/internals/effects.ts
index 6152683b..3d50bf63 100644
--- a/packages/nanoviews/src/internals/effects.ts
+++ b/packages/nanoviews/src/internals/effects.ts
@@ -13,8 +13,9 @@ import type { EffectScopeSwapperCallback } from './types/index.js'
export function deferScopeBindContext(context = getContext()) {
const factory = boundDeferScope()
- // Render under the injection context, start strictly outside of it
- return (fn: () => void, replace?: DeferredScope): DeferredScope => startScope(unsafeRun(context, factory, fn, replace))
+ // Render under the injection context; starting the scope is not the
+ // renderer's move
+ return (fn: () => void, replace?: DeferredScope): DeferredScope => unsafeRun(context, factory, fn, replace)
}
export function effectScopeSwapper(
@@ -23,9 +24,14 @@ export function effectScopeSwapper(
) {
let prev: DeferredScope | undefined
+ // The swap starts what it returns. Before the mount walk the scope's
+ // anchor is still lazy and the start is a no-op, so the walk keeps the
+ // block's position; after it the swapped-in content comes up on the
+ // swap's own stack, so a swap correcting a write made mid-render leaves
+ // no scope behind that is rendered but never started
effect(() => {
const value = $signal()
- prev = untracked(() => callback(prev, value))
+ prev = untracked(() => startScope(callback(prev, value)))
}, true)
}
diff --git a/packages/nanoviews/src/internals/flow/decide.ts b/packages/nanoviews/src/internals/flow/decide.ts
index 8285be5b..5a31b09f 100644
--- a/packages/nanoviews/src/internals/flow/decide.ts
+++ b/packages/nanoviews/src/internals/flow/decide.ts
@@ -2,6 +2,7 @@ import {
type Accessor,
type ValueOrAccessor,
type DeferredScope,
+ effect,
isAccessor
} from 'kida'
import type { Child } from '../types/index.js'
@@ -39,6 +40,12 @@ export function reactiveDecide(
insertChildBeforeAnchor(decider(condition), end)
}, destroyPrev))
+ // The echo: a branch that writes the condition back does it from inside
+ // the running swapper, which cannot be re-queued by its own propagation.
+ // This second subscriber is idle at that moment, so its read settles the
+ // condition and re-queues the parked swapper for the corrective swap
+ effect(() => void $condition(), true)
+
return fragment
}