De frontend
De frontend van je module is een Angular-library die Tabbr tijdens het draaien laadt (Native Federation). Het
vertrekpunt is één klasse: de moduleklasse in src/app/module/module.ts.
De moduleklasse
Section titled “De moduleklasse”Je klasse erft van SimpleModuleBase uit @tabbr/module-sdk. In de constructor geef je je module-id en je
routes mee; via signals vertel je Tabbr wat je module verder bijdraagt.
import { computed, signal } from '@angular/core';import { FullDetailContribution, MenuContribution, SimpleModuleBase, WidgetDefinition } from '@tabbr/module-sdk';import { I18N_BUNDLES } from './i18n-bundles';import { MODULE_ID, MODULE_PATH } from './module-config';
export class Module extends SimpleModuleBase { static override readonly i18nBundles = I18N_BUNDLES;
/** een widget op het dashboard */ readonly widgetDefinitions = signal<WidgetDefinition[]>([{ moduleSource: MODULE_ID, name: 'Mijn bevoegdheden', defaultStyleClasses: 'w-full sm:w-100', componentType: () => import('./widget/my-widget.component').then(m => m.MyWidgetComponent), }]);
constructor() { // de pagina's van je module super(MODULE_ID, [{ path: MODULE_PATH, loadComponent: () => import('./page/page.component').then(m => m.PageComponent), }]);
// je instellingenpagina, onder Instellingen › <je module> this.settingsRoutes = signal([{ path: MODULE_ID, loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent), }]);
// een item in het menu this.menu = computed<MenuContribution[]>(() => [{ moduleId: MODULE_ID, order: 56, createNavigationItemsForMenu: (menu: any) => menu.addRootMenuItem({ id: `${MODULE_ID}.page`, title: 'Bevoegdheden', type: 'basic', icon: 'heroicons_outline:identification', link: `/${MODULE_PATH}`, }), }]); }
/** een sectie op de detailpagina van een persoon (en dus ook op het eigen profiel) */ getFullDetailContribution(entityName: string, entity: any): FullDetailContribution[] { if (entityName !== 'person' || !entity?.id) return []; return [{ id: MODULE_ID, title: 'Bevoegdheden & competenties', order: 40, inputs: { entityName, entity }, component: import('./profile/person-section.component').then(m => m.PersonSectionComponent), }]; }}Wat een module kan bijdragen
Section titled “Wat een module kan bijdragen”| Lid | Wat het doet |
|---|---|
routes (constructor) |
De pagina’s van je module, onder de URL van de club. |
settingsRoutes |
Je eigen instellingenpagina in Instellingen. |
menu |
Menu-items, via createNavigationItemsForMenu(menu) met addRootMenuItem, addMenuItem of addAdminMenuItem. |
widgetDefinitions |
Widgets voor het dashboard. |
getFullDetailContribution() |
Secties op de detailpagina van een record — op een persoon verschijnen ze ook op het profiel. |
messageRecipients(search) |
Ontvangers in het e-mailscherm, bv. “Groepen (module Groepen)”. |
userPrompts |
Vragen aan de gebruiker: bij het openen van de app of als banner (API 1.3). |
provides |
De frontend van een contract dat je module aanbiedt (API 1.3). |
static i18nBundles |
Je vertalingen, zodat Tabbr je titel en beschrijving kan tonen. |
Wat Tabbr je geeft: de tokens
Section titled “Wat Tabbr je geeft: de tokens”Alles wat je module van Tabbr nodig heeft, injecteer je via tokens uit @tabbr/module-sdk.
| Token | Wat het geeft |
|---|---|
TABBR_API_HOST |
De basis-URL van de API. |
ACTIVE_CLUB_CODE, ACTIVE_LANGUAGE |
De club en de taal van de gebruiker, als signals. |
SETTINGS_PAGE_HOST |
Voor je instellingenpagina: bindForm({ dirty, valid, save, cancel }) toont Opslaan en Annuleren in de header. |
PAGE_HEADER_ACTIONS |
Zet de acties van je scherm in de header van de app (zie hieronder). |
MESSAGING_COMPOSER |
Het e-mailscherm van Tabbr openen: compose({ to, subject }). |
FILTER_EDITOR_COMPONENT, SCRIPT_EDITOR_COMPONENT |
De filter- en scripteditor van Tabbr, in je eigen scherm. |
MODULE_CONTRACTS |
Contracten van andere modules: get(name). |
PERSON_PUBLIC_API, FLIGHT_PUBLIC_API, AIRPLANE_PUBLIC_API |
Bv. personNow(): de ingelogde persoon. |
Injecteer de tokens die niet in elke context bestaan (de dev preview, een oudere Tabbr) altijd optioneel:
private readonly composer = inject(MESSAGING_COMPOSER, { optional: true });Schermacties in de header
Section titled “Schermacties in de header”Op elk scherm van Tabbr staan de acties — Nieuw, Importeren, Opslaan, Annuleren… — bovenaan, in
de header van de app, nooit onderaan een formulier. Je module doet hetzelfde met PAGE_HEADER_ACTIONS. Elke
module van Tabbr heeft daarvoor een kleine directive:
import { Directive, inject, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';import { PAGE_HEADER_ACTIONS } from '@tabbr/module-sdk';
/** <div *grpHeaderActions>…knoppen…</div>: in de header van de app; zonder header blijven ze ter plaatse. */@Directive({ selector: '[grpHeaderActions]' })export class HeaderActionsDirective implements OnInit, OnDestroy { private readonly template = inject(TemplateRef); private readonly viewContainer = inject(ViewContainerRef); private readonly header = inject(PAGE_HEADER_ACTIONS, { optional: true }); private remove: (() => void) | null = null;
ngOnInit(): void { if (this.header) this.remove = this.header.show(this.template); else this.viewContainer.createEmbeddedView(this.template); }
ngOnDestroy(): void { this.remove?.(); }}<div *grpHeaderActions> <button mat-flat-button (click)="newGroup()">Nieuwe groep</button></div>Toon Opslaan en Annuleren alleen zolang er niet-bewaarde wijzigingen zijn. Een dialoog houdt zijn eigen knoppen.
Gegevens lezen en schrijven
Section titled “Gegevens lezen en schrijven”injectModuleEntity(naam) geeft je toegang tot een entiteit — van je eigen module of van Tabbr — met de
toegangsregels van de club:
import { injectModuleEntity } from '@tabbr/module-sdk';
interface Group { id: string; name: string; parent?: { id: string } }
export class GroupsStore { private readonly groups = injectModuleEntity<Group>('group');
async rootGroups() { return this.groups.queryAll({ where: [{ field: 'parent', operator: 'IS_EMPTY' }] }); }
async rename(id: string, name: string) { await this.groups.update(id, { name }); }}| Methode | Wat ze doet |
|---|---|
query({ where, anyOf, q, page }) |
Eén pagina (50 records), met filters, OF-groepen en vrije zoektekst. |
queryAll(…), count(…), countBy(field) |
Alles, het aantal, het aantal per waarde. |
getById(id), create(…), update(id, …), delete(id) |
Eén record. |
relatedFilter(field, query) |
Een filter op een gerelateerd record. |
fieldMetadata() |
De velden — ook de eigen velden van de club. |
Operators: EQ, NEQ, GT, LT, GTE, LTE, IS_EMPTY, IS_NOT_EMPTY. Een datum mag today() zijn.
Instellingen van je module bewaar je met injectModuleApi(moduleId): loadSettings(), saveSettings(…), en
trigger(naam) om een trigger van je backend met de hand te starten.
Lange lijsten
Section titled “Lange lijsten”Een lange lijst laadt pagina per pagina terwijl je scrolt. De SDK doet het werk:
import { incrementalList, loadMoreWhenVisible } from '@tabbr/module-sdk';
readonly list = incrementalList(() => { const status = this.status(); // de lijst begint opnieuw als dit signal verandert return (page) => this.consents.query({ where: [{ field: 'status', value: status }], page });});private readonly end = viewChild<ElementRef>('end');
constructor() { loadMoreWhenVisible(this.list, this.end);}@for (row of list.rows(); track row.id) { … }<div #end class="h-5"></div>incrementalList geeft rows, total, loading, loadingMore, hasMore, error, loadMore() en
reload().
Vertalingen
Section titled “Vertalingen”Je vertalingen staan in src/app/module/i18n/nl.ts (en en.ts, …), genest onder je module-id. Voor je routes
geef je ze mee met provideModuleI18n({ bundles }) uit @tabbr/module-sdk/i18n, in de providers van de route.