Skip to content

Adding a UI

Rimitive provides the view package with view-specific modules. el() is the foundation for creating elements. It’s renderer-agnostic, but we’ll use the DOM adapter here.

Add the view modules to your service:

service.ts
import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
import { createDOMAdapter } from '@rimitive/view/adapters/dom';
import { createElModule } from '@rimitive/view/el';
import { MountModule } from '@rimitive/view/deps/mount';
export const svc = compose(
SignalModule,
ComputedModule,
EffectModule,
createElModule(createDOMAdapter()),
MountModule
);
export type Service = typeof svc;

el() is curried: el(tag)(children):

// A div tag
el('div')();
// An h1 with text ("Hello, World" are children)
el('h1')('Hello, World');
// A div with `el` children
el('div')(
el('h1')('Title'),
el('p')('Some text')
);

el returns a UI spec with a stable reference to the underlying element.


Use .props() to set reactive attributes and events:

const isDisabled = signal(false);
const button = el('button').props({
className: 'primary',
disabled: isDisabled,
onclick: () => console.log('clicked'),
})('Click me');
isDisabled(true); // button becomes disabled, class changes

Props are type-safe, driven by the adapter provided.


Pass a computed for reactive text:

const count = signal(0);
const display = el('div')(
computed(() => `Count: ${count()}`)
);
count(5); // display updates to "Count: 5"

UI components in Rimitive are functions that return UI specs:

const Greeting = (name: string) =>
el('div')(
el('h2')(`Hello, ${name}!`),
el('p')('Welcome to Rimitive.')
);
const DoubleGreeting = () => el('div')(
Greeting('Ada'),
Greeting('Grace')
);

Here’s a working example:

Loading sandbox...

Just like behaviors, components can be wrapped in a service function for portability:

import type { Service } from './service';
import { counter } from './behaviors/counter';
const Counter = (svc: Service) => {
const { el, computed } = svc;
const useCounter = svc(counter);
return (initial: number) => {
const { count, doubled, increment, decrement, reset } = useCounter(initial);
return el('div')(
el('div')(computed(() => `Count: ${count()} (doubled: ${doubled()})`)),
el('div')(
el('button').props({ onclick: decrement })('-'),
el('button').props({ onclick: increment })('+'),
el('button').props({ onclick: reset })('Reset')
)
);
};
};
// Components can compose other components in the service function
const Counters = (svc: Service) => {
const { el } = svc;
const CounterComponent = svc(Counter);
return () =>
el('div')(
CounterComponent(0),
CounterComponent(100)
);
};

This is useful for testing, SSR, or sharing components across different service contexts.


Use mount() to actually attach to the DOM:

const App = () => el('div')(
el('h1')('My App')
);
const app = mount(App());
document.body.appendChild(app.element!);