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.
| 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=.... |
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>
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:
record.IsSaved && record.Dirty), orBaseFormComponent.HasUnsavedChangesBeyondRecord: pending related-record
edits/deletes or HasAdditionalUnsavedChanges), 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.)
Toolbar buttons in <mj-form-toolbar> are extensible through BaseFormToolbarAction and @RegisterFormToolbarAction(metadata, priority?).
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
}
}
You can subclass an existing toolbar action plugin (such as RecordLinksToolbarAction) to override it either globally (EntityScope: '*') or for specific entities.
ResolveFormToolbarActionsForEntity walks the prototype chain and merges ancestor registration metadata ({ ...ancestorMetadata, ...childMetadata }). You only need to declare the properties you are overriding (Key, Icon, Order, Pinnable, and Mode are inherited automatically).Key:
Sub.prototype instanceof Super) always overrides its superclass.EntityScope overrides a global ('*') scope of equal priority.Priority wins.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
}
[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>:
[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>
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>
<mj-record-link> ComponentRenders 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>
MjRecordPreviewPopoverComponent & @RegisterClass)The singleton MjRecordPreviewPopoverService mounts a single <mj-record-preview-popover> at document.body featuring the Option A UX:
9FAF60…C577), Quick Edit ([✎]), and Open in New Tab ([↗]) sit in the top-right header.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.Email, URL, Title, or values > 22 characters) automatically span both columns (grid-column: 1 / -1) so they never truncate prematurely.Cancel / Save footer bar and emits BeforeQuickEditSave (cancellable via args.Cancel = true) and AfterQuickEditSaved / RecordUpdated.You can subclass MjRecordPreviewPopoverComponent and register your custom preview card with @RegisterClass so MjRecordPreviewPopoverService instantiates your subclass dynamically via MJGlobal.Instance.ClassFactory:
@RegisterClass(MjRecordPreviewPopoverComponent)@RegisterClass(MjRecordPreviewPopoverComponent, 'MJ_BizApps_Common:*') or @RegisterClass(MjRecordPreviewPopoverComponent, 'schema:mj_bizapps_common')@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);
}
}
<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:
RecordCommentsToolbarAction, key 'comments'): Displays the total open comment count across the record and toggles the slide-over Record Comments Drawer.<mj-form-field>): Shows an inline comment count badge on fields with active threads (or a hover trigger to start a thread), capturing a quotedValue snapshot of the field's value at the time of commenting. Fields with open comments are never collapsed away as empty in read mode.<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.@Mention Notifications: Enforces Row-Level Security and Field-Level Security (including write-time restricted-field classification and AnchorMetadataObject.restrictedFields stamping) and dispatches MJ: User Notifications on @mention only to users permitted to read both the record and the anchored/referenced fields.