Skip to content
Orbix Angular · Bootstrap 5 v1.0.1

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') },
FieldMeaning
pathURL of the page (relative to the base path)
idPage id: marks the current menu entry (the menu entry with the same id)
titleTranslation key of the document title (… · Orbix)
layoutapp, auth (plus variant) or blank
attrsOptional attributes set on <html> while the page is shown
loadThe 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 HttpClient and provideHttpClient() in src/app/app.config.ts, or the resource() / 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' }],
    },
  ],
},
KeyMeaning
idUnique navigation id; the page with the same id marks the entry as current and opens its group
toRoute path of the page
keyTranslation key of the label
iconIcon id in the sprite: lc-* (Lucide), tb-* (Tabler), fd-i-* or ox-i-* (Orbix)
badgeOptional counter: { count: 4, tone: 'primary' }

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).