From 6ec88944464434370b2eb87ecf42cb25a213f90e Mon Sep 17 00:00:00 2001 From: skirtle <65301168+skirtles-code@users.noreply.github.com> Date: Mon, 18 May 2026 17:01:46 +0100 Subject: [PATCH] Add `countChildren` --- packages/docs/src/api.md | 21 +++++++++++++++ packages/docs/src/guide/iterators.md | 3 ++- packages/docs/src/guide/other-helpers.md | 27 +++++++++++++++++++ .../src/__tests__/iterators.spec.ts | 27 +++++++++++++++++++ packages/vue-vnode-utils/src/index.ts | 1 + packages/vue-vnode-utils/src/iterators.ts | 17 ++++++++++++ 6 files changed, 95 insertions(+), 1 deletion(-) diff --git a/packages/docs/src/api.md b/packages/docs/src/api.md index ba2ce88..28a0ff9 100644 --- a/packages/docs/src/api.md +++ b/packages/docs/src/api.md @@ -60,6 +60,27 @@ The passed array and its contents will be left unmodified. Any fragment nodes wi * [Example - Inserting between children](/examples.html#inserting-between-children) * [Guide - Inserting new nodes](/guide/inserting-new-nodes.html) +## countChildren() + +### Type + +```ts +function countChildren( + children: VNodeArrayChildren, + options: IterationOptions = ALL_VNODES +): number +``` + +### Description + +Counts the number of 'top-level' VNodes. The children of a fragment will be considered 'top-level' nodes rather than the fragment itself. + +The [`options`](#iterationoptions) object can be used to decide which node types should be counted. If no options object is passed then all nodes will be counted. If an `options` object is passed, all nodes will be skipped by default unless explicitly ruled in. + +### See also + +* [Guide - Other helpers](/guide/other-helpers.html) + ## eachChild() ### Type diff --git a/packages/docs/src/guide/iterators.md b/packages/docs/src/guide/iterators.md index 70da26d..745ac3f 100644 --- a/packages/docs/src/guide/iterators.md +++ b/packages/docs/src/guide/iterators.md @@ -86,7 +86,8 @@ export default function ChildComponent(_, { slots }) { 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=) +For the specific case of counting children, the dedicated [`countChildren()`](./other-helpers#counting-children) helper could be used instead. -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. +While these examples need to display the count, a more common scenario involves only needing to know whether the count is 0. The [`isEmpty()`](./other-helpers#checking-for-an-empty-slot) 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 `
  • ` 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 `
  • ` element. diff --git a/packages/docs/src/guide/other-helpers.md b/packages/docs/src/guide/other-helpers.md index 04da8ca..04854cf 100644 --- a/packages/docs/src/guide/other-helpers.md +++ b/packages/docs/src/guide/other-helpers.md @@ -1,5 +1,32 @@ # Other helpers +## Counting children + +A helper for counting the children. + +This uses the same iteration logic as the various [iterators](./iterators). Fragments are not counted directly, with the children of any fragments being counted instead. + +The second argument of `countChildren()` is optional and allows [iteration options](/api.html#iterationoptions) to be specified, controlling the types of VNodes that are counted. + +```js +import { h } from 'vue' +import { countChildren, SKIP_COMMENTS } from '@skirtle/vue-vnode-utils' + +function ChildComponent(_, { slots }) { + const children = slots.default?.() ?? [] + const count = countChildren(children, 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/#eNp9VGtv0zAU/StXAamtaJPykJDCxoBp4v0Qm8QHgiBLnNara0e20xVV/e8c22nWdBvaPjS+55577vU93kSv6zpeNSxKoyNTaF5bMsw29ctM8mWttKUNaZYXlq8YbanSakkD4Add/BM39ge38wtlc9EC4qR3Gl8Z4DNZKGksccuWho472uHPTBLKWLa2KQ2+s3IwJjNX1ylZ3aDquBd/qxmTHaLKhbkFeSMgsM+RyV+jGwku8loIiBiO6PglbVx6pTQNbySSqoLUUQiT/4pdKvIcqzsFMf6PkjA7TA0fgNUitwxfREeXjbVK0qtC8GJxnEVt7SzyYaJzR5jjwIGTgA6ZAjOcXGOIE+um2OI7elpNIBmMXi2XQW3H6wmA4VUL8dL3whjYJvTkxkZb9LEL3BK937ofOXje8ZL1BPvMRPCd0KQ/CEQO++lBonEUVmqyzGusjJJYSj961PcBNJfuLiOLXpkF11awBOs4WUlVskljufCgLJpbW5s0SRpZL2ZxoZbJffikhK7Dw5iZ5eRSq2vDdFyyFfRk0e7CofRwvyG188v8bqNsqFCNtKdzLkrN5JjOP77/9vv06+fPZ18uzruc+2R6B7G1pypZlTfCUtVIWAgX5UlPFQpJJu3w9xjVjFDW0LZd37DYRVsc1+jDaM0zncQwwskJ/fy1h3VqAeypHu4YDuR7cxE8bRstaT4clHwFC3pr0833H88TKFN6uPE/tn9G3r+usCt54vCNAHxXbEQpySZ4xPkYl4A7MPavYCYujJt+u7GPCIVCx5dKl0yn9Lhek1GCl/RgOp2+cKFlrmdcYg1rhKf1uj1cYztLO0/pyXR3WOdlyeWsg6FyBiVthbxYzDQ0lyk9qJ67P590R+WiKPYqBzqakpPjKWFVT9mrF8Lo1BpcScVnB67AVtdcMP21dkvQdwdeFHX9wZ+5t6odMHLmrFjccX5l1sE43zTDzq/g8C5mIZrZED47/4LXYi+4VGUjgP5P8DvDFLDESgbYG0wMsvdwXu177xN0f2HO1pZJs2uqe2w9PovgDLfr97V+I/dp/Kzz7PYf9mdPuQ==) | [Options API](https://play.vuejs.org/#eNp9Vftv0zAQ/ldOAamdaJPykJDCYMA08X6ITeIHgiCLncara0f2pSuq+r9zfiRry4Y2qfXdd9/d+e5zN8mrtk1XHU/y5NhWRrT4olBi2WqD8FFY/C6wudBYSqiNXsIozfas6ZUdFQqgUHztYxivy04ibJy10kSkuEKbBwPsczrTdhLiAViJ5fioBxqOnVH9CUAgXxLNj/4MsAHka8xh9I2z0QRso69zQNPxyHmAemM4VwOuLqW9A/hadvyAr0f9DF/82cW6zyXHRrObDl3cKyn7TgossNYGxpVWFn0boGvARtjU9zR03HeZOgZ47lP3nlhBSFwo+j/OhnHRgcJaWSKnE8DxZYeoFbyspKgWz4skllQk3g1w7hKUZHDgLKBDpKTxTK9pPlN0A4r4gR5WU+qFGH0bQoWpDLyegDCijhDfyo6bbnkTenR3DdvhYm8pevcq/LSI561gfK9gH5lJ0Rea7V8EeQ772YMkkyTs+nRZtrTLWpEOwtiig5obJlskL+1CGJQ8I8VMV0ozPu1QSA8qkgaxtXmWdapdzFNa/uwufMaorkNjyu1yemn0teUmZXxF9RRJP3Cq9FB4VGoU6gYa2EaBEikpcnBUulN42gjJDFcTOP/w7uuv0y+fPp19vjgfYu4qk4j+EXbdqQoFDcqTnvYKH/+aUDYrNVrYxpUOG1/F5DRG76bWPNNJShI5OYEfXlQR66ol4F7V457hoPyjoL/4UDTjERMr0m18IYbzb88TKHO4v/Fftr+PovRDyhOH7yTB+2RHkIPqgkZ+UioaAs3A4h/JbVpZd/txYx8AJQodX2rDuMnhYbsGq6VgcG82mz3zz0Rp5kLRGrbknrXraFzTdjJscng0641tyZhQ8wFGmQuqJGYoq8XcUM0sh3v1U/fng27JXFXVTuZABzNw5XhKkqqn3MsX3NQpWhpJLeYHqnBPupDcfGndEuyrg14Uff3e29zbFS+YYhpeLW6xX9l1EM5Xw2nnV6TwwYdUNMfgPjv/TK/FjnOpWScJ/R/nN063QEusVYC9phujsndwvtp3XifU/YU9WyNXtm+qf3zjz1NCynC7flfrN+U+Tp8Mmt3+Bfk4a4M=) + +If you need more fine-grained control over the counting you can use [`eachChild()` or `reduceChildren()`](./iterators) instead. + ## Extracting a single child Slots return an array of children, but some components only allow for one child in their slot, like Vue's built-in components `` and ``. `extractSingleChild()` can be used to pull out a meaningful root node from a slot. It skips over fragments, comments and text nodes, trying to find a component or element node. diff --git a/packages/vue-vnode-utils/src/__tests__/iterators.spec.ts b/packages/vue-vnode-utils/src/__tests__/iterators.spec.ts index 0097d63..bed6479 100644 --- a/packages/vue-vnode-utils/src/__tests__/iterators.spec.ts +++ b/packages/vue-vnode-utils/src/__tests__/iterators.spec.ts @@ -28,6 +28,7 @@ import { ALL_VNODES, betweenChildren, COMPONENTS_AND_ELEMENTS, + countChildren, eachChild, everyChild, extractSingleChild, @@ -1615,3 +1616,29 @@ describe('extractSingleChild', () => { expect(out).toBe(node) }) }) + +describe('countChildren', () => { + it('countChildren - 129c', () => { + expect(countChildren([])).toBe(0) + expect(countChildren([''])).toBe(1) + expect(countChildren(['Text'])).toBe(1) + expect(countChildren(['Text', 'Text'])).toBe(2) + expect(countChildren([['Text', 'Text', ['Text']], 'Text'])).toBe(4) + expect(countChildren([null])).toBe(1) + expect(countChildren([false])).toBe(1) + expect(countChildren([true])).toBe(1) + expect(countChildren([h('div')])).toBe(1) + expect(countChildren([h('div', {}, [h('span')])])).toBe(1) + expect(countChildren([h({ template: 'abc' })])).toBe(1) + }) + + it('countChildren - 9294', () => { + const children = [[], ['Text'], '', h('div'), h('span'), null, h({ template: 'abc' })] + + expect(countChildren(children)).toBe(6) + expect(countChildren(children, ALL_VNODES)).toBe(6) + expect(countChildren(children, COMPONENTS_AND_ELEMENTS)).toBe(3) + expect(countChildren(children, SKIP_COMMENTS)).toBe(5) + expect(countChildren(children, {})).toBe(0) + }) +}) diff --git a/packages/vue-vnode-utils/src/index.ts b/packages/vue-vnode-utils/src/index.ts index f4915b7..0163a38 100644 --- a/packages/vue-vnode-utils/src/index.ts +++ b/packages/vue-vnode-utils/src/index.ts @@ -16,6 +16,7 @@ export { ALL_VNODES, betweenChildren, COMPONENTS_AND_ELEMENTS, + countChildren, eachChild, everyChild, extractSingleChild, diff --git a/packages/vue-vnode-utils/src/iterators.ts b/packages/vue-vnode-utils/src/iterators.ts index 492c596..b0d8fce 100644 --- a/packages/vue-vnode-utils/src/iterators.ts +++ b/packages/vue-vnode-utils/src/iterators.ts @@ -429,3 +429,20 @@ export const extractSingleChild = (children: VNodeArrayChildren): VNode | undefi return node } + +export const countChildren = ( + children: VNodeArrayChildren, + options: IterationOptions = ALL_VNODES +) => { + if (__DEV__) { + checkArguments('count', [children, options], ['array', 'object']) + } + + let count = 0 + + someChildInternal(children, () => { + ++count + }, options) + + return count +}