MODULE TEMPLATE
Comprehensive guidelines for AI-assisted development within the Editor platform
Module Creation Template - editor2 Platform
Optimized for AI Code Generation Version: 1.0.0 | Last Updated: December 27, 2025
Quick Context
What: Template for creating business modules in editor2 ecosystem
Where: libs/libs-modules/<module-name>/
Library Alias: @libs-modules/<module-name>
Contract: Must implement BrowserModuleContract from @libs-core/module-base
Generation Methods (Choose Based on Use Case)
Method 1: AI Platform Generator (Complete Platforms)
Use for: Multi-module business platforms
Route: /modularity/ai-platform
Output: Complete universe with 5-10 modules
Time: 5-10 minutes
Method 2: Module Workbench (Single Modules - Guided)
Use for: Individual modules with step-by-step guidance
Route: /modularity/workbench
Output: Single Nx library module
Time: 10-15 minutes (visual) or 2-4 hours (code-first)
Method 3: AI Builder (Quick Scaffolding)
Use for: Quick module scaffolding
Route: /modularity/ai-builder
Output: Module definition (can export to Nx library)
Time: 1-2 minutes
Method 4: Modular Universe (Visual Canvas)
Use for: Visual universe building with AI-generated modules
Route: /modular-universe
Output: Visual universe structure
Time: Variable (drag-drop modules)
Module Generation Command (Code-First)
# Generate new module library
npx nx g @nx/angular:lib <module-name> --directory=libs/libs-modules --standalone --changeDetection=OnPush
# Example: Product Catalog
npx nx g @nx/angular:lib product-catalog --directory=libs/libs-modules --standalone --changeDetection=OnPush
# Update tsconfig.base.json paths after generation:
"@libs-modules/<module-name>": ["libs/libs-modules/<module-name>/src/index.ts"]
Required File Structure
libs/libs-modules/<module-name>/
├── src/
│ ├── lib/
│ │ ├── actions/ # Public actions (cross-module)
│ │ │ ├── index.ts
│ │ │ ├── navigate-to-<entity>.action.ts
│ │ │ ├── create-<entity>.action.ts
│ │ │ ├── update-<entity>.action.ts
│ │ │ └── delete-<entity>.action.ts
│ │ ├── components/ # UI components (separate files!)
│ │ │ ├── index.ts
│ │ │ ├── <entity>-list/
│ │ │ │ ├── <entity>-list.component.ts
│ │ │ │ ├── <entity>-list.component.html
│ │ │ │ └── <entity>-list.component.scss
│ │ │ ├── <entity>-detail/
│ │ │ │ ├── <entity>-detail.component.ts
│ │ │ │ ├── <entity>-detail.component.html
│ │ │ │ └── <entity>-detail.component.scss
│ │ │ ├── <entity>-form/
│ │ │ │ ├── <entity>-form.component.ts
│ │ │ │ ├── <entity>-form.component.html
│ │ │ │ └── <entity>-form.component.scss
│ │ │ └── <entity>-filters/
│ │ │ ├── <entity>-filters.component.ts
│ │ │ ├── <entity>-filters.component.html
│ │ │ └── <entity>-filters.component.scss
│ │ ├── data/ # Data access layer
│ │ │ ├── repositories/
│ │ │ │ ├── index.ts
│ │ │ │ └── <entity>.repository.ts
│ │ │ └── index.ts
│ │ ├── embeddables/ # Widget components
│ │ │ ├── index.ts
│ │ │ └── <entity>-widget/
│ │ │ ├── <entity>-widget.component.ts
│ │ │ ├── <entity>-widget.component.html
│ │ │ └── <entity>-widget.component.scss
│ │ ├── events/ # Domain events
│ │ │ ├── index.ts
│ │ │ └── <entity>.events.ts
│ │ ├── models/ # Interfaces
│ │ │ ├── index.ts
│ │ │ ├── <entity>.model.ts
│ │ │ └── <entity>-filters.model.ts
│ │ ├── services/ # Module services
│ │ │ ├── index.ts
│ │ │ └── <entity>-api.service.ts
│ │ ├── <module-name>.module.ts # Main module class
│ │ └── <module-name>.routes.ts # Angular routes
│ ├── index.ts # Barrel exports
│ └── test-setup.ts
├── project.json
├── tsconfig.json
├── tsconfig.lib.json
├── vite.config.mts
└── README.md
Core Library Imports (MANDATORY)
// Module base contract
import { BrowserModuleContract, ModuleManifest, ModuleDecorator } from '@libs-core/module-base';
// Logging
import { LoggerService } from '@libs-core/log-system';
// Action registry
import { PublicAction, ExecutableAction, ActionContext, ActionParams } from '@libs-core/action-registry';
// Security
import { authGuard, permissionGuard, roleGuard, ContextService } from '@libs-core/security';
import { SystemRoles } from '@libs-core/shared-models';
// Data access
import { BaseRepository } from '@libs-core/data-access';
// Event system
import { EventBusService, DomainEvent } from '@libs-core/event-system';
// Shared models (use these FIRST before creating local types)
import { BaseEntity, AuditFields, ApiResponse, Permission } from '@libs-core/shared-models';
// Flash persistence (editor2-specific)
import { autoFlashState, GgStorage } from '@storage-system';
Component Template (CRITICAL - Separate Files)
TypeScript File (.component.ts)
// libs/libs-modules/<module-name>/src/lib/components/<entity>-list/<entity>-list.component.ts
import { Component, inject, signal, computed, input, output, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterModule } from '@angular/router';
import { LoggerService } from '@libs-core/log-system';
import { autoFlashState } from '@storage-system';
import { <Entity>Repository } from '../../data/repositories/<entity>.repository';
import { <Entity>, <Entity>Status, <Entity>Filters } from '../../models';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'lib-<entity>-list',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './<entity>-list.component.html', // ✅ Separate file
styleUrl: './<entity>-list.component.scss', // ✅ Separate file
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class <Entity>ListComponent implements OnInit {
private readonly repository = inject(<Entity>Repository);
private readonly router = inject(Router);
private readonly logger = inject(LoggerService).child({ source: '<Entity>ListComponent' });
// Component state
readonly entities = signal<<Entity>[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
// Persistent filters using flash-persistence
readonly filters = autoFlashState<<Entity>Filters>('<module-name>-<entity>-filters', {
status: 'all',
searchTerm: '',
page: 1,
pageSize: 20,
});
// Computed values
readonly filteredEntities = computed(() => {
const filters = this.filters();
let result = this.entities();
if (filters.status !== 'all') {
result = result.filter(e => e.status === filters.status);
}
if (filters.searchTerm) {
const term = filters.searchTerm.toLowerCase();
result = result.filter(e => e.name.toLowerCase().includes(term));
}
return result;
});
readonly isEmpty = computed(() => this.filteredEntities().length === 0);
readonly hasEntities = computed(() => this.entities().length > 0);
ngOnInit(): void {
this.loadEntities();
}
private async loadEntities(): Promise<void> {
this.loading.set(true);
this.error.set(null);
try {
// MUST use findAll() from BaseRepository
const entities = await firstValueFrom(this.repository.findAll());
this.entities.set(entities);
this.logger.info('Loaded entities', { count: entities.length });
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
this.error.set(errorMessage);
this.logger.error('Failed to load entities', { error: errorMessage });
} finally {
this.loading.set(false);
}
}
onFilterChange(newFilters: Partial<<Entity>Filters>): void {
this.filters.update(current => ({ ...current, ...newFilters }));
}
onEntityClick(entity: <Entity>): void {
void this.router.navigate(['/<base-path>', entity.id]);
}
onCreate(): void {
void this.router.navigate(['/<base-path>/create']);
}
async onRefresh(): Promise<void> {
await this.loadEntities();
}
}
HTML Template (.component.html)
<!-- <entity>-list/<entity>-list.component.html -->
<div class="page-container">
<header class="page-header">
<h1><Entity> Management</h1>
<div class="header-actions">
<button class="btn-refresh" (click)="onRefresh()" [disabled]="loading()">
Refresh
</button>
<button class="btn-primary" (click)="onCreate()">
Create New
</button>
</div>
</header>
@if (loading()) {
<div class="loading-state">
<p>Loading <entities>...</p>
</div>
} @else if (error()) {
<div class="error-state">
<p>Error: {{ error() }}</p>
<button (click)="onRefresh()">Retry</button>
</div>
} @else {
<div class="filters-section">
<div class="filter-group">
<label>Status:</label>
<select [value]="filters().status"
(change)="onFilterChange({ status: $any($event.target).value })">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="filter-group">
<label>Search:</label>
<input type="text"
[value]="filters().searchTerm"
(input)="onFilterChange({ searchTerm: $any($event.target).value })"
placeholder="Search <entities>...">
</div>
</div>
@if (isEmpty()) {
<div class="empty-state">
<p>No <entities> found</p>
@if (filters().status !== 'all' || filters().searchTerm) {
<button (click)="onFilterChange({ status: 'all', searchTerm: '' })">
Clear Filters
</button>
}
</div>
} @else {
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@for (entity of filteredEntities(); track entity.id) {
<tr (click)="onEntityClick(entity)">
<td>{{ entity.name }}</td>
<td>
<span class="status-badge" [class]="'status-' + entity.status">
{{ entity.status }}
</span>
</td>
<td>{{ entity.createdAt | date:'short' }}</td>
<td class="actions-cell">
<a [routerLink]="['../', entity.id]">View</a>
<a [routerLink]="['../', entity.id, 'edit']">Edit</a>
</td>
</tr>
}
</tbody>
</table>
<div class="pagination">
<p>Showing {{ filteredEntities().length }} of {{ entities().length }} <entities></p>
</div>
}
}
</div>
SCSS Styles (.component.scss)
// <entity>-list/<entity>-list.component.scss
.page-container {
padding: var(--spacing-4);
backdrop-filter: blur(16px);
background: rgba(var(--ux-surface-rgb), 0.8);
border-radius: var(--radius-lg);
min-height: 100%;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-6);
padding-bottom: var(--spacing-4);
border-bottom: 1px solid var(--ux-border);
h1 {
margin: 0;
color: var(--ux-text-primary);
font-size: var(--font-size-2xl);
font-weight: 600;
}
.header-actions {
display: flex;
gap: var(--spacing-3);
button {
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-md);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.btn-refresh {
background: rgba(var(--ux-surface-rgb), 0.6);
border: 1px solid var(--ux-border);
color: var(--ux-text-primary);
&:hover:not(:disabled) {
background: rgba(var(--ux-surface-rgb), 0.8);
}
}
.btn-primary {
background: var(--ux-primary);
border: none;
color: white;
&:hover:not(:disabled) {
background: var(--ux-primary-hover);
}
}
}
}
.filters-section {
display: flex;
gap: var(--spacing-4);
margin-bottom: var(--spacing-4);
padding: var(--spacing-4);
backdrop-filter: blur(8px);
background: rgba(var(--ux-surface-rgb), 0.4);
border-radius: var(--radius-md);
.filter-group {
display: flex;
align-items: center;
gap: var(--spacing-2);
label {
color: var(--ux-text-secondary);
font-size: var(--font-size-sm);
font-weight: 500;
}
select,
input {
padding: var(--spacing-2) var(--spacing-3);
background: rgba(var(--ux-surface-rgb), 0.6);
border: 1px solid var(--ux-border);
border-radius: var(--radius-sm);
color: var(--ux-text-primary);
font-size: var(--font-size-sm);
&:focus {
outline: none;
border-color: var(--ux-primary);
}
}
}
}
.loading-state,
.error-state,
.empty-state {
padding: var(--spacing-12);
text-align: center;
p {
color: var(--ux-text-secondary);
font-size: var(--font-size-lg);
margin: 0 0 var(--spacing-4) 0;
}
button {
padding: var(--spacing-2) var(--spacing-4);
background: var(--ux-primary);
border: none;
border-radius: var(--radius-md);
color: white;
cursor: pointer;
&:hover {
background: var(--ux-primary-hover);
}
}
}
.error-state p {
color: var(--ux-error);
}
.data-table {
width: 100%;
border-collapse: collapse;
thead {
background: rgba(var(--ux-surface-rgb), 0.6);
th {
padding: var(--spacing-3) var(--spacing-4);
text-align: left;
color: var(--ux-text-secondary);
font-size: var(--font-size-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid var(--ux-border);
}
}
tbody {
tr {
border-bottom: 1px solid var(--ux-border);
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: rgba(var(--ux-primary-rgb), 0.05);
}
td {
padding: var(--spacing-3) var(--spacing-4);
color: var(--ux-text-primary);
font-size: var(--font-size-sm);
}
.status-badge {
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
&.status-active {
background: rgba(var(--ux-success-rgb), 0.1);
color: var(--ux-success);
}
&.status-pending {
background: rgba(var(--ux-warning-rgb), 0.1);
color: var(--ux-warning);
}
&.status-inactive {
background: rgba(var(--ux-error-rgb), 0.1);
color: var(--ux-error);
}
}
.actions-cell {
display: flex;
gap: var(--spacing-2);
a {
color: var(--ux-primary);
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
}
}
}
.pagination {
margin-top: var(--spacing-4);
padding: var(--spacing-3);
text-align: center;
p {
margin: 0;
color: var(--ux-text-secondary);
font-size: var(--font-size-sm);
}
}
Widget Component Template (Embeddables)
TypeScript File
// embeddables/<entity>-widget/<entity>-widget.component.ts
import { Component, inject, signal, computed, input, output, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { LoggerService } from '@libs-core/log-system';
import { <Entity>Repository } from '../../data/repositories/<entity>.repository';
import { <Entity> } from '../../models/<entity>.model';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'lib-<entity>-widget',
standalone: true,
imports: [CommonModule],
templateUrl: './<entity>-widget.component.html',
styleUrl: './<entity>-widget.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class <Entity>WidgetComponent implements OnInit {
private readonly repository = inject(<Entity>Repository);
private readonly router = inject(Router);
private readonly logger = inject(LoggerService).child({ source: '<Entity>WidgetComponent' });
// Widget inputs (defined in manifest.embeddableComponents)
readonly widgetId = input<string>('');
readonly title = input<string>('<Entities>');
readonly limit = input<number>(5);
readonly showStatus = input<boolean>(true);
// Widget outputs
readonly entitySelected = output<<Entity>>();
// Component state
readonly entities = signal<<Entity>[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
// Computed
readonly displayedEntities = computed(() =>
this.entities().slice(0, this.limit())
);
readonly isEmpty = computed(() => this.displayedEntities().length === 0);
ngOnInit(): void {
this.loadEntities();
}
private async loadEntities(): Promise<void> {
this.loading.set(true);
this.error.set(null);
try {
const entities = await firstValueFrom(this.repository.findAll());
this.entities.set(entities);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
this.error.set(errorMessage);
this.logger.error('Failed to load entities', { error: errorMessage });
} finally {
this.loading.set(false);
}
}
onEntityClick(entity: <Entity>): void {
this.entitySelected.emit(entity);
}
onViewAll(): void {
void this.router.navigate(['/<base-path>']);
}
}
HTML Template
<!-- <entity>-widget/<entity>-widget.component.html -->
<div class="widget-container">
<div class="widget-header">
<h3>{{ title() }}</h3>
<button class="view-all-btn" (click)="onViewAll()">
View All
</button>
</div>
@if (loading()) {
<div class="widget-loading">
<p>Loading...</p>
</div>
} @else if (error()) {
<div class="widget-error">
<p>{{ error() }}</p>
</div>
} @else if (isEmpty()) {
<div class="widget-empty">
<p>No <entities> found</p>
</div>
} @else {
<ul class="widget-list">
@for (entity of displayedEntities(); track entity.id) {
<li class="widget-item" (click)="onEntityClick(entity)">
<span class="entity-name">{{ entity.name }}</span>
@if (showStatus() && entity.status) {
<span class="entity-status" [class]="'status-' + entity.status">
{{ entity.status }}
</span>
}
</li>
}
</ul>
}
</div>
SCSS Styles
// <entity>-widget/<entity>-widget.component.scss
.widget-container {
padding: var(--spacing-4);
backdrop-filter: blur(12px);
background: rgba(var(--ux-surface-rgb), 0.8);
border: 1px solid var(--ux-border);
border-radius: var(--radius-lg);
height: 100%;
display: flex;
flex-direction: column;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-4);
padding-bottom: var(--spacing-3);
border-bottom: 1px solid var(--ux-border);
h3 {
margin: 0;
color: var(--ux-text-primary);
font-size: var(--font-size-lg);
font-weight: 600;
}
.view-all-btn {
padding: var(--spacing-1) var(--spacing-3);
background: transparent;
border: 1px solid var(--ux-primary);
border-radius: var(--radius-sm);
color: var(--ux-primary);
font-size: var(--font-size-sm);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: var(--ux-primary);
color: white;
}
}
}
.widget-loading,
.widget-error,
.widget-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
p {
margin: 0;
color: var(--ux-text-secondary);
font-size: var(--font-size-sm);
}
}
.widget-error p {
color: var(--ux-error);
}
.widget-list {
list-style: none;
padding: 0;
margin: 0;
flex: 1;
overflow-y: auto;
}
.widget-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-3);
border-bottom: 1px solid var(--ux-border);
cursor: pointer;
transition: background 0.2s ease;
&:last-child {
border-bottom: none;
}
&:hover {
background: rgba(var(--ux-primary-rgb), 0.05);
}
.entity-name {
color: var(--ux-text-primary);
font-size: var(--font-size-sm);
font-weight: 500;
}
.entity-status {
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
&.status-active {
background: rgba(var(--ux-success-rgb), 0.1);
color: var(--ux-success);
}
&.status-pending {
background: rgba(var(--ux-warning-rgb), 0.1);
color: var(--ux-warning);
}
&.status-inactive {
background: rgba(var(--ux-error-rgb), 0.1);
color: var(--ux-error);
}
}
}
Module Class Template
// <module-name>/<module-name>.module.ts
import { Injectable, inject } from '@angular/core';
import { Observable, of } from 'rxjs';
import { BrowserModuleContract, ModuleManifest, ModuleDecorator } from '@libs-core/module-base';
import { LoggerService } from '@libs-core/log-system';
import { EventBusService } from '@libs-core/event-system';
import { <Entity>Repository } from './data/repositories/<entity>.repository';
@ModuleDecorator({
id: '<module-name>',
version: '1.0.0',
name: '<Module Name>',
description: '<Module description>',
permissions: ['<resource>:read', '<resource>:write']
})
@Injectable({ providedIn: 'root' })
export class <ModuleName>Module implements BrowserModuleContract {
private readonly logger = inject(LoggerService).child({ source: '<ModuleName>Module' });
private readonly eventBus = inject(EventBusService);
private readonly repository = inject(<Entity>Repository);
readonly manifest: ModuleManifest = {
id: '<module-name>',
version: '1.0.0',
name: '<Module Name>',
description: '<Module description>',
permissions: ['<resource>:read', '<resource>:write'],
publicActions: [
{
id: '<resource>:navigate-to-<entity>',
name: 'Navigate to <Entity>',
description: 'Navigate to <entity> detail page',
parameters: [{ name: '<entity>Id', type: 'string', required: true }],
},
// ... more actions
],
embeddableComponents: [
{
id: '<entity>-widget',
name: '<Entity> Widget',
description: 'Display list of <entities>',
inputs: [
{ name: 'limit', type: 'number', default: 5 },
{ name: 'showStatus', type: 'boolean', default: true },
],
outputs: [
{ name: 'entitySelected', type: '<Entity>' },
],
},
],
};
configure<T = Record<string, unknown>>(config: T): Observable<void> {
this.logger.info('Configuring module', config);
return of(void 0);
}
initialize(): Observable<void> {
this.logger.info('Initializing module');
// Setup services, load initial data if needed
return of(void 0);
}
start(): Observable<void> {
this.logger.info('Starting module');
this.eventBus.emit({
type: 'MODULE_STARTED',
payload: { moduleId: this.manifest.id },
});
return of(void 0);
}
stop(): Observable<void> {
this.logger.info('Stopping module');
return of(void 0);
}
destroy(): Observable<void> {
this.logger.info('Destroying module');
return of(void 0);
}
}
Routes Template
// <module-name>/<module-name>.routes.ts
import { Routes } from '@angular/router';
import { authGuard, permissionGuard, roleGuard } from '@libs-core/security';
import { SystemRoles } from '@libs-core/shared-models';
export const <moduleName>Routes: Routes = [
{
path: '',
redirectTo: 'list',
pathMatch: 'full',
},
{
path: 'list',
loadComponent: () => import('./components/<entity>-list/<entity>-list.component')
.then(m => m.<Entity>ListComponent),
canActivate: [authGuard, permissionGuard('<resource>:read')],
data: {
title: '<Entities>',
breadcrumb: '<Entities>',
permissions: ['<resource>:read'],
animation: 'fade',
},
},
{
path: 'create',
loadComponent: () => import('./components/<entity>-form/<entity>-form.component')
.then(m => m.<Entity>FormComponent),
canActivate: [authGuard, permissionGuard('<resource>:create')],
data: {
title: 'Create <Entity>',
breadcrumb: 'Create',
permissions: ['<resource>:create'],
mode: 'create',
animation: 'slideLeft',
},
},
{
path: ':id',
loadComponent: () => import('./components/<entity>-detail/<entity>-detail.component')
.then(m => m.<Entity>DetailComponent),
canActivate: [authGuard, permissionGuard('<resource>:read')],
data: {
title: '<Entity> Detail',
breadcrumb: 'Detail',
permissions: ['<resource>:read'],
animation: 'slideLeft',
},
},
{
path: ':id/edit',
loadComponent: () => import('./components/<entity>-form/<entity>-form.component')
.then(m => m.<Entity>FormComponent),
canActivate: [authGuard, permissionGuard('<resource>:update')],
data: {
title: 'Edit <Entity>',
breadcrumb: 'Edit',
permissions: ['<resource>:update'],
mode: 'edit',
animation: 'slideLeft',
},
},
];
Model Template
// models/<entity>.model.ts
import { BaseEntity } from '@libs-core/shared-models';
/**
* <Entity> status enum
*/
export type <Entity>Status = 'active' | 'inactive' | 'pending';
/**
* <Entity> interface
* Extends BaseEntity which provides: id, createdAt, updatedAt
*/
export interface <Entity> extends BaseEntity {
name: string;
description: string;
status: <Entity>Status;
// Add entity-specific fields
}
/**
* Create <Entity> request
*/
export interface Create<Entity>Request {
name: string;
description: string;
status?: <Entity>Status;
}
/**
* Update <Entity> request
*/
export interface Update<Entity>Request {
name?: string;
description?: string;
status?: <Entity>Status;
}
/**
* <Entity> filters
*/
export interface <Entity>Filters {
status?: <Entity>Status | 'all';
searchTerm?: string;
page?: number;
pageSize?: number;
}
Action Template
// actions/navigate-to-<entity>.action.ts
import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PublicAction, ExecutableAction, ActionContext, ActionParams } from '@libs-core/action-registry';
import { LoggerService } from '@libs-core/log-system';
export interface NavigateTo<Entity>Input extends ActionParams {
<entity>Id: string;
mode?: 'detail' | 'edit';
}
export interface NavigateTo<Entity>Output {
success: boolean;
targetRoute: string;
}
@PublicAction({
id: '<resource>:navigate-to-<entity>',
name: 'Navigate to <Entity>',
description: 'Navigate to <entity> detail or edit page',
permissions: ['<resource>:read'],
tags: ['navigation', '<resource>'],
parameters: [
{
name: '<entity>Id',
type: 'string',
required: true,
description: '<Entity> ID to navigate to',
},
{
name: 'mode',
type: 'string',
required: false,
description: 'Navigation mode (detail or edit)',
default: 'detail',
},
],
config: { category: 'navigation', module: '<module-name>' },
})
@Injectable({ providedIn: 'root' })
export class NavigateTo<Entity>Action implements ExecutableAction<NavigateTo<Entity>Output> {
readonly metadata = {
id: '<resource>:navigate-to-<entity>',
name: 'Navigate to <Entity>',
description: 'Navigate to <entity> detail or edit page',
requiredPermissions: ['<resource>:read'] as const,
category: 'navigation',
enabled: true,
};
private readonly router = inject(Router);
private readonly logger = inject(LoggerService).child({ source: 'NavigateTo<Entity>Action' });
execute(
params: NavigateTo<Entity>Input,
context: ActionContext
): Observable<NavigateTo<Entity>Output> {
this.logger.info('Navigating to <entity>', { <entity>Id: params.<entity>Id, mode: params.mode });
const route = params.mode === 'edit'
? ['/<base-path>', params.<entity>Id, 'edit']
: ['/<base-path>', params.<entity>Id];
return of({ success: true, targetRoute: route.join('/') }).pipe(
tap(() => this.router.navigate(route))
);
}
validate(params: NavigateTo<Entity>Input): Observable<boolean> {
return of(!!params.<entity>Id);
}
cleanup(): void {
// Cleanup if needed
}
}
Service Template
// services/<entity>-api.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { LoggerService } from '@libs-core/log-system';
import { ApiResponse } from '@libs-core/shared-models';
import { <Entity>, <Entity>Filters, Create<Entity>Request, Update<Entity>Request } from '../models';
@Injectable({ providedIn: 'root' })
export class <Entity>ApiService {
private readonly http = inject(HttpClient);
private readonly logger = inject(LoggerService).child({ source: '<Entity>ApiService' });
private readonly baseUrl = '/api/<entities>';
get<Entities>(filters: <Entity>Filters): Observable<ApiResponse<{ <entities>: <Entity>[] }>> {
this.logger.debug('Fetching <entities>', { filters });
const params = new URLSearchParams();
if (filters.status && filters.status !== 'all') {
params.set('status', filters.status);
}
if (filters.searchTerm) {
params.set('q', filters.searchTerm);
}
if (filters.page) {
params.set('page', filters.page.toString());
}
if (filters.pageSize) {
params.set('pageSize', filters.pageSize.toString());
}
return this.http.get<ApiResponse<{ <entities>: <Entity>[] }>>(
`${this.baseUrl}?${params.toString()}`
);
}
get<Entity>ById(id: string): Observable<ApiResponse<<Entity>>> {
this.logger.debug('Fetching <entity> by id', { id });
return this.http.get<ApiResponse<<Entity>>>(`${this.baseUrl}/${id}`);
}
create<Entity>(data: Create<Entity>Request): Observable<ApiResponse<<Entity>>> {
this.logger.debug('Creating <entity>', { data });
return this.http.post<ApiResponse<<Entity>>>(this.baseUrl, data);
}
update<Entity>(id: string, data: Update<Entity>Request): Observable<ApiResponse<<Entity>>> {
this.logger.debug('Updating <entity>', { id, data });
return this.http.put<ApiResponse<<Entity>>>(`${this.baseUrl}/${id}`, data);
}
delete<Entity>(id: string): Observable<ApiResponse<void>> {
this.logger.debug('Deleting <entity>', { id });
return this.http.delete<ApiResponse<void>>(`${this.baseUrl}/${id}`);
}
}
Event Template
// events/<entity>.events.ts
import { DomainEvent, DomainEventPayload } from '@libs-core/event-system';
import { <Entity> } from '../models/<entity>.model';
// <Entity> Created Event
export interface <Entity>CreatedPayload extends DomainEventPayload {
<entity>: <Entity>;
createdBy: string;
}
export class <Entity>CreatedEvent extends DomainEvent<<Entity>CreatedPayload> {
static readonly TYPE = '<entity>.created';
constructor(payload: <Entity>CreatedPayload) {
super(<Entity>CreatedEvent.TYPE, '<module-name>', payload);
}
}
// <Entity> Updated Event
export interface <Entity>UpdatedPayload extends DomainEventPayload {
<entity>: <Entity>;
changes: Partial<<Entity>>;
updatedBy: string;
}
export class <Entity>UpdatedEvent extends DomainEvent<<Entity>UpdatedPayload> {
static readonly TYPE = '<entity>.updated';
constructor(payload: <Entity>UpdatedPayload) {
super(<Entity>UpdatedEvent.TYPE, '<module-name>', payload);
}
}
// <Entity> Deleted Event
export interface <Entity>DeletedPayload extends DomainEventPayload {
<entity>Id: string;
deletedBy: string;
}
export class <Entity>DeletedEvent extends DomainEvent<<Entity>DeletedPayload> {
static readonly TYPE = '<entity>.deleted';
constructor(payload: <Entity>DeletedPayload) {
super(<Entity>DeletedEvent.TYPE, '<module-name>', payload);
}
}
Placeholder Variables
When generating a module, replace these placeholders:
| Placeholder | Example | Description |
|---|---|---|
<module-name> |
product-catalog |
Module folder name (kebab-case) |
<module-id> |
product-catalog |
Module ID in manifest |
<ModuleName> |
ProductCatalog |
Module class prefix (PascalCase) |
<moduleName> |
productCatalog |
Module variable name (camelCase) |
<Entity> |
Product |
Main entity (PascalCase) |
<entity> |
product |
Entity lowercase |
<entities> |
products |
Entity plural lowercase |
<Entities> |
Products |
Entity plural (PascalCase) |
<base-path> |
products |
URL base path |
<resource> |
products |
Permission resource name |
Module Lifecycle
All modules must implement these lifecycle methods:
REGISTERED → CONFIGURED → INITIALIZED → STARTED → STOPPED → DESTROYED
configure<T>(config: T): Observable<void> // Apply configuration
initialize(): Observable<void> // Setup services, connections
start(): Observable<void> // Start the module
stop(): Observable<void> // Stop the module
destroy(): Observable<void> // Cleanup resources
Integration with editor2 Systems
Flash Persistence (MUST)
// For persistent UI state
readonly settings = autoFlashState('<module-name>-settings', {
viewMode: 'grid',
sortBy: 'name',
});
// Flash persistence handles:
// ✅ Automatic IndexedDB sync
// ✅ Tenant context
// ✅ Conflict resolution
Theme System (MUST)
// Always use CSS variables
.my-component {
background: var(--ux-surface);
color: var(--ux-text-primary);
backdrop-filter: blur(16px);
background: rgba(var(--ux-surface-rgb), 0.8);
}
Intelligence Pyramid (MUST)
// Before generation
await mcp.preflight({ task: 'Generate product-catalog module' });
// After generation
await mcp.log_decision({
type: 'implementation',
question: 'How to structure product-catalog module?',
chosen: 'CRUD with inventory tracking',
reasoning: 'User requested stock management',
confidence: 0.9,
files: ['product-catalog.module.ts'],
});
Validation Checklist
Before marking module complete:
Structure Validation
- All required directories present
- All components use separate .ts, .html, .scss files
- Barrel exports complete in index.ts files
- Module implements BrowserModuleContract
- Routes defined with guards
Integration Validation
- Uses @storage-system (autoFlashState) for persistent state
- Uses @theme-system CSS variables (no hardcoded colors)
- Uses @libs-core libraries appropriately
- Repository extends BaseRepository
- Actions use @PublicAction decorator
- Events extend DomainEvent
Quality Validation
- Standalone components with OnPush
- inject() function for DI
- Signals for local state
- RxJS for async operations
- Proper error handling
- Logging with context
Documentation
- README.md explains module purpose
- All public APIs documented
- Integration examples provided
Related Documentation
- AI-RULES.md - Mandatory coding rules
- QUICK-REFERENCE.md - Copy-paste snippets
- Module Creation Guide - Complete guide
- Hybrid Module Creation - Visual + code approach
Version: 1.0.0
Last Updated: December 27, 2025
Platform: editor2
Maintained By: editor2 development team