Writing an editor (page)¶
Copy of
sumaris-app/.agents/rules/editor.md(rule written for AI coding agents, always applied). Do not edit here: runnpm run site:sync-rulesto update it.
An editor is a full-page component that loads, displays, and saves one entity, orchestrating several child forms and tables. It owns the data lifecycle (route → load → setValue → getValue → save).
- Use the file suffix
.page.<extension>(see common rules); - Use
changeDetection: ChangeDetectionStrategy.OnPush; - An editor extends one of the editor base classes below, according to the entity kind.
Class hierarchy¶
From the most generic to the most specialized — pick the lowest class that fits:
-
AppEditor<T, ID, O>(@sumaris-net/ngx-components): base editor. Save/load/error lifecycle, child-form aggregation, toolbar, navigation. Key members:abstract load(id, opts),abstract save(event, opts),abstract reload(),abstract get isNewData();addForm(path, form)/addForms(forms)/removeForm(form);markAsSaving()/markAsSaved(),saving,savingSubject,submitted;cancel(),unload(),goBack(),saveIfDirtyAndConfirm(),saveDirtyChildren(),scrollToTop(),showToast().
-
AppTabEditor<T, ID, O>: adds tab support (MatTabGroup). Members:selectedTabIndex(get/set),selectedTabIndexChange,tabGroup,tabCount,enableSwipe;setSelectedTabIndex(),onTabChange(),onSwipeTab(),realignInkBar(),updateRoute();protected abstract getFirstInvalidTabIndex()— used to jump to the first tab in error on save.
-
AppEntityEditor<T, S, ID, ...>: generic entity editor backed by anIEntityService. This is where the data lifecycle lives. Members:data: T,service,dataType,usageMode,isOnFieldMode,isNewData,titleSubject,onUpdateView;load(id, opts),save(event, opts),saveAndClose(),delete(),reload(),close();updateView(data, opts),updateViewState(),updateTabAndRoute(),getValue(),setError(),resetError();- Override hooks (see below):
onNewEntity,onEntityLoaded,onEntitySaved,onEntityDeleted,computeUsageMode,canUserWrite,getJsonValueToSave; - Abstract members to implement:
computeTitle(data),get form(),getFirstInvalidTabIndex(),registerForms(),setValue(data, opts).
-
AppDataEntityEditor<T, S, ID, ..., ST, C>(src/app/data/form/data-editor.class.ts): adds program / strategy / PMFM resolution and quality context for data entities. Adds:- Injected services:
programRefService,strategyRefService,referentialRefService,configService,personService,context, etc.; - RxState properties + observables:
program/program$,programLabel/programLabel$,strategy/strategy$,acquisitionLevel,requiredStrategy,strategyFilter,pmfms/pmfms$,appliedProgram; - Hooks:
setProgram(program),setStrategy(strategy),loadStrategy(),onConfigLoaded(),translateFormPath(),_reloadProgram/_reloadStrategy; - Constructor options carry
acquisitionLevel,strategyResolution,strategyAcquisitionLevels, etc. - The state type extends
AppDataEditorState.
- Injected services:
-
AppRootDataEntityEditor<T, S, ID, ..., ST, C>(src/app/data/form/root-data-editor.class.ts): for root data entities (Trip,ObservedLocation,Landing…) that have their own quality lifecycle (control/terminate/validate),programcontrol, observers, and offline synchronization. Adds:network,programControl,startListenProgramChanges();computeUsageMode()derives FIELD mode fromsynchronizationStatus === 'DIRTY';canUserWrite()returns false oncevalidationDateis set;getValue()re-adds the (possibly disabled) program control;- The state type extends
RootDataEntityEditorState.
Note: child entities (e.g.
Operation) extendAppDataEntityEditor; root entities extendAppRootDataEntityEditor.
@Component declaration¶
@Component({
selector: 'app-my-entity-page',
templateUrl: './my-entity.page.html',
styleUrls: ['./my-entity.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOutAnimation],
providers: [
{ provide: APP_DATA_ENTITY_EDITOR, useExisting: forwardRef(() => MyEntityPage) },
RxState,
{ provide: ContextService, useExisting: MyEntityContextService },
],
})
export class MyEntityPage
extends AppRootDataEntityEditor<MyEntity, MyEntityService, number, /* LO, SO */, MyEntityPageState, MyEntityContextService>
implements OnInit, OnDestroy { /* ... */ }
- Declare a
MyEntityPageStateinterface (extendingRootDataEntityEditorStateorAppDataEditorState) above the class for dynamic attributes; - Reference child components with
@ViewChild('formRef', { static: true }) form: MyEntityForm;.
Constructor¶
Pass the entity class, the data service, and an options object to super():
constructor(injector: Injector, /* extra services */) {
super(injector, MyEntity, injector.get(MyEntityService), {
pathIdAttribute: 'myEntityId', // route param holding the id
tabCount: 2, // number of tabs
i18nPrefix: 'MY_ENTITY.', // i18n key prefix / i18nContext
acquisitionLevel: AcquisitionLevelCodes.MY_ENTITY,
strategyAcquisitionLevels: [/* ... */],
settingsId: MyEntityPageSettingsEnum.PAGE_ID,
enableListenChanges: true,
});
this.defaultBackHref = '/my-entities';
}
Key methods to implement / override¶
registerForms()(required): register every child form/table so the editor tracks their dirty/valid state:protected registerForms() { this.addForms([this.myEntityForm, this.measurementsForm, this.childTable]); }get form()(required): return the mainFormGroup(usuallythis.myEntityForm.form);setValue(data)(required): dispatch the loaded entity into each child form/table. Await async children withPromise.all([...]);getValue(): callawait super.getValue()then merge values pulled from child forms/tables back into the entity;getJsonValueToSave(): callawait super.getJsonValueToSave()then patch the JSON sent to the server (e.g. nested sub-entities);computeTitle(data)(required): return the page title (translateMY_ENTITY.NEW.TITLEfor new data,MY_ENTITY.EDIT.TITLEotherwise);getFirstInvalidTabIndex()(required): return the index of the first tab whose form/table is invalid (or -1);onNewEntity(data, opts): set defaults for a new entity (dates, program, vessel from the search filter/context, tab visibility), thenmarkAsReady()ormarkAsDirty();onEntityLoaded(data, opts): post-load setup (state connections, permissions);updateViewState(data, opts): callsuper, then show/hide tabs (updateTabsState(data));canUserWrite(data, opts): permission check (delegates to the data service).
Lifecycle¶
ngOnInit(): callsuper.ngOnInit(), then wire reactive state viathis._state.connect('key', source$)(e.g. from child form value changes) andthis._state.hold(...)for side effects;- The base class drives the flow automatically: route id →
load()→onNewEntity()/onEntityLoaded()→setValue()→updateView(). On save:getValue()→getJsonValueToSave()→service.save()→onEntitySaved(); ngOnDestroy(): callsuper.ngOnDestroy()and clean up custom subscriptions.
Quality lifecycle¶
For root entities, the control/terminate/validate logic lives in the service (see service-quality rules); the page exposes the toolbar buttons that call it. Some child editors implement IDataEntityQualityService<T> directly.
Reference examples¶
- Root data editor:
src/app/trip/trip/trip.page.ts,src/app/trip/observedlocation/observed-location.page.ts,src/app/trip/landing/landing.page.ts; - Data (child) editor:
src/app/trip/operation/operation.page.ts; - Base classes:
src/app/data/form/data-editor.class.ts,src/app/data/form/root-data-editor.class.ts.