Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 49 additions & 30 deletions frontend/src/components/ui/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,7 @@ export function calculatePages({
}

/**
* Pagination
*
* Persists via a search param in the URL. Defaults to `page`, but can be set with the `name` attribute.
*
* Usage example:
* ```ts
* <btrix-pagination totalCount="11" @page-change=${console.log}>
* </btrix-pagination>
* ```
*
* You can have multiple paginations on one page by setting different names:
* ```ts
* <btrix-pagination name="page-a" totalCount="11" @page-change=${console.log}>
* </btrix-pagination>
* <btrix-pagination name="page-b" totalCount="2" @page-change=${console.log}>
* </btrix-pagination>
* ```
* Displays navigation links for paginated content.
*
* @fires page-change {PageChangeEvent}
*/
Expand Down Expand Up @@ -157,6 +141,7 @@ export class Pagination extends LitElement {
];

searchParams = new SearchParamsController(this, (params) => {
if (this.disablePersist) return;
const page = parsePage(params.get(this.name));
if (this._page !== page) {
this.dispatchEvent(
Expand All @@ -172,6 +157,9 @@ export class Pagination extends LitElement {
@state()
private _page = 1;

/**
* Current page
*/
@property({ type: Number })
set page(page: number) {
if (page !== this._page) {
Expand All @@ -184,18 +172,41 @@ export class Pagination extends LitElement {
return this._page;
}

/**
* Name of search param in URL.
* You can have multiple pagination elements in one view by setting different names.
*/
@property({ type: String })
name = "page";

/**
* Total number of items in the collection.
*/
@property({ type: Number })
totalCount = 0;

/**
* Size of the paginated set
*
* @TODO Rename to `pageSize` as to not confuse with Shoelace `size`
*/
@property({ type: Number })
size = 10;

/**
* Display pagination as minimally functional controls (previous, current, and next)
*
* @TODO Switch to a more standard attribute
*/
@property({ type: Boolean })
compact = false;

/**
* Disable persisting current page in the URL
*/
@property({ type: Boolean })
disablePersist = false;

@state()
private inputValue = "";

Expand All @@ -207,18 +218,22 @@ export class Pagination extends LitElement {
super.connectedCallback();
}

async willUpdate(changedProperties: PropertyValues<this>) {
async willUpdate(
changedProperties: PropertyValues<this> & Map<string, unknown>,
) {
if (changedProperties.has("totalCount") || changedProperties.has("size")) {
this.calculatePages();
}

const parsedPage = parseFloat(
this.searchParams.searchParams.get(this.name) ?? "1",
);
if (parsedPage != this._page) {
const page = parsePage(this.searchParams.searchParams.get(this.name));
const constrainedPage = Math.max(1, Math.min(this.pages, page));
this.onPageChange(constrainedPage, { dispatch: false });
if (!this.disablePersist) {
const parsedPage = parseFloat(
this.searchParams.searchParams.get(this.name) ?? "1",
);
if (parsedPage != this._page) {
const page = parsePage(this.searchParams.searchParams.get(this.name));
const constrainedPage = Math.max(1, Math.min(this.pages, page));
this.onPageChange(constrainedPage, { dispatch: false });
}
}

// if page is out of bounds, clamp it & dispatch an event to re-fetch data
Expand All @@ -230,8 +245,8 @@ export class Pagination extends LitElement {
this.onPageChange(constrainedPage, { dispatch: true });
}

if (changedProperties.get("page") && this._page) {
this.inputValue = `${this._page}`;
if (changedProperties.get("_page")) {
this.inputValue = `${this.page}`;
}
}

Expand Down Expand Up @@ -407,10 +422,14 @@ export class Pagination extends LitElement {
}

private setPage(page: number) {
if (page === 1) {
this.searchParams.delete(this.name);
if (!this.disablePersist) {
if (page === 1) {
this.searchParams.delete(this.name);
} else {
this.searchParams.set(this.name, page.toString());
}
} else {
this.searchParams.set(this.name, page.toString());
this._page = page;
}
}

Expand Down
51 changes: 51 additions & 0 deletions frontend/src/stories/components/Pagination.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";

import { renderComponent, type RenderProps } from "./Pagination";

const meta = {
title: "Components/Pagination",
component: "btrix-pagination",
tags: ["autodocs"],
decorators: (story) =>
html` <div class="flex justify-center px-20 py-10">${story()}</div>`,
render: renderComponent,
argTypes: {
searchParams: { table: { disable: true } },
},
args: {
totalCount: 10,
page: 1,
size: 1,
},
} satisfies Meta<RenderProps>;

export default meta;
type Story = StoryObj<RenderProps>;

export const Basic: Story = {
args: {},
};

/**
* By default, the current page persists between page reloads via a search param in the URL.
* You can disable pagination persistence by setting `disablePersist`.
*/
export const DisablePersistence: Story = {
args: {
disablePersist: true,
},
};

/**
* Pagination can be displayed with a reduced amount of controls to fit a smaller visual space.
* Only the controls for the previous, current, and next page will be visible. Users can jump
* to a page by entering the page number in the input field for the current page.
*
* This should be used sparingly, such as for paginating secondary content in a view.
*/
export const Compact: Story = {
args: {
compact: true,
},
};
30 changes: 30 additions & 0 deletions frontend/src/stories/components/Pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";

import type { Pagination } from "@/components/ui/pagination";

import "@/components/ui/pagination";

export type RenderProps = Pagination;

export const renderComponent = ({
page,
name,
totalCount,
size,
compact,
disablePersist,
}: Partial<RenderProps>) => {
return html`
<btrix-pagination
page=${ifDefined(page)}
name=${ifDefined(name)}
totalCount=${ifDefined(totalCount)}
size=${ifDefined(size)}
?compact=${compact}
?disablePersist=${disablePersist}
@page-change=${console.log}
>
</btrix-pagination>
`;
};
Loading