Simple and elegant component-based UI library
—
Lightweight component pattern for simple, stateless components without full lifecycle management. Pure components are optimized for performance and simplicity, ideal for presentational components or when you need fine-grained control over rendering.
Creates a pure component factory function that produces lightweight component instances.
/**
* Lift a riot component interface into a pure riot object
* @param func - RiotPureComponent factory function
* @returns The original function marked as pure for internal handling
*/
function pure<
InitialProps extends DefaultProps = DefaultProps,
Context = any,
FactoryFunction = PureComponentFactoryFunction<InitialProps, Context>
>(func: FactoryFunction): FactoryFunction;Usage Example:
import { pure } from "riot";
// Create a pure component factory
const createSimpleCard = pure(({ slots, attributes, props }) => {
return {
mount(element, context) {
element.innerHTML = `
<div class="card">
<h3>${props.title}</h3>
<p>${props.content}</p>
</div>
`;
},
update(context) {
// Update logic if needed
if (context && context.newContent) {
const p = element.querySelector('p');
p.textContent = context.newContent;
}
},
unmount(keepRootElement) {
if (!keepRootElement) {
element.innerHTML = '';
}
}
};
});
// Use the pure component
const element = document.getElementById("card-container");
const cardComponent = createSimpleCard({
props: { title: "Pure Card", content: "This is a pure component" }
});
cardComponent.mount(element);Pure components implement a simplified interface without the full RiotComponent lifecycle:
interface RiotPureComponent<Context = object> {
/** Mount component to DOM element */
mount(element: HTMLElement, context?: Context): void;
/** Update component with new context */
update(context?: Context): void;
/** Unmount component from DOM */
unmount(keepRootElement: boolean): void;
}The factory function receives component metadata and returns a pure component instance:
interface PureComponentFactoryFunction<
InitialProps extends DefaultProps = DefaultProps,
Context = any
> {
({
slots,
attributes,
props,
}: {
slots?: TagSlotData<Context>[];
attributes?: AttributeExpressionData<Context>[];
props?: InitialProps;
}): RiotPureComponent<Context>;
}import { pure } from "riot";
const createButton = pure(({ props, attributes }) => {
let element;
return {
mount(el) {
element = el;
element.innerHTML = `
<button class="btn ${props.variant || 'primary'}">
${props.label || 'Click me'}
</button>
`;
// Add event listeners
const button = element.querySelector('button');
button.addEventListener('click', props.onClick || (() => {}));
},
update(context) {
if (context && context.label) {
const button = element.querySelector('button');
button.textContent = context.label;
}
},
unmount(keepRootElement) {
if (!keepRootElement && element) {
element.innerHTML = '';
}
}
};
});
// Usage
const buttonComponent = createButton({
props: {
label: "Save",
variant: "success",
onClick: () => console.log("Saved!")
}
});const createDataTable = pure(({ props }) => {
let element;
return {
mount(el, context) {
element = el;
this.render(props.data || []);
},
update(context) {
if (context && context.data) {
this.render(context.data);
}
},
render(data) {
const tableHTML = `
<table class="data-table">
<thead>
<tr>${props.columns.map(col => `<th>${col.title}</th>`).join('')}</tr>
</thead>
<tbody>
${data.map(row => `
<tr>${props.columns.map(col => `<td>${row[col.key]}</td>`).join('')}</tr>
`).join('')}
</tbody>
</table>
`;
element.innerHTML = tableHTML;
},
unmount(keepRootElement) {
if (!keepRootElement && element) {
element.innerHTML = '';
}
}
};
});
// Usage
const tableComponent = createDataTable({
props: {
columns: [
{ key: 'name', title: 'Name' },
{ key: 'email', title: 'Email' }
],
data: [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
]
}
});Even though called "pure", these components can maintain internal state:
const createCounter = pure(({ props }) => {
let element;
let count = props.initial || 0;
return {
mount(el) {
element = el;
this.render();
// Add event listeners
element.addEventListener('click', (e) => {
if (e.target.matches('.increment')) {
count++;
this.render();
} else if (e.target.matches('.decrement')) {
count--;
this.render();
}
});
},
update(context) {
if (context && typeof context.count === 'number') {
count = context.count;
this.render();
}
},
render() {
element.innerHTML = `
<div class="counter">
<button class="decrement">-</button>
<span class="count">${count}</span>
<button class="increment">+</button>
</div>
`;
},
unmount(keepRootElement) {
if (!keepRootElement && element) {
element.innerHTML = '';
}
}
};
});interface RiotPureComponent<Context = object> {
mount(element: HTMLElement, context?: Context): void;
update(context?: Context): void;
unmount(keepRootElement: boolean): void;
}
interface PureComponentFactoryFunction<
InitialProps extends DefaultProps = DefaultProps,
Context = any
> {
({
slots,
attributes,
props,
}: {
slots?: TagSlotData<Context>[];
attributes?: AttributeExpressionData<Context>[];
props?: InitialProps;
}): RiotPureComponent<Context>;
}
type DefaultProps = Record<PropertyKey, any>;
type TagSlotData<Context = any> = {
id: string;
html: string;
bindings: BindingData<Context>[];
};
type AttributeExpressionData<Context = any> = {
name: string;
evaluate: (context?: Context) => any;
};Install with Tessl CLI
npx tessl i tessl/npm-riot