Skip to main content

Modular UI

How to wire blong-browser into a suite and write UI pages for a realm.

See Browser UI for the concept overview.


Adding blong-browser to a Suite

Include the blong-browser realm in the suite's browser.ts entry point alongside the application realms:

// browser.ts
import {browser} from '@feasibleone/blong';
import pkg from './package.json' with {type: 'json'};

export default browser(blong => ({
url: import.meta.url,
pkg: {name: pkg.name, version: pkg.version},
children: [
async function blongUi() {
return import('@feasibleone/blong-browser/browser.ts');
},
async function marine() {
return import('./marine/browser.ts');
},
],
config: {
default: {
blongUi: {},
marine: {},
},
},
}));

The blong-browser realm automatically registers the portal shell, auth handling, backend adapter, storage adapter, and (in storybook/integration environments) the mock adapter. No explicit initialisation is needed.


Contributing Pages from a Realm

A realm contributes pages by placing handler files that end with .component in its layer. The portal orchestrator discovers them automatically by file-name pattern.

Minimal component handler

// marine/component/marineCoralBrowse.component.ts
import {handler} from '@feasibleone/blong';

export default handler(() => ({
'marine.coral.browse': async () => ({
title: 'Corals',
permission: 'marine.coral.browse',
component: async () => {
const {CoralBrowse} = await import('./CoralBrowse.js');
return CoralBrowse;
},
}),
}));

For the common case of CRUD pages, use the Model System instead — see Schema based UI.

Adding menu items

Place a .portal file in the component layer. The portal orchestrator imports all *.portal files to build the navigation menu.

// marine/component/marine.portal.ts
import {handler} from '@feasibleone/blong';

export default handler(({handler: {portalMenuItem}}) => ({
async 'marine.portal.params'() {
return {
menu: [{
title: 'Marine',
items: [
await portalMenuItem('marine.coral.browse'),
await portalMenuItem('marine.family.browse'),
],
}],
};
},
}));

Using Editor Directly

For pages that need custom logic beyond what the model system provides, use the Editor component directly in a React component:

import {Editor} from '@feasibleone/blong-browser';
import type {IEnrichedSchema} from '@feasibleone/blong-browser';

export function CoralOpen({schema, coralId}: {schema: IEnrichedSchema; coralId: number}) {
return (
<Editor
schema={schema}
cards={{
main: {
label: 'Coral Details',
widgets: ['coral.coralName', 'coral.familyId', 'coral.maxDepth'],
className: 'col-12 md:col-8',
},
notes: {
label: 'Notes',
widgets: ['coral.description'],
className: 'col-12 md:col-4',
},
}}
layouts={{edit: ['main', 'notes']}}
loadAction="marine.coral.get"
loadParams={{coralId}}
saveAction="marine.coral.edit"
editable
/>
);
}

Using Explorer Directly

import {Explorer} from '@feasibleone/blong-browser';

export function CoralList({schema}: {schema: IEnrichedSchema}) {
return (
<Explorer
schema={schema}
listAction="marine.coral.find"
selectionMode="single"
toolbar={[{
label: 'Create',
icon: 'pi pi-plus',
action: 'marine.coral.new',
permission: 'marine.coral.new',
}]}
/>
);
}

Layout Types

Flat layout

layouts={{ edit: ['main', ['contacts', 'notes']] }}
// main = full width; contacts + notes = stacked in second column

Tabbed layout

layouts={{
edit: {
items: [
{id: 'basic', label: 'Basic', icon: 'pi pi-id-card', widgets: ['main']},
{id: 'contacts', label: 'Contacts', icon: 'pi pi-phone', widgets: ['contacts']},
],
},
}}

Steps layout

layouts={{
edit: {
type: 'steps',
items: [
{id: 'step1', label: 'Identity', widgets: ['identity']},
{id: 'step2', label: 'Details', widgets: ['details']},
{id: 'step3', label: 'Review', widgets: ['review']},
],
},
}}
layouts={{
edit: {
orientation: 'left',
items: [
{id: 'groupA', label: 'Group A', icon: 'pi pi-cog', widgets: ['card1', 'card2']},
{id: 'groupB', label: 'Group B', icon: 'pi pi-user', widgets: ['card3']},
],
},
}}

Accessing Dispatch and Schema

