browsertrix/frontend/src/controllers/searchParams.ts
Emma Segal-Grossman eeda4cd9ff
Persist pagination state in url (#2538)
Closes #1944 

## Changes
- Pagination stores page number in url search params, rather than
internal state, allowing going back to a specific page in a list
- Pagination navigation pushes to history stack, and listens to history
changes to be able to respond to browser history navigation
(back/forward)
- Search parameter reactive controller powers pagination component
- Pagination component allows for multiple simultaneous paginations via
custom `name` property

## Manual testing

1. Log in as any role
2. Go to one of the list views on an org with enough items in the list
to span more than one page
3. Click on one of the pages, and navigate back in your browser. The
selected page should respect this navigation and return to the initial
numbered page.
4. Navigate forward in your browser. The selected page should respect
this navigation and switch to the numbered page from the previous step.
5. Click on a non-default page, and then click on one of the items in
the list to go to its detail page. Then, using your browser's back
button, return to the list page. You should be on the same numbered page
as before.

---------

Co-authored-by: sua yoo <sua@suayoo.com>
2025-04-09 15:40:30 -04:00

58 lines
1.6 KiB
TypeScript

import type { ReactiveController, ReactiveControllerHost } from "lit";
export class SearchParamsController implements ReactiveController {
private readonly host: ReactiveControllerHost;
private readonly changeHandler?: (
searchParams: URLSearchParams,
prevParams: URLSearchParams,
) => void;
private prevParams = new URLSearchParams(location.search);
public get searchParams() {
return new URLSearchParams(location.search);
}
public set(
update: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),
options: { replace?: boolean; data?: unknown } = { replace: false },
) {
this.prevParams = new URLSearchParams(this.searchParams);
const url = new URL(location.toString());
url.search =
typeof update === "function"
? update(this.searchParams).toString()
: update.toString();
if (options.replace) {
history.replaceState(options.data, "", url);
} else {
history.pushState(options.data, "", url);
}
}
constructor(
host: ReactiveControllerHost,
onChange?: (
searchParams: URLSearchParams,
prevParams: URLSearchParams,
) => void,
) {
this.host = host;
host.addController(this);
this.changeHandler = onChange;
}
hostConnected(): void {
window.addEventListener("popstate", this.onPopState);
}
hostDisconnected(): void {
window.removeEventListener("popstate", this.onPopState);
}
private readonly onPopState = (_e: PopStateEvent) => {
this.changeHandler?.(this.searchParams, this.prevParams);
this.prevParams = new URLSearchParams(this.searchParams);
};
}