{"_id":"@dennisregalado/shopify-fragment-plugin","name":"@dennisregalado/shopify-fragment-plugin","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@dennisregalado/shopify-fragment-plugin","amdName":"ShopifyFragmentPlugin","version":"1.0.0","description":"A swup plugin for dynamically replacing containers with Shopify search parameter support for variants and filtering","type":"module","source":"src/index.ts","main":"./dist/index.cjs","module":"./dist/index.module.js","unpkg":"./dist/index.umd.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.modern.js","require":"./dist/index.cjs"}},"author":{"name":"Dennis Regalado","email":"dennis.regalado.dev@gmail.com"},"contributors":[{"name":"Rasso Hilber","email":"mail@rassohilber.com","url":"https://rassohilber.com"},{"name":"Philipp Daun","email":"daun@daun.ltd","url":"https://philippdaun.net"}],"scripts":{"prepare":"husky","build":"swup package:build","dev":"swup package:dev","lint":"swup package:lint","format":"swup package:format","prepublishOnly":"npm run build","test":"npm run test:unit && npm run test:e2e","test:unit":"vitest run --config ./tests/config/vitest.config.ts","test:unit:watch":"vitest --config ./tests/config/vitest.config.ts","test:e2e":"npx playwright test --config ./tests/config/playwright.config.ts","test:e2e:dev":"npx playwright test --ui --config ./tests/config/playwright.config.ts","test:e2e:serve":"./tests/prepare.js && npx serve -n -S -L -p 8274 --config ./tests/config/serve.json"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/dennisregalado/swup-shopify-fragment-plugin.git"},"dependencies":{"@swup/plugin":"^4.0.0"},"devDependencies":{"@playwright/test":"^1.39.0","@swup/cli":"^5.0.2","@types/jsdom":"^21.1.4","husky":"^9.0.11","jsdom":"^22.1.0","lint-staged":"^15.2.2","serve":"^14.2.1","vitest":"^0.34.6"},"peerDependencies":{"swup":"^4.6.0"},"browserslist":["extends @swup/browserslist-config"],"prettier":"@swup/prettier-config","_id":"@dennisregalado/shopify-fragment-plugin@1.0.0","gitHead":"e1b22da12a3211291e00c973fbf32241c03e809f","bugs":{"url":"https://github.com/dennisregalado/swup-shopify-fragment-plugin/issues"},"homepage":"https://github.com/dennisregalado/swup-shopify-fragment-plugin#readme","_nodeVersion":"22.18.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-n+EgCo8jkryaT5Dq+suq/Kp/w+D7iVlx4gzKjygifr6V3fm8YMTJ3BnMFFUMYVApKhGzx5xHmrBvUEz591Jnfw==","shasum":"c755a726cae1b61623ee7b678060007b5edcdee9","tarball":"https://registry.npmjs.org/@dennisregalado/shopify-fragment-plugin/-/shopify-fragment-plugin-1.0.0.tgz","fileCount":35,"unpackedSize":354475,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIAaYi9H4bO5GknOParIiU3edi+bm9goUt/fAk728s2s4AiEAgnAUVCoLjH0zWtexg8LrE60E/37D4qudIpi9UM94f4M="}]},"_npmUser":{"name":"dennisregalado","email":"dennisregalad@gmail.com"},"directories":{},"maintainers":[{"name":"dennisregalado","email":"dennisregalad@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/shopify-fragment-plugin_1.0.0_1756323922642_0.9385102151519846"},"_hasShrinkwrap":false}},"time":{"created":"2025-08-27T19:45:22.567Z","1.0.0":"2025-08-27T19:45:22.840Z","modified":"2025-08-27T19:45:23.052Z"},"maintainers":[{"name":"dennisregalado","email":"dennisregalad@gmail.com"}],"description":"A swup plugin for dynamically replacing containers with Shopify search parameter support for variants and filtering","homepage":"https://github.com/dennisregalado/swup-shopify-fragment-plugin#readme","repository":{"type":"git","url":"git+https://github.com/dennisregalado/swup-shopify-fragment-plugin.git"},"contributors":[{"name":"Rasso Hilber","email":"mail@rassohilber.com","url":"https://rassohilber.com"},{"name":"Philipp Daun","email":"daun@daun.ltd","url":"https://philippdaun.net"}],"author":{"name":"Dennis Regalado","email":"dennis.regalado.dev@gmail.com"},"bugs":{"url":"https://github.com/dennisregalado/swup-shopify-fragment-plugin/issues"},"license":"MIT","readme":"# Swup Fragment Plugin\n\n<!-- swup-docs-ignore-start -->\n\n[![npm version](https://img.shields.io/npm/v/@swup/fragment-plugin.svg)](https://www.npmjs.com/package/@swup/fragment-plugin)\n[![Unit Tests](https://img.shields.io/github/actions/workflow/status/swup/fragment-plugin/unit-tests.yml?branch=main&label=vitest)](https://github.com/swup/fragment-plugin/actions/workflows/unit-tests.yml)\n[![E2E Tests](https://img.shields.io/github/actions/workflow/status/swup/fragment-plugin/e2e-tests.yml?branch=main&label=playwright)](https://github.com/swup/fragment-plugin/actions/workflows/e2e-tests.yml)\n[![License](https://img.shields.io/github/license/swup/fragment-plugin.svg)](https://github.com/swup/fragment-plugin/blob/main/LICENSE)\n\n<!-- swup-docs-ignore-end -->\n\nA [swup](https://swup.js.org) plugin for dynamically replacing containers based on rules 🧩\n\n- Selectively replace containers instead of the main swup containers, based on custom rules\n- Improve orientation by animating only the parts of the page that have actually changed\n- Give your site the polish and snappiness of a single-page app\n\n## Use cases\n\nAll of the following scenarios require updating only a small content fragment instead of\nperforming a full page transition:\n\n- a filter UI that live-updates its list of results on every interaction\n- a detail overlay that shows on top of the currently open content\n- a tab group that should update only itself when selecting one of the tabs\n\nIf you are looking for selectively replacing forms on submission, you should have a look at\n[Forms Plugin](https://swup.js.org/plugins/forms-plugin/#inline-forms).\n\n## Demo\n\nSee the plugin in action in [this interactive demo](https://swup-fragment-plugin.netlify.app)\n\n<div data-video data-screencast>\n\nhttps://github.com/swup/fragment-plugin/assets/869813/ecaf15d7-ec72-43e8-898a-64f61330c6f5\n\n</div>\n\n## Table of contents\n\n- [Installation](#installation)\n- [How it works](#how-it-works)\n- [Example](#example)\n- [Options](#options)\n- [How rules are matched](#how-rules-are-matched)\n- [How fragment containers are found](#how-fragment-containers-are-found)\n- [Advanced use cases](#advanced-use-cases)\n- [Skip animations using `<template>`](#skip-animations-using-template)\n- [API Methods](#api-methods)\n\n## Installation\n\nInstall the plugin from npm and import it into your bundle.\n\n```bash\nnpm install @swup/fragment-plugin\n```\n\n```js\nimport SwupFragmentPlugin from '@swup/fragment-plugin';\n```\n\nOr include the minified production file from a CDN:\n\n```html\n<script src=\"https://unpkg.com/@swup/fragment-plugin@1\"></script>\n```\n\n## How it works\n\nWhen a visit is determined to be a fragment visit, the plugin will:\n\n- **update only** the contents of the elements matching the rule's `containers`\n- **not update** the default [containers](https://swup.js.org/options/#containers) replaced on all other visits\n- **wait** for CSS transitions on those fragment elements using [scoped animations](https://swup.js.org/options/#animation-scope)\n- **preserve** the current scroll position upon navigation\n- add a `to-[name]` class to the elements if the current `rule` has a `name` key\n- **ignore** elements that already match the current visit's URL\n\n## Example\n\n### Content filter: only update a list of results\n\nImagine a website with a `/users/` page that displays a list of users. Above the user list, there\nis a filter UI to choose which users to display. Selecting a filter will trigger a visit\nto the narrowed-down user list at `/users/filter/x/`. The only part that has changed is the\nlist of users, so that's what we'd like to replace and animate instead of the whole content\ncontainer.\n\n```html\n<body>\n  <header>Website</header>\n  <main id=\"swup\" class=\"transition-main\" class=\"transition-main\">\n    <h1>Users</h1>\n    <!-- A list of filters for the users: selecting one will update the list below -->\n    <ul>\n      <a href=\"/users/filter/1/\">Filter 1</a>\n      <a href=\"/users/filter/2/\">Filter 2</a>\n      <a href=\"/users/filter/3/\">Filter 2</a>\n    </ul>\n    <!-- The list of users, filtered by the criteria above -->\n    <ul id=\"users\">\n      <li><a href=\"/user/1/\">User 1</a></li>\n      <li><a href=\"/user/2/\">User 2</a></li>\n      <li><a href=\"/user/3/\">User 3</a></li>\n    </ul>\n  </main>\n</body>\n```\n\nUsing Fragment Plugin, we can update **only** the `#users` list when clicking one of the filters.\nThe plugin expects an array of rules to recognize and handle fragment visits:\n\n```js\nconst swup = new Swup({\n  plugins: [\n    new SwupFragmentPlugin({\n      rules: [\n        {\n          from: '/users/:filter?',\n          to: '/users/:filter?',\n          containers: ['#users']\n        }\n      ]\n    })\n  ]\n});\n```\n\nNow we can add custom animations for our fragment rule:\n\n```css\n/*\n* The default animation, for visits without matching fragment rules\n*/\nhtml.is-changing .transition-main {\n  transition: opacity 250ms;\n  opacity: 1;\n}\nhtml.is-animating .transition-main {\n  opacity: 0;\n}\n\n/*\n* The animation when filtering users\n*/\n#users.is-changing {\n  transition: opacity 250ms;\n}\n#users.is-animating {\n  opacity: 0;\n}\n```\n\n## Options\n\n```ts\n/** A path to match URLs against */\ntype Path = string | RegExp | Array<string | RegExp>;\n\n/** A fragment rule */\nexport type Rule = {\n  from: Path;\n  to: Path;\n  containers: string[];\n  name?: string;\n  scroll?: boolean | string;\n  focus?: boolean | string;\n  if?: Predicate;\n};\n\n/** The plugin options */\nexport type Options = {\n  rules: Rule[];\n  debug?: boolean;\n};\n```\n\n### `rules`\n\nThe rules that define whether a visit will be considered a fragment visit. Each rule consists of\nmandatory `from` and `to` URL paths, an array of selectors `containers`, as well as an optional\n`name` of this rule to allow scoped styling.\n\nThe rule's `from`/`to` paths are converted to a regular expression by [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and matched against the current browser URL. If you want to create an either/or path, you can also provide an array of paths, for example:\n\n```js\n{\n  rules: [\n    {\n      from: ['/users', '/users/:filter?'],\n      to: ['/users', '/users/:filter?'],\n      containers: ['#user-list']\n    }\n  ];\n}\n```\n\n#### `rule.from`\n\nRequired, Type: `string | string[]` – The path(s) to match against the previous URL\n\n#### `rule.to`\n\nRequired, Type: `string | string[]` – The path(s) to match against the next URL\n\n#### `rule.containers`\n\nRequired, Type: `string[]` – Selectors of containers to be replaced if the visit matches.\n\n**Notes**\n\n- **only IDs and no nested selectors are allowed**. `#my-element` is valid, while\n  `.my-element` or `#wrap #child` both will throw an error.\n- if **any** of the selectors in `containers` doesn't return a match in the current document, the rule will be skipped.\n- Fragment elements **must either match a swup container or be a descendant of one of them**\n\n#### `rule.name`\n\nOptional, Type: `string` – A name for this rule to allow scoped styling, ideally in `kebab-case`\n\n#### `rule.scroll`\n\nOptional, Type: `boolean | string` – By default, scrolling will be disabled for fragment visits.\nUsing this option, you can re-enable it for selected visits:\n\n- `true` will scroll to the top\n- `'#my-element'` will scroll to the first element matching the selector\n\n#### `rule.focus`\n\nOptional, Type: `boolean | string` – If you have [Accessibility Plugin](https://github.com/swup/a11y-plugin/) installed, you can adjust which element to focus for the visit [as described here](https://github.com/swup/a11y-plugin/#visita11yfocus).\n\n#### `rule.if`\n\nOptional, Type: `(visit) => boolean` – A predicate function that allows for fine-grained control over the matching behavior of a rule. The function receives the current [visit](https://swup.js.org/visit/) as a parameter. If the function returns `false`, the related rule is being skipped for the current visit, even if it matches the current route.\n\n### `debug`\n\nOptional, Type: `boolean`, Default `false`. Set to `true` for debug information in the console.\n\n```js\n{\n  debug: true;\n}\n```\n\n> [!IMPORTANT] to keep the bundle size small, UMD builds are stripped from all debug messages, so `debug` won't have an effect there.\n\n## How rules are matched\n\n- The first matching rule in your `rules` array will be used for the current visit\n- If no rule matches the current visit, the default content containers defined in swup's options will be replaced\n\n## How fragment containers are found\n\n- The `containers` of the matching rule **need to be shared between the current and the incoming document**\n- For each selector in the `containers` array, the **first** matching element in the DOM will be selected\n- If a visit isn't be considered a reload of the current page, fragment elements that already match the new URL will be ignored\n\n## Advanced use cases\n\nCreating the rules for your fragment visits should be enough to enable dynamic updates on most\nsites. However, there are some advanced use cases that require adding certain attributes to the\nfragment elements themselves or to links on the page. These tend to be situations where **modal dialogs** are involved and swup doesn't know which page the modal was opened from.\n\n### Fragment URL\n\nUse the `data-swup-fragment-url` attribute to uniquely identify fragment elements.\n\nIn scenarios where a modal is rendered on top of other content, leaving or closing the modal to\nthe same URL it was opened from should ideally not update the content behind it as\nnothing has changed. Fragment plugin will normally do that by keeping track of URLs. However,\nwhen swup was initialized on a subpage with an already-visible modal, the plugin doesn't know which URL the content behind it corresponds to. Hence, we need to tell swup manually so it can persist content when closing the modal.\n\n```diff\n<!-- the modal -->\n<dialog open id=\"modal\">\n  <main>\n    <h1>User 1</h1>\n    <p>Lorem ipsum dolor sit amet...</p>\n  </main>\n</dialog>\n<!-- the content behind the modal -->\n<main>\n  <section\n    id=\"list\"\n+   data-swup-fragment-url=\"/users/\"\n    >\n    <ul>\n      <li>User 1</li>\n      <li>User 2</li>\n      <li>User 3</li>\n    </ul>\n  </section>\n</main>\n```\n\n### Link to another fragment\n\nUse the `data-swup-link-to-fragment` attribute to automatically update links pointing to a fragment.\n\nConsider again an overlay rendered on top of other content. To implement a close button for that\noverlay, we could ideally point a link at the URL of the content where the overlay is closed. The\nfragment plugin will then handle the animation and replacing of the overlay. However, knowing\nwhere to point that link requires knowing where the current overlay was opened from.\n\n`data-swup-link-to-fragment` automates that by keeping the `href` attribute of a link in sync with the currently\ntracked URL of the fragment matching the selector provided by the attribute. The code below will make sure the close button will always point at the last known URL of the `#list` fragment to allow seamlessly closing the overlay:\n\n```diff\n<dialog open id=\"modal\">\n  <main>\n    <!-- `href` will be synced to the fragment URL of #list at runtime: -->\n+   <a href=\"\" data-swup-link-to-fragment=\"#list\">Close</a>\n    <h1>User 1</h1>\n    <p>Lorem ipsum dolor sit amet...</p>\n  </main>\n</dialog>\n<main>\n  <section id=\"list\"\n    data-swup-fragment-url=\"/users/\">\n    <ul>\n      <li>User 1</li>\n      <li>User 2</li>\n      <li>User 3</li>\n    </ul>\n  </section>\n</main>\n```\n\n> [!TIP]\n> To keep your markup semantic and accessible, we recommend you **always provide a default value**\n> for the link's `href` attribute, even though it will be updated automatically at runtime:\n\n```diff\n<a\n+ href=\"/users/\"\n  data-swup-link-to-fragment=\"#list\">Close</a>\n```\n\n## Skip animations using `<template>`\n\nIf all elements of a visit are `<template>` elements, the `out`/`in`-animation will automatically be skipped. This can come in handy for modals:\n\n```js\n{\n  from: '/overview/',\n  to: '/detail/:id',\n  containers: ['#modal']\n},\n{\n  from: '/detail/:id',\n  to: '/overview/',\n  containers: ['#modal']\n}\n```\n\n```html\n<!-- /overview/: provide a <template> as a placeholder for the modal -->\n<template id=\"modal\"></template>\n<main>\n  <ul>\n    <!-- list of items that will open in the #modal -->\n  </ul>\n</main>\n```\n\n```html\n<!-- /detail/1 -->\n<dialog open id=\"modal\">\n  <main>\n    <h1>Detail 1</h1>\n  </main>\n</dialog>\n<main>\n  <ul>\n    <!-- list of items that will open in the #modal -->\n  </ul>\n</main>\n```\n\n> [!TIP]\n> Fragment Plugin will detect `<dialog open>`-fragment elements automatically on every page view and\n> move them to the [top layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer)\n> automatically. This has the benefit of simplified accesssiblity handling and styling.\n\n## API methods\n\nFragment plugin provides a few API functions for advanced use cases. To be able to access those, you'll need to keep a reference to the plugin during instanciation:\n\n```js\nconst fragmentPlugin = new SwupFragmentPlugin({ rules });\nconst swup = new Swup({ plugins: [fragmentPlugin] });\n/** You can now call the plugin's public API, for example: */\nfragmentPlugin.getFragmentVisit(route);\n```\n\n### `getFragmentVisit(route)`\n\nGet information about the fragment visit for a given route. Returns either `FragmentVisit` or `undefined`.\n\n```js\n/**\n * Get information about which containers will\n * be replaced when hovering over links:\n */\ndocument.querySelectorAll('a[href]').forEach((el) => {\n  el.addEventListener('mouseenter', () => {\n    const fragmentVisit = fragmentPlugin.getFragmentVisit({\n      from: window.location.href, // the current URL\n      to: el.href // the URL of the link\n    });\n    console.log(`will replace ${fragmentVisit?.containers || swup.options.containers}`);\n  });\n});\n```\n\n### `prependRule(rule)`\n\nPrepends a [rule](#type-signature-rule) to the array of rules.\n\n```js\nfragmentPlugin.prependRule({ from: '/foo/', to: '/bar/', containers: ['#foobar'] });\n```\n\n### `appendRule(rule)`\n\nAppends a [rule](#type-signature-rule) to the array of rules.\n\n```js\nfragmentPlugin.prependRule({ from: '/baz/', to: '/bat/', containers: ['#bazbat'] });\n```\n\n### `getRules()`\n\nGet a **clone** of all registered fragment rules\n\n```js\nconsole.log(fragmentPlugin.getRules());\n```\n\n### `setRules(rules)`\n\nOverwrite all fragment rules with the provided rules. This methods provides the lowest-level access to the rules. For example, you could use it to remove all rules with the name `foobar`:\n\n```js\nfragmentPlugin.setRules(fragmentPlugin.getRules().filter((rule) => rule.name !== 'foobar'));\n```\n","readmeFilename":"README.md","_rev":"1-e57ef943fdd1803601d5e72ce8ba3adf"}