Inside a React component rendered by blong-browser, use the context hooks:

import {useBlongUi} from '@feasibleone/blong-browser';

const {dispatch, schemaRegistry} = useBlongUi();

// Call any registered handler
const result = await dispatch('marine.coral.find', {coralName: 'Brain'});

// Get enriched schema for an object
const schema = await schemaRegistry.resolve('marine.coral');

Storybook Pattern

Two Storybook patterns exist:

Component-level stories (blong-browser internal)

Stories mock the dispatch function to develop and test individual components in isolation. Use the withDispatch decorator from .storybook/dispatch.js:

// CoralOpen.stories.tsx
import {withDispatch} from '../../../.storybook/dispatch.js';
import {coralEditorFixture, coralStoryValue} from '@feasibleone/blong-marine/meta/storybook.js';

export default {
title: 'marine/CoralOpen',
decorators: [withDispatch({
coralCoralGet: () => Promise.resolve(coralStoryValue),
coralCoralEdit: params => Promise.resolve(params),
})],
};

export const Default = {
render: () => <Editor schema={coralEditorFixture.schema} cards={coralEditorFixture.cards}
loadAction="coralCoralGet" saveAction="coralCoralEdit" editable />,
};

Model page stories (ui-demo)

For end-to-end model page stories, use the Model component with withBlong(browser) in the .storybook/preview.tsx. This loads the full blong platform including the mock adapter:

// .storybook/preview.tsx
import withBlong from '@feasibleone/blong-browser/storybook.tsx';
import browser from '../browser.ts';

export default {
decorators: [withBlong(browser)],
parameters: {layout: 'fullscreen'},
};

Then stories use the page() helper:

// coral/Coral.stories.tsx
import {page} from '../../storyHelper.js';

export const CoralBrowse = page('marine.coral.browse');
export const CoralOpen = page('marine.coral.open', 1);
export const CoralNew = page('marine.coral.new');
export const CoralOpenSplit = page('marine.coral.open', 1, {layout: 'editSplit'});

Internationalisation (i18n)

blong-browser has a lightweight translation system built on appStore.

Text component

<Text> is the universal translation primitive. Its string children act as both the translation key and the English fallback:

import {Text} from '@feasibleone/blong-browser';

<Text>Save</Text>
<Text params={{field: 'Name', minLength: 3}}>
{'{field} must be at least {minLength} characters'}
</Text>

Button auto-translation

The blong Button wrapper auto-translates its string label via <Text>. Always import Button from blong-browser rather than primereact to get automatic translation:

import {Button} from '@feasibleone/blong-browser'; // ✅ translates label
<Button label="Save" icon="pi pi-check" />

Activating a language

import {useAppStore} from '@feasibleone/blong-browser';

useAppStore.getState().setTranslations({
Save: 'Запази',
'{field} is required': '{field} е задължително',
});
useAppStore.getState().setLanguage('bg');

PrimeReact widget locale via Theme.languages

Register custom PrimeReact locale data through IThemeConfig.languages, which Theme passes to addLocale automatically. Fetch locale data from primefaces/primelocale:

<App
dispatch={dispatch}
theme={{
name: 'lara-light-blue',
languages: {
bg: {
accept: 'Да', cancel: 'Отказ',
emptyMessage: 'Не са открити резултати',
dateFormat: 'dd/mm/yy', firstDayOfWeek: 1,
// … full locale object
},
},
}}
/>

Calling setLanguage('bg') activates the PrimeReact locale via locale('bg') in Theme.

Storybook language stories

Set lang: '<locale>' as a story arg to activate a language for that story — withDispatch picks it up from context.args.lang:

export const ToolbarBG: Story = {...Toolbar};
ToolbarBG.args = {lang: 'bg'};

Testing

Add an interaction test with a play() function:

Default.play = async ({canvasElement}) => {
const canvas = within(canvasElement);
// Wait for load
await canvas.findByDisplayValue('Brain Coral');
// Click Edit
await userEvent.click(canvas.getByTitle('Edit'));
// Change a field
await userEvent.clear(canvas.getByLabelText('Coral Name'));
await userEvent.type(canvas.getByLabelText('Coral Name'), 'Star Coral');
// Save
await userEvent.click(canvas.getByTitle('Save'));
};