@memberjunction/ng-base-forms
The foundation for rendering and editing MemberJunction entity records in Angular — and for presenting any entity form as a full-page tab, a modal dialog, a slide-in panel, or a floating window, from one set of forms with no per-surface code and no regeneration.
Start with the architecture guide: /guides/FORMS_ARCHITECTURE_GUIDE.md — the big picture, how generated/custom/interactive forms coexist, and how to use every surface. This README is a quick map of the package.
Using these as general-purpose database controls (outside a form)? See STANDALONE_USAGE.md —
<mj-form-field>,<mj-entity-form-host>, the overlay shells,<mj-explorer-entity-data-grid>, and<mj-collapsible-panel>are all data-bindable controls you can drop into any Angular component with just aBaseEntityand an import ofBaseFormsModule.
What’s in here
Section titled “What’s in here”| Area | Key exports | Notes |
|---|---|---|
| Form base | BaseFormComponent, BaseFormSectionComponent, MjRecordFormContainerComponent, MjCollapsiblePanelComponent, MjFormFieldComponent | The form itself + its toolbar/section container. Generated forms extend BaseFormComponent. |
| Form host | MjEntityFormHostComponent (<mj-entity-form-host>) | Presentation-agnostic: resolve → load → create → bind → re-emit → teardown. Supports full forms and single sections (SectionName). Standard-form switch: FormMode, ShowFormModeSwitch, FormModeChange, SwitchFormMode() — see Standard-form switch. |
| Overlay shells | MjFormDialogComponent, MjFormSlideInComponent, MjFormWindowComponent (standalone) | Declarative dialog / slide-in / floating-window around the host. |
| Imperative API | MJFormPresenterService.Open(...) → MJFormRef | One call to open any form on any surface. |
| Per-instance config | EntityFormConfig + TAB_/DIALOG_/SLIDEIN_FORM_CONFIG | Toolbar / related sections / collapsibility / width / record-links. Applied via the form reference — no regeneration. |
| Form resolution | FormResolverService | Picks generated / custom / interactive-override form + variants (cached via InteractiveFormsEngine). FormResolution.standard, ResolveStandardForm() and HasStandardFormAlternative() expose the generated form a custom one hides. |
| Form extension | BaseFormPanel + <mj-form-panel-slot> + <mj-form-contributions> | Inject custom sections; claim a related-entity grid; fill in unbaked DisplayInForm relationships. See PANELS.md and /plans/form-contributions.md. |
| Form chrome | ResolveFormChrome, BaseFormPolicy, MJ: Form Chrome Rules | L0–L4 stack: inclusion, Auto ranker, install overlay, user order. Policy is cosmetics only. Left-nav related grids fill leftover column height (display: contents on slot hosts; no accordion pixel pin). SetSectionRowCount upserts so contribution badges appear. Section search matches title/MatchesSearch, not IsVisible. See Forms Architecture §7d. |
| Pluggable toolbar actions | BaseFormToolbarAction, @RegisterFormToolbarAction, RecordLinksToolbarAction, ResolveFormToolbarActionsForEntity | Register toolbar buttons with EntityScope ('*', exact entity, glob wildcard, schema:*, or array) and lifecycle hooks (IsApplicable, OnInit, OnPrewarm, OnPinStateChanged, OnRecordSaved, OnRecordRefreshed, OnExecute, OnDestroy). Supports subclassing & prototype-chain metadata inheritance. See Pluggable toolbar actions and Forms Architecture §7e. |
| Record links & hover cards | MjRecordLinksComponent (<mj-record-links>), MjRecordPreviewPopoverComponent (<mj-record-preview-popover>), MjRecordPreviewDirective ([mjRecordPreview]) | Bidirectional record links drawer (outbound FKs, inbound related entities, polymorphic links) with pin-aware pre-warming and Sage LinksSummary context; hover preview cards on read-mode FK links. See Forms Architecture §7f. |
| Time-travel version preview | MjRecordFormContainerComponent (ActiveVersionPreview, OnPreviewVersionSelected, OnStepVersionPreview, OnExitVersionPreview), FormSlotCoordinator.VersionPreviewChanged$ | Rehydrates historical FullRecordJSON + ChangesJSON snapshots directly inside the form with a top Time-Travel Scrubber Bar and changed-field jump chips. See Forms Architecture §7g. |
| New-record defaults | BaseFormComponent.NewRecordValues / NewRecordValuesForJoinFields | Related grids copy every join field onto the child. Explorer persists them as /record/:entity/new?NewRecordValues=.... |
Quick start
Section titled “Quick start”import { MJFormPresenterService, DIALOG_FORM_CONFIG } from '@memberjunction/ng-base-forms';
const ref = this.forms.Open({ EntityName: 'MJ: AI Agents', RecordId: id, // omit → new record Presentation: 'slide-in', // 'dialog' | 'slide-in' | 'window' Config: { ShowRelatedEntities: false },});const saved = await ref.AfterSaved(); // BaseEntity | null<mj-form-dialog [EntityName]="'Users'" [RecordID]="id" [(Visible)]="show" (Saved)="onSaved($event)"></mj-form-dialog>Standard-form switch
Section titled “Standard-form switch”A custom class form registered at a higher priority, or an active interactive
override (EntityFormOverride), hides the CodeGen-generated form for its
entity. Before MJ#4755 the generated form was unreachable — even a hand-built
record URL resolved to the custom form — and an entity whose custom form only
handles existing records had no way to create one.
<mj-entity-form-host> now shows a slim “Open standard form” / “Back to custom
view” strip whenever a custom form hides the generated one. Entities whose only
registration is the generated form (the common case) get no strip.
| Member | Kind | Purpose |
|---|---|---|
FormMode: EntityFormMode | @Input | 'default' (the resolver’s normal pick) or 'standard' (the generated form). A change after the view initializes reloads the form and does not emit FormModeChange. If the entity has no standard form, the host falls back to the default and logs. |
ShowFormModeSwitch = true | @Input | Set false to hide the strip; FormMode is still honoured. |
FormModeChange: EventEmitter<EntityFormMode> | @Output | Emits when the mode changes from inside the host — the strip, SwitchFormMode(), or picking an interactive variant (which returns to 'default') — so the surface can persist it. |
SwitchFormMode(mode): boolean | method | Switch programmatically. Returns false when refused (see below), true otherwise (including when already in mode). |
What “standard” means. The standard form is the lowest-priority
BaseFormComponent registration for the entity
(FormResolverService.ResolveStandardForm). CodeGen registers its form with no
explicit priority, and a custom form either declares a higher one or extends the
generated class (so it registers later and gets a higher auto priority).
HasStandardFormAlternative(resolution) is true only when that form exists and
is not the one already mounted.
Unsaved work is never discarded. A switch remounts the form, so the host
refuses it — returning false and emitting a warning Notification — when:
- a saved record is dirty (
record.IsSaved && record.Dirty), or - the form holds unsaved work outside the record’s own fields
(
BaseFormComponent.HasUnsavedChangesBeyondRecord: pending related-record edits/deletes orHasAdditionalUnsavedChanges), for new and saved records alike.
A new record keeps its values. An unsaved new record (with no other pending work) switches freely: the live record instance is carried into the other form, so fields already typed survive the switch.
Permissions are unchanged. The standard form enforces the same entity permissions as any other form; switching grants nothing.
In Explorer the mode lives in the record tab’s form query param
(?form=standard), which is the source of truth. NavigationOptions.formMode,
a ?form=standard deep link, back/forward and tab re-focus all reach the
mounted record through OnQueryParamsChanged, which switches via
SwitchFormMode (so the unsaved-work guard still applies; a refused switch
writes the on-screen mode back to the URL). A strip switch writes the param
back, so the URL records the requested mode, and a record URL without form
means the default form. (When the entity has no standard form, the host falls
back to the default form even if form=standard is requested.)
Pluggable toolbar actions
Section titled “Pluggable toolbar actions”Toolbar buttons in <mj-form-toolbar> are extensible through BaseFormToolbarAction and @RegisterFormToolbarAction(metadata, priority?).
1. Registering a new toolbar action
Section titled “1. Registering a new toolbar action”import { BaseFormToolbarAction, RegisterFormToolbarAction, type FormToolbarActionLifecycleContext,} from '@memberjunction/ng-base-forms';
@RegisterFormToolbarAction({ Key: 'audit-verify', MenuLabel: 'Verify Record', Tooltip: 'Run verification checks', Icon: 'fa-solid fa-circle-check', EntityScope: ['MJ_BizApps_Common: People', 'MJ_BizApps_Orders:*'], // '*', exact name, glob, 'schema:x', or string[] Order: 50, Pinnable: true, Mode: 'action', // 'action' | 'toggle-drawer'})export class VerifyRecordToolbarAction extends BaseFormToolbarAction { public override async OnPrewarm(ctx: FormToolbarActionLifecycleContext): Promise<void> { if (!ctx.IsPinned) return; // Avoid background queries unless pinned to the toolbar ctx.SetBadge('✓'); ctx.RequestChangeDetection(); }
public override async OnExecute(ctx: FormToolbarActionLifecycleContext): Promise<void> { // Handle button click }}2. Subclassing & overriding an existing toolbar button
Section titled “2. Subclassing & overriding an existing toolbar button”You can subclass an existing toolbar action plugin (such as RecordLinksToolbarAction) to override it either globally (EntityScope: '*') or for specific entities.
- Prototype metadata inheritance:
ResolveFormToolbarActionsForEntitywalks the prototype chain and merges ancestor registration metadata ({ ...ancestorMetadata, ...childMetadata }). You only need to declare the properties you are overriding (Key,Icon,Order,Pinnable, andModeare inherited automatically). - 3-tier winner resolution per
Key:- A subclass (
Sub.prototype instanceof Super) always overrides its superclass. - An entity-specific
EntityScopeoverrides a global ('*') scope of equal priority. - Higher ClassFactory
Prioritywins.
- A subclass (
import { RecordLinksToolbarAction, RegisterFormToolbarAction,} from '@memberjunction/ng-base-forms';
@RegisterFormToolbarAction({ EntityScope: 'MJ_BizApps_Common: People', MenuLabel: 'Person Graph',})export class PersonLinksToolbarAction extends RecordLinksToolbarAction { // Inherits Key: 'links', Icon, Order, Pinnable, and Mode from RecordLinksToolbarAction}Declarative Record Links ([mjRecordLink] & <mj-record-link>)
Section titled “Declarative Record Links ([mjRecordLink] & <mj-record-link>)”Any custom form header, BaseFormPanel, or dashboard widget can attach MemberJunction’s full record-linking UX (hover-intent <mj-record-preview-popover>, inline Quick Edit with live propagation back to virtual display fields on Record, standard + polymorphic FK resolution, and click/Ctrl+Click FormNavigationEvent navigation) using [mjRecordLink] or <mj-record-link>:
1. Field-Driven Mode ([mjRecordLink]="Record" FieldName="...")
Section titled “1. Field-Driven Mode ([mjRecordLink]="Record" FieldName="...")”Resolves the target entity, primary key, and companion virtual display column (e.g., CurrentOrganizationName) directly from Record.EntityInfo.Fields metadata (supporting both standard RelatedEntity FKs and polymorphic EntityIDFieldName pairs):
<button type="button" class="biz-hero__stat-val" [mjRecordLink]="Record" FieldName="CurrentOrganizationID" (Navigate)="Navigate.emit($event)"> {{ Record.CurrentOrganizationName }}</button>2. Direct Mode (mjRecordLink [EntityName]="..." [RecordID]="...")
Section titled “2. Direct Mode (mjRecordLink [EntityName]="..." [RecordID]="...")”Use when linking from a custom projection, DTO, or list row where no BaseEntity field metadata is involved:
<button type="button" mjRecordLink [EntityName]="'MJ_BizApps_Common: People'" [RecordID]="row.PersonID" [DisplayName]="row.PersonName" (Navigate)="Navigate.emit($event)"> {{ row.PersonName }}</button>3. Ready-Made <mj-record-link> Component
Section titled “3. Ready-Made <mj-record-link> Component”Renders an inline hyperlink, pill chip (Variant="chip"), or link box (Variant="box") and automatically resolves the target record’s display name via GetEntityRecordName when DisplayName is omitted:
<mj-record-link [Record]="Record" FieldName="CurrentOrganizationID" Variant="chip" (Navigate)="Navigate.emit($event)"></mj-record-link>4. Pluggable Hover Preview Card (MjRecordPreviewPopoverComponent & @RegisterClass)
Section titled “4. Pluggable Hover Preview Card (MjRecordPreviewPopoverComponent & @RegisterClass)”The singleton MjRecordPreviewPopoverService mounts a single <mj-record-preview-popover> at document.body featuring the Option A UX:
- Header action micro-cluster: Copyable compact Record ID (
9FAF60…C577), Quick Edit ([✎]), and Open in New Tab ([↗]) sit in the top-right header. - Clickable record title: Clicking the record title (
Mathieu Grantham →) navigates directly to the record (Ctrl/Cmd-click or middle-click opens in a new tab), eliminating redundant bottom buttons in Read mode. - Auto-spanning wide fields: Wide values (
Email,URL,Title, or values> 22characters) automatically span both columns (grid-column: 1 / -1) so they never truncate prematurely. - Contextual Quick Edit footer & cancellable save event: Entering Quick Edit reveals a slim
Cancel/Savefooter bar and emitsBeforeQuickEditSave(cancellable viaargs.Cancel = true) andAfterQuickEditSaved/RecordUpdated.
You can subclass MjRecordPreviewPopoverComponent and register your custom preview card with @RegisterClass so MjRecordPreviewPopoverService instantiates your subclass dynamically via MJGlobal.Instance.ClassFactory:
- Global override (all entities):
@RegisterClass(MjRecordPreviewPopoverComponent) - Schema or wildcard override:
@RegisterClass(MjRecordPreviewPopoverComponent, 'MJ_BizApps_Common:*')or@RegisterClass(MjRecordPreviewPopoverComponent, 'schema:mj_bizapps_common') - Entity-specific override:
@RegisterClass(MjRecordPreviewPopoverComponent, 'MJ_BizApps_Common: People')
import { RegisterClass } from '@memberjunction/global';import { EntityFieldInfo, EntityInfo } from '@memberjunction/core';import { MjRecordPreviewPopoverComponent } from '@memberjunction/ng-base-forms';
@RegisterClass(MjRecordPreviewPopoverComponent, 'MJ_BizApps_Common: People')export class CustomPersonPreviewPopoverComponent extends MjRecordPreviewPopoverComponent { protected override selectPreviewFields(entityInfo: EntityInfo): EntityFieldInfo[] { // Customize which summary fields appear on the People hover card return super.selectPreviewFields(entityInfo); }}Anchored Record Comments (<mj-record-comments>)
Section titled “Anchored Record Comments (<mj-record-comments>)”Every saved record form automatically provides threaded conversations (MJ: Conversations + MJ: Conversation Details) anchored at the Record, Field, Section, Group (Left-Nav Tab), or Contribution level:
- Form Toolbar Action (
RecordCommentsToolbarAction, key'comments'): Displays the total open comment count across the record and toggles the slide-over Record Comments Drawer. - Field-Level Trigger & Badge (
<mj-form-field>): Shows an inline comment count badge on fields with active threads (or a hover trigger to start a thread), capturing aquotedValuesnapshot of the field’s value at the time of commenting. Fields with open comments are never collapsed away as empty in read mode. - Section & Left-Nav Rail Rollup (
<mj-collapsible-panel>,<mj-record-form-container>,FormSectionIndicators): Open field and section comments roll up into section header chips and left-nav group badges. Clicking any anchor chip in the drawer or popover expands the target group/section, scrolls the field into view, and pulses it. - RLS & FLS Security +
@MentionNotifications: Enforces Row-Level Security and Field-Level Security (including write-time restricted-field classification andAnchorMetadataObject.restrictedFieldsstamping) and dispatchesMJ: User Notificationson@mentiononly to users permitted to read both the record and the anchored/referenced fields.
Related docs
Section titled “Related docs”- Architecture (read first): /guides/FORMS_ARCHITECTURE_GUIDE.md
- Anchored Conversations & Record Commenting: /guides/ANCHORED_CONVERSATIONS_AND_RECORD_COMMENTS_GUIDE.md
- Inject custom sections into a form: PANELS.md
- Form chrome (rail / More / inclusion): Forms Architecture §7d
- Pluggable toolbar actions & Record Links: Forms Architecture §7e–§7g
- Custom-form override + toolbar pattern: packages/Angular/CLAUDE.md