Skip to content

Commit

Permalink
feat: support for light dom signal host, invokable api
Browse files Browse the repository at this point in the history
  • Loading branch information
JulianCataldo committed Dec 21, 2024
1 parent 8790c39 commit 3470044
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 24 deletions.
8 changes: 8 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@
"@lit-labs/ssr-client": "^1.1.7",
"urlpattern-polyfill": "^10.0.0"
},
"peerDependencies": {
"@lit-labs/signals": "^0.1.1"
},
"peerDependenciesMeta": {
"@lit-labs/signals": {
"optional": true
}
},
"devDependencies": {
"@gracile/internal-tsconfigs": "workspace:^",
"tsx": "^4.19.1",
Expand Down
39 changes: 26 additions & 13 deletions packages/client/src/hydration-client-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ import { routeImports } from 'gracile:client:routes';
import { premiseUrl } from '@gracile/internal-utils/paths';
import { URLPattern } from 'urlpattern-polyfill/urlpattern';

async function init() {
async function init(options?: HydrationOptions) {
const url = new URL(globalThis.document.location.href);
const urlPattern = new URLPattern({
baseURL: url.href,
});
// NOTE: Remove hash and query.
const baseURL = `${url.origin}${url.pathname}`;
const urlPattern = new URLPattern({ baseURL });

let route: RouteModule | undefined;
let parameters: Parameters = {};

for (const [pattern, routeImport] of routeImports.entries()) {
const match = urlPattern.exec(pattern, url.href);
const match = urlPattern.exec(pattern, baseURL);

if (match) {
// eslint-disable-next-line unicorn/no-await-expression-member
const loaded = (await routeImport()).default(RouteModule);
route = loaded;
const loaded = await routeImport();
route = loaded.default(RouteModule);
parameters = match.pathname.groups;
break;
}
Expand All @@ -30,14 +31,26 @@ async function init() {
const propertiesUrl = premiseUrl(url.pathname, 'props');
const properties = await fetch(propertiesUrl).then((r) => r.json());

const template = route.template({
const rootValue = route.template({
url,
params: parameters,
props: properties,
});
hydrate(template, document.body);

let hydrationOptions;
if (options?.signalHost) {
const { SignalHost } = await import('./signal-host.js');
hydrationOptions = { host: new SignalHost() };
}
hydrate(rootValue, document.body, hydrationOptions);
}

document.addEventListener('DOMContentLoaded', () => {
void init();
});
export interface HydrationOptions {
signalHost?: boolean | undefined;
}

export function createHydrationRoot(options?: HydrationOptions) {
document.addEventListener('DOMContentLoaded', () => {
void init(options);
});
}
35 changes: 35 additions & 0 deletions packages/client/src/signal-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// FROM: https://lit.dev/playground/#gist=3a8740445931d4d3745509b093cec034

import type { WatchDirective } from '@lit-labs/signals';

export class SignalHost {
private isPendingUpdate = false;

private __pendingWatches = new Set<WatchDirective<unknown>>();

protected _updateWatchDirective(directive: WatchDirective<unknown>): void {
this.__pendingWatches.add(directive);
this.requestUpdate();
}
protected _clearWatchDirective(directive: WatchDirective<unknown>): void {
this.__pendingWatches.delete(directive);
}

protected requestUpdate() {
if (this.isPendingUpdate) return;

this.isPendingUpdate = true;

queueMicrotask(() => {
try {
for (const directive of this.__pendingWatches) directive.commit();
} catch {
// Empty
} finally {
this.__pendingWatches.clear();
}

this.isPendingUpdate = false;
});
}
}
5 changes: 4 additions & 1 deletion packages/gracile/src/hydration-full.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
import '@gracile/client/hydration-client-route';
export {
type HydrationOptions,
createHydrationRoot,
} from '@gracile/client/hydration-client-route';
37 changes: 27 additions & 10 deletions packages/labs/client-router/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import 'urlpattern-polyfill';
import '@gracile/client/lit-element-hydrate-support';

import { render, type TemplateResult } from 'lit';
import { render, type RenderOptions, type TemplateResult } from 'lit';
import { RouteModule } from '@gracile/engine/routes/route';
// eslint-disable-next-line import-x/no-unresolved
import { routeImports, enabled } from 'gracile:client:routes';
import { hydrate } from '@lit-labs/ssr-client';
import { premiseUrl } from '@gracile/internal-utils/paths';
import { SignalHost } from '@gracile/client/signal-host';

import { GracileRouter } from './_internal/gracile-client-router.js';
import * as prefetching from './_internal/prefetching.js';
Expand Down Expand Up @@ -72,11 +73,26 @@ export function prefetchRoutePremises(options: {
void prefetching.prefetch(preProperties);
}

export interface GracileRouterConfig extends Config {
morph?: (incoming: Document, target: Document) => void;
}
export type GracileRouterConfig = Partial<
Pick<Config, 'plugins' | 'routes' | 'signalHost'>
>;

/**
* Client-side routing that takes over the SSRed markup and browser navigation.
* @param config Router instance configuration.
* @param config.plugins Router plugins.
* @param config.routes Additional routes.
* @version experimental
* @example
* `./src/client-router.ts`
* ```js
* import { createRouter } from '@gracile-labs/client-router/create';
*
* export const router = createRouter();
* ```
* @returns The client router as an observable or controllable event target.
*/

// TODO: Caching mechanisms optimisations
export function createRouter(config?: GracileRouterConfig) {
const serverRoutes: RouteDefinition[] = [];

Expand All @@ -85,6 +101,9 @@ export function createRouter(config?: GracileRouterConfig) {
const cachedDocuments = new Map<string, Document>();
const cachedTemplates = new Map<string, TemplateResult>();

const hydrationOptions: RenderOptions = {};
if (config?.signalHost) hydrationOptions.host = new SignalHost();

// MARK: Virtual routes.
for (const [path, routeImport] of routeImports.entries()) {
serverRoutes.push({
Expand Down Expand Up @@ -144,7 +163,6 @@ export function createRouter(config?: GracileRouterConfig) {
),
);
}

if (!cachedTemplateForRoute) {
premiseToFetch.push(
fetch(premiseUrl(url.pathname, 'props')).then((r) =>
Expand Down Expand Up @@ -271,12 +289,11 @@ export function createRouter(config?: GracileRouterConfig) {
if (!renderedTemplate) throw new Error('Cannot render template');

// MARK: Hydrate or render

if (!isInitiallyHydrated) {
try {
// TODO: try
requestIdleCallback(() => {});
hydrate(renderedTemplate, document.body);
hydrate(renderedTemplate, document.body, hydrationOptions);
isInitiallyHydrated = true;

return;
Expand All @@ -291,7 +308,7 @@ export function createRouter(config?: GracileRouterConfig) {
// We don't want to re-render for hash changes, too.
// TODO: early detection
if (url.pathname !== previousPathname) {
render(renderedTemplate, document.body);
render(renderedTemplate, document.body, hydrationOptions);

// MARK: Extras meta
if (parsedDocument) {
Expand Down Expand Up @@ -335,7 +352,7 @@ export function createRouter(config?: GracileRouterConfig) {
// TODO: offline, generic error page support…
],

routes: config?.routes.length
routes: config?.routes?.length
? [...serverRoutes, ...config.routes]
: serverRoutes,
});
Expand Down
1 change: 1 addition & 0 deletions packages/labs/client-router/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Config {
fallback?: string | undefined;
plugins?: Plugin[] | undefined;
routes: RouteDefinition[];
signalHost?: boolean | undefined;
}

export interface Plugin {
Expand Down

0 comments on commit 3470044

Please sign in to comment.