Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/nanoviews/.size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "All publics (Brotli)",
"path": "dist/index.js",
"import": "*",
"limit": "6.8 kB"
"limit": "6.75 kB"
},
{
"name": "Average usage (Gzip)",
Expand Down
2 changes: 1 addition & 1 deletion packages/nanoviews/src/flow/for.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function createRandom(seed: number) {
}

describe('nanoviews', () => {
describe('logic', () => {
describe('flow', () => {
describe('for', () => {
it('should handle static array', () => {
const { container } = render(StaticValue())
Expand Down
2 changes: 1 addition & 1 deletion packages/nanoviews/src/flow/if.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const {
} = composeStories(Stories)

describe('nanoviews', () => {
describe('logic', () => {
describe('flow', () => {
describe('if', () => {
it('should handle static value', () => {
const { container } = render(StaticValue())
Expand Down
12 changes: 6 additions & 6 deletions packages/nanoviews/src/flow/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {
isAccessor,
boolean
} from 'kida'
import {
type TruthyValueOrSignal,
type FalsyValueOrSignal,
type Child,
decide
import type {
TruthyValueOrSignal,
FalsyValueOrSignal,
Child
} from '../internals/index.js'
import { swap_ } from './swap.js'

/**
* Decide which child to render based on condition
Expand All @@ -24,7 +24,7 @@ export function if_<T>($value: T) {
return (
then_: (value: TruthyValueOrSignal<T>) => Child,
else_?: (value: FalsyValueOrSignal<T>) => Child
) => decide(
) => swap_(
isAccessor($value) ? boolean($value) : $value as boolean,
confition => (
confition
Expand Down
1 change: 1 addition & 0 deletions packages/nanoviews/src/flow/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './swap.js'
export * from './if.js'
export * from './switch.js'
export * from './for.js'
Expand Down
49 changes: 49 additions & 0 deletions packages/nanoviews/src/flow/swap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
describe,
it,
expect,
vi
} from 'vitest'
import { render } from '@nanoviews/testing-library'
import { signal } from 'kida'
import {
b,
i
} from '../elements/elements.js'
import { swap_ } from './swap.js'

describe('nanoviews', () => {
describe('flow', () => {
describe('swap', () => {
it('should render a static value without subscribing', () => {
const render_ = vi.fn((value: string) => b()(value))
const { container } = render(() => swap_('static', render_))

expect(container.innerHTML).toBe('<div><b>static</b></div>')
expect(render_).toHaveBeenCalledTimes(1)
})

it('should build the child anew on every change', () => {
const $tab = signal('list')
const { container } = render(() => swap_(
$tab,
tab => (tab === 'list' ? b()(tab) : i()(tab))
))
const [first] = container.getElementsByTagName('b')

expect(container.innerHTML).toBe('<div><b>list</b></div>')

$tab('grid')

expect(container.innerHTML).toBe('<div><i>grid</i></div>')

// the child is rebuilt, not updated: the node the first value made
// is gone rather than reused
$tab('list')

expect(container.innerHTML).toBe('<div><b>list</b></div>')
expect(container.getElementsByTagName('b')[0]).not.toBe(first)
})
})
})
})
26 changes: 26 additions & 0 deletions packages/nanoviews/src/flow/swap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
type ValueOrAccessor,
isAccessor
} from 'kida'
import {
type Child,
swap
} from '../internals/index.js'

/**
* Render a child decided by a value. Unlike a binding, which updates content
* in place, the child is built anew every time the value changes.
* @param $value - Static value or store
* @param render - Function that returns child for the value
* @returns Block that renders the child and swaps it on change
*/
export function swap_<T>(
$value: ValueOrAccessor<T>,
render: (value: T) => Child
) {
if (isAccessor($value)) {
return swap($value, render)
}

return render($value)
}
2 changes: 1 addition & 1 deletion packages/nanoviews/src/flow/switch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const {
} = composeStories(Stories)

describe('nanoviews', () => {
describe('logic', () => {
describe('flow', () => {
describe('switch', () => {
it('should handle static value', () => {
const { container } = render(StaticValue())
Expand Down
8 changes: 3 additions & 5 deletions packages/nanoviews/src/flow/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import type {
MaybeAccessorValue,
ValueOrAccessor
} from 'kida'
import {
type Child,
decide
} from '../internals/index.js'
import type { Child } from '../internals/index.js'
import { swap_ } from './swap.js'

export type SwitchCase<T> = readonly [T | typeof default_, () => Child]

Expand All @@ -25,7 +23,7 @@ export function switch_<T>($value: ValueOrAccessor<T>) {
return (...cases: SwitchCase<Value>[]) => {
const casesMap = new Map<unknown, () => Child>(cases)

return decide($value, value => (
return swap_($value, value => (
casesMap.has(value)
? casesMap.get(value)!()
: casesMap.get(default_)?.()
Expand Down
67 changes: 0 additions & 67 deletions packages/nanoviews/src/internals/flow/decide.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/nanoviews/src/internals/flow/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './decide.js'
export * from './swap.js'
export * from './loop.js'
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
context,
inject
} from '../../component/context.js'
import { decide } from './decide.js'
import { swap } from './swap.js'

describe('nanoviews', () => {
describe('internals', () => {
describe('logic', () => {
describe('decide', () => {
describe('flow', () => {
describe('swap', () => {
it('should save context', () => {
const $value = signal(true)
const ThemeContext = () => 'light'
Expand All @@ -26,7 +26,7 @@ describe('nanoviews', () => {
const decider = vi.fn(value => (value ? ComponentA() : ComponentB()))
const Swapper = () => context(
[provide(ThemeContext, 'dark')],
() => decide($value, decider)
() => swap($value, decider)
)
const { container } = render(Swapper)

Expand Down
48 changes: 48 additions & 0 deletions packages/nanoviews/src/internals/flow/swap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
type Accessor,
type DeferredScope,
effect
} from 'kida'
import type { Child } from '../types/index.js'
import {
deferScopeBindContext,
effectScopeSwapper
} from '../effects.js'
import { createTextNode } from '../elements/text.js'
import {
insertChildBeforeAnchor,
removeBetween
} from '../elements/child.js'

export function swap<T>(
$value: Accessor<T>,
render: (value: T) => Child
) {
const start = createTextNode()
const end = createTextNode()
const deferScope = deferScopeBindContext()
const fragment = document.createDocumentFragment()

fragment.append(start, end)

// The replaced scope is destroyed first, while its DOM is still
// attached; then the body removes it and renders the new content
effectScopeSwapper($value, (
destroyPrev: DeferredScope | undefined,
value: T
) => deferScope(() => {
if (destroyPrev !== undefined) {
removeBetween(start, end)
}

insertChildBeforeAnchor(render(value), end)
}, destroyPrev))

// The echo: content that writes the value 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 value
// and re-queues the parked swapper for the corrective swap
effect(() => void $value(), true)

return fragment
}
3 changes: 1 addition & 2 deletions todo.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@

- 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_ crashes on duplicate track keys (pre-existing, loop.ts reconcile matched/stashed)
- 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?
- rename decide? export as public - as Dynamic in solidjs
- typed effectattrs <T>
- additional arg for record in for$? as$
- controls addEventListener to events system
Expand Down