Skip to content

Commit 9d88762

Browse files
committed
chore: forAwait$ reverse option
1 parent cd97717 commit 9d88762

File tree

8 files changed

+150
-36
lines changed

8 files changed

+150
-36
lines changed

packages/nanoviews/src/internals/logic/async.spec.ts

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,39 +53,37 @@ describe('nanoviews', () => {
5353
})
5454

5555
it('should render with initial footer', () => {
56-
it('should render without initial value', () => {
57-
let addBlock: GetChildHook
58-
let setFooterBlock: GetChildHook
59-
let resetBlocks: GetChildHook
60-
const list = createAsyncList('Footer', (a, s, r) => {
61-
addBlock = a
62-
setFooterBlock = s
63-
resetBlocks = r
64-
})
65-
const { container } = render(list)
56+
let addBlock: GetChildHook
57+
let setFooterBlock: GetChildHook
58+
let resetBlocks: GetChildHook
59+
const list = createAsyncList('Footer', (a, s, r) => {
60+
addBlock = a
61+
setFooterBlock = s
62+
resetBlocks = r
63+
})
64+
const { container } = render(list)
6665

67-
expect(container.innerHTML).toBe('<div>Footer</div>')
66+
expect(container.innerHTML).toBe('<div>Footer</div>')
6867

69-
addBlock!(() => createElement('div')('A'))
68+
addBlock!(() => createElement('div')('A'))
7069

71-
expect(container.innerHTML).toBe('<div><div>A</div>Footer</div>')
70+
expect(container.innerHTML).toBe('<div><div>A</div>Footer</div>')
7271

73-
setFooterBlock!(() => 'retooF')
72+
setFooterBlock!(() => 'retooF')
7473

75-
expect(container.innerHTML).toBe('<div><div>A</div>retooF</div>')
74+
expect(container.innerHTML).toBe('<div><div>A</div>retooF</div>')
7675

77-
addBlock!(() => createElement('div')('B'))
76+
addBlock!(() => createElement('div')('B'))
7877

79-
expect(container.innerHTML).toBe('<div><div>A</div><div>B</div>retooF</div>')
78+
expect(container.innerHTML).toBe('<div><div>A</div><div>B</div>retooF</div>')
8079

81-
resetBlocks!()
80+
resetBlocks!()
8281

83-
expect(container.innerHTML).toBe('<div></div>')
82+
expect(container.innerHTML).toBe('<div></div>')
8483

85-
resetBlocks!(() => 'Footer')
84+
resetBlocks!(() => 'Footer')
8685

87-
expect(container.innerHTML).toBe('<div>Footer</div>')
88-
})
86+
expect(container.innerHTML).toBe('<div>Footer</div>')
8987
})
9088

9189
it('should call destroy function', () => {
@@ -131,6 +129,40 @@ describe('nanoviews', () => {
131129

132130
expect(container.innerHTML).toBe('<div><div>(C) dark</div></div>')
133131
})
132+
133+
it('should render in reversed order', () => {
134+
let addBlock: GetChildHook
135+
let setFooterBlock: GetChildHook
136+
let resetBlocks: GetChildHook
137+
const list = createAsyncList('Footer', (a, s, r) => {
138+
addBlock = a
139+
setFooterBlock = s
140+
resetBlocks = r
141+
}, true)
142+
const { container } = render(list)
143+
144+
expect(container.innerHTML).toBe('<div>Footer</div>')
145+
146+
addBlock!(() => createElement('div')('A'))
147+
148+
expect(container.innerHTML).toBe('<div>Footer<div>A</div></div>')
149+
150+
setFooterBlock!(() => 'retooF')
151+
152+
expect(container.innerHTML).toBe('<div>retooF<div>A</div></div>')
153+
154+
addBlock!(() => createElement('div')('B'))
155+
156+
expect(container.innerHTML).toBe('<div>retooF<div>B</div><div>A</div></div>')
157+
158+
resetBlocks!()
159+
160+
expect(container.innerHTML).toBe('<div></div>')
161+
162+
resetBlocks!(() => 'Footer')
163+
164+
expect(container.innerHTML).toBe('<div>Footer</div>')
165+
})
134166
})
135167
})
136168
})

