Skip to content

Commit

Permalink
frontend: initial web components support
Browse files Browse the repository at this point in the history
  • Loading branch information
terrablue committed Dec 21, 2023
1 parent 1fd95cd commit 089c9e3
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 2 deletions.
84 changes: 84 additions & 0 deletions docs/modules/web-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Web Components

This handler module serves web components with the `.webc` extension.

## Install

`npm install @primate/frontend`

## Configure

Import and initialize the module in your configuration.

```js caption=primate.config.js
import { webc } from "@primate/frontend";

export default {
modules: [
webc(),
],
};
```

## Use

Create an web component in `components`.

```html caption=components/post-index.webc
<script>
import post_link from "./post-link.webc";
export default ({posts}) => `
<h1>All posts</h1>
${posts.map(post => post_link({post}))}
`;
export const mounted = root => {
root.querySelector("h1").addEventListener("click",
_ => console.log("title clicked!"));
}
</script>
```

And another component for display post links.

```html caption=components/post-link.webc
<script>
export default ({post}) => `
<h2><a href="/post/view/${post.id}">${post.title}</a></h2>
`;
</script>
```

Create a route and serve the `post-index` component.

```js caption=routes/webc.js
import { view } from "primate";

const posts = [{
id: 1,
title: "First post",
}];

export default {
get() {
return view("post-index.webc", { posts });
},
};
```

Your rendered web component will be accessible at http://localhost:6161/webc.

## Configuration options

### extension

Default `".webc"`

The file extension associated with web components.

## Resources

* [Repository][repo]

[repo]: https://github.com/primatejs/primate/tree/master/packages/frontend
4 changes: 2 additions & 2 deletions packages/frontend/src/frontends/common/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default async ({
return {
async server(component) {
const target_base = app.runpath(location.server, location.components);
const code = await compile.server(await component.text());
const code = await compile.server(await component.text(), component, app);
const path = target_base.join(`${component.path}.js`.replace(source, ""));
await path.directory.create();
await path.write(code.replaceAll(extensions.from, extensions.to));
Expand All @@ -62,7 +62,7 @@ export default async ({
const { path } = component;

const file_string = `./${build}/${name}`;
const { js, css } = await compile.client(await component.text());
const { js, css } = await compile.client(await component.text(), component, app);
{
const code = js.replaceAll(extensions.from, extensions.to);
const src = `${path}.js`.replace(`${source}`, _ => build);
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/frontends/common/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as load } from "./load.js";
export { default as module } from "./module.js";
export { default as peers } from "./peers.js";
export { default as respond } from "./respond.js";
export { default as normalize } from "./normalize.js";
1 change: 1 addition & 0 deletions packages/frontend/src/frontends/exports.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as webc } from "./webc/module.js";
export { default as htmx } from "./htmx/module.js";
export { default as markdown } from "./markdown/module.js";
export { default as react } from "./react/module.js";
Expand Down
21 changes: 21 additions & 0 deletions packages/frontend/src/frontends/webc/client/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default (name, props) => `
import * as components from "app";
globalThis.customElements.define("p-wrap-with", class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" });
const id = this.getAttribute("id");
const wrapped = globalThis.registry[id];
this.shadowRoot.appendChild(wrapped);
wrapped.render();
delete globalThis.registry[id];
}
});
globalThis.registry = {};
const element = globalThis.document.createElement("${name}");
element.props = ${JSON.stringify(props)};
globalThis.document.body.appendChild(element)
element.render();
`;
1 change: 1 addition & 0 deletions packages/frontend/src/frontends/webc/client/exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./default.js";
24 changes: 24 additions & 0 deletions packages/frontend/src/frontends/webc/client/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default name => `import * as impl from "./${name}.webc.impl.js";
globalThis.customElements.define("${name}", class extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.attachShadow({ mode: "open" });
}
render() {
this.shadowRoot.innerHTML = impl.default(this.props);
impl.mounted?.(this.shadowRoot);
}
});
export default props => {
const element = globalThis.document.createElement("${name}");
const uuid = crypto.randomUUID();
element.props = props;
globalThis.registry[uuid] = element;
return \`<p-wrap-with id="\${uuid}"></p-wrap-with>\`;
}`;
34 changes: 34 additions & 0 deletions packages/frontend/src/frontends/webc/imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import client from "./client/render.js";

const script_re = /(?<=<script)>(?<code>.*?)(?=<\/script>)/gus;

const extension = ".webc";

const extensions = {
from: extension,
to: `${extension}.js`,
};

export const compile = {
async server(text, component, app) {
const { location } = app.config;
const source = app.path.components;
const base = app.runpath(location.server, location.components);
const path = base.join(`${component.path}.js.impl.js`.replace(source, ""));
const script = await Promise.all([...text.matchAll(script_re)]
.map(({ groups: { code } }) => code));
await path.write(script);
return client(component.base);
},
async client(text, component, app) {
const { location } = app.config;
const source = app.path.components;
const base = app.runpath(location.client, location.components);
const path = base.join(`${component.path}.js.impl.js`.replace(source, ""));
const [script] = await Promise.all([...text.matchAll(script_re)]
.map(({ groups: { code } }) => code));
await path.write(script.replaceAll(extensions.from, extensions.to));
return { js: client(component.base) };
},
};

48 changes: 48 additions & 0 deletions packages/frontend/src/frontends/webc/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { normalize, compile, respond } from "../common/exports.js";
import client from "./client/exports.js";

const handler = (name, props = {}, options = {}) =>
async app => {
const [component] = name.split(".");
const assets = [await app.inline(client(component, props), "module")];
const head = assets.map(asset => asset.head).join("\n");
const script = assets.map(asset => asset.csp).join(" ");
const headers = { script };
const body = "";

return respond({ app, head, headers, body, options });
};

export default ({
extension = ".webc",
} = {}) => {
const name = "webc";
const rootname = name;
let imports = {};
const normalized = normalize(name);

return {
name: `primate:${name}`,
async init(app, next) {
imports = await import("./imports.js");

return next(app);
},
async register(app, next) {
app.register(extension, {
handle: handler,
compile: {
...await compile({
app,
extension,
rootname,
compile: imports.compile,
normalize: normalized,
}),
},
});

return next(app);
},
};
};
1 change: 1 addition & 0 deletions packages/website/primate.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export default {
"React",
"Solid",
"Vue",
"Web Components",
"HTMX",
"Handlebars",
"Markdown",
Expand Down

0 comments on commit 089c9e3

Please sign in to comment.