Skip to content

Commit

Permalink
Add reduceChildren iterator (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
skirtles-code authored Oct 12, 2024
1 parent 91e5a13 commit d4bdaa2
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 9 deletions.
31 changes: 31 additions & 0 deletions packages/docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,37 @@ Options that can be passed to the iterators to filter the node types that should

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

## reduceChildren() <Badge text="0.2+" />

### Type

```ts
function reduceChildren<R>(
children: VNodeArrayChildren,
callback: (previousValue: R, vnode: VNode) => R,
initialValue: R,
options: IterationOptions = ALL_VNODES
): R
```

### Description

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.

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.

`reduceChildren()` will return the final value of the reduction, i.e. the value returned by the callback for the last VNode.

Unlike `Array.prototype.reduce`, the `initialValue` is a required argument for `reduceChildren()`.

The callback will be passed fully instantiated VNodes. Children will be converted to VNodes as required.

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.

### See also

* [Guide - Iterators](/guide/iterators.html)

## replaceChildren()

### Type
Expand Down
40 changes: 31 additions & 9 deletions packages/docs/src/guide/iterators.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

`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:

| Array | vue-vnode-utils |
|-----------|----------------------|
| forEach() | eachChild() |
| every() | everyChild() |
| find() | findChild() |
| some() | someChild() |
| Array | vue-vnode-utils |
|-----------|------------------|
| forEach() | eachChild() |
| every() | everyChild() |
| find() | findChild() |
| some() | someChild() |
| reduce() | reduceChildren() |

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.
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.

```js
import { eachChild } from '@skirtle/vue-vnode-utils'
Expand All @@ -35,7 +36,7 @@ The iterator callback will be passed a fully instantiated VNode, even if the ori
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.
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`.
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`.
So if we only want to iterate over `text` nodes we can pass `{ text: true }` as the third argument.
Expand Down Expand Up @@ -65,6 +66,27 @@ See it on the SFC Playground: [Composition API](https://play.vuejs.org/#eNp1VG1P
The example uses `SKIP_COMMENTS` to skip over the comment nodes created by the falsy `v-if` conditions.
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.
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.
```js
import { h } from 'vue'
import { reduceChildren, SKIP_COMMENTS } from '@skirtle/vue-vnode-utils'

export default function ChildComponent(_, { slots }) {
const children = slots.default?.() ?? []

const count = reduceChildren(children, sum => sum + 1, 0, SKIP_COMMENTS)

return h('div', [
h('div', `Child count: ${count}`),
count ? h('ul', children) : null
])
}
```
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=)
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.
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.
35 changes: 35 additions & 0 deletions packages/vue-vnode-utils/src/__tests__/iterators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
extractSingleChild,
findChild,
isEmpty,
reduceChildren,
replaceChildren,
SKIP_COMMENTS,
someChild
Expand Down Expand Up @@ -1517,6 +1518,40 @@ describe('findChild', () => {
})
})

describe('reduceChildren', () => {
it('reduceChildren - 0c8b', () => {
const startNodes = [h('p'), h({}), [false, 'text', h('span')]].map(toVNode)

const calledFor: VNode[] = []

let length = reduceChildren(startNodes, (value, vnode) => {
calledFor.push(vnode)
return value + 1
}, 0)

expect(length).toBe(5)
expect(calledFor).toHaveLength(5)
expect(calledFor[0]).toBe(startNodes[0])
expect(isComponent(calledFor[1])).toBe(true)
expect(isComment(calledFor[2])).toBe(true)
expect(isText(calledFor[3]) && getText(calledFor[3])).toBe('text')
expect(calledFor[4].type).toBe('span')

calledFor.length = 0

length = reduceChildren(startNodes, (value, vnode) => {
calledFor.push(vnode)
return value + 1
}, 0, COMPONENTS_AND_ELEMENTS)

expect(length).toBe(3)
expect(calledFor).toHaveLength(3)
expect(calledFor[0]).toBe(startNodes[0])
expect(isComponent(calledFor[1])).toBe(true)
expect(calledFor[2].type).toBe('span')
})
})

describe('isEmpty', () => {
it('isEmpty - 819a', () => {
expect(isEmpty([])).toBe(true)
Expand Down
17 changes: 17 additions & 0 deletions packages/vue-vnode-utils/src/iterators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,23 @@ export const findChild = (
return node
}

export const reduceChildren = <R>(
children: VNodeArrayChildren,
callback: (previousValue: R, vnode: VNode) => R,
initialValue: R,
options: IterationOptions = ALL_VNODES
): R => {
if (__DEV__) {
checkArguments('reduceChildren', [children, callback, null, options], ['array', 'function', 'null', 'object'])
}

someChildInternal(children, (vnode) => {
initialValue = callback(initialValue, vnode)
}, options)

return initialValue
}

const COLLAPSIBLE_WHITESPACE_RE = /\S|\u00a0/

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

0 comments on commit d4bdaa2

Please sign in to comment.