Skip to content

Commit

Permalink
implement sorting
Browse files Browse the repository at this point in the history
floodfx committed Jan 27, 2022

Verified

This commit was signed with the committer’s verified signature.
booc0mtaco Holloway
1 parent b39b879 commit 894c95a
Showing 4 changed files with 273 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/examples/index.ts
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/examples/servers/component.ts
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ export class ServersLiveViewComponent extends BaseLiveViewComponent<ServersConte

private link_body(server: Server) {
return html`
🤖 ${server.name}
<button>🤖 ${server.name}</button>
`
}

182 changes: 182 additions & 0 deletions src/examples/sorting/component.ts
Original file line number Diff line number Diff line change
@@ -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<SortContext, PaginateOptions & SortOptions> implements
LiveViewExternalEventListener<SortContext, "select-per-page", Pick<PaginateOptions & SortOptions, "perPage">>,
LiveViewExternalEventListener<SortContext, "change-sort", Pick<PaginateOptions & SortOptions, "sort_by" | "sortOrder">> {

mount(params: any, session: any, socket: LiveViewSocket<SortContext>) {
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<PaginateOptions & SortOptions>, url: string, socket: LiveViewSocket<SortContext>): 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`
<h1>Food Bank Donations</h1>
<div id="donations">
<form phx-change="select-per-page">
Show
<select name="perPage">
${options_for_select([5, 10, 15, 20].map(n => String(n)), String(perPage))}
</select>
<label for="perPage">per page</label>
</form>
<div class="wrapper">
<table>
<thead>
<tr>
<th class="item" phx-click="change-sort" phx-value-sort_by="id">
${this.sort_emoji(sort_by, "id", sortOrder)}Item
</th>
<th phx-click="change-sort" phx-value-sort_by="quantity">
${this.sort_emoji(sort_by, "quantity", sortOrder)}Quantity
</th>
<th phx-click="change-sort" phx-value-sort_by="days_until_expires">
${this.sort_emoji(sort_by, "days_until_expires", sortOrder)}Days Until Expires
</th>
</tr>
</thead>
<tbody>
${this.renderDonations(donations)}
</tbody>
</table>
<div class="footer">
<div class="pagination">
${page > 1 ? this.paginationLink("Previous", page - 1, perPage, sort_by, sortOrder, "previous") : ""}
${this.pageLinks(page, perPage, sort_by, sortOrder,)}
${this.paginationLink("Next", page + 1, perPage, sort_by, sortOrder, "next")}
</div>
</div>
</div>
</div>
`
};

handleEvent(event: "select-per-page" | "change-sort", params: StringPropertyValues<Pick<PaginateOptions & SortOptions, "perPage" | "sort_by" | "sortOrder">>, socket: LiveViewSocket<SortContext>): 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`<button>${text}</button>`, {
to: {
path: "/sort",
params: { page, perPage, sort_by, sortOrder }
},
class: className
})
}

renderDonations(donations: Donation[]) {
return donations.map(donation => html`
<tr>
<td class="item">
<span class="id">${donation.id}</span>
${donation.emoji} ${donation.item}
</td>
<td>
${donation.quantity} lbs
</td>
<td>
<span>
${this.expiresDecoration(donation)}
</span>
</td>
</tr>
`)
}

expiresDecoration(donation: Donation) {
if (almostExpired(donation)) {
return html`<mark>${donation.days_until_expires}</mark>`
} else {
return donation.days_until_expires
}
}

}

88 changes: 88 additions & 0 deletions src/examples/sorting/data.ts
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 894c95a

Please sign in to comment.