diff --git a/packages/docs/.vitepress/config.mts b/packages/docs/.vitepress/config.mts index 19e8b98..afd65a2 100644 --- a/packages/docs/.vitepress/config.mts +++ b/packages/docs/.vitepress/config.mts @@ -136,6 +136,9 @@ export default ({ mode }: { mode: string }) => defineConfigWithTheme({ }, { text: 'Other helpers', link: '/guide/other-helpers.html' + }, { + text: 'With meta', + link: '/guide/with-meta.html' } ] }, { diff --git a/packages/docs/src/api.md b/packages/docs/src/api.md index 28a0ff9..ee45428 100644 --- a/packages/docs/src/api.md +++ b/packages/docs/src/api.md @@ -80,6 +80,7 @@ The [`options`](#iterationoptions) object can be used to decide which node types ### See also * [Guide - Other helpers](/guide/other-helpers.html) +* [Guide - With meta](/guide/with-meta.html) ## eachChild() diff --git a/packages/docs/src/guide/iterators.md b/packages/docs/src/guide/iterators.md index 745ac3f..e892af6 100644 --- a/packages/docs/src/guide/iterators.md +++ b/packages/docs/src/guide/iterators.md @@ -91,3 +91,19 @@ For the specific case of counting children, the dedicated [`countChildren()`](./ 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. + +The iteration index and total node count aren't passed to the callback by default. It's relatively easy to implement that yourself: + +```js +const children = slots.default?.() ?? [] +const count = countChildren(children) +let index = -1 + +eachChild(children, (vnode) => { + index++ + + // ... +}) +``` + +But if you'd like the index and length to be passed to the callback you can use the [`with-meta`](./with-meta) iterators instead. diff --git a/packages/docs/src/guide/other-helpers.md b/packages/docs/src/guide/other-helpers.md index 04854cf..6a45fef 100644 --- a/packages/docs/src/guide/other-helpers.md +++ b/packages/docs/src/guide/other-helpers.md @@ -25,6 +25,8 @@ function ChildComponent(_, { slots }) { 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 the child count inside the callback of an iterator then you might prefer to use [`with-meta`](./with-meta) instead, which will pass the count to the iteration callback. + If you need more fine-grained control over the counting you can use [`eachChild()` or `reduceChildren()`](./iterators) instead. ## Extracting a single child diff --git a/packages/docs/src/guide/with-meta.md b/packages/docs/src/guide/with-meta.md new file mode 100644 index 0000000..aaff2bc --- /dev/null +++ b/packages/docs/src/guide/with-meta.md @@ -0,0 +1,97 @@ +--- +# Updated automatically in production builds: see the VitePress config. +packageVersions: + vue: 3.5.34 + '@skirtle/vue-vnode-utils': 0.3.0 +--- +# With meta + +There are alternative versions of the iterator functions, with added support for accessing extra metadata for the current node. + +Currently, this metadata just includes the iteration index and length. + +To use this metadata you'll need to import the functions from `@skirtle/vue-vnode-utils/with-meta` rather than `@skirtle/vue-vnode-utils`. + +## Accessing meta from callbacks + +The meta is passed in an object as the final argument of the callback for each iterator. + +The `length` property is a number and should be equivalent to calling [`countChildren()`](./other-helpers#counting-children). It will use the same iteration options as the iterator, so if you're skipping nodes then those nodes won't be included in the `length`. For most iterators, the `length` is the number of times the callback will be called during the iteration. + +The `index` is zero-based, counting up to `length - 1`. + +The `length` is calculated lazily, so if it isn't accessed the nodes won't be counted. + +[`betweenChildren()`](./inserting-new-nodes) behaves a little differently. The `index` will still start at zero, but it will only count up to `length - 2`. The `length` will still reflect the number of nodes, like it would for the other iterators, but that won't match the number of calls to the callback. + +## Example + +```js +import { h } from 'vue' +import { addProps } from '@skirtle/vue-vnode-utils/with-meta' + +export default function AddStripes(_, { slots }) { + const children = addProps(slots.default(), (vnode, { index, length }) => { + return { + class: [ + index % 2 ? 'odd' : 'even', + { + first: index === 0, + last: index === length - 1 + } + ] + } + }) + + return h('div', children) +} +``` + +See it on the SFC Playground: [Composition API](https://play.vuejs.org/#eNqFVFtr2zAU/isHw4gDid3u8uI1u9KHDbaVdW/zGG504qiVJSPJaUrIf98n2bm0tJ0JROf26Vy+o03ysW2zVcdJkZy5uZWtJ8e+a9+VWjatsZ42ZHkxoblp2s6zoC0trGlohKDR3umjEJce0ewGa5YfVNm1g2ep50Y7D6BOe5oF1PTNeKeVnhsH7e6aNB3T7B19q/wya6p1ejIZzlKnESFbVarjCZ2ejMdAOcv77JE3BIC1qvIMiehMakDSatoYwWpWJjG+TMjftQxRd80VW8jAhngSTtUap1Mce4RDLVGGRsgVEBfGBgCS+IUCBv/wbTakabsd3HP491BHfYHiLN+nmkySvpnTpmrRMqMxk02IKQcD4AuKmqD74G6k9YpzDGK60qht2nmpXH4r/XLasK+Ce5ksvW9dkeedbm/qDP3Nn4wU0vlD+ENzxq6ZXllz69hmglfIsUxCNihyi+zvDRy57/mzfJwzG6qEuLCmdXv7/2uKROJ1BBC8qDrladHpuZdGH7Ew/TsBvlPGA3zcN22g31IqYVmDa7vr0+iHkiJcOp5QGu8OEFILXk9Isa496oikHEZgsSdW7yTgq8q5gn7vZOqD6QW9pPc0MkKMqKARr1iPJgenfXz4FtI6XwyBs9mMTo48iXDDPeuQ1pROD14D5Yj+9Icob7Ei4X/IeZmOQMgRtnroBszwwxC9Q5sWsn5AwLCWUrH90YZG3ydipZS5/Rp13mIld/r5kuc3vX5RKXcwXLt1T80Ly+DSistkb/OVrRnLGcznl995jfPeiAXuFLyfMf5kZxQ4Y3Tv9qnTAnkf+cV0v0QKSl3/cudrz9rtqgoVxIZF/zIBET8/U/sh3VfZ66NdcP5OYQ/mLixC1laiD8FB4NICr1a7fht7XuosDn3HUWVsgTEJWAnvkK2lnnrTPggJRLgXUVtmvTeDa731qprf1BbvnShIyXrp7ziUH8EfTybQ84nYKzy4j0cm238hNxSj) | [Options API](https://play.vuejs.org/#eNqFVNtu2zAM/RXCwBAH8CXd5SVLunVDHzZgW7HubR4Gx2JsNbJkSHLqIsi/j/ItTtF2gYGIInl4O9TBu6qqaF+jt/RWJtO8speJ5GWltIUrxm4tXaGBrVYlzKL4dBXdmVkiE4lNa8twm9bCwiGRAJkiAInSmmV3ARMsJx8D5wrAUpv688FGo621HCQHU0u7hHedfJw6ugi1RTbic4ulOUGNYN9SW0Rl2viLoD9z6duCm6hFD+BiMZ9PAySSvlU89oIEgq5EapEkgBWXFBn2YakYinXitTiJB/ahQhJlXW5Qk0yBSFy4U9rQ6YKOHcKpF61MN4zvCXGrtAMATp8rp7d3v8MBJBzbDMk8JvsOajIQuljFk1S9wOvmGJZpRdNSkmbctifpFRRg7F/ifTQ7rq3AmNgQ7iVVF9aWCxPfc1uEJdrUmSdeYW1llnFcy2qXUxfL+FlPxo09uT9WR2jKcKPVvUEdMdxTjok3zICyP+Ma5d6z8gAFHHtCEiKRcFSkjN1oVZlR//+anuLwtpaZ5UpOSOv/DQjfCGUJvGdZpqSxkBVcMI0S1mN4v7Wjklo4fx6A38Z2EFwybAIQKHNLdcxhffks/UVqaIF+DzKR3DnDK3gNH2CmGJvBEma4RzmjtRiMRn/323JtaIU6x/V6DYuJJQBFONP2aYVwcbLqSQfwZ7omtDPuv8+58GdEyVkwdoPUZEdDtIbatOX5IwK6/eUC9Y/KNfqciKkQ6v5re2d1jX3C5FNgtuvut6kwJ8WdaTpq3mgkLu0x8UadTXWOtJ5OfX37HRs6j0pa4VqQ9QvKn2iUIM4o2Zl9qiWjvCd2bbpfWgpymf8y141FaYaqXAXDw+WsiYifX6j9lO6b6O1kF4x9ELQHmXGLEFUp61zowCjokl6xqnnf9jyRUTv0gaNC6SWNiZEW6CXSOZehVdUjF0eEM49cI8pRTVzrtJs02+WaXjx6eQXPC/uArvwW/OlkHD2f8d2IGp/29I7/AH7AK5M=) + +## Accessing the `with-meta` iterators + +Metadata isn't included in the default iterators, to avoid the added overhead. You'll need to use alternative versions of the iterators to get the metadata. + +### With a bundler + +If you're using a bundler then you just need to change `@skirtle/vue-vnode-utils` to `@skirtle/vue-vnode-utils/with-meta` in your imports to get access to the metadata: + +```js +import { addProps } from '@skirtle/vue-vnode-utils/with-meta' +``` + +All exports from `@skirtle/vue-vnode-utils` are re-exported by `@skirtle/vue-vnode-utils/with-meta`, even those that don't include meta. You can also mix `@skirtle/vue-vnode-utils` and `@skirtle/vue-vnode-utils/with-meta` and functions will be tree-shaken across the two packages. + +### CDN - global build + +To use meta with a global build, add `with-meta` to the path: + +```html-vue + +``` + +The global variable is called `VueVNodeUtils`, the same as with the normal build. You wouldn't load both in the same project. + +### CDN - ES module build + +Similar to the global build, you need to add `with-meta` to the path: + +```html-vue + + +``` + +The `with-meta` module includes a copy of everything it needs, you don't need to include `@skirtle/vue-vnode-utils` in the import map. + +The package name is usually arbitrary, though using `@skirtle/vue-vnode-utils/with-meta` is recommended unless you have a good reason to use an alternative. diff --git a/packages/vue-vnode-utils/package.json b/packages/vue-vnode-utils/package.json index 264b0fe..d47e7a2 100644 --- a/packages/vue-vnode-utils/package.json +++ b/packages/vue-vnode-utils/package.json @@ -25,6 +25,11 @@ "import": "./dist/vue-vnode-utils.esm-bundler.js", "require": "./dist/vue-vnode-utils.cjs" }, + "./with-meta": { + "types": "./dist/with-meta/vue-vnode-utils.d.ts", + "import": "./dist/with-meta/vue-vnode-utils.esm-bundler.js", + "require": "./dist/with-meta/vue-vnode-utils.cjs" + }, "./dist/*": "./dist/*", "./package.json": "./package.json" }, diff --git a/packages/vue-vnode-utils/src/__tests__/with-meta.spec.ts b/packages/vue-vnode-utils/src/__tests__/with-meta.spec.ts new file mode 100644 index 0000000..87c0eda --- /dev/null +++ b/packages/vue-vnode-utils/src/__tests__/with-meta.spec.ts @@ -0,0 +1,499 @@ +import { describe, expect, it } from 'vitest' +import { h, isVNode, type VNode } from 'vue' +import { + addProps, + ALL_VNODES, + betweenChildren, + COMPONENTS_AND_ELEMENTS, + eachChild, + everyChild, + findChild, + getText, + isElement, + isText, + reduceChildren, + replaceChildren, + someChild +} from '../with-meta' + +function getAllKeys(obj: object) { + const keys: string[] = [] + + for (const key in obj) { + keys.push(key) + } + + return keys +} + +describe('someChild', () => { + it('someChild - 39f9', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + let out = someChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + return false + }) + + expect(out).toBe(false) + expect(count).toBe(3) + + count = 0 + + out = someChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + count++ + return false + }, COMPONENTS_AND_ELEMENTS) + + expect(out).toBe(false) + expect(count).toBe(2) + + count = 0 + + out = someChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + count++ + return count === 2 + }) + + expect(out).toBe(true) + expect(count).toBe(2) + }) +}) + +describe('everyChild', () => { + it('everyChild - 1d78', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + let out = everyChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + return true + }) + + expect(out).toBe(true) + expect(count).toBe(3) + + count = 0 + + out = everyChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + count++ + return true + }, COMPONENTS_AND_ELEMENTS) + + expect(out).toBe(true) + expect(count).toBe(2) + + count = 0 + + out = everyChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + count++ + return count !== 2 + }) + + expect(out).toBe(false) + expect(count).toBe(2) + }) +}) + +describe('eachChild', () => { + it('eachChild - 2558', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + let out = eachChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + return !(count % 2) + }) + + expect(out).toBeUndefined() + expect(count).toBe(3) + + count = 0 + + out = eachChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + count++ + return count % 2 + }, COMPONENTS_AND_ELEMENTS) + + expect(out).toBeUndefined() + expect(count).toBe(2) + }) +}) + +describe('findChild', () => { + it('findChild - abc2', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + let out = findChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + return false + }) + + expect(out).toBeUndefined() + expect(count).toBe(3) + + count = 0 + + out = findChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + count++ + return false + }, COMPONENTS_AND_ELEMENTS) + + expect(out).toBeUndefined() + expect(count).toBe(2) + + count = 0 + + out = findChild(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + count++ + return count === 2 + }) + + expect(isText(out)).toBe(true) + expect(count).toBe(2) + }) +}) + +describe('reduceChildren', () => { + it('reduceChildren - 0ddb', () => { + const startNodes = ['1', '2', '3'] + + let count = 0 + + let out = reduceChildren(startNodes, (acc, node, meta) => { + expect(acc).toBe([0, 1, 3][count]) + expect(isVNode(node)).toBe(true) + expect(getText(node)).toBe(String(count + 1)) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + return acc + count + }, 0) + + expect(out).toBe(6) + expect(count).toBe(3) + + count = 0 + + out = reduceChildren([h('div'), 'Some text', h('p')], (acc, node, meta) => { + expect(acc).toBe([0, 1][count]) + expect(isVNode(node)).toBe(true) + expect(isText(node)).toBe(false) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + + count++ + return acc + count + }, 0, COMPONENTS_AND_ELEMENTS) + + expect(out).toBe(3) + expect(count).toBe(2) + }) +}) + +describe('addProps', () => { + it('addProps - 5ad7', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + const nodes = addProps(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(isElement(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + + return { + class: 'red' + } + }) + + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(3) + expect(count).toBe(2) + + const node = nodes[0] as VNode + + expect(isElement(node)).toBe(true) + expect(node.type).toBe('div') + expect(node.props?.class).toBe('red') + + count = 0 + + const unchanged = addProps(startNodes, () => { + ++count + }, { component: true }) + + expect(count).toBe(0) + expect(unchanged).toBe(startNodes) + }) +}) + +describe('replaceChildren', () => { + it('replaceChildren - 9c3c', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + let nodes = replaceChildren(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + + if (count === 2) { + return [] + } + }) + + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(2) + expect(count).toBe(3) + + let node = nodes[0] as VNode + expect(isElement(node)).toBe(true) + expect(node.type).toBe('div') + + node = nodes[1] as VNode + expect(isElement(node)).toBe(true) + expect(node.type).toBe('p') + + count = 0 + + nodes = replaceChildren(startNodes, (node, meta) => { + expect(isVNode(node)).toBe(true) + expect(isElement(node)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(2) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + + if (count === 2) { + return [] + } + }, COMPONENTS_AND_ELEMENTS) + + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(2) + expect(count).toBe(2) + + node = nodes[0] as VNode + expect(isElement(node)).toBe(true) + expect(node.type).toBe('div') + + node = nodes[1] as VNode + expect(isText(node)).toBe(true) + expect(getText(node)).toBe('Some text') + + count = 0 + + nodes = replaceChildren([true, false, null], () => { + ++count + }) + + expect(count).toBe(0) + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(3) + }) +}) + +describe('betweenChildren', () => { + it('betweenChildren - 719e', () => { + const startNodes = [h('div'), 'Some text', h('p')] + + let count = 0 + + let nodes = betweenChildren(startNodes, (prev, next, meta) => { + expect(isVNode(prev)).toBe(true) + expect(isVNode(next)).toBe(true) + + let metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + expect(meta.index).toBe(count) + expect(meta.length).toBe(3) + + // `length` property should still be enumerable + metaProperties = getAllKeys(meta) + expect(metaProperties.includes('index')).toBe(true) + expect(metaProperties.includes('length')).toBe(true) + + count++ + + return String(count) + }) + + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(5) + expect(count).toBe(2) + + let node = nodes[0] as VNode + expect(isElement(node)).toBe(true) + expect(node.type).toBe('div') + + node = nodes[1] as VNode + expect(isText(node)).toBe(true) + expect(getText(node)).toBe('1') + + node = nodes[2] as VNode + expect(isText(node)).toBe(true) + expect(getText(node)).toBe('Some text') + + node = nodes[3] as VNode + expect(isText(node)).toBe(true) + expect(getText(node)).toBe('2') + + node = nodes[4] as VNode + expect(isElement(node)).toBe(true) + expect(node.type).toBe('p') + + count = 0 + + const comments = [true, false, null] + + nodes = betweenChildren(comments, () => { + ++count + }) + + expect(count).toBe(0) + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(3) + + nodes = betweenChildren(comments, () => { + ++count + }, ALL_VNODES) + + expect(count).toBe(2) + expect(Array.isArray(nodes)).toBe(true) + expect(nodes).toHaveLength(3) + }) +}) diff --git a/packages/vue-vnode-utils/src/iterators.ts b/packages/vue-vnode-utils/src/iterators.ts index b0d8fce..e3d55e7 100644 --- a/packages/vue-vnode-utils/src/iterators.ts +++ b/packages/vue-vnode-utils/src/iterators.ts @@ -20,7 +20,7 @@ const warn = (method: string, msg: string) => { console.warn(`[${method}] ${msg}`) } -const checkArguments = (method: string, passed: unknown[], expected: string[]) => { +export const checkArguments = (method: string, passed: unknown[], expected: string[]) => { for (let index = 0; index < passed.length; ++index) { const t = typeOf(passed[index]) const expect = expected[index] diff --git a/packages/vue-vnode-utils/src/with-meta/index.ts b/packages/vue-vnode-utils/src/with-meta/index.ts new file mode 100644 index 0000000..e8ede77 --- /dev/null +++ b/packages/vue-vnode-utils/src/with-meta/index.ts @@ -0,0 +1,31 @@ +export { + ALL_VNODES, + COMPONENTS_AND_ELEMENTS, + countChildren, + extractSingleChild, + getText, + getType, + isComment, + isComponent, + isElement, + isEmpty, + isFragment, + isFunctionalComponent, + isStatefulComponent, + isStatic, + isText, + type IterationOptions, + SKIP_COMMENTS +} from '@skirtle/vue-vnode-utils' + +export { + addProps, + betweenChildren, + eachChild, + findChild, + everyChild, + type IterationMeta, + reduceChildren, + replaceChildren, + someChild +} from './iterators' diff --git a/packages/vue-vnode-utils/src/with-meta/iterators.ts b/packages/vue-vnode-utils/src/with-meta/iterators.ts new file mode 100644 index 0000000..8b1e09e --- /dev/null +++ b/packages/vue-vnode-utils/src/with-meta/iterators.ts @@ -0,0 +1,172 @@ +import type { VNode, VNodeArrayChildren } from 'vue' + +import type { IterationOptions } from '@skirtle/vue-vnode-utils' +import { + addProps as addPropsRaw, + ALL_VNODES, + betweenChildren as betweenChildrenRaw, + COMPONENTS_AND_ELEMENTS, + countChildren, + eachChild as eachChildRaw, + everyChild as everyChildRaw, + findChild as findChildRaw, + reduceChildren as reduceChildrenRaw, + replaceChildren as replaceChildrenRaw, + SKIP_COMMENTS, + someChild as someChildRaw +} from '@skirtle/vue-vnode-utils' + +// TODO: Should probably move `checkArguments` elsewhere +import { checkArguments } from '../iterators' + +type AnyFunction = (...args: never) => unknown + +function checkFunction(method: string, callback: AnyFunction) { + checkArguments(method, [callback], ['function']) +} + +export type IterationMeta = { + readonly index: number + readonly length: number +} + +function setPropertyValue(obj: object, key: string, value: unknown): any { + return Object.defineProperty(obj, key, { + value, + enumerable: true + }) +} + +function createMetaFactory(children: VNodeArrayChildren, options: IterationOptions): () => IterationMeta { + let index = -1 + + const baseMeta = { + get length() { + const length = countChildren(children, options) + setPropertyValue(baseMeta, 'length', length) + return length + } + } + + return () => { + return setPropertyValue(Object.create(baseMeta), 'index', ++index) + } +} + +function withMeta( + iterator: (children: VNodeArrayChildren, callback: (vnode: VNode) => CallbackReturn, options: IterationOptions) => IteratorReturn, + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => CallbackReturn, + options: IterationOptions +): IteratorReturn { + const metaFactory = createMetaFactory(children, options) + + return iterator(children, (vnode: VNode) => { + return callback(vnode, metaFactory()) + }, options) +} + +export const addProps = ( + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => (Record | null | void), + options: IterationOptions = COMPONENTS_AND_ELEMENTS +): VNodeArrayChildren => { + if (__DEV__) { + checkFunction('addProps', callback) + } + + return withMeta(addPropsRaw, children, callback, options) +} + +export const replaceChildren = ( + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => (VNode | VNodeArrayChildren | string | number | void), + options: IterationOptions = SKIP_COMMENTS +): VNodeArrayChildren => { + if (__DEV__) { + checkFunction('replaceChildren', callback) + } + + return withMeta(replaceChildrenRaw, children, callback, options) +} + +export const betweenChildren = ( + children: VNodeArrayChildren, + callback: (previousVNode: VNode, nextVNode: VNode, meta: IterationMeta) => (VNode | VNodeArrayChildren | string | number | void), + options: IterationOptions = SKIP_COMMENTS +): VNodeArrayChildren => { + if (__DEV__) { + checkFunction('betweenChildren', callback) + } + + const metaFactory = createMetaFactory(children, options) + + return betweenChildrenRaw(children, (previousVNode: VNode, nextVNode: VNode) => { + return callback(previousVNode, nextVNode, metaFactory()) + }, options) +} + +export const someChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => unknown, + options: IterationOptions = ALL_VNODES +): boolean => { + if (__DEV__) { + checkFunction('someChild', callback) + } + + return withMeta(someChildRaw, children, callback, options) +} + +export const everyChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => unknown, + options: IterationOptions = ALL_VNODES +): boolean => { + if (__DEV__) { + checkFunction('everyChild', callback) + } + + return withMeta(everyChildRaw, children, callback, options) +} + +export const eachChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => void, + options: IterationOptions = ALL_VNODES +): void => { + if (__DEV__) { + checkFunction('eachChild', callback) + } + + return withMeta(eachChildRaw, children, callback, options) +} + +export const findChild = ( + children: VNodeArrayChildren, + callback: (vnode: VNode, meta: IterationMeta) => unknown, + options: IterationOptions = ALL_VNODES +): (VNode | undefined) => { + if (__DEV__) { + checkFunction('findChild', callback) + } + + return withMeta(findChildRaw, children, callback, options) +} + +export const reduceChildren = ( + children: VNodeArrayChildren, + callback: (previousValue: R, vnode: VNode, meta: IterationMeta) => R, + initialValue: R, + options: IterationOptions = ALL_VNODES +): R => { + if (__DEV__) { + checkFunction('reduceChildren', callback) + } + + const metaFactory = createMetaFactory(children, options) + + return reduceChildrenRaw(children, (previousValue: R, vnode: VNode) => { + return callback(previousValue, vnode, metaFactory()) + }, initialValue, options) +} diff --git a/packages/vue-vnode-utils/tsconfig.app.json b/packages/vue-vnode-utils/tsconfig.app.json index 02a220a..0e7edcc 100644 --- a/packages/vue-vnode-utils/tsconfig.app.json +++ b/packages/vue-vnode-utils/tsconfig.app.json @@ -3,6 +3,9 @@ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "paths": { + "@skirtle/vue-vnode-utils": ["./src/index"] + } } } diff --git a/packages/vue-vnode-utils/tsdown.config.ts b/packages/vue-vnode-utils/tsdown.config.ts index 37c8420..e45ee9a 100644 --- a/packages/vue-vnode-utils/tsdown.config.ts +++ b/packages/vue-vnode-utils/tsdown.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'tsdown' +import { fileURLToPath, URL } from 'node:url' const base = defineConfig({ entry: { @@ -67,11 +68,87 @@ const iifeProd = defineConfig({ ...suffix('global.prod.js') }) +const withMetaBase = defineConfig({ + ...base, + outDir: 'dist/with-meta', + entry: { + 'vue-vnode-utils': 'src/with-meta/index.ts' + }, + alias: { + '@skirtle/vue-vnode-utils': fileURLToPath(new URL('./src', import.meta.url)) + } +}) + +const withMetaCjs = defineConfig({ + ...withMetaBase, + format: ['cjs'], + define: { + __DEV__: `!(process.env.NODE_ENV === 'production')` + }, + alias: {}, + deps: { + neverBundle: [ + 'vue', + '@skirtle/vue-vnode-utils' + ] + } +}) + +const withMetaEsmBundler = defineConfig({ + ...withMetaCjs, + format: ['esm'], + dts: { tsconfig: './tsconfig.app.json' }, + outExtensions: () => ({ js: '.esm-bundler.js', dts: '.d.ts' }), + deps: { + neverBundle: [ + 'vue', + '@skirtle/vue-vnode-utils' + ] + } +}) + +const withMetaEsmBrowserDev = defineConfig({ + ...withMetaBase, + format: ['esm'], + define: { + __DEV__: 'true' + }, + ...suffix('esm-browser.dev.js') +}) + +const withMetaIifeDev = defineConfig({ + ...withMetaEsmBrowserDev, + format: ['iife'], + ...suffix('global.dev.js') +}) + +const withMetaEsmBrowserProd = defineConfig({ + ...withMetaBase, + format: ['esm'], + minify: true, + define: { + __DEV__: 'false' + }, + ...suffix('esm-browser.prod.js') +}) + +const withMetaIifeProd = defineConfig({ + ...withMetaEsmBrowserProd, + format: ['iife'], + ...suffix('global.prod.js') +}) + export default [ cjs, esmBundler, esmBrowserDev, iifeDev, esmBrowserProd, - iifeProd + iifeProd, + withMetaCjs, + withMetaEsmBundler, + withMetaEsmBrowserDev, + withMetaIifeDev, + withMetaEsmBrowserProd, + withMetaIifeProd ] diff --git a/packages/vue-vnode-utils/vite.config.mts b/packages/vue-vnode-utils/vite.config.mts index ccd4d6a..791094a 100644 --- a/packages/vue-vnode-utils/vite.config.mts +++ b/packages/vue-vnode-utils/vite.config.mts @@ -34,6 +34,13 @@ export default defineConfig(({ mode }): UserConfig => { dtsPlugin ], + resolve: { + alias: { + // Alias so `with-meta` can import from the root package + '@skirtle/vue-vnode-utils': fileURLToPath(new URL('./src/index.ts', import.meta.url)) + } + }, + build: { target: 'es2019', emptyOutDir: false,