diff --git a/src/examples/index.ts b/src/examples/index.ts index 907c9497..f34bd2b9 100644 --- a/src/examples/index.ts +++ b/src/examples/index.ts @@ -7,6 +7,7 @@ import { SearchLiveViewComponent } from './live-search/component'; import { PaginateLiveViewComponent } from './pagination/component'; import { SalesDashboardLiveViewComponent } from './sales_dashboard_liveview'; import { ServersLiveViewComponent } from './servers/component'; +import { SortLiveViewComponent } from './sorting/component'; const lvServer = new LiveViewServer({ // port: 3002, @@ -22,6 +23,7 @@ export const router: LiveViewRouter = { "/autocomplete": new AutocompleteLiveViewComponent(), "/light": new LightLiveViewComponent(), "/paginate": new PaginateLiveViewComponent(), + "/sort": new SortLiveViewComponent(), } // register all routes diff --git a/src/examples/servers/component.ts b/src/examples/servers/component.ts index fd126a2e..356b2b6f 100644 --- a/src/examples/servers/component.ts +++ b/src/examples/servers/component.ts @@ -83,7 +83,7 @@ export class ServersLiveViewComponent extends BaseLiveViewComponent🤖 ${server.name} ` } diff --git a/src/examples/sorting/component.ts b/src/examples/sorting/component.ts new file mode 100644 index 00000000..5bc1cfca --- /dev/null +++ b/src/examples/sorting/component.ts @@ -0,0 +1,182 @@ +import { options_for_select } from "../../server/templates/helpers/options_for_select"; +import { live_patch } from "../../server/templates/helpers/live_patch"; +import html, { HtmlSafeString, join } from "../../server/templates"; +import { BaseLiveViewComponent, LiveViewExternalEventListener, LiveViewSocket, StringPropertyValues } from "../../server/types"; +import { almostExpired, Donation, listItems, donations } from "./data"; + +export interface PaginateOptions { + page: number; + perPage: number; +} + +export interface SortOptions { + sort_by: keyof Donation; + sortOrder: "asc" | "desc"; +} + +export interface SortContext { + options: PaginateOptions & SortOptions; + donations: Donation[] +} + +export class SortLiveViewComponent extends BaseLiveViewComponent implements + LiveViewExternalEventListener>, + LiveViewExternalEventListener> { + + mount(params: any, session: any, socket: LiveViewSocket) { + const paginateOptions: PaginateOptions = { + page: 1, + perPage: 10, + } + const sortOptions: SortOptions = { + sort_by: "item", + sortOrder: "asc" + } + return { + options: { ...paginateOptions, ...sortOptions }, + donations: listItems(paginateOptions, sortOptions) + }; + }; + + handleParams(params: StringPropertyValues, url: string, socket: LiveViewSocket): SortContext { + const page = Number(params.page || 1); + const perPage = Number(params.perPage || 10); + const validSortBy = Object.keys(donations[0]).includes(params.sort_by) + const sort_by = validSortBy ? params.sort_by as keyof Donation : "item"; + const sortOrder = params.sortOrder === "desc" ? "desc" : "asc"; + return { + options: { page, perPage, sort_by, sortOrder }, + donations: listItems({ page, perPage }, { sort_by, sortOrder }) + }; + } + + render(context: SortContext) { + const { options: { perPage, page, sortOrder, sort_by }, donations } = context; + return html` +

Food Bank Donations

+
+
+ Show + + +
+
+ + + + + + + + + + ${this.renderDonations(donations)} + +
+ ${this.sort_emoji(sort_by, "id", sortOrder)}Item + + ${this.sort_emoji(sort_by, "quantity", sortOrder)}Quantity + + ${this.sort_emoji(sort_by, "days_until_expires", sortOrder)}Days Until Expires +
+ +
+
+ ` + }; + + handleEvent(event: "select-per-page" | "change-sort", params: StringPropertyValues>, socket: LiveViewSocket): SortContext { + console.log("handleEvent", event, params); + const page = socket.context.options.page; + let perPage = socket.context.options.perPage; + let sort_by = socket.context.options.sort_by; + let sortOrder = socket.context.options.sortOrder; + + if (event === "select-per-page") { + perPage = Number(params.perPage || socket.context.options.perPage); + } + + if (event === "change-sort") { + const incoming_sort_by = params.sort_by as keyof Donation; + // if already sorted by this column, reverse the order + if (sort_by === incoming_sort_by) { + sortOrder = sortOrder === "asc" ? "desc" : "asc"; + } else { + sort_by = incoming_sort_by; + } + } + + + this.pushPatch(socket, { to: { path: "/sort", params: { page: String(page), perPage: String(perPage), sortOrder, sort_by } } }); + + return { + options: { page, perPage, sort_by, sortOrder }, + donations: listItems({ page, perPage }, { sort_by, sortOrder }) + }; + } + + sort_emoji(sort_by: keyof Donation, sort_by_value: string, sortOrder: "asc" | "desc") { + return sort_by === sort_by_value ? sortOrder === "asc" ? "👇" : "☝️" : "" + } + + pageLinks(page: number, perPage: number, sort_by: keyof Donation, sortOrder: "asc" | "desc") { + let links: HtmlSafeString[] = []; + for (var p = page - 2; p <= page + 2; p++) { + if (p > 0) { + links.push(this.paginationLink(String(p), p, perPage, sort_by, sortOrder, p === page ? "active" : "")) + } + } + return join(links, "") + } + + paginationLink(text: string, pageNum: number, perPageNum: number, sort_by: keyof Donation, sortOrder: "asc" | "desc", className: string) { + const page = String(pageNum); + const perPage = String(perPageNum); + return live_patch(html``, { + to: { + path: "/sort", + params: { page, perPage, sort_by, sortOrder } + }, + class: className + }) + } + + renderDonations(donations: Donation[]) { + return donations.map(donation => html` + + + ${donation.id} + ${donation.emoji} ${donation.item} + + + ${donation.quantity} lbs + + + + ${this.expiresDecoration(donation)} + + + + `) + } + + expiresDecoration(donation: Donation) { + if (almostExpired(donation)) { + return html`${donation.days_until_expires}` + } else { + return donation.days_until_expires + } + } + +} + diff --git a/src/examples/sorting/data.ts b/src/examples/sorting/data.ts new file mode 100644 index 00000000..8c73f9f8 --- /dev/null +++ b/src/examples/sorting/data.ts @@ -0,0 +1,88 @@ +import { PaginateOptions, SortOptions } from "./component"; + + +export interface Donation { + id: number; + emoji: string + item: string + quantity: number + days_until_expires: number +} + +const items = [ + { emoji: "☕️", item: "Coffee" }, + { emoji: "🥛", item: "Milk" }, + { emoji: "🥩", item: "Beef" }, + { emoji: "🍗", item: "Chicken" }, + { emoji: "🍖", item: "Pork" }, + { emoji: "🍗", item: "Turkey" }, + { emoji: "🥔", item: "Potatoes" }, + { emoji: "🥣", item: "Cereal" }, + { emoji: "🥣", item: "Oatmeal" }, + { emoji: "🥚", item: "Eggs" }, + { emoji: "🥓", item: "Bacon" }, + { emoji: "🧀", item: "Cheese" }, + { emoji: "🥬", item: "Lettuce" }, + { emoji: "🥒", item: "Cucumber" }, + { emoji: "🐠", item: "Smoked Salmon" }, + { emoji: "🐟", item: "Tuna" }, + { emoji: "🐡", item: "Halibut" }, + { emoji: "🥦", item: "Broccoli" }, + { emoji: "🧅", item: "Onions" }, + { emoji: "🍊", item: "Oranges" }, + { emoji: "🍯", item: "Honey" }, + { emoji: "🍞", item: "Sourdough Bread" }, + { emoji: "🥖", item: "French Bread" }, + { emoji: "🍐", item: "Pear" }, + { emoji: "🥜", item: "Nuts" }, + { emoji: "🍎", item: "Apples" }, + { emoji: "🥥", item: "Coconut" }, + { emoji: "🧈", item: "Butter" }, + { emoji: "🧀", item: "Mozzarella" }, + { emoji: "🍅", item: "Tomatoes" }, + { emoji: "🍄", item: "Mushrooms" }, + { emoji: "🍚", item: "Rice" }, + { emoji: "🍜", item: "Pasta" }, + { emoji: "🍌", item: "Banana" }, + { emoji: "🥕", item: "Carrots" }, + { emoji: "🍋", item: "Lemons" }, + { emoji: "🍉", item: "Watermelons" }, + { emoji: "🍇", item: "Grapes" }, + { emoji: "🍓", item: "Strawberries" }, + { emoji: "🍈", item: "Melons" }, + { emoji: "🍒", item: "Cherries" }, + { emoji: "🍑", item: "Peaches" }, + { emoji: "🍍", item: "Pineapples" }, + { emoji: "🥝", item: "Kiwis" }, + { emoji: "🍆", item: "Eggplants" }, + { emoji: "🥑", item: "Avocados" }, + { emoji: "🌶", item: "Peppers" }, + { emoji: "🌽", item: "Corn" }, + { emoji: "🍠", item: "Sweet Potatoes" }, + { emoji: "🥯", item: "Bagels" }, + { emoji: "🥫", item: "Soup" }, + { emoji: "🍪", item: "Cookies" } +] + +export const donations: Donation[] = items.map((item, id) => { + const quantity = Math.floor(Math.random() * 20) + 1; + const days_until_expires = Math.floor(Math.random() * 30) + 1; + return { ...item, quantity, days_until_expires, id: (id + 1) } +}) + +export const listItems = (paginateOptions: PaginateOptions, sortOptions: SortOptions) => { + const { page, perPage } = paginateOptions; + const { sort_by, sortOrder } = sortOptions; + const sorted = donations.sort((a, b) => { + if (a[sort_by] < b[sort_by]) { + return sortOrder === "asc" ? -1 : 1; + } + if (a[sort_by] > b[sort_by]) { + return sortOrder === "asc" ? 1 : -1; + } + return 0; + }); + return sorted.slice((page - 1) * perPage, page * perPage) +} + +export const almostExpired = (donation: Donation) => donation.days_until_expires <= 10 \ No newline at end of file