🔌 Plugin UI System

Extension Slots

Extension slots are named insertion points in the UI where plugins can contribute components. They enable a loosely coupled architecture where the host application defines "where" and plugins define "what."

How It Works

Diagram; its source is available below
Diagram source
graph LR
    subgraph Host["Host Page"]
        Slot["<gau-page-extension-slot slotId='dashboard-widgets'>"]
    end

    subgraph PluginA["Plugin A"]
        WA["WidgetA (order: 10)"]
    end

    subgraph PluginB["Plugin B"]
        WB["WidgetB (order: 20)"]
    end

    WA --> Slot
    WB --> Slot
  1. The host application places a <gau-page-extension-slot> component in its template
  2. Plugins register extensions for that slot
  3. The slot component renders all registered extensions in order

Built-in Slots

Dashboard Slots

Slot ID (string value)ConstantDescription
dashboard-widgetsPAGE_EXTENSION_SLOTS.DASHBOARD_WIDGETSDashboard metric widgets
dashboard-windowsPAGE_EXTENSION_SLOTS.DASHBOARD_WINDOWSDashboard window panels

Tab Slots

Tab slot IDs correspond to tabsetId values used in PageTabRegistryService:

Slot ID (string value)ConstantDescription
dashboard-pagePAGE_EXTENSION_SLOTS.DASHBOARD_TABSDashboard page tab bar
timesheet-pagePAGE_EXTENSION_SLOTS.TIMESHEET_TABSTimesheet page tabs
time-activity-pagePAGE_EXTENSION_SLOTS.TIME_ACTIVITY_TABSTime & activity tabs
employee-edit-pagePAGE_EXTENSION_SLOTS.EMPLOYEE_EDIT_TABSEmployee edit tabs
Caution

Note that tab slot IDs end with -page, not -tabs. For example, DASHBOARD_TABS maps to the string 'dashboard-page', not 'dashboard-tabs'. Use the constants to avoid errors.

Layout Slots

Slot ID (string value)ConstantDescription
settings-tabsPAGE_EXTENSION_SLOTS.SETTINGS_TABSSettings page tabs
integrations-listPAGE_EXTENSION_SLOTS.INTEGRATIONS_LISTIntegrations listing
page-sectionsPAGE_EXTENSION_SLOTS.PAGE_SECTIONSGeneric page sections
header-toolbarPAGE_EXTENSION_SLOTS.HEADER_TOOLBARHeader toolbar items
sidebar-footerPAGE_EXTENSION_SLOTS.SIDEBAR_FOOTERSidebar footer area
user-menu-itemsPAGE_EXTENSION_SLOTS.USER_MENU_ITEMSUser dropdown menu

Registering Extensions

In a Plugin Definition (Declarative)

The simplest approach — declare extensions in your plugin definition:

export const MyPlugin = defineDeclarativePlugin('my-plugin', {
  extensions: [
    {
      id: 'my-plugin:time-widget',
      slotId: PAGE_EXTENSION_SLOTS.DASHBOARD_WIDGETS,
      component: TimeWidgetComponent,
      order: 10,
      title: 'Time Tracking',
      description: 'Shows time tracked today'
    }
  ]
});

Programmatic Registration

Register extensions at runtime via PageExtensionRegistryService:

@Injectable()
export class MyPluginService {
  private readonly registry = inject(PageExtensionRegistryService);

  registerWidgets() {
    this.registry.register({
      id: 'my-plugin:dynamic-widget',
      slotId: 'dashboard-widgets',
      component: DynamicWidgetComponent,
      order: 30
    }, { pluginId: 'my-plugin' });
  }
}

Batch Registration

this.registry.registerAll([
  { id: 'widget-a', slotId: 'dashboard-widgets', component: WidgetA, order: 10 },
  { id: 'widget-b', slotId: 'dashboard-widgets', component: WidgetB, order: 20 }
], { pluginId: 'my-plugin' });

Extension Definition Fields