packages/nanoviews/src/internals/logic/async.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export function getAbortFromController(
8585
* Create host block that can asynchronously render children list
8686
* @param initialFooter - Initial footer child
8787
* @param mutator - Mutator callback function
88+
* @param reverse - Reverse order of children
8889
* @returns Host block
8990
*/
9091
export function createAsyncList(
@@ -93,8 +94,10 @@ export function createAsyncList(
9394
add: GetChildHook,
9495
setFooter: GetChildHook,
9596
reset: GetChildHook
96-
) => Effect<void> | void
97+
) => Effect<void> | void,
98+
reverse?: boolean
9799
) {
100+
const insertMode = reverse ? 1 : -1
98101
const [getCurrentContextStack, provideContext] = getContextDiftsContainer()
99102
const context = getCurrentContextStack()
100103
let footer: Block | null = childToBlock(initialFooter)
@@ -103,7 +106,7 @@ export function createAsyncList(
103106
const child = provideContext(context, getChild)
104107

105108
if (!isEmpty(child)) {
106-
blocks!.splice(-1, 0, swap(footer!, childToBlock(child), true))
109+
blocks!.splice(-1, 0, swap(footer!, childToBlock(child), insertMode))
107110
// eslint-disable-next-line @typescript-eslint/no-use-before-define
108111
proxyBlock.n = blocks![0].n
109112
}

packages/nanoviews/src/internals/logic/swap.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,29 @@ describe('nanoviews', () => {
3737
expect(container.innerHTML).toBe('<div><div>A</div></div>')
3838
})
3939

40-
it('should only insert element', () => {
40+
it('should only insert element before', () => {
4141
const a = createElement('div')('A')
4242
const b = createElement('div')('B')
4343
const { container } = render(a)
4444

4545
expect(container.innerHTML).toBe('<div><div>A</div></div>')
4646

47-
swap(a, b, true)
47+
swap(a, b, -1)
4848

4949
expect(container.innerHTML).toBe('<div><div>B</div><div>A</div></div>')
5050
})
51+
52+
it('should only insert element after', () => {
53+
const a = createElement('div')('A')
54+
const b = createElement('div')('B')
55+
const { container } = render(a)
56+
57+
expect(container.innerHTML).toBe('<div><div>A</div></div>')
58+
59+
swap(a, b, 1)
60+
61+
expect(container.innerHTML).toBe('<div><div>A</div><div>B</div></div>')
62+
})
5163
})
5264

5365
describe('createSwapper', () => {

packages/nanoviews/src/internals/logic/swap.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,27 @@ import { getContextDiftsContainer } from './context.js'
1414
* Swap blocks
1515
* @param prevBlock - Previous block
1616
* @param nextBlock - Next block
17-
* @param insertOnly - Insert next block without removing previous block
17+
* @param insertOnlyMode - Insert next block before or after without removing previous block
1818
* @returns Next block
1919
*/
2020
export function swap(
2121
prevBlock: Block,
2222
nextBlock: Block,
23-
insertOnly?: boolean
23+
insertOnlyMode?: 1 | -1
2424
) {
2525
if (prevBlock === nextBlock) {
2626
return prevBlock
2727
}
2828

29-
const anchor = prevBlock.n
29+
const prevNode = prevBlock.n
30+
const anchor = insertOnlyMode as number > 0
31+
? prevNode!.nextSibling
32+
: prevNode
3033

3134
nextBlock.c()
32-
nextBlock.m(anchor!.parentNode!, anchor)
35+
nextBlock.m(prevNode!.parentNode!, anchor)
3336

34-
if (!insertOnly) {
37+
if (!insertOnlyMode) {
3538
prevBlock.d()
3639
}
3740

packages/nanoviews/src/logic/forAwait.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { render } from '@nanoviews/testing-library'
88
import { atom } from 'nanostores'
99
import * as Stories from './forAwait.stories.js'
1010

11-
const { PendingState: Demo } = composeStories(Stories)
11+
const {
12+
PendingState: Demo,
13+
Reversed: ReversedDemo
14+
} = composeStories(Stories)
1215

1316
function withResolvers<T>() {
1417
let resolve: (value: T) => void
@@ -126,6 +129,36 @@ describe('nanoviews', () => {
126129

127130
expect(container.innerHTML).toBe('<div><ul><li>Item #0: Number one</li><b>Loading...</b></ul></div>')
128131
})
132+
133+
it('should handle async iterable in reversed order', async () => {
134+
const [stream, promises] = mockStream<string>()
135+
const { container } = render(ReversedDemo({
136+
getStream: () => stream
137+
}))
138+
139+
expect(container.innerHTML).toBe('<div><ul><b>Loading...</b></ul></div>')
140+
141+
promises[0].resolve('First')
142+
143+
await promises[0].promise
144+
// Step over next async generator microtask
145+
await Promise.resolve()
146+
147+
expect(container.innerHTML).toBe('<div><ul><b>Loading...</b><li>Item #0: First</li></ul></div>')
148+
149+
promises[1].resolve('Second')
150+
151+
await promises[1].promise
152+
// Step over next async generator microtask
153+
await Promise.resolve()
154+
155+
expect(container.innerHTML).toBe('<div><ul><b>Loading...</b><li>Item #1: Second</li><li>Item #0: First</li></ul></div>')
156+
157+
// Step over next async generator microtask
158+
await Promise.resolve()
159+
160+
expect(container.innerHTML).toBe('<div><ul><b>Total: 2</b><li>Item #1: Second</li><li>Item #0: First</li></ul></div>')
161+
})
129162
})
130163
})
131164
})

packages/nanoviews/src/logic/forAwait.stories.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ function Demo({ getStream }: { getStream(): AtomStream }) {
4444
)
4545
}
4646

