diff --git a/.editorconfig b/.editorconfig index 94ce435..dc7a41d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,5 @@ +root = true + [*] charset = utf-8 indent_style = space @@ -5,3 +7,6 @@ indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + +[*.md] +indent_size = 4 diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 0000000..8dbc30f --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1 @@ +github: [skirtles-code] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4ec9f2d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + ci: + if: github.repository == 'skirtles-code/vue-vnode-utils' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Lint + run: pnpm run lint + - name: Type check + run: pnpm run type-check + - name: Build + run: pnpm run build + - name: Test + run: pnpm run test:unit diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..0388b6c --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,53 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy to GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ['main'] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: 'pages' + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Build + run: pnpm run docs:build + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload docs dist directory + path: './packages/docs/dist' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 7cc64f4..7b1efd9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,11 @@ dist dist-ssr coverage *.local +packages/docs/.vitepress/cache +packages/vue-vnode-utils/README.md + +/cypress/videos/ +/cypress/screenshots/ # Editor directories and files .vscode/* @@ -23,3 +28,5 @@ coverage *.njsproj *.sln *.sw? + +*.tsbuildinfo diff --git a/LICENSE b/LICENSE index 6820e1c..ed67f3f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2022, skirtle +Copyright (c) 2022-2025, skirtle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 58861bd..857b92d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Utilities for manipulating Vue 3 VNodes. * Docs: * GitHub: -* Example: [SFC Playground](https://sfc.vuejs.org/#eNqNVE1v2zAM/SuEd7AD+KsbdsmSdl1POwzYYcAOy7A6MZOotSVBkl0Xgf/7KNlKE7cZFuQg8eM9Por0IbiVMm0bDObBQm8UkwY0mkZerzirpVAGDqBwG8NG1LIxWEIPWyVqCCkpPAbd7VlV3lGI4MjNGJFm52bLQykrvhFcG0JsKHRp4aMPM29lBmtNVs8XRTNYXsO3wuzTuuiiPB7PjEcOIW2LqsEYrvLZjFAW2SCDBNCFwGRVGKQbwIJxgoQ2qUWJ1XIVuPxVAOZZIl15U69R0Z2w6ZrbU9HR6YqOA8K5IGcD+IJboXA4L0rWEgMZLCAw+ltBY779HQ7Aoe/H8IziR9ft1hC7ZZn0jfyL7KgkiIOh6UldyPRBC05vd7B5q9FBbHNwFmujptv7KtgbI/U8y/R2Y1/iQadC7TI6pYrawGpMUdfJWoknjYqAV0HsMT7rR6ZMhTY6aTm1L2kMqxzRC3DD5eMupYfLLsWTWm2mxjPaEttzagrOyNGiShTyEpV9octyJqGvJFlY6n1PXXw9nC9LcDr++7dH/gBFWX5XQuoY1mieELlDJOoYmP6BnYlptunRNugdR6RLDXLrgZ0jKHFbNBUR2ZrdUkZ/YqLVlTAa+pl/YkUuxWHYk9EGUCFtmKddDknUXQd5k1LwzQ38+m3phvgsg5+qkGCobrAFaTu8Bdzbib6+92EnkBNtkXfFEDlF5/UAsC1EQ19G/1HB8Bt17KOQGMMYhqCXiHFl6HA0nhR/W5Zg9ghh/Zy4SkLYVIXWYAQUVXUs/A0h/h1PFUyLH4s7K9jhz08Y/7fWr5zG0bhyXbtDOvHQT9G/Sp0M2quKByTi9LnTpvqEsazeV2Z3Yvrx1Oa5sl/O1Asc5K+Fot2aw5XsQIuKlfAuz/NP1lUXasf4HD6Sy1u65ImVZj+H93kuO2eU1HHGdy6ODAOxIwv6v8RcJvg=) +* Example: [SFC Playground](https://play.vuejs.org/#eNqNVE1vm0AQ/SsjegBLGEirXqidNM2ph0o9VOqhVA0247AN7K52F4fI4r93dmEdmyRVkA+78/XeG545BNdSJvsOgzxY6a1i0oBG08nLgrNWCmXgAAp3MWxFKzuDFQywU6KFkJrCY9FNzZrqhkoER26miiQ9D1scain4VnBtaGJHpWs7Pvqw8FFmsNUU9XhRtID1JXwrTZ20ZR9l8XRmPHITkn3ZdBjDRbZY0JRVOsogAXShYbIpDdINYMU4jYT9shUVNusicP1FAOZRIl15125Q0Z1m0zWzp7Kn0wUdxwnnglwM4AvuhMLxvKrYnhAoYAcCo58VNPXb53AADsMwladUP6Wud4bQLcpsb5RfpUclQRyMS1+2pUz+asHp3R1sXzElCC0HF7Gxz/qeKdNgSttf7jlJX3aGNa6oCGpjpM7TtOPy/i6hpaev1RNTbebBBHW73CjxoFElFe6JTxFYZNI3ENPnBngy2qnF6pdtdYCyqr4rIXUMGzQPiNxNVMhjYPoH9iYm/9BitugTx0mvCXEWxN4BVLgru4aALGdn/OhPTLC6EUbDsPBrVJRSHEYvTjGABsnFHnY9NtEW3MirhIqvruDXbws31qcp/FSlBEO8wRLS1iAl3FrXXN76spORM22RT8UQOUXnfADYDqJxL1P+qGB8Jh11FBJiGMNY9FQx2ZIOx+AJ+euqAlMjhO3j0jEJYduUWoMRUDbNkfgLQvx7PFUwJz+ROyPs5ucniG/l+pWTJY2j69Yd0omH3kX/ozoz2jPG4yTC9L3zpfqGidbgmdn/xPwDpc1jY79OiRc4yt8IVaHK4UL2oEXDKniXZdknm2pLdcd4Dh8p5SP98oFVps7hfZbJ3gUlbZzxO1dHgRHYgQXDPzta8GY=) ## Installation @@ -65,4 +65,4 @@ Some browsers do not yet have full support for import maps. MIT -Copyright © 2022, skirtle +Copyright © 2022-2025, skirtle diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts deleted file mode 100644 index 1a34ea6..0000000 --- a/docs/.vitepress/theme/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { App } from 'vue' -import DefaultTheme from 'vitepress/theme' -import LiveExample from '../../components/live-example.vue' - -export default { - ...DefaultTheme, - enhanceApp({ app }: { app: App }) { - app.component('live-example', LiveExample) - } -} diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md deleted file mode 100644 index 30015e1..0000000 --- a/docs/guide/introduction.md +++ /dev/null @@ -1,70 +0,0 @@ -# What is vue-vnode-utils? - -`vue-vnode-utils` is a collection of functions that can be used to manipulate Vue VNodes. They are intended to be used inside [`render()`](https://vuejs.org/guide/extras/render-function.html) functions. The most common use case would be manipulating an array of child VNodes returned by a slot: - -```vue - -``` - -Working with the array of children is sometimes relatively straightforward, but it can become complicated if the slotted content is using directives such as `v-for` or `v-if`. In particular, a `v-for` will introduce a fragment VNode, which wraps around the VNodes that represent the components and elements generated by the `v-for`. - -In general, manipulating VNodes is quite difficult, but `vue-vnode-utils` can help with some of the most common cases. The various helpers can be used to iterate over the 'top-level' child VNodes, including those wrapped in fragments. They can then be used to add or remove VNodes, or change the props of the existing VNodes. - ---- - -An example of adding a CSS `class` to the children in a slot: - -```js -import { h } from 'vue' -import { addProps } from '@skirtle/vue-vnode-utils' - -export default { - render() { - const children = addProps(this.$slots.default(), () => { - return { - class: 'my-child' - } - }) - - return h('div', children) - } -} -``` - -See it on the SFC Playground: [Composition API](https://sfc.vuejs.org/#eNp1VMuO2zAM/BXCPcQB/EqLXtw8ut1zi556qYvCiZVEu7YkSLI3QZB/70ix89omyIEaDociKfoQPCmVdC0L8mBqVporS4bZVs0LwRsltaUDabaOaCUb1VpW0ZHWWjY0QtDoTHre8rp6BkUKJmzPSNJb2OVBSCFWUhgLxRbUmZMPP40HlFvWGKBDvjAc02xO30u7TZpyF2ZRb3MReoWkK+uWRTTJxmOoTNNTGSgAB4ipurQMJ6IpF5CkLm5kxepZEfj4IiC7VwxH0TZLpnGGNo6Zs8odrAnMk8JtQR4DWvFu/o2tpWbT1NkXGMkAO23i+Lvaein3OxxI0PHY0+9C509ry/QFnd51E+A0PdcXRMFpFHFTquTFSIGJHlxc0TuQOCePOAyjcOci2FqrTJ6mZr1y83kxidSbFFai0RzesISZJl5q+WaYhnARRIPGV/PKta2ZY8edQFPj1vLaJ7oIt0K9bhKMM33ER4nG3oM3aSvW3aYGOYWjYzrWTFRMu7k9LueO+q4kJ4sxHNHF90/2shrXS7H9/yIcqKyqn1oqc/Y/KtuvAtv5sIqty7ZGuLuJX8DwbwQxU0sLpfEwOA2XFnTaiR4j7IrbHMf99QPybn18IPrmZRcJAhYL+v3HpbyOWblq0RZEDPcOLzrRfaLzBa4QSNWlMTmNmn3s9VDY4OrfNgys5mD3GttwhLc9wqelvwQonupH3E/kfqGN3ddum5Mh2ekqS6kx2ZwmakdG1ryiD1mWfXGuptQbLmIrVU6f1a7HdvEbr+w2p49Z1oMKHeBiM7BOqX264PgP0vu57A==) | [Options API](https://sfc.vuejs.org/#eNp1VMmO2zAM/RXCLZAEiJe06MV1kk7n3KKnXuoenFiJNWMtkGSPg8D/PpS8ZZkEOYiP5HsiRfrsPUkZ1BXxYi/Re0Wl2aScMimUgeeClvmzQIMTbuCgBINZEF7DNnmW8pSTxiXl5JBVpYFzygH2Q5SOOwBuSC3WLm06QJ6ZbL4Y4hQxleKDZakqbmL42tntZaJVqQzJRw1qCNMT1Uj2KzNFwLJmHi37M+VzU1AdOPYlrKLF4lIg5fhPwrExaCC1LDND0AJIKEdlqH0mclKuU8/xpB6YkyRo8ortiEIbhdCM7Clr8LTCY8dw3Q+HIZrTevOTHIQiSWjPE4xiCFtuoPi3lfZU9nc+A4fWXR7Db1I3TwdD1IQmN0+JYBKO9XlLr5sDn2UyeNGC44y4jqa9A4XHlqcezoG1U68wRuo4DPVhb4fjRQdCHUM8BQqbQxkJiGb+Tok3TRQSpx6+Y8fxQ79SZUpio/2aY1P9ytDSCU3EFZevR3wyFj6KxxK1uQWvZHNSX0tjcIiOmihfEZ4TZd/tcTk3oXclDeODXbzfl4+W7QwFtP2OdSs1OrI8/6OE1KP/UdkP97C75rQRe8G1AV0K8/c3ZmtYg1uDzxbS2ByXuw0wY7uFf//7Jl3k7m1RSIuZw/XmE98SMHO9uVvA0UaaMtP4VZixk++48O6dox/eFhfx6ktQzGc4uLPlKO029aMV1eZU2v0MBu5OdycUNiGGlWxAi5Lm8CmKou/WxTJ1pNw3QsbwTTY91vhvNDdFDF+iqAclFkv5cYjqpJ2c174DBlzG3w==) - ---- - -An example of inserting an `
` between each of the children: - -```js -import { h } from 'vue' -import { betweenChildren } from '@skirtle/vue-vnode-utils' - -export default { - render() { - const children = betweenChildren(this.$slots.default(), () => h('hr')) - - return h('div', children) - } -} -``` - -See it on the SFC Playground: [Composition API](https://sfc.vuejs.org/#eNp1lE2P2jAQhv/KKD0kSPmiVS8pH93uuT320lRVIAPxbmJbthNYofz3jp0ESrYgDvbrmWc8Hl4u3pOUcdeil3krvVdMGtBoWrnJOWukUAYuoPAQwl40sjVYQg8HJRrwKcm/Bj1XrC6fKURw5GaMiJN72dahlJzvBdeGiC2Fri0++LSYVGaw0aRO9YJgAesNfC9MFTfFOUjDcc144AhxV9QthrBMFwuirJKhDWqANgSTdWGQdgArxgkJXdSIEut17rn83APzJpG2vG12qGhPbNqmdlWcabWk5UC4b8hppJas23zDg1C4Suz6JlMxki0bGH1tbyPKfi4X4ND3Y/gsdfN0MKhu6mr2miSukmt/XugNo4iaQsYvWnCa6MXm5eMBFc7AKVajUdh97lXGSJ0liT7s7XxedCzUMaFVrOhxWIMx6ibaKXHSqAice+HE+KpfmTI12uio4/SoUWtY7QrdwC2Xr8eYxpk8iqcWtZmLd2VL7O5LU3BCBx2qSCEvUdm5PW5nFvquJYulMfT0iu9/sjdr/GuK6v9GuMAOzQmROw4VvIY96t45As8uu8RD0dZEsRdyPgz+hMTUtTAa+sU0P0VHisNgjVEDsow1kI39+YPw1kUukZ7PYbcxJWy38Ov3fcZ+uup6fvngBgvHalXgV8q3Tpsg42XogH6qvr3uvi60zsB34Mg62T2mDz39j4zoxZA8QJwF7ADm/tXmrbbmjWeooemdUDTPDJbyDFrUrIQPaZp+sUdNoY6MR0bIDD7L86idoxMrTZXBxzQdRVmUJePHKWq4gavq9X8BjCK8dQ==) | [Options API](https://sfc.vuejs.org/#eNp1VE1z2jAQ/Ss7bmcMM/iDdnpxgTbNuT32Uvdg8IKV2JJGWgMZwn/PSgYDTmA4aJ9239Ou9HwIHrSOty0GWTCzKyM0LXIpGq0MwWMl6vJRcSBREqyNaiCMk1vYFYe5zCXufVGJ66KtCQ65BFids2zWATAgddhx4soByoKK0ficZ5BaI8+Ro2olZfC1i4/XhU6lJSx7DUHY2AtVT/a7oCpuiv0onZzWQo6oEjb27BOYpuPxtUAu+T9L+sFwwNS6Lgg5ApgJycqwjRpVYj3PA8+TB0AvGjmUbbNEwzELcZi6VbHn1ZSXHcPtPDzGaCm2i1+4VgZniVtfYBZj2HGD4L/r9ETlfocDSDj6w3P6oHTxsCY0F3Q2uEoGZ0nfXzAJuncQNYWOn6yS/Eb8RPPTBgv3I88DfgcuzoOKSNssSex65R7Hk42V2SS8ig0PRzQYo22ipVE7i4aJ84DvseP4aZ+FoRpddrSVPNSoJVF7oQtxK/Xzhq+sSe7lc4uWhuCNbInbW2lOTnhjiyYyKEs07t7utzNIfdfS+fnwFN/75SOzHaCC48ljnaX6jSXSDlF6Hhbs0+51f9eO3WkvxlgpaQlsrejvH662MAfvhs8OsjwjX/sj5orXV/j3/7pqdT7NfHi+0YVwAlw6X0A1CisTOm/dmJthfovhhHtc1YXlj0ToaaP+uxGyyXspb82PPGnppXaGjAfVXZ9LZbjrDKZ6D1bVooRPaZp+d1tNYTZCRqR0Bt/0/oTto50oqcrgS5qeQF2UpZCbc1Z3Aq8aHN8A8svKIg==) - ---- - -There are more detailed examples, with demos, later in the guide. The key thing to appreciate is that these components will work even if the slotted content is using directives like `v-for` or `v-if`. The helper functions will walk the tree of fragments created by `v-for` and skip over the hidden comment nodes created by `v-if`, yielding the component and element nodes that would typically be regarded as the 'direct' children. - -Is manipulating VNodes like this actually a good idea? Probably not. Most of the time it'd be better to solve the problem some other way. But if you find yourself in one of those rare edge cases where you really do need to tweak VNodes then it's probably better to use a library to smooth over some of the problems for you. - -`vue-vnode-utils` doesn't replicate functionality that is already present in Vue itself. In particular, you should be familiar with [`isVNode()`](https://vuejs.org/api/render-function.html#isvnode) and [`cloneVNode()`](https://vuejs.org/api/render-function.html#clonevnode). That said, the helpers in `vue-vnode-utils` are designed in such a way that those core functions are often unnecessary. diff --git a/docs/guide/iterators.md b/docs/guide/iterators.md deleted file mode 100644 index 77f8d11..0000000 --- a/docs/guide/iterators.md +++ /dev/null @@ -1,70 +0,0 @@ -# Iterators - -`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() | - -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. - -```js -import { eachChild } from '@skirtle/vue-vnode-utils' - -// Inside a render function -const children = slots.default?.() ?? [] - -eachChild(children, (vnode) => { - console.log(vnode.type) -}) -``` - -Just like the array methods... - -* `eachChild()` will ignore the value returned by the callback. When iteration is complete it returns `undefined`. -* `everyChild()` will stop iterating if the callback returns a falsy value. When iteration is complete it returns either `true` or `false`. -* `findChild()` will stop iterating if the callback returns a truthy value and return the corresponding VNode. -* `someChild()` will stop iterating if the callback returns a truthy value. When iteration is complete it returns either `true` or `false`. - -The array of children does not necessarily need to contain fully instantiated VNodes. For example, text nodes can be represented as just strings and fragments can be arrays. This is consistent with how Vue handles the children passed to `h`, and how it handles values returned from render functions or within slot functions. If you obtained your array of children by calling a slot function then Vue will have already promoted the children to full VNodes, but the iterators don't require that. - -The iterator callback will be passed a fully instantiated VNode, even if the original child array contained some other representation of the child. This is a design choice. `vue-vnode-utils` aims to remove the burden of worrying about the various edge cases, but here we have to pick between two different edge cases. The callbacks only need to worry about handling full VNodes, but the tradeoff is that that exact VNode may not be in the original array. In practice, this should only affect text and comment nodes, and only in cases where they haven't come from a slot function. - -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`. - -So if we only want to iterate over `text` nodes we can pass `{ text: true }` as the third argument. - -There are constants available for the most common combinations: `ALL_VNODES`, `SKIP_COMMENTS` and `COMPONENTS_AND_ELEMENTS`. The iterators all default to `ALL_VNODES`. - -The following example defines a functional component that counts the number of children in its slot. The count is displayed above the list and the wrapper `