Skip to content

Commit

Permalink
feat(partials): support <button> (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
uki00a committed Mar 29, 2024
1 parent 7e0c951 commit 14db5d5
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 46 deletions.
186 changes: 150 additions & 36 deletions internal/fresh/partials.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,161 @@
import { assertExists } from "$std/assert/assert_exists.ts";
import { assertNotStrictEquals } from "$std/assert/assert_not_strict_equals.ts";
import { assertStrictEquals } from "$std/assert/assert_strict_equals.ts";
import { assertSpyCalls, spy } from "$std/testing/mock.ts";
import {
createPartialMarkerComment,
enablePartialNavigation,
extractPartialBoundaries,
} from "./partials.ts";
import { createDocument } from "../jsdom/mod.ts";

Deno.test("createPartialMarkerComment", () => {
assertStrictEquals(
createPartialMarkerComment({ name: "counter", index: 0 }),
"frsh-partial:counter:0:",
);
assertStrictEquals(
createPartialMarkerComment({ name: "snackbar", index: 0, endMarker: true }),
"/frsh-partial:snackbar:0:",
);
Deno.test({
name: "createPartialMarkerComment",
permissions: "none",
fn: () => {
assertStrictEquals(
createPartialMarkerComment({ name: "counter", index: 0 }),
"frsh-partial:counter:0:",
);
assertStrictEquals(
createPartialMarkerComment({
name: "snackbar",
index: 0,
endMarker: true,
}),
"/frsh-partial:snackbar:0:",
);
},
});

Deno.test("extractPartialBoundaries", () => {
const document = createDocument(`
<html>
<body>
<h1>hello</h1>
<main>
<!-- test -->
<div>foo</div>
<!--frsh-partial:page:0:-->
<section>
<h2>Test page</h2>
<div>hi!</div>
</section>
<!--/frsh-partial:page:0:-->
</main>
</body>
</html>`);
const boundaries = extractPartialBoundaries(document);
assertStrictEquals(boundaries[0].name, "page");
assertStrictEquals(boundaries[0].key, "0");
assertNotStrictEquals(boundaries[0].start, boundaries[0].end);
assertStrictEquals(
boundaries[0].start.parentNode,
boundaries[0].end.parentNode,
);

assertStrictEquals(boundaries.length, 1);
Deno.test({
name: "extractPartialBoundaries",
permissions: "none",
fn: () => {
const document = createDocument(`
<html>
<body>
<h1>hello</h1>
<main>
<!-- test -->
<div>foo</div>
<!--frsh-partial:page:0:-->
<section>
<h2>Test page</h2>
<div>hi!</div>
</section>
<!--/frsh-partial:page:0:-->
</main>
</body>
</html>`);
const boundaries = extractPartialBoundaries(document);
assertStrictEquals(boundaries[0].name, "page");
assertStrictEquals(boundaries[0].key, "0");
assertNotStrictEquals(boundaries[0].start, boundaries[0].end);
assertStrictEquals(
boundaries[0].start.parentNode,
boundaries[0].end.parentNode,
);

assertStrictEquals(boundaries.length, 1);
},
});

Deno.test({
name: "enablePartialNavigation",
permissions: "none",
fn: async (t) => {
await t.step("enables partial navigation for `<a>`", () => {
const anchorWithFpartialId = crypto.randomUUID();
const anchorWithoutFpartialId = crypto.randomUUID();
const partialLink = "/pages/foo";
const href = "/docs/index";
const doc = createDocument(`<div id="container">
<a id="${anchorWithFpartialId}" f-partial="${partialLink}" href="/pages/bar">with f-partial</button>
<a id="${anchorWithoutFpartialId}" href="${href}">without f-partial</button>
</div>`);
const container = doc.getElementById("container");
assertExists(container);

const updatePartials = spy<unknown, [Event, Request], Promise<unknown>>();
const origin = "http://localhost:8000";
enablePartialNavigation(
container,
origin,
updatePartials,
);
assertSpyCalls(updatePartials, 0);
{
const anchor = doc.getElementById(
anchorWithoutFpartialId,
);
assertExists(anchor);
anchor.click();
assertSpyCalls(updatePartials, 1);
const [{ args: [event, request] }] = updatePartials.calls;
assertStrictEquals(event.type, "click");
assertStrictEquals(request.method, "GET");
assertStrictEquals(
request.url,
`${origin}${href}?fresh-partial=true`,
);
}

{
const anchor = doc.getElementById(anchorWithFpartialId);
assertExists(anchor);
anchor.click();
assertSpyCalls(updatePartials, 2);
const [, { args: [event, request] }] = updatePartials.calls;
assertStrictEquals(event.type, "click");
assertStrictEquals(request.method, "GET");
assertStrictEquals(
request.url,
`${origin}${partialLink}?fresh-partial=true`,
);
}
});

await t.step("supports <button>", () => {
const buttonWithFpartialId = "button-with-f-partial";
const buttonWithoutFpartialId = "button-without0f-partial";
const partialLink = "/pages/foo";
const doc = createDocument(`<div id="container">
<button id="${buttonWithFpartialId}" f-partial="${partialLink}">with f-partial</button>
<button id="${buttonWithoutFpartialId}">without f-partial</button>
</div>`);
const container = doc.getElementById("container");
assertExists(container);
const updatePartials = spy<unknown, [Event, Request], Promise<unknown>>();
const origin = "http://localhost:8000";
enablePartialNavigation(
container,
origin,
updatePartials,
);
assertSpyCalls(updatePartials, 0);
{
const buttonWithoutFpartial = doc.getElementById(
buttonWithoutFpartialId,
);
assertExists(buttonWithoutFpartial);
buttonWithoutFpartial.click();
assertSpyCalls(updatePartials, 0);
}

{
const buttonWithFpartial = doc.getElementById(buttonWithFpartialId);
assertExists(buttonWithFpartial);
buttonWithFpartial.click();
assertSpyCalls(updatePartials, 1);
const [{ args: [event, request] }] = updatePartials.calls;
assertStrictEquals(event.type, "click");
assertStrictEquals(request.method, "GET");
assertStrictEquals(
request.url,
`${origin}${partialLink}?fresh-partial=true`,
);
}
});
},
});
38 changes: 28 additions & 10 deletions internal/fresh/partials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ export function enablePartialNavigation(
updatePartials: PartialsUpdater,
): () => void {
const events: Array<
[HTMLElement, string, (event: Event) => unknown]
[Element, string, (event: Event) => unknown]
> = [];

function addEventListener(
element: HTMLElement,
element: Element,
event: string,
listener: (event: Event) => unknown,
) {
Expand All @@ -132,23 +132,37 @@ export function enablePartialNavigation(
events.push([element, event, listener]);
}

function createClickListener(href: string) {
function onClick(event: Event): void {
const url = new URL(href, origin);
url.searchParams.set(kFreshPartialQueryParam, "true");
const request = new Request(url);
updatePartials(event, request);
}
return onClick;
}

const anchors = container.querySelectorAll("a");
for (const anchor of anchors) {
const partial = anchor.getAttribute(kFreshPartialAttribute);
const href = partial ?? anchor.getAttribute("href");
if (href == null || !href.startsWith("/")) {
if (!isPartialLink(href)) {
continue;
}

addEventListener(anchor, "click", (event) => {
const url = new URL(href, origin);
url.searchParams.set(kFreshPartialQueryParam, "true");
const request = new Request(url);
updatePartials(event, request);
});
addEventListener(anchor, "click", createClickListener(href));
}

// TODO: support buttons.
const buttons = container.querySelectorAll(
`button[${kFreshPartialAttribute}]`,
);
for (const button of buttons) {
const href = button.getAttribute(kFreshPartialAttribute);
if (!isPartialLink(href)) {
continue;
}
addEventListener(button, "click", createClickListener(href));
}
// TODO: support form partials.

function cleanup(): void {
Expand All @@ -161,6 +175,10 @@ export function enablePartialNavigation(
return cleanup;
}

function isPartialLink(href: string | null): href is string {
return href != null && href.startsWith("/");
}

interface ManifestAccessor {
(): Manifest | undefined;
}
Expand Down

0 comments on commit 14db5d5

Please sign in to comment.