Frontend Cheatsheet
Quick reference for the Fullfinity frontend framework.
Module Structure
Section titled “Module Structure”my_module/└── static/ └── src/ └── js/ └── components.js # Your components hereComponents are auto-loaded based on module dependencies.
Components
Section titled “Components”Register a Component
Section titled “Register a Component”const { registry, html, useState } = window.fullfinity;
registry.component('MyComponent', function({ prop1, prop2 }) { const [state, setState] = useState(initialValue);
return html` <div class="my-component"> ${prop1} </div> `;});Use in Template (Jinja2)
Section titled “Use in Template (Jinja2)”<div data-component="MyComponent" data-props='{"prop1": "value", "prop2": {{ variable | tojson }}}'></div>HTML Template Syntax
Section titled “HTML Template Syntax”const { html } = window.fullfinity;
// Elementshtml`<div class="container">Content</div>`
// Expressionshtml`<span>${value}</span>`
// Eventshtml`<button onClick=${handleClick}>Click</button>`
// Conditionalhtml`${condition && html`<div>Shown</div>`}`html`${condition ? html`<div>A</div>` : html`<div>B</div>`}`
// Listshtml`<ul>${items.map(i => html`<li key=${i.id}>${i.name}</li>`)}</ul>`
// Nested componentshtml`<${ChildComponent} prop="value" />`
// Refshtml`<input ref=${inputRef} />`const { useState, // Local state useEffect, // Side effects useRef, // Mutable ref useCallback, // Memoized callback useMemo, // Memoized value useStore, // Access store useSlot, // Access slot (re-renders on change) useLifecycle, // Lifecycle callbacks} = window.fullfinity;useState
Section titled “useState”const [value, setValue] = useState(initial);setValue(newValue); // DirectsetValue(prev => prev + 1); // FunctionaluseEffect
Section titled “useEffect”// Run on every renderuseEffect(() => { /* ... */ });
// Run once on mountuseEffect(() => { // setup return () => { /* cleanup */ };}, []);
// Run when deps changeuseEffect(() => { /* ... */ }, [dep1, dep2]);useRef
Section titled “useRef”const ref = useRef(initialValue);ref.current = newValue; // Mutable, doesn't trigger re-render
// DOM refconst inputRef = useRef(null);html`<input ref=${inputRef} />`;inputRef.current.focus();useLifecycle
Section titled “useLifecycle”const { useLifecycle } = window.fullfinity;
useLifecycle({ onMounted: () => console.log('Mounted'), onUpdated: () => console.log('Updated'), onWillUnmount: () => console.log('Unmounting')});Stores
Section titled “Stores”Create Store
Section titled “Create Store”const { registry, bus } = window.fullfinity;
registry.store('cart', { items: [], add(item) { this.items.push(item); fullfinity.notifyStore('cart'); // Trigger re-renders bus.emit('cart:updated'); // Notify listeners }});Use Store
Section titled “Use Store”const { useStore } = window.fullfinity;const cart = useStore('cart');cart.add({ id: 1, name: 'Product' });Event Bus
Section titled “Event Bus”const { bus } = window.fullfinity;
// Subscribeconst unsubscribe = bus.on('event:name', (data) => { });
// Emitbus.emit('event:name', { key: 'value' });
// Unsubscribeunsubscribe();
// Oncebus.once('event:name', (data) => { });Patching
Section titled “Patching”Wrap Component
Section titled “Wrap Component”const { patch, html } = window.fullfinity;
patch('ExistingComponent', (Original) => function(props) { return html` <div class="wrapper"> <${Original} ...${props} /> <div>Extra content</div> </div> `;}, { id: 'my-wrapper' });With Priority & Conditions
Section titled “With Priority & Conditions”patch('Component', patcher, { id: 'my-patch', // Required for unpatch/after priority: 20, // Lower runs first (default: 10) after: 'other-patch', // Run after this patch when: () => condition // Only apply if true});Modify Props
Section titled “Modify Props”patch('Component', (Original) => function(props) { const modifiedProps = { ...props, newProp: 'value' }; return html`<${Original} ...${modifiedProps} />`;}, { id: 'props-modifier' });Remove Patch
Section titled “Remove Patch”const { unpatch } = window.fullfinity;unpatch('Component', 'my-patch');Method Patching
Section titled “Method Patching”const { patchMethod, unpatchMethod } = window.fullfinity;
// Multiple patches stack in priority orderpatchMethod('CartWidget', 'computeTotal', function(_super, items) { return _super(items) * 0.9; // 10% discount}, { id: 'discount', priority: 10 });
patchMethod('CartWidget', 'computeTotal', function(_super, items) { return _super(items) + 5; // Add shipping (runs after discount)}, { id: 'shipping', priority: 20 });
// Remove a method patchunpatchMethod('CartWidget', 'computeTotal', 'discount');Extension Slots
Section titled “Extension Slots”Define Slot (in component)
Section titled “Define Slot (in component)”const { useSlot } = window.fullfinity;
registry.component('ProductCard', function(props) { // useSlot re-renders when addons register const extras = useSlot('ProductCard:afterPrice'); return html` <div class="product"> <span class="price">${props.price}</span> ${extras.map(C => html`<${C} ...${props} />`)} </div> `;});Register into Slot
Section titled “Register into Slot”registry.slot('ProductCard:afterPrice', LoyaltyBadge, { priority: 10, id: 'loyalty', when: () => userHasLoyalty});Remove from Slot
Section titled “Remove from Slot”registry.removeSlot('ProductCard:afterPrice', 'loyalty');Common Patterns
Section titled “Common Patterns”Fetch Data
Section titled “Fetch Data”registry.component('DataList', function({ endpoint }) { const { useState, useEffect, html } = window.fullfinity; const [data, setData] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { fetch(endpoint) .then(r => r.json()) .then(setData) .finally(() => setLoading(false)); }, [endpoint]);
if (loading) return html`<div>Loading...</div>`; return html`<ul>${data.map(i => html`<li>${i.name}</li>`)}</ul>`;});Form Handling
Section titled “Form Handling”registry.component('Form', function({ onSubmit }) { const { useState, html } = window.fullfinity; const [value, setValue] = useState('');
function handleSubmit(e) { e.preventDefault(); onSubmit(value); }
return html` <form onSubmit=${handleSubmit}> <input value=${value} onInput=${e => setValue(e.target.value)} /> <button type="submit">Submit</button> </form> `;});Conditional Classes
Section titled “Conditional Classes”html`<div class=${'base ' + (active ? 'active' : '')}>...</div>`html`<div class=${['base', active && 'active'].filter(Boolean).join(' ')}>...</div>`API Reference
Section titled “API Reference”Registry
Section titled “Registry”registry.component('Name', fn) // Registerregistry.component('Name') // Getregistry.getOriginal('Name') // Get unpatched originalregistry.hasComponent('Name') // Checkregistry.listComponents() // List all
registry.store('name', obj) // Registerregistry.store('name') // Getregistry.hasStore('name') // Checkregistry.listStores() // List all
registry.slot('Name:slot', Comp, opts) // Register into slotregistry.getSlot('Name:slot') // Get slot componentsregistry.removeSlot('Name:slot', id) // Remove from slotregistry.listSlots('Name') // List slotsData (server API)
Section titled “Data (server API)”// Primitive — use for bespoke, access-controlled routes (/help/…, /shop/api/…, /blog/…)api.request(path, { method, body, headers, ...fetchOptions }) // JSON request → parsed body (or null)
// Model-endpoint helpers (all built on api.request)api.fetch(model, id, { fields }) // Read one record by idapi.query(model, { filter, fields, limit, offset, order }) // Query records → { records, total }api.create(model, data, { ctx, fields }) // Create a record → created recordapi.update(model, id, data, { ctx, fields }) // Update a record → updated recordapi.execute(model, method, { ids, args }) // Call a model method → resultapi.delete(model, id) // Delete a record by idUse these instead of hand-rolling fetch(). api.request sets JSON headers,
serializes the body, throws an Error (with the server’s message) on a non-2xx
response, and returns the parsed JSON. It passes extra fetch options straight
through — e.g. api.request(url, { credentials: 'omit' }) for a public route.
The model helpers hit the internal-user record endpoints; for the public portal
or storefront, call api.request against your own access-controlled route (which
enforces contact/session scoping), not the generic model endpoints.
// bespoke route exampleconst items = await api.request('/help/suggest?q=' + encodeURIComponent(q), { credentials: 'omit' });await api.request('/shop/api/cart/add', { method: 'POST', body: { variant_id, qty } });Toasts
Section titled “Toasts”// Pick an intent — colour, icon and duration come with it. Never hand-roll a toast.toast.success(message, title?) // 4s toast.warning(message, title?) // 6stoast.error(message, title?) // 8s toast.info(message, title?) // 5stoast.neutral(message, title?) // 4s
toast.show({ type, title, message, duration }) // intent decided at runtime; → { close }See Components → Toasts.
Bottom chrome
Section titled “Bottom chrome”// Bars pinned to the bottom edge STACK. Lower order sits closer to the edge:// an ecommerce buy bar claims 0, the website admin toolbar claims 10.const release = bottomChrome.claim(el, { order: 0 }); // → () => voidbottomChrome.release(el); // or release explicitlybottomChrome.measure(); // recompute now (rarely needed)Read in CSS — always with the 0px fallback, since both properties are absent when nothing
is claimed:
.my-bar { bottom: var(--ff-chrome-offset, 0px); } /* in the stack */.my-launcher { bottom: calc(1rem + var(--ff-bottom-chrome, 0px)); } /* floating above it */See Components → Bottom Chrome.
PinInput
Section titled “PinInput”// One box per character for a short code. Controlled: `value` is the whole string.html`<${PinInput} label="Verification code" length=${6} value=${code} onInput=${setCode} onComplete=${submit} />`Patching
Section titled “Patching”fullfinity.patch('Name', fn, opts) // Patch componentfullfinity.unpatch('Name', 'patch-id') // Remove patchfullfinity.patchMethod('Name', 'method', fn, opts) // Patch methodfullfinity.unpatchMethod('Name', 'method', 'id') // Remove method patchfullfinity.listPatches('Name') // List patch IDsfullfinity.listMethodPatches('Name', 'method') // List method patch IDsMounting
Section titled “Mounting”fullfinity.mountAll() // Mount all registered componentsfullfinity.mountComponent('Name') // Mount specific componentfullfinity.unmount(element) // Unmount from elementUtilities
Section titled “Utilities”fullfinity.notifyStore('name') // Trigger store re-rendersfullfinity.bus.on/emit/off/once // Event busDebugging
Section titled “Debugging”// In browser consolewindow.fullfinity.registry.listComponents() // See all componentswindow.fullfinity.registry.listStores() // See all storeswindow.fullfinity.registry.store('cart') // Inspect store
// Check if component existswindow.fullfinity.registry.hasComponent('ProductCard')
// Inspect patcheswindow.fullfinity.listPatches('ProductCard') // See patch IDswindow.fullfinity.registry.getOriginal('ProductCard') // Get unpatched
// Inspect slotswindow.fullfinity.registry.listSlots('ProductCard') // See slotswindow.fullfinity.registry.getSlot('ProductCard:afterPrice') // Get slot contents