diff --git a/packages/nanoviews/src/flow/for.spec.ts b/packages/nanoviews/src/flow/for.spec.ts index 5d448e68..bb1a9d6e 100644 --- a/packages/nanoviews/src/flow/for.spec.ts +++ b/packages/nanoviews/src/flow/for.spec.ts @@ -1002,6 +1002,36 @@ describe('nanoviews', () => { expect(runs).toEqual([0]) }) + it('should render the placeholder for an absent array', () => { + const items = signal(undefined) + const { container } = render(() => ul()( + for_(items, trackById)( + item => li()(record(item).$name), + () => li()('nobody') + ) + )) + + expect(container.innerHTML).toBe('
') + + items([ + { + id: 1, + name: 'Yatoro' + } + ]) + + expect(container.innerHTML).toBe('
') + + // no array is the same emptiness as an empty one + items(undefined) + + expect(container.innerHTML).toBe('
') + + items([]) + + expect(container.innerHTML).toBe('
') + }) + describe('fuzz', () => { // A named test pins a shape someone thought of; these walk sequences // nobody did. Both invariants are checked after every step: the rows diff --git a/packages/nanoviews/src/flow/for.ts b/packages/nanoviews/src/flow/for.ts index a2228202..168301fb 100644 --- a/packages/nanoviews/src/flow/for.ts +++ b/packages/nanoviews/src/flow/for.ts @@ -2,6 +2,7 @@ import { type Accessor, type ReadableSignal, type WritableSignal, + type EmptyValue, isAccessor } from 'kida' import { @@ -51,8 +52,12 @@ type StaticEach = ( type UnknownTrack = (item: unknown, index: number) => unknown +// A signal is invariant, so the nullable array is a separate arm rather than +// a widening of the first: `WritableSignal` does not fit +// `WritableSignal`, and widening would take the rows of +// every existing caller down to read-only export function for_( - $items: WritableSignal, + $items: WritableSignal | WritableSignal, track?: (item: T, index: number) => unknown ): ( each_: WritableEach, @@ -60,7 +65,7 @@ export function for_( ) => Child export function for_( - $items: Accessor, + $items: Accessor, track?: (item: T, index: number) => unknown ): ( each_: ReadableEach, @@ -68,7 +73,7 @@ export function for_( ) => Child export function for_( - $items: T[] + $items: T[] | EmptyValue ): ( each_: StaticEach, else_?: () => Child @@ -81,7 +86,7 @@ export function for_( * @returns Function to receive each_ and else_ functions */ export function for_( - $items: unknown[] | Accessor, + $items: unknown[] | EmptyValue | Accessor, track?: UnknownTrack ) { if (isAccessor($items)) { diff --git a/packages/nanoviews/src/internals/flow/loop.ts b/packages/nanoviews/src/internals/flow/loop.ts index f8eb26e9..1c58bf18 100644 --- a/packages/nanoviews/src/internals/flow/loop.ts +++ b/packages/nanoviews/src/internals/flow/loop.ts @@ -19,7 +19,10 @@ import { nextValue, assignIndex } from 'kida' -import type { Child } from '../types/index.js' +import type { + Child, + EmptyValue +} from '../types/index.js' import { deferScopeBindContext, effectScopeSwapper @@ -341,7 +344,7 @@ function reconcile( } export function loop( - $items: Accessor, + $items: Accessor, each_: AnyEach, else_?: () => Child, track: UnknownTrack = (_, i) => i @@ -389,9 +392,11 @@ export function loop( effectScopeSwapper($items, ( destroyPrev: DeferredScope | undefined, - items: unknown[] + items: unknown[] | EmptyValue ) => { - const itemsCount = items.length + // No array at all is the same emptiness as an empty one: the rows go and + // the placeholder comes, and nothing below ever reaches into it + const itemsCount = items?.length if (itemsCount && destroyPrev !== undefined && !isPlaceholder) { // [...m] -> [...n] @@ -405,7 +410,7 @@ export function loop( reconcile, itemsList, blocksMap, - $items, + $items as Accessor, each_, track, end, @@ -438,7 +443,7 @@ export function loop( reconcile( itemsList, blocksMap, - $items, + $items as Accessor, each_, track, end, diff --git a/todo.txt b/todo.txt index 0c1950d9..219b7a0e 100644 --- a/todo.txt +++ b/todo.txt @@ -44,13 +44,11 @@ - hooks/effects naming convention - error boundaries: cleanup half-created scope on render throw (stopScope in catch) - rows/content are not parent-linked, teardown must be explicit -- for loop array or empty check for else - for_ duplicate track keys: unsupported by design, must keep failing loudly - the crash lands a step away from the cause, so it needs a clear throw at the point of detection - rework return slot$ to look like element/component? fn.prop is faster than {f,p} - util to transform static props to signal props? - typed effectattrs - additional arg for record in for$? as$ -- controls addEventListener to events system - static index for untracked loop? for_($items)(