Skip to content

Commit d4bdaa2

Browse files
Add reduceChildren iterator (#8)
1 parent 91e5a13 commit d4bdaa2

File tree

4 files changed

+114
-9
lines changed

4 files changed

+114
-9
lines changed

packages/docs/src/api.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,37 @@ Options that can be passed to the iterators to filter the node types that should
377377

378378
Some common configurations are available via the constants `ALL_VNODES`, `COMPONENTS_AND_ELEMENTS` and `SKIP_COMMENTS`.
379379

380+
## reduceChildren() <Badge text="0.2+" />
381+
382+
### Type
383+
384+
```ts
385+
function reduceChildren<R>(
386+
children: VNodeArrayChildren,
387+
callback: (previousValue: R, vnode: VNode) => R,
388+
initialValue: R,
389+
options: IterationOptions = ALL_VNODES
390+
): R
391+
```
392+
393+
### Description
394+
395+
An iterator for 'top-level' nodes, comparable to `Array.protoype.reduce`. The children of a fragment will be considered 'top-level' nodes rather than the fragment itself.
396+
397+
The callback will be called for each VNode, which will be passed as the second argument. The first argument will be the previous value of the reduction. The callback should return the new value of the reduction.
398+
399+
`reduceChildren()` will return the final value of the reduction, i.e. the value returned by the callback for the last VNode.
400+
401+
Unlike `Array.prototype.reduce`, the `initialValue` is a required argument for `reduceChildren()`.
402+
403+
The callback will be passed fully instantiated VNodes. Children will be converted to VNodes as required.
404+
405+
The [`options`](#iterationoptions) object can be used to decide which node types should be passed to the callback. If no options object is passed then all nodes will be iterated. If an `options` object is passed, all nodes will be skipped by default unless explicitly ruled in.
406+
407+
### See also
408+
409+
* [Guide - Iterators](/guide/iterators.html)
410+
380411
## replaceChildren()
381412

382413
### Type

packages/docs/src/guide/iterators.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
`vue-vnode-utils` provides several iterator functions that can be used to walk slot VNodes *without* modifying them. They are roughly equivalent to the iterator methods found on arrays:
44

5-
| Array | vue-vnode-utils |
6-
|-----------|----------------------|
7-
| forEach() | eachChild() |
8-
| every() | everyChild() |
9-
| find() | findChild() |
10-
| some() | someChild() |
5+
| Array | vue-vnode-utils |
6+
|-----------|------------------|
7+
| forEach() | eachChild() |
8+
| every() | everyChild() |
9+
| find() | findChild() |
10+
| some() | someChild() |
11+
| reduce() | reduceChildren() |
1112

12-
Each of the iterators takes three arguments. The first is the array of children to iterate, which is usually created by calling a slot function. The second is a callback function that will be passed the top-level VNodes in the order they appear.
13+
Most of the iterators take three arguments, except `reduceChildren()` which takes four. The first is the array of children to iterate, which is usually created by calling a slot function. The second is a callback function that will be passed the top-level VNodes in the order they appear.
1314

1415
```js
1516
import { eachChild } from '@skirtle/vue-vnode-utils'
@@ -35,7 +36,7 @@ The iterator callback will be passed a fully instantiated VNode, even if the ori
3536
3637
Fragment nodes are never passed to the iterator callback. Instead, the iterator will iterate through the children of the fragment. The iterators do not walk the children of any other node type, just fragments. They are only attempting to iterate what would generally be considered the 'top-level' VNodes.
3738
38-
The optional third argument for each iterator is an object containing [iteration options](/api.html#iterationoptions). The iterators will usually pass all node types to the callback, but the options can be used to restrict iteration to specific types of node. The available node types are `component`, `element`, `text`, `comment` and `static`.
39+
The optional final argument for each iterator is an object containing [iteration options](/api.html#iterationoptions). The iterators will usually pass all node types to the callback, but the options can be used to restrict iteration to specific types of node. The available node types are `component`, `element`, `text`, `comment` and `static`.
3940
4041
So if we only want to iterate over `text` nodes we can pass `{ text: true }` as the third argument.
4142
@@ -65,6 +66,27 @@ See it on the SFC Playground: [Composition API](https://play.vuejs.org/#eNp1VG1P
6566
6667
The example uses `SKIP_COMMENTS` to skip over the comment nodes created by the falsy `v-if` conditions.
6768
68-
While this example needs to display the count, a more common scenario involves only needing to know whether the count is 0. The [`isEmpty()`](/api.html#isempty) helper can be used in that case.
69+
We could also implement this example using `reduceChildren()`. Unlike the other iterators, `reduceChildren()` takes four arguments. The third argument should be the initial value of the reduction. This is similar to the native Array method `reduce()`, but with `reduceChildren()` the initial value is not optional.
70+
71+
```js
72+
import { h } from 'vue'
73+
import { reduceChildren, SKIP_COMMENTS } from '@skirtle/vue-vnode-utils'
74+
75+
export default function ChildComponent(_, { slots }) {
76+
const children = slots.default?.() ?? []
77+
78+
const count = reduceChildren(children, sum => sum + 1, 0, SKIP_COMMENTS)
79+
80+
return h('div', [
81+
h('div', `Child count: ${count}`),
82+
count ? h('ul', children) : null
83+
])
84+
}
85+
```
86+
87+
See it on the SFC Playground: [Composition API](https://play.vuejs.org/#eNp9VGuP0zAQ/CurgNRWtEl5SEjhjgNOJ94PcSfxgSDIJU5r6tiR7fSKqv53xnaaPrhDrdTaO56dXe94Hb1smnjZsiiNTkyheWPJMNs2zzPJ60ZpS2vSLC8sXzLaUKVVTQPgB338Azf2G7fzK2Vz0QHi5GA3/m2Az2ShpLHELasNnfa0w++ZJKSxbGVTGnxl5WBMZq5uUrK6RdbxQfy1Zkz2iCoX5h/IKwGBhxyZ/DHaSXCRl0JAxHBEp89p7Y5XStNwJ5FUFaSOQpj8KnZHcc6xul0Q43uShN6ha1gA1ojcMqyITq5ba5WkF4XgxeI0i7rcWeTDRJeOMMeGAycBHU4K9HBygyZOrOtih+/paTmBZDB6tVwGtT2vJwCGVx3ES98Lo2HrUJNrG21Qxzbwj+j90n3LwfOGl+xAsD+ZCL4Vmhw2ApHjeg4g0TgKIzWp8wYjoySG0rce+X0AxaXby8iiF2bBtRUswThOllKVbNJaLjwoi+bWNiZNklY2i1lcqDq5C5+U0HW8GTNTT661ujFMxyVbQk8WbS8cSo/nG1J7v8xvN4ozUtkW7HzORamZHNPl+7dffp5//vjx4tPVZX/oLp3eQmzluUpW5a2wVLUSHsJNedJzhUySSTv8OUY6I5Q1tOnmN0x20SXHPfowavNMZzGccHZG33/4hu/wqpXWm3Vf+nBLA5O1tXOQ+3lAD8c0PSoLrgt8Gs+KljQfDkq+hDu962m3/uXJQ8KU7q/9n82vkbe2k+OEnDl8KwDfKhhRSrIN9nEWx/3geoz9I5iJC+MuphvmB4REoRfXSpdMp/SwWZFRgpd0bzqdPnOhOtczLjGhDcLTZtVtrjC4pZ2n9Gi63WzysuRy1sOQOYOSLkNeLGYamsuU7lVP3ccfuiVzURR7mQMdTcnJ8ZRwsac8yBfCqNQaXFTFZ0eGwcA3XDD9uXHjcWgcPDbq5p3fc89Y12CcmbNiccv+b7MKnvqiGeywhPn7mIVoZkP44vITHpK9YK3KVgD9n+BXhi5gvJUMsFfoGGTv4bzat95CqP7KXKwsk2ZbVP8Oe3wWwTPOBXeVvpP7OH7S23nzF2B7Vmo=) | [Options API](https://play.vuejs.org/#eNp9Vftv0zAQ/ldOAamdaJPykJDCxoBp4v0Qm8QPBEEWO42pa0f2pSuq+r9zfiRry4Y2qfXdd9/d+e5zN8nLtk1XHU/y5NhWRrT4vFBi2WqD8EFY/CawudRYSqiNXsIozfas6W87KhRAofjaxzBel51E2DhrpYlIcYU2DwbY53Sm7STEA7ASy/FRDzQcO6P6E4BAviSa7/0ZYAPI15jD6CtnownYRl/ngKbjkfMA9dpwrgZcXUp7B/CV7PgBX4/6Eb74s4t1n0uOjWY3Hbq4l1L2nRRYYK0NjCutLPo2QNeAjbCp72nouO8ydQxw4lP3nlhBSFwo+j/OhnHRgcJaWSKnE8DxVYeoFbyopKgWJ0USSyoS7wa4cAlKMjhwFtAhUtJ4ptc0nym6AUX8QA+rKfVCjL4NocJUBl5PQBhRR4hvZcdNt7wJPbq7hu1wsbcUvXsVflrE80Ywvlewj8yk6AvN9i+CPIf97EGSSRJ2fbosW9plrUgHYWzRQc0Nky2SF3YhDEqekWKmK6UZn3YopAcVSYPY2jzLOtUu5iktf3YXPmNU16Ex5XY5vTL62nKTMr6ieoqkHzhVeig8KjUKdQMNbKNAiZQUOTgMZ13FzxohmeFqAhfv3375efb548fzT5cXQ9BddRLTP8quO1WhoEl50rNe4uOfE0pnpUYL27jTYeWrmJzm6N3Um2c6TUkjp6fw3atqB687hQTeL33c05AyuyWcPPcfD+DhBGYHbR31fPENacYjJlYk6fh4DOdfnjwkzOH+xn/Z/jqKr0Io5NThO0nwvoIjyEF1QT4/KBnNh8Zj8Y/kNq2sG0xc5gdAicJdXGnDuMnhYbsGq6VgcG82mz3zL0hp5kLRhrbknrXraFzT4jJscng0641tyZhQ8wFGmQuqJGYoq8XcUM0sh3v1U/fng27JXFXVTuZABzNw5XhKUrGn3MsX3NQpWhpULeYHgnGvvZDcfG7deuwLhx4bff3O29yzFi+YYhpeLW6x/7broKkvhpMcViT+wYdUNMfgPr/4RA/JjnOpWScJ/R/nV063QOutVYC9ohujsndwvtq3XkLU/aU9XyNXtm+qf5fjL1dCmnEquKv1m3Ifp08GOW//AlHHcjQ=)
88+
89+
90+
While these examples need to display the count, a more common scenario involves only needing to know whether the count is 0. The [`isEmpty()`](/api.html#isempty) helper can be used in that case.
6991
7092
It is worth noting that the count here is just a count of the VNodes. It is not necessarily an accurate count of the number of `<li>` elements. If any of the children had been a component it would have added 1 to the count, even though a component wouldn't necessarily render exactly one `<li>` element.

packages/vue-vnode-utils/src/__tests__/iterators.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
extractSingleChild,
3434
findChild,
3535
isEmpty,
36+
reduceChildren,
3637
replaceChildren,
3738
SKIP_COMMENTS,
3839
someChild
@@ -1517,6 +1518,40 @@ describe('findChild', () => {
15171518
})
15181519
})
15191520

1521+
describe('reduceChildren', () => {
1522+
it('reduceChildren - 0c8b', () => {
1523+
const startNodes = [h('p'), h({}), [false, 'text', h('span')]].map(toVNode)
1524+
1525+
const calledFor: VNode[] = []
1526+
1527+
let length = reduceChildren(startNodes, (value, vnode) => {
1528+
calledFor.push(vnode)
1529+
return value + 1
1530+
}, 0)
1531+
1532+
expect(length).toBe(5)
1533+
expect(calledFor).toHaveLength(5)
1534+
expect(calledFor[0]).toBe(startNodes[0])
1535+
expect(isComponent(calledFor[1])).toBe(true)
1536+
expect(isComment(calledFor[2])).toBe(true)
1537+
expect(isText(calledFor[3]) && getText(calledFor[3])).toBe('text')
1538+
expect(calledFor[4].type).toBe('span')
1539+
1540+
calledFor.length = 0
1541+
1542+
length = reduceChildren(startNodes, (value, vnode) => {
1543+
calledFor.push(vnode)
1544+
return value + 1
1545+
}, 0, COMPONENTS_AND_ELEMENTS)
1546+
1547+
expect(length).toBe(3)
1548+
expect(calledFor).toHaveLength(3)
1549+
expect(calledFor[0]).toBe(startNodes[0])
1550+
expect(isComponent(calledFor[1])).toBe(true)
1551+
expect(calledFor[2].type).toBe('span')
1552+
})
1553+
})
1554+
15201555
describe('isEmpty', () => {
15211556
it('isEmpty - 819a', () => {
15221557
expect(isEmpty([])).toBe(true)

packages/vue-vnode-utils/src/iterators.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,23 @@ export const findChild = (
355355
return node
356356
}
357357

358+
export const reduceChildren = <R>(
359+
children: VNodeArrayChildren,
360+
callback: (previousValue: R, vnode: VNode) => R,
361+
initialValue: R,
362+
options: IterationOptions = ALL_VNODES
363+
): R => {
364+
if (__DEV__) {
365+
checkArguments('reduceChildren', [children, callback, null, options], ['array', 'function', 'null', 'object'])
366+
}
367+
368+
someChildInternal(children, (vnode) => {
369+
initialValue = callback(initialValue, vnode)
370+
}, options)
371+
372+
return initialValue
373+
}
374+
358375
const COLLAPSIBLE_WHITESPACE_RE = /\S|\u00a0/
359376

360377
export const isEmpty = (children: VNodeArrayChildren): boolean => {

0 commit comments

Comments
 (0)