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
5 changes: 5 additions & 0 deletions .changeset/odd-dots-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@layerstack/svelte-state': patch
---

Add PaginationState
97 changes: 97 additions & 0 deletions packages/svelte-state/src/lib/paginationState.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { clamp } from '@layerstack/utils';

export type PaginationOptions = {
/** Initial page */
page?: number;

/** Number of items per page */
perPage?: number;

/** Total number of items */
total?: number;
};

export class PaginationState {
#page: number;
#perPage: number;
#total: number;

constructor(options: PaginationOptions = {}) {
this.#page = options.page ?? 1;
this.#perPage = options.perPage ?? 25;
this.#total = options.total ?? 0;
}

get page() {
return this.#page;
}

set page(value: number) {
// Do not allow page to exceed bounds (ex. call nextPage() when on last page)
this.#page = clamp(value, 1, this.totalPages);
}

get perPage() {
return this.#perPage;
}

set perPage(value: number) {
this.#perPage = value;
}

get total() {
return this.#total;
}

set total(value: number) {
this.#total = value;
}

get totalPages() {
return Math.ceil(this.total / this.perPage);
}

get from() {
return Math.min(this.total, Math.max(0, (this.page - 1) * this.perPage + 1));
}

get to() {
return Math.min(this.total, this.page * this.perPage);
}

get isFirst() {
return this.page === 1;
}

get isLast() {
return this.page >= this.totalPages;
}

get hasPrevious() {
return this.page > 1 && this.totalPages > 0;
}

get hasNext() {
return this.page < this.totalPages;
}

nextPage = () => {
this.page = this.page + 1;
};

prevPage = () => {
this.page = this.page - 1;
};

firstPage = () => {
this.page = 1;
};

lastPage = () => {
this.page = Math.ceil(this.total / this.perPage);
};

slice<T>(data: T[]) {
return data.slice((this.page - 1) * this.perPage, this.page * this.perPage);
}
}
139 changes: 139 additions & 0 deletions packages/svelte-state/src/lib/paginationState.test.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, it, expect } from 'vitest';

import { PaginationState } from './paginationState.svelte.js';

