Repository: swup/swup
Pull request #935 (2024-07-23 12:03:22)

Add `visit.meta` key for custom data
**Description**

- Add an empty object `visit.meta = {}` that can hold user data tied to a specific visit
- Typed as `Record<string, unknown>` for type-safe usage without custom interface extension
- Can bet set manually from hook handler or directly from `swup.navigate()`

**Set in hook**

```js
swup.hooks.on('visit:start', (visit) => {
  visit.meta.lorem = 'ipsum';
})
```

**Set from API**

```js
swup.navigate(url, { meta: { lorem: 'ipsum' } })
```

**Checks**

- [x] The PR is submitted to the `master` branch
- [x] The code was linted before pushing (`npm run lint`)
- [x] All tests are passing (`npm run test`)
- [x] New or updated tests are included
- [ ] The documentation was updated as required



Patch:
diff --git a/src/modules/Visit.ts b/src/modules/Visit.ts
index b0d5d27..17ed6c7 100644
--- a/src/modules/Visit.ts
+++ b/src/modules/Visit.ts
@@ -114,6 +114,8 @@ export class Visit {
 	history: VisitHistory;
 	/** Scroll behavior on this visit */
 	scroll: VisitScroll;
+	/** User-defined metadata */
+	meta: Record<string, unknown>;
 
 	constructor(swup: Swup, options: VisitInitOptions) {
 		const { to, from, hash, el, event } = options;
@@ -145,7 +147,9 @@ export class Visit {
 			reset: true,
 			target: undefined
 		};
+		this.meta = {};
 	}
+
 	/** @internal */
 	advance(state: VisitState) {
 		if (this.state < state) {
diff --git a/src/modules/navigate.ts b/src/modules/navigate.ts
index 7793f89..ac88935 100755
--- a/src/modules/navigate.ts
+++ b/src/modules/navigate.ts
@@ -19,6 +19,8 @@ type NavigationOptions = {
 	history?: HistoryAction;
 	/** Whether this visit should read from or write to the cache. */
 	cache?: CacheControl;
+	/** Custom metadata associated with this visit. */
+	meta?: Record<string, unknown>;
 };
 
 /**
@@ -106,6 +108,9 @@ export async function performNavigation(
 		visit.animation.name = animation;
 	}
 
+	// Get custom metadata from option
+	visit.meta = options.meta || {};
+
 	// Sanitize cache option
 	if (typeof options.cache === 'object') {
 		visit.cache.read = options.cache.read ?? visit.cache.read;


Test patch:
diff --git a/tests/functional/visit-object.spec.ts b/tests/functional/visit-object.spec.ts
index 388fb2b..be115e2 100644
--- a/tests/functional/visit-object.spec.ts
+++ b/tests/functional/visit-object.spec.ts
@@ -156,4 +156,15 @@ test.describe('visit object', () => {
 		await expectToBeAt(page, '/page-3.html', 'Page 3');
 		expect(await page.evaluate(() => window.data)).toBe(supported);
 	});
+
+	test('passes along custom metadata from api', async ({ page }) => {
+		await page.evaluate(() => {
+			window.data = null;
+			window._swup.hooks.on('visit:start', (visit) => (window.data = visit.meta));
+		});
+
+		navigateWithSwup(page, '/page-2.html', { meta: { lorem: 'ipsum' } });
+		await expectToBeAt(page, '/page-2.html', 'Page 2');
+		expect(await page.evaluate(() => window.data)).toMatchObject({ lorem: 'ipsum' });
+	});
 });
diff --git a/tests/unit/visit.test.ts b/tests/unit/visit.test.ts
index 0b8f22d..a4e97c2 100644
--- a/tests/unit/visit.test.ts
+++ b/tests/unit/visit.test.ts
@@ -93,4 +93,9 @@ describe('Visit', () => {
 			target: undefined
 		});
 	});
+
+	it('has a meta object', () => {
+		expect(visit.meta).to.be.an('object');
+		expect(visit.meta).toEqual({});
+	});
 });