Adding a page and a menu item
Add a page
1. Create the component: src/pages/reports/sales.ts and its template src/pages/reports/sales.html.
// src/pages/reports/sales.ts
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { PageHeader } from '@/components/shell/page-header';
import { t } from '@/core/i18n';
import { injectPageScript } from '@/core/page-script';
/** Sales report (/reports/sales). */
@Component({
selector: 'ox-reports-sales-page',
imports: [PageHeader],
templateUrl: './sales.html',
host: { class: 'ox-host' },
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export default class ReportsSalesPage {
protected readonly t = t;
constructor() {
injectPageScript();
}
}<!-- src/pages/reports/sales.html -->
<div oxPageHeader [title]="'reports.sales.title'" [subtitle]="'reports.sales.subtitle'" [section]="'nav.reports'" [sectionHref]="'/reports/sales'"></div>
<div class="fd-page">
<section class="fd-card">
<div class="fd-card__body">
<p data-i18n="reports.sales.empty">{{ t('reports.sales.empty') }}</p>
</div>
</section>
</div>The page is the default export of its file (the router loads it with loadComponent). The host class ox-host
gives the page's host element no box, so the markup lays out as in plain HTML. The page header is a component with
an attribute selector (div[oxPageHeader]): its inputs are translation keys, bound with [input]="'key'".
2. Register the route in src/app/pages.ts:
{ path: '/reports/sales', id: 'reports-sales', title: 'reports.sales.title', layout: 'app', load: () => import('@/pages/reports/sales') },| Field | Meaning |
|---|---|
path | URL of the page (relative to the base path) |
id | Page id: marks the current menu entry (the menu entry with the same id) |
title | Translation key of the document title (… · Orbix) |
layout | app, auth (plus variant) or blank |
attrs | Optional attributes set on <html> while the page is shown |
load | The page component, loaded on demand (its own JavaScript chunk) |
src/app/app.routes.ts builds the Angular Router configuration from this list: one route per layout (the layout
component) with its pages as child routes; the page data goes to the route data (page), read by the
CurrentPage service (current menu entry, <html> attributes) and the title strategy. Route parameters work as
usual: add a route with path: 'reports/:id' in app.routes.ts and read it with input() (with
withComponentInputBinding()) or inject(ActivatedRoute).
3. Add the texts to every src/locales/<locale>/ folder, for example a new reports.json imported in each
index.ts (see Languages):
{
"sales": {
"title": "Sales report",
"subtitle": "Revenue by channel and region",
"empty": "No sales in this period yet."
}
}4. Optional page script. Code that works on the rendered markup (third-party widgets, charts, timers) goes in a
function passed to injectPageScript(). It runs once the page has rendered; listeners on document/window added
with listen(), timers added with every() and charts are released when the page is destroyed:
// src/scripts/reports.ts
import { listen } from '../core/scope';
export function init(): void {
listen(document, 'keydown', (event) => {
if (event.key === 'r' && event.altKey) document.querySelector<HTMLButtonElement>('[data-report-refresh]')?.click();
});
}import { init } from '@/scripts/reports';
// ...
constructor() {
injectPageScript(init);
}For state that belongs to Angular (filters, lists, forms), use signals (signal, computed), template bindings,
@for / @if and services inside the component.
Visit http://localhost:4200/reports/sales.
Pages that need real data fetch it from your API in a service (for example with
HttpClientandprovideHttpClient()insrc/app/app.config.ts, or theresource()/httpResource()APIs) and render it with the same markup (@for,@if).
Add a menu item
The sidebar, the horizontal menu, the two-column rail and the command palette are all built from src/app/menu.ts.
Sections hold items and groups; groups hold children:
{
id: 'reports',
key: 'nav.reports', // section title (translation key)
icon: 'lc-chart-column',
items: [
{ id: 'reports-sales', to: '/reports/sales', key: 'nav.sales_report', icon: 'lc-chart-line' },
{
id: 'reports-more',
key: 'nav.more_reports',
icon: 'lc-folder-open',
children: [{ id: 'reports-stock', to: '/reports/stock', key: 'nav.stock_report' }],
},
],
},| Key | Meaning |
|---|---|
id | Unique navigation id; the page with the same id marks the entry as current and opens its group |
to | Route path of the page |
key | Translation key of the label |
icon | Icon id in the sprite: lc-* (Lucide), tb-* (Tabler), fd-i-* or ox-i-* (Orbix) |
badge | Optional counter: { count: 4, tone: 'primary' } |
Links
Use routerLink for links between pages: <a class="fd-btn fd-btn--primary" routerLink="/reports/sales">
(import RouterLink in the component). It adds no classes of its own, so the fd-* markup stays exactly as
designed. From code, use inject(Router).navigateByUrl('/reports/sales'). Plain <a href> links created at
runtime (for example inside a data grid cell) are handled by the router as well, so the app never reloads.
Icons
Every icon is an SVG <use> reference to the sprite inlined in index.html:
<svg class="fd-icon" aria-hidden="true"><use href="#fd-i-search"/></svg> <!-- design system -->
<svg class="fd-icon fd-icon--sm" aria-hidden="true"><use href="#lc-chart-line"/></svg> <!-- Lucide -->Browse the sets on the Icons pages of the template. Only the icons you reference are included in the sprite:
after adding a new lc-* or tb-* icon, run npm run assets (it also runs with npm start, npm run build and
npm test).