describe('PaginationState', () => {
it('should initialize with default values', () => {
const paginationState = new PaginationState();
expect(paginationState.page).toEqual(1);
expect(paginationState.perPage).toEqual(25);
expect(paginationState.total).toEqual(0);
expect(paginationState.totalPages).toEqual(0);
expect(paginationState.from).toEqual(0);
expect(paginationState.to).toEqual(0);
expect(paginationState.isFirst).toEqual(true);
expect(paginationState.isLast).toEqual(true);
expect(paginationState.hasPrevious).toEqual(false);
expect(paginationState.hasNext).toEqual(false);
});

it('should initialize with total only', () => {
const paginationState = new PaginationState({ total: 100 });
expect(paginationState.page).toEqual(1);
expect(paginationState.perPage).toEqual(25);
expect(paginationState.total).toEqual(100);
expect(paginationState.totalPages).toEqual(4);
expect(paginationState.from).toEqual(1);
expect(paginationState.to).toEqual(25);
expect(paginationState.isFirst).toEqual(true);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(false);
expect(paginationState.hasNext).toEqual(true);
});

it('should initialize with page', () => {
const paginationState = new PaginationState({ page: 2, total: 100 });
expect(paginationState.page).toEqual(2);
expect(paginationState.perPage).toEqual(25);
expect(paginationState.total).toEqual(100);
expect(paginationState.totalPages).toEqual(4);
expect(paginationState.from).toEqual(26);
expect(paginationState.to).toEqual(50);
expect(paginationState.isFirst).toEqual(false);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(true);
expect(paginationState.hasNext).toEqual(true);
});

it('should initialize with perPage', () => {
const paginationState = new PaginationState({ perPage: 10, total: 100 });
expect(paginationState.page).toEqual(1);
expect(paginationState.perPage).toEqual(10);
expect(paginationState.total).toEqual(100);
expect(paginationState.totalPages).toEqual(10);
expect(paginationState.from).toEqual(1);
expect(paginationState.to).toEqual(10);
expect(paginationState.isFirst).toEqual(true);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(false);
expect(paginationState.hasNext).toEqual(true);
});

it('should increment page', () => {
const paginationState = new PaginationState({ total: 100 });
expect(paginationState.page).toEqual(1);
expect(paginationState.from).toEqual(1);
expect(paginationState.to).toEqual(25);
expect(paginationState.isFirst).toEqual(true);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(false);
expect(paginationState.hasNext).toEqual(true);

paginationState.nextPage();
expect(paginationState.page).toEqual(2);
expect(paginationState.from).toEqual(26);
expect(paginationState.to).toEqual(50);
expect(paginationState.isFirst).toEqual(false);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(true);
expect(paginationState.hasNext).toEqual(true);
});

it('should decrement page', () => {
const paginationState = new PaginationState({ page: 2, total: 100 });
expect(paginationState.page).toEqual(2);
expect(paginationState.from).toEqual(26);
expect(paginationState.to).toEqual(50);
expect(paginationState.isFirst).toEqual(false);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(true);
expect(paginationState.hasNext).toEqual(true);

paginationState.prevPage();
expect(paginationState.page).toEqual(1);
expect(paginationState.from).toEqual(1);
expect(paginationState.to).toEqual(25);
expect(paginationState.isFirst).toEqual(true);
expect(paginationState.isLast).toEqual(false);
expect(paginationState.hasPrevious).toEqual(false);
expect(paginationState.hasNext).toEqual(true);
});

it('should clamp page', () => {
const paginationState = new PaginationState({ page: 4, total: 100 });
expect(paginationState.page).toEqual(4);
expect(paginationState.from).toEqual(76);
expect(paginationState.to).toEqual(100);
expect(paginationState.isFirst).toEqual(false);
expect(paginationState.isLast).toEqual(true);
expect(paginationState.hasPrevious).toEqual(true);
expect(paginationState.hasNext).toEqual(false);

paginationState.nextPage();
expect(paginationState.page).toEqual(4);
expect(paginationState.from).toEqual(76);
expect(paginationState.to).toEqual(100);
expect(paginationState.isFirst).toEqual(false);
expect(paginationState.isLast).toEqual(true);
expect(paginationState.hasPrevious).toEqual(true);
expect(paginationState.hasNext).toEqual(false);
});

it('should slice data', () => {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const paginationState = new PaginationState({ perPage: 5, total: data.length });
expect(paginationState.slice(data)).toEqual([1, 2, 3, 4, 5]);

paginationState.nextPage();
expect(paginationState.slice(data)).toEqual([6, 7, 8, 9, 10]);

paginationState.nextPage();
expect(paginationState.slice(data)).toEqual([6, 7, 8, 9, 10]); // clamped

paginationState.prevPage();
expect(paginationState.slice(data)).toEqual([1, 2, 3, 4, 5]);

paginationState.prevPage();
expect(paginationState.slice(data)).toEqual([1, 2, 3, 4, 5]); // clamped
});
});
8 changes: 7 additions & 1 deletion sites/docs/src/routes/_NavMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
'styles',
];

const state = ['MediaQueryPresets', 'SelectionState', 'TimerState', 'UniqueState'];
const state = [
'MediaQueryPresets',
'PaginationState',
'SelectionState',
'TimerState',
'UniqueState',
];

const stores = [
'changeStore',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import Code from '$docs/Code.svelte';
import Preview from '$docs/Preview.svelte';

import { PaginationState } from '$svelte-state/paginationState.svelte.js';
</script>

<h1>Usage</h1>

<Code
source={`import { PaginationState } from '@layerstack/svelte-state';

const state = new PaginationState({ total: 100 });

state.page
state.perPage
state.total
state.totalPages
state.from
state.to
state.isFirst
state.isLast
state.hasPrevious
state.hasNext
state.slice(data)
`}
language="javascript"
/>
14 changes: 14 additions & 0 deletions sites/docs/src/routes/docs/svelte-state/PaginationState/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import source from '$svelte-state/paginationState.svelte.js?raw';
import pageSource from './+page.svelte?raw';

export async function load() {
return {
meta: {
source,
pageSource,
description:
'Manage pagination state including current page and page navigation (next/previous/first/last). See related Paginate/Pagination components',
related: ['components/Paginate', 'components/Pagination'],
},
};
}