Reusable, standalone Angular UI components for MemberJunction applications — the house replacement for Kendo UI. Every component is token-driven (--mj-* design tokens from _tokens.scss), dark-mode safe by construction, and keyboard/ARIA accessible.
The rule for consuming apps: check here before building. If a control, layout region, overlay, or pattern exists in this package, use it. Hand-rolling an app-prefixed equivalent (a bespoke button, chip, empty state, dialog…) creates divergence this package exists to eliminate. If what you need doesn't exist, propose it — generic components belong here; app-domain components belong in the app's shared package.
The package ships standalone components — no NgModule required (the one exception is MJAccordionModule, a convenience bundle). Import the symbols you use directly:
import { MJButtonDirective, MJEmptyStateComponent, MJDialogComponent } from '@memberjunction/ng-ui-components';
@Component({
standalone: true,
imports: [MJButtonDirective, MJEmptyStateComponent, MJDialogComponent],
...
})
Dependencies: @angular/cdk (overlays) and angular-split (splitter) come along as package dependencies; @angular/common, @angular/core, and rxjs are peers.
Components attach styles one of three ways, and one of them needs host cooperation:
styles in the component (most components) — nothing to do.styleUrls compiled into the component (page-header, slide-panel) — nothing to do.button, dialog, dropdown, combobox, input, datepicker, splitter, accordion, window, chip, switch/progress) ship their .scss to dist and expect the host application to import them globally.If your app runs inside MJ Explorer (explorer-app), category 3 is already handled — the shell imports all of them. A standalone host must add these to its global stylesheet:
@import '@memberjunction/ng-ui-components/dist/lib/button/button';
@import '@memberjunction/ng-ui-components/dist/lib/dialog/dialog';
@import '@memberjunction/ng-ui-components/dist/lib/dropdown/dropdown';
@import '@memberjunction/ng-ui-components/dist/lib/combobox/combobox';
@import '@memberjunction/ng-ui-components/dist/lib/input/input';
@import '@memberjunction/ng-ui-components/dist/lib/input/input-switch-progress';
@import '@memberjunction/ng-ui-components/dist/lib/input/chip';
@import '@memberjunction/ng-ui-components/dist/lib/datepicker/datepicker';
@import '@memberjunction/ng-ui-components/dist/lib/splitter/splitter';
@import '@memberjunction/ng-ui-components/dist/lib/accordion/accordion';
@import '@memberjunction/ng-ui-components/dist/lib/window/window';
All colors resolve through --mj-* semantic tokens, so light/dark theming requires zero component-level work.
mj-combobox, mj-dropdown, mj-datepicker, mj-numeric-input, and mj-switch all implement ControlValueAccessor — they work with [(ngModel)] and reactive forms, including setDisabledState.
button[mjButton] / a[mjButton] — MJButtonDirectiveAttribute directive that styles a native button or anchor. Variants: primary | secondary (default) | outline | flat | danger | icon | success | warning. Sizes: sm | md (default) | lg. Toggle mode via [toggleable] + [(selected)] (sets aria-pressed). Dev-mode warns when an icon button lacks an accessible name — pass ariaLabel.
<button mjButton variant="primary" (click)="save()">Save</button>
<button mjButton variant="icon" ariaLabel="Remove" (click)="remove()"><i class="fa-solid fa-xmark"></i></button>
<button mjButton variant="flat" [toggleable]="true" [(selected)]="isActive">Toggle</button>
mj-combobox — MJComboboxComponentEditable text-input combobox (CDK overlay): type-to-filter, keyboard nav, optional custom values ([AllowCustom]). Inputs: Data, TextField, ValueField, ValuePrimitive, Filterable (default true), Placeholder, Disabled. Outputs: ValueChange, FilterChange. Custom item template via <ng-template #mjComboboxItem>.
<mj-combobox [Data]="categories" TextField="text" ValueField="value"
[(ngModel)]="selected" [ValuePrimitive]="true" [AllowCustom]="true" />
mj-dropdown — MJDropdownComponentNon-editable select (CDK overlay) with optional in-panel filter ([Filterable], default false) and DefaultItem clear option. Same Data/TextField/ValueField/ValuePrimitive contract as combobox. Custom item template via <ng-template #mjDropdownItem>.
<mj-dropdown [Data]="items" TextField="name" ValueField="id"
[(ngModel)]="selectedId" [ValuePrimitive]="true" />
mj-datepicker — MJDatepickerComponentText input + calendar popup with Min/Max range disabling and typed-input parsing. Accepts Date | string | null through forms; emits ValueChange: Date | null. (Display format is currently fixed at MM/dd/yyyy.)
<mj-datepicker [(ngModel)]="dueDate" [Min]="minDate" [Max]="maxDate" Placeholder="Select a date" />
mj-numeric-input — MJNumericInputComponentNative number input that clamps to Min/Max, rounds to Decimals, and formats on blur. Value flows through forms only (no separate output).
<mj-numeric-input [(ngModel)]="quantity" [Min]="0" [Max]="100" [Decimals]="2" />
mj-switch — MJSwitchComponentBoolean toggle with role="switch", optional OnLabel/OffLabel.
<mj-switch [(ngModel)]="isEnabled" OnLabel="On" OffLabel="Off" />
[mjClickable] — MJClickableDirectiveRetrofits a non-semantic element (card, tile, row) into an accessible control: sets role, tabindex, aria-label, and makes Enter/Space fire (click). Pass the accessible name as the attribute value; role may be button (default) or link; testId emits data-testid.
<div class="app-card" [mjClickable]="app.Name" (click)="open(app)">…</div>
calendar/calendar-utils exports pure helpers shared with the datepicker: BuildCalendarWeeks, FormatDate, FormatDateTime, GetMonthYearLabel, WEEK_DAYS, MONTH_NAMES.
The page-* family composes into the standard page skeleton:
<mj-page-layout>
<mj-page-header Title="Agents" Icon="fa-solid fa-robot" Subtitle="Manage your agents">
<span meta><mj-stat-badge [Count]="filtered" [Total]="total" Label="agents" /></span>
<div actions><button mjButton variant="primary">New</button></div>
<div toolbar><mj-page-search [Value]="q" (ValueChange)="q = $event" /></div>
</mj-page-header>
<mj-page-body>
<!-- content; scrolling and the 24px gutter are handled for you -->
</mj-page-body>
</mj-page-layout>
And the left-nav shell variant for multi-section pages:
<mj-page-layout>
<mj-page-header Title="Admin" Icon="fa-solid fa-gear" />
<mj-page-body [Flex]="true" [Padding]="false" Direction="row">
<mj-left-nav [Sections]="sections" [ActiveId]="activeId" (ItemClicked)="go($event)" />
<mj-left-nav-content [Loading]="loading" [Error]="err">
<mj-page-header-interior Subtitle="Runtime state">
<div actions><button mjButton size="sm">+ Add</button></div>
</mj-page-header-interior>
<mj-page-body-interior><!-- sub-page content --></mj-page-body-interior>
</mj-left-nav-content>
</mj-page-body>
</mj-page-layout>
mj-page-layout — outer shellFlex-column, full-height, overflow:hidden, page background. No inputs.
mj-page-header — page chrome bandInputs: Title, Icon, Subtitle. Slots: [meta] (badges under the title), [actions] (right-aligned), [toolbar] (secondary row).
mj-page-body — scrolling body regionInputs: Padding (default true; false removes the gutter), Flex (default false), Direction: 'row' | 'column' (row stacks to column at ≤700px).
mj-page-header-interior / mj-page-body-interiorThe same pair for sub-pages inside a left-nav shell — card-like chrome, responsive padding. Header-interior adds Role/AriaLabel inputs and the same three slots.
mj-left-nav / mj-left-nav-content — MJLeftNavComponentCanonical left rail: sections, flat + tree items (badges, descriptions, disabled), off-canvas drawer at ≤700px. Inputs: Sections: MJLeftNavSection[], ActiveId, ExpandedIds, Width (240), MobileTitle. Outputs: ItemClicked, ItemToggled. Slots: [header], [footer]. Pair with mj-left-nav-content, which provides Loading/Error states and hides (not destroys) content while busy.
mj-tab-nav — MJTabNavComponentData-driven tab strip. Tabs: TabConfig[] (key, label, icon?, badge?, badgeVariant?: 'default'|'error'|'warning'|'success'), ActiveKey, (TabChange) emits the key.
mj-page-search — MJPageSearchComponentCompact toolbar search input. Placeholder, Value, Icon; (ValueChange) on input.
mj-slide-panel — MjSlidePanelComponentChrome-only slide-in panel from the right (resizable) or centered dialog (Mode: 'slide' | 'dialog'). Inputs include Title, Visible, Resizable, MinWidthPx, MaxWidthRatio, WidthPx, and a CanClose guard callback. Outputs: Closed, WidthChanged. Dialog-mode body projects into [dialog-content].
Re-export of angular-split — use <as-split> / <as-split-area> directly:
<as-split direction="horizontal">
<as-split-area [size]="30">Left</as-split-area>
<as-split-area [size]="70">Right</as-split-area>
</as-split>
mj-accordion-panel — MJAccordionPanelComponent (+ directives)Collapsible panel replacing Kendo panelbar/expansion panel. Inputs: Title, Expanded (+ExpandedChange), Disabled, Variant: 'default'|'primary'|'secondary', Size: 'sm'|'md', Bare, FlushBody, Fill (consume leftover height). Rich content via structural directives — *mjAccordionTitle, *mjAccordionActions (rendered outside the toggle button), and *mjAccordionBody (lazy — instantiated on first expand, then kept alive). Import MJAccordionModule to get the panel plus all three directives in one symbol.
<mj-accordion-panel [Expanded]="open" (ExpandedChange)="open = $event">
<ng-template mjAccordionTitle><i class="fa-solid fa-code"></i> Template Editor</ng-template>
<ng-template mjAccordionBody><mj-code-editor /></ng-template>
</mj-accordion-panel>
mj-dialog — MJDialogComponentModal with backdrop, Esc/backdrop close (Closeable), body scroll lock. Inputs: Visible, Title, Size: 'sm'|'md'|'lg'|'xl'|'auto' (400/600/800/1000px), Width/Height/MinWidth, Role: 'dialog'|'alertdialog'. Output: Close. Visible is not two-way — set it false in your (Close) handler. Slots: default body, mj-dialog-titlebar, mj-dialog-actions.
<mj-dialog [Visible]="show" Title="Confirm" Size="md" (Close)="show = false">
<p>Are you sure?</p>
<mj-dialog-actions>
<button mjButton variant="primary" (click)="confirm()">Yes</button>
<button mjButton (click)="show = false">No</button>
</mj-dialog-actions>
</mj-dialog>
MJDialogService — programmatic dialogsOpen(settings): MJDialogRef with { title, content: string | Type, actions: [{ text, primary?, themeColor? }], width, … }. ref.Result (Observable) emits the clicked action or undefined on dismiss; ref.Content.instance exposes a mounted content component; ref.Close(result?).
mj-confirm-dialog — MJConfirmDialogComponentPurpose-built confirmation on top of mj-dialog. Inputs: [(Visible)] (two-way), Type: 'default'|'danger'|'warning'|'info', Title, Message, DetailMessage, ConfirmText, CancelText, Icon, Processing (spinner + blocks dismissal for async work). Outputs: Confirmed (stays open — you close it when the work finishes), Cancelled (auto-closes). Confirm button is leftmost, per MJ dialog convention.
MJConfirmService — promise-based confirmsawait Confirm({ message, title?, detail?, type?, … }) → boolean, and ConfirmDelete(...) which forces danger styling and "Delete". Mounts above mj-window (z-index 20000).
private confirm = inject(MJConfirmService);
if (await this.confirm.ConfirmDelete({ message: 'Delete this view?', detail: 'Cannot be undone.' })) { … }
mj-window — MJWindowComponentFloating, non-modal panel (no backdrop): Draggable, Resizable (8 handles), State: 'default' | 'maximized' with geometry save/restore, Top/Left/Width/Height/MinWidth/MinHeight. Outputs: Close, StateChange, Resize. Methods: SetPosition, SetSize, GetWindowElement. Slots: default body, mj-window-titlebar, mj-window-actions. Visible is not two-way.
mj-filter-popover — MJFilterPopoverComponentFilter trigger button + CDK popover on desktop, docked bottom-sheet on mobile. Inputs: Label, Icon, ActiveCount (badge), ShowClearAll. Output: ClearAllRequested. Project your filter UI (dropdowns, chips) as content.
mj-empty-state — MJEmptyStateComponentCentered placeholder for empty/error/no-results states. Inputs: Variant: 'empty'|'no-results'|'success'|'warning'|'error' (drives default icon + live-region role), Size: 'compact'|'default'|'large', Icon, Title, Message, built-in CTA via ActionText/ActionIcon/ActionVariant with (Action). Extra CTAs project into [actions]; richer body projects as default content.
<mj-empty-state Variant="error" Title="Couldn't load permissions" [Message]="error"
ActionText="Try Again" ActionIcon="fa-solid fa-rotate-right" (Action)="loadData()" />
mj-stat-badge — MJStatBadgeComponentRead-only stat pill, designed for the page-header [meta] slot. Count, Total (renders "X of Y label"), Label, Icon, Variant: 'default'|'success'|'error'|'warning'|'running'|'info'.
<mj-stat-badge [Count]="filteredCount" [Total]="totalCount" Label="agents" />
mj-alert — MJAlertComponentPersistent in-flow banner (not a toast). Variant: 'info'|'success'|'warning'|'error', Size: 'sm'|'md', Title, Message, Dismissible + (Dismissed), action buttons via [actions] slot. Correct ARIA live role per variant automatically.
mj-progress-bar — MJProgressBarComponentValue (0–100, clamped) or Type="infinite" for indeterminate. Styles ship in the global input-switch-progress sheet (see Styling model).
mj-view-toggle — MJViewToggleComponentSegmented control for view modes. Options: { key, icon?, label?, title? }[], ActiveKey, (KeyChange). Provide title for icon-only options.
mj-refresh-button — MJRefreshButtonComponentCanonical refresh: Loading spins + disables, label auto-hides on mobile. (Clicked) suppressed while loading/disabled.
mj-filter-chip — MJFilterChipComponentClickable filter pill: Label, Icon, Count, Active (+aria-pressed), (Clicked).
mj-filter-field — MJFilterFieldComponentLabeled row (icon + uppercase label) wrapping any projected widget — the escape hatch inside filter UIs.
mj-filter-panel — MJFilterPanelComponentConfig-driven filter stack: Fields: FilterFieldConfig[] with type: 'text' | 'dropdown' | 'chips' (chips support multi), [(Values)] as a plain record, Reset handling, plus projected custom mj-filter-fields.
mj-applied-filters — MJAppliedFiltersComponent"Filtered by …" removable chip row. Renders nothing when Filters is empty, so it's safe to place unconditionally. (Remove), (ClearAll).
standalone: true, use --mj-* tokens for every color (no hardcoded hexes), support both themes, and be keyboard/ARIA accessible..scss, register it in the copy-assets script in package.json and add it to the import list in this README and in explorer-app's global styles.