Skip to content

Commit

Permalink
Merge pull request #11 from floodfx/sorting
Browse files Browse the repository at this point in the history
implement sorting
  • Loading branch information
floodfx authored Jan 27, 2022
2 parents b39b879 + 596bf8c commit 4b6f09a
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,6 +23,7 @@ export const router: LiveViewRouter = {
"/autocomplete": new AutocompleteLiveViewComponent(),
"/light": new LightLiveViewComponent(),
"/paginate": new PaginateLiveViewComponent(),
"/sort": new SortLiveViewComponent(),
}

// register all routes
Expand Down
2 changes: 1 addition & 1 deletion src/examples/servers/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class ServersLiveViewComponent extends BaseLiveViewComponent<ServersConte

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

Expand Down
181 changes: 181 additions & 0 deletions src/examples/sorting/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 {
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 4b6f09a

Please sign in to comment.