Cookbook
Every TypeScript recipe below is sourced from a file compiled by
npm run typecheck:docs.
Mount a reactive browser application
import { createApp, html } from '@gluonjs/core';
import { ref } from '@gluonjs/reactivity';
const count = ref(2);
createApp(() => html`
<main>
<h1>Count ${count.value}</h1>
<button type="button" @click=${() => { count.value += 1; }}>Increment</button>
</main>
`).mount(document.querySelector('#app')!);
Build a searchable keyed list with owned styles
import {
createApp,
createStyleSheetOwner,
css,
html,
repeat,
} from '@gluonjs/core';
import {
computed,
ref,
} from '@gluonjs/reactivity';
interface Product {
readonly id: string;
readonly name: string;
readonly price: string;
}
const products: readonly Product[] = [
{ id: 'orbit-lamp', name: 'Orbit Lamp', price: '$128' },
{ id: 'field-tote', name: 'Field Tote', price: '$84' },
];
const query = ref('');
const visibleProducts = computed(() => {
const needle = query.value.trim().toLowerCase();
return needle
? products.filter((product) => product.name.toLowerCase().includes(needle))
: products;
});
const styles = css`
main { max-inline-size: 40rem; margin: 2rem auto; font: 1rem/1.5 system-ui; }
label, article { display: grid; gap: 0.5rem; }
section { display: grid; gap: 1rem; margin-block-start: 1.5rem; }
article { padding: 1rem; border: 1px solid #d8d8d8; }
`;
const styleOwner = createStyleSheetOwner(document);
styleOwner.retain(styles);
const app = createApp(() => html`
<main>
<h1>Products</h1>
<label>
Search
<input
type="search"
.value=${query.value}
@input=${(event: Event) => {
query.value = (event.currentTarget as HTMLInputElement).value;
}}
>
</label>
<p>${visibleProducts.value.length} result(s)</p>
<section>
${repeat(
visibleProducts.value,
(product) => product.id,
(product) => html`
<article>
<strong>${product.name}</strong>
<span>${product.price}</span>
</article>
`,
)}
</section>
</main>
`);
app.onUnmounted(() => styleOwner.dispose());
const mounted = app.mount(document.querySelector('#app')!);
window.addEventListener('pagehide', () => mounted.unmount(), { once: true });
Compose Atom, Molecule, and Organism boundaries
import {
Button,
defineUiAtom,
} from '@gluonjs/atoms';
import {
css,
html,
} from '@gluonjs/core';
import { defineMolecule } from '@gluonjs/molecules';
import { defineOrganism } from '@gluonjs/organisms';
export const StockBadge = defineUiAtom<
{ readonly status: 'available' | 'back-order' },
'span'
>({
displayName: 'StockBadge',
tag: 'span',
nativeProps: ({ status }) => ({
class: `stock-badge stock-badge--${status}`,
children: status === 'available' ? 'In stock' : 'Back order',
}),
style: {
id: 'example-stock-badge',
sheet: css`
.stock-badge { font-weight: 700; }
.stock-badge--available { color: #315d19; }
.stock-badge--back-order { color: #815400; }
`,
},
});
export const ProductSummary = defineMolecule((props: Readonly<{
name: string;
price: string;
status: 'available' | 'back-order';
}>) => html`
<article>
<h2>${props.name}</h2>
<p>${props.price}</p>
${StockBadge({ status: props.status })}
</article>
`, 'ProductSummary');
export const CatalogSection = defineOrganism((props: Readonly<{
heading: string;
}>) => html`
<section aria-labelledby="catalog-heading">
<h1 id="catalog-heading">${props.heading}</h1>
${ProductSummary({
name: 'Orbit Lamp',
price: '$128',
status: 'available',
})}
${Button({ label: 'View all products' })}
</section>
`, 'CatalogSection');
Publish a Custom Element
import { GluonElement, defineElement, html } from '@gluonjs/core';
export class GluonCounter extends GluonElement {
count = 0;
increment(): void {
this.count += 1;
this.emit('change', { value: this.count });
void this.requestUpdate();
}
protected override render() {
return html`<button type="button" @click=${() => this.increment()}>${this.count}</button>`;
}
}
defineElement('gluon-counter', GluonCounter);
Compose Router and Store ownership
import { createApp, html } from '@gluonjs/core';
import { createRouter, createRouterPlugin, createWebHistory, RouterView } from '@gluonjs/router';
import { createStoreManager, defineStore } from '@gluonjs/store';
const counter = defineStore('counter', () => ({ count: 0 }), {
actions: (store) => ({ increment: () => { store.count += 1; } }),
});
const stores = createStoreManager();
const state = counter.use(stores);
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/', component: () => html`<button @click=${state.increment}>${state.count}</button>` }],
});
await router.isReady();
const app = createApp(() => html`<main>${RouterView()}</main>`);
app.use(createRouterPlugin(router));
app.onUnmounted(() => stores.dispose());
app.mount(document.querySelector('#app')!);
Render on the server
import { html } from '@gluonjs/core';
import { renderToString, serializeSsrState } from '@gluonjs/ssr';
const state = { route: '/products/orbit-lamp', bag: [] };
const rendered = await renderToString(html`<main><h1>Orbit Lamp</h1></main>`);
export const responseBody = `<!doctype html>
<main id="app">${rendered}</main>
<script type="application/json" data-gluon-state>${serializeSsrState(state)}</script>`;
Test through public utilities
import { html } from '@gluonjs/core';
import { cleanupFixtures, mountComponent } from '@gluonjs/test-utils';
const fixture = mountComponent(
({ label }: Readonly<{ label: string }>) => html`<button>${label}</button>`,
{ props: { label: 'Save' } },
);
if (fixture.get('button').textContent !== 'Save') throw new Error('Expected Save');
await cleanupFixtures();
Host a Gluon element from Vue
The host treats the Gluon component as a standards-based Custom Element. It
passes the production product and configuration as properties, supplies native
slots, and observes configuration-change and add-to-bag; it does not
translate the Gluon component into a Vue component.
import { adoptStyles, unadoptStyles } from '@gluonjs/core';
import { createApp as createVueApp, type App } from 'vue';
import { shopStyles } from '../../examples/shop/src/styles.js';
import { registerProductConfigurator } from '../../examples/shop/src/product-configurator.js';
import VueProductHost from './VueProductHost.vue';
import { vueHostStyles } from './vue-host-styles.js';
export interface VueHostMount {
readonly app: App<Element>;
unmount(): void;
}
export function mountVueHost(target: string | Element = '#vue-host'): VueHostMount {
registerProductConfigurator();
adoptStyles(document, shopStyles, vueHostStyles);
const app = createVueApp(VueProductHost);
app.mount(target);
return Object.freeze({
app,
unmount() {
app.unmount();
unadoptStyles(document, vueHostStyles, shopStyles);
},
});
}
if (document.querySelector('#vue-host')) mountVueHost();
Run the compiled plain HTML host or the Vue host.