FieldTypeDescription
idstringUnique extension identifier
slotIdstringTarget slot ID
componentType<any>Angular component class
loadComponent() => Promise<Type<any>>Lazy-loaded component
ordernumberSort order (lower = first, default 999)
titlestringDisplay title
descriptionstringDescription text
pluginIdstringOwning plugin ID
hiddenbooleanHide the extension
visible(context) => boolean | Promise<boolean>Custom visibility function
permissionsstring[]All required (AND logic)
permissionsAnystring[]Any required (OR logic)
featureKeystringFeature flag gate
wrapperExtensionWrapperConfigWrapping behavior (see below)
lifecycleExtensionLifecycleHooksMount/unmount hooks
metadataExtensionMetadataArbitrary metadata

Extension Wrapper Types

The wrapper field controls how the extension is visually wrapped when rendered in a slot:

TypeDescription
'none'No wrapper — component renders as-is
'card'Nebular card styling
'widget'Widget card styling (compact)
'window'Window panel styling
'panel'Panel styling
'custom'Custom wrapper component (provide via ExtensionWrapperConfig.component)
{
  id: 'my-widget',
  slotId: 'dashboard-widgets',
  component: MyWidgetComponent,
  wrapper: { type: 'card', title: 'My Widget' }
}

Visibility Control

Extensions support fine-grained visibility rules.

Permission-Based

{
  id: 'admin-widget',
  slotId: 'dashboard-widgets',
  component: AdminWidgetComponent,
  // User must have ALL of these permissions
  permissions: ['ADMIN_DASHBOARD_VIEW', 'ORG_EXPENSES_VIEW'],
  // OR: User must have ANY of these permissions
  permissionsAny: ['SUPER_ADMIN', 'ORG_ADMIN']
}

Feature Flag

{
  id: 'beta-widget',
  slotId: 'dashboard-widgets',
  component: BetaWidgetComponent,
  featureKey: 'FEATURE_BETA_DASHBOARD'
}

Custom Visibility Function

{
  id: 'conditional-widget',
  slotId: 'dashboard-widgets',
  component: ConditionalWidgetComponent,
  visible: (context) => {
    // Sync or async check
    return context.organization?.name !== 'Demo';
  }
}

Reactive Visibility

The slot component supports reactive visibility updates:

<gau-page-extension-slot
  [slotId]="'dashboard-widgets'"
  [visibilityContext]="{ user: currentUser, organization: currentOrg }"
></gau-page-extension-slot>

Using Extension Slots in Templates

Basic Usage

<gau-page-extension-slot [slotId]="'dashboard-widgets'"></gau-page-extension-slot>

With Visibility Context

<gau-page-extension-slot
  [slotId]="'dashboard-widgets'"
  [visibilityContext]="{ user: user, organization: org, data: { period: selectedPeriod } }"
></gau-page-extension-slot>

Reactive Subscriptions

In your component code, subscribe to extension changes:

// Non-reactive (snapshot)
const widgets = this.registry.getExtensions('dashboard-widgets');

// Reactive (auto-updates)
this.registry.getExtensions$('dashboard-widgets').subscribe(widgets => {
  console.log(`${widgets.length} widgets registered`);
});

// With visibility filtering
const visible = await this.registry.getVisibleExtensions('dashboard-widgets', {
  user: currentUser,
  organization: currentOrg
});

Dynamic Slot Registration

Plugins can create their own extension points:

// Register a custom slot
this.registry.registerSlot({
  id: 'my-plugin:custom-area',
  name: 'Custom Plugin Area',
  description: 'Extension point for my-plugin customization',
  multiple: true,
  maxExtensions: 5
}, { pluginId: 'my-plugin' });

// Other plugins can now contribute to 'my-plugin:custom-area'

Cleanup

When a plugin is destroyed, all its extensions and slots are automatically removed:

// Remove all extensions and slots registered by a plugin
this.registry.deregisterByPlugin('my-plugin');

This is called automatically during plugin teardown if extensions were registered with a pluginId.

Edit this page

Copyright © 2023-Present Ever Co. LTD.Docs by Sourcey