47+
function ReversedDemo({ getStream }: { getStream(): AtomStream }) {
48+
return ul()(
49+
forAwait$(getStream(), true)(
50+
pending$(() => b()('Loading...')),
51+
each$((value, i) => li()(`Item #${i}: ${value}`)),
52+
then$(value => b()('Total: ', value)),
53+
catch$(error => b()('Rejected: ', String(error)))
54+
)
55+
)
56+
}
57+
4758
export const PendingState: Story = {
4859
args: {
4960
getStream: mockStream(() => new Promise(() => { /* pending */ }), 3)
@@ -122,3 +133,17 @@ export const DelayedReject: Story = {
122133
},
123134
render: nanoStory(Demo)
124135
}
136+
137+
export const Reversed: Story = {
138+
args: {
139+
getStream: mockStream(i => Promise.resolve(`${i * i}`), 3)
140+
},
141+
render: nanoStory(ReversedDemo)
142+
}
143+
144+
export const DelayedReversed: Story = {
145+
args: {
146+
getStream: mockStream(i => delayedResolve(`${i * i}`, 2000), 3)
147+
},
148+
render: nanoStory(ReversedDemo)
149+
}

packages/nanoviews/src/logic/forAwait.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ function cancellableForAwait<T>(
6464
/**
6565
* Iterate over async iterable and render each value, pending, resolved or rejected state
6666
* @param $asyncIterable - Async iterable or store with it
67-
* @param $abortController - AbortController or store with it
67+
* @param $abortControllerOrReverse - AbortController or store with it, or render items in reversed order
68+
* @param maybeReverse - Render items in reversed order
6869
* @returns Async iterable renderer
6970
*/
7071
export function forAwait$<T>(
7172
$asyncIterable: ValueOrStore<AsyncIterable<T>>,
72-
$abortController?: ValueOrStore<AbortController | EmptyValue>
73+
$abortControllerOrReverse?: ValueOrStore<AbortController | EmptyValue> | boolean,
74+
maybeReverse?: boolean
7375
): ChildrenBlockWithOnlySlots<[PendingSlot, EachSlot<T>, ThenSlot<number>, CatchSlot], Node> {
76+
const [$abortController, reverse] = typeof $abortControllerOrReverse === 'boolean'
77+
? [undefined, $abortControllerOrReverse]
78+
: [$abortControllerOrReverse, maybeReverse]
7479
const abort = getAbortFromController($abortController)
7580

7681
return getChildren(
@@ -113,6 +118,6 @@ export function forAwait$<T>(
113118
unsubscribe = null
114119
cancel = null
115120
}
116-
})
121+
}, reverse)
117122
)
118123
}

packages/nanoviews/vite.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export default defineConfig({
2020
environment: 'happy-dom',
2121
setupFiles: ['./test/setup.ts'],
2222
coverage: {
23-
reporter: ['lcovonly', 'text']
23+
reporter: ['lcovonly', 'text'],
24+
include: ['**', '!**/*.stories.ts']
2425
}
2526
}
2627
})

0 commit comments

Comments
 (0)