Module Public Actions Guide
How to correctly implement publicActions in BrowserModuleContract
Module Public Actions Guide
Overview
The editor monorepo uses a simpler ModuleAction interface for public actions, different from the gograb-space monorepo's ExecutableAction classes.
⚠️ Important Distinction
Editor Monorepo (@libs-core/module-base)
// Simple object literals
publicActions?: ModuleAction[];
interface ModuleAction {
id: string;
moduleId: string; // Module that provides this action
displayName: string;
description?: string;
permissions?: string[];
handler: (params?: Record<string, unknown>) => Observable<unknown>;
}
Gograb-Space Monorepo (@libs-core/action-registry)
// Complex classes with decorators
publicActions?: Type<ExecutableAction>[];
@PublicAction({ id: '...', parameters: [...] })
class MyAction implements ExecutableAction { }
✅ Correct Pattern (Editor Monorepo)
Example: User Management Module
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { BrowserModuleContract, ModuleManifest, ModuleAction } from '@libs-core/module-base';
@Injectable({ providedIn: 'root' })
export class UserManagementModule implements BrowserModuleContract {
readonly manifest: ModuleManifest = {
id: 'user-management',
version: '1.0.0',
name: 'User Management',
description: 'User CRUD operations',
// ... other manifest properties
};
// ✅ CORRECT: Simple object literals with ModuleAction interface
readonly publicActions: ModuleAction[] = [
{
id: 'users:create',
moduleId: 'user-management', // ⭐ Required!
displayName: 'Create User',
description: 'Create a new user in the system',
permissions: ['users:create'],
handler: (params?) => this.createUser(params)
},
{
id: 'users:update',
moduleId: 'user-management', // ⭐ Required!
displayName: 'Update User',
description: 'Update an existing user',
permissions: ['users:update'],
handler: (params?) => this.updateUser(params)
},
{
id: 'users:delete',
moduleId: 'user-management', // ⭐ Required!
displayName: 'Delete User',
description: 'Delete a user from the system',
permissions: ['users:delete'],
handler: (params?) => this.deleteUser(params)
}
];
// Action handler methods
private createUser(params?: Record<string, unknown>): Observable<unknown> {
console.log('Creating user:', params);
// Validate params manually
if (!params?.name) {
throw new Error('User name is required');
}
// Implementation...
return of({ success: true, userId: 'usr_123' });
}
private updateUser(params?: Record<string, unknown>): Observable<unknown> {
console.log('Updating user:', params);
// Implementation...
return of({ success: true });
}
private deleteUser(params?: Record<string, unknown>): Observable<unknown> {
console.log('Deleting user:', params);
// Implementation...
return of({ success: true });
}
// Lifecycle methods...
configure<T>(config: T): Observable<void> { return of(void 0); }
initialize(): Observable<void> { return of(void 0); }
start(): Observable<void> { return of(void 0); }
stop(): Observable<void> { return of(void 0); }
destroy(): Observable<void> { return of(void 0); }
}
❌ Incorrect Patterns
❌ Missing moduleId
publicActions: ModuleAction[] = [
{
id: 'users:create',
// ❌ Missing moduleId!
displayName: 'Create User',
handler: (params?) => of({})
}
];
❌ Extra Properties
publicActions: ModuleAction[] = [
{
id: 'users:create',
moduleId: 'user-management',
displayName: 'Create User',
category: 'crud', // ❌ Not in ModuleAction interface!
parameters: [...] // ❌ Not in ModuleAction interface!
handler: (params?) => of({})
}
];
❌ Using ExecutableAction Classes
// ❌ Wrong! This is for gograb-space monorepo
@PublicAction({ id: 'users:create' })
class CreateUserAction implements ExecutableAction { }
publicActions = [CreateUserAction]; // ❌ Won't compile in editor monorepo
🎯 Best Practices
1. Always Include moduleId
{
id: 'users:create',
moduleId: 'user-management', // ⭐ Matches manifest.id
displayName: 'Create User',
handler: (params?) => this.createUser(params)
}
2. Keep Handlers Simple
// ✅ Good: Delegate to private methods
handler: (params?) => this.createUser(params)
// ❌ Avoid: Complex inline logic
handler: (params?) => {
if (!params) return of({ error: 'No params' });
const user = { name: params.name, email: params.email };
return this.http.post('/users', user); // Too complex!
}
3. Return Observables
// ✅ Correct
handler: (params?) => of({ success: true })
// ✅ Also correct
handler: (params?) => this.http.post('/users', params)
// ❌ Wrong
handler: (params?) => { return { success: true }; } // Not an Observable!
4. Validate Parameters in Handler
private createUser(params?: Record<string, unknown>): Observable<unknown> {
// ✅ Validate manually
if (!params?.name || !params?.email) {
return throwError(() => new Error('Name and email are required'));
}
// Process...
return of({ success: true });
}
📋 Complete Module Template
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import {
BrowserModuleContract,
ModuleManifest,
ModuleAction,
ModuleDecorator
} from '@libs-core/module-base';
@ModuleDecorator({
id: 'my-module',
version: '1.0.0',
name: 'My Module',
description: 'Module description'
})
@Injectable({ providedIn: 'root' })
export class MyModule implements BrowserModuleContract {
readonly manifest: ModuleManifest = {
id: 'my-module',
version: '1.0.0',
name: 'My Module',
description: 'Module description',
author: 'GoGrab Team',
icon: 'fas fa-cube'
};
// ✅ Public actions as instance property
readonly publicActions: ModuleAction[] = [
{
id: 'my-module:action-1',
moduleId: 'my-module',
displayName: 'Action 1',
description: 'First action',
permissions: ['my-module:read'],
handler: (params?) => this.handleAction1(params)
},
{
id: 'my-module:action-2',
moduleId: 'my-module',
displayName: 'Action 2',
description: 'Second action',
permissions: ['my-module:write'],
handler: (params?) => this.handleAction2(params)
}
];
readonly routes = []; // Add routes as needed
// Action handlers
private handleAction1(params?: Record<string, unknown>): Observable<unknown> {
console.log('Action 1 executed:', params);
return of({ success: true });
}
private handleAction2(params?: Record<string, unknown>): Observable<unknown> {
console.log('Action 2 executed:', params);
return of({ success: true });
}
// Lifecycle methods
configure<T>(config: T): Observable<void> {
console.log('Configuring module...');
return of(void 0);
}
initialize(): Observable<void> {
console.log('Initializing module...');
return of(void 0);
}
start(): Observable<void> {
console.log('Starting module...');
return of(void 0);
}
stop(): Observable<void> {
console.log('Stopping module...');
return of(void 0);
}
destroy(): Observable<void> {
console.log('Destroying module...');
return of(void 0);
}
}
🔍 Verification
Test your publicActions:
// In a component or service
const registry = inject(ModuleRegistryService);
// Invoke an action
registry.invokeModuleAction('my-module', 'my-module:action-1', {
param1: 'value1'
}).subscribe({
next: (result) => console.log('Action result:', result),
error: (err) => console.error('Action failed:', err)
});
📚 Related Documentation
Status: ✅ Production Pattern
Last Updated: November 1, 2025
Monorepo: Editor (not gograb-space)