Module Migration Template - Converting Widgets to Modules
Step-by-step template for converting widgets to modules when platform-agnostic support is needed
Module Migration Template - Converting Widgets to Modules
Use this template when you need to convert a widget to a module for platform-agnostic support, routes, permissions, or lifecycle management.
When to Use This Template
Convert a widget to a module when:
- ✅ Component needs platform-agnostic support (Flutter)
- ✅ Component needs routes or navigation
- ✅ Component needs permissions or access control
- ✅ Component needs public actions (API)
- ✅ Component needs complex lifecycle management
- ✅ Component needs runtime configuration
Step-by-Step Migration Process
Step 1: Create Module Directory Structure
modules/structural/{module-name}/
├── src/
│ ├── index.ts
│ └── lib/
│ ├── components/
│ │ ├── index.ts
│ │ └── {component-name}/
│ │ ├── {component-name}.component.ts
│ │ ├── {component-name}.component.html
│ │ └── {component-name}.component.scss
│ ├── models/
│ │ ├── index.ts
│ │ └── {module-name}-config.types.ts
│ ├── {module-name}.manifest.ts
│ └── {module-name}.module.ts
├── package.json
├── project.json
├── tsconfig.json
└── tsconfig.lib.json
Step 2: Move/Copy Widget Component
Option A: Move widget from libs/libs-ui/* to modules/structural/{module-name}/src/lib/components/
Option B: Keep widget in libs/libs-ui/* and import it in module (recommended for reusability)
// modules/structural/{module-name}/src/lib/components/{component-name}/{component-name}.component.ts
import { Component } from '@angular/core';
// Import original widget if keeping it separate
// OR copy widget code here if moving it
Step 3: Create Module Configuration Types
// modules/structural/{module-name}/src/lib/models/{module-name}-config.types.ts
export interface {ModuleName}Config {
// Module-specific configuration
// Example:
// theme?: 'light' | 'dark';
// position?: 'header' | 'footer' | 'sidebar';
// items?: MenuItem[];
}
export interface {ModuleName}Options {
// Runtime options
// Example:
// collapsible?: boolean;
// defaultOpen?: boolean;
}
Step 4: Create Module Manifest
// modules/structural/{module-name}/src/lib/{module-name}.manifest.ts
import { ModuleManifest } from '@libs-core/module-base';
import { {ComponentName}Component } from './components/{component-name}/{component-name}.component';
export const {MODULE_NAME}_MANIFEST: ModuleManifest = {
id: '{module-name}',
version: '1.0.0',
name: '{Module Display Name}',
description: '{Module description}',
icon: 'fas fa-{icon-name}',
// Routes (if needed)
routes: [
// {
// path: '{route-path}',
// component: {ComponentName}Component,
// permissions: ['{permission}']
// }
],
// Navigation (if needed)
navigation: {
menuItems: [
// {
// id: '{menu-item-id}',
// label: '{Menu Item}',
// icon: '{icon}',
// route: '{route}',
// order: 10,
// permissions: ['{permission}']
// }
]
},
// Expose widget as embeddable component
embeddableComponents: [
{
id: '{module-name}-widget',
componentType: {ComponentName}Component,
displayName: '{Component Display Name}',
description: '{Component description}',
category: 'widget',
permissions: []
}
],
// Public actions (if needed)
publicActions: [
// Action classes here
],
// Permissions (if needed)
permissions: [
// {
// id: '{permission-id}',
// name: '{permission-name}',
// resource: '{resource}',
// action: '{action}'
// }
],
author: 'GoGrab Team',
tags: ['{tag1}', '{tag2}']
};
Step 5: Create Module Class
// modules/structural/{module-name}/src/lib/{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 { {ComponentName}Component } from './components/{component-name}/{component-name}.component';
import { {MODULE_NAME}_MANIFEST } from './{module-name}.manifest';
import { {ModuleName}Config } from './models/{module-name}-config.types';
/**
* {Module Display Name} Module
*
* {Module description}
*
* Features:
* - Feature 1
* - Feature 2
* - Feature 3
*
* @implements {BrowserModuleContract}
*/
@ModuleDecorator({
id: '{module-name}',
version: '1.0.0',
name: '{Module Display Name}',
description: '{Module description}',
permissions: [],
navigation: undefined,
author: 'GoGrab Team',
tags: ['{tag1}', '{tag2}'],
})
@Injectable({
providedIn: 'root',
})
export class {ModuleName}Module implements BrowserModuleContract {
private readonly logger = inject(LoggerService).child({
source: '{ModuleName}Module',
});
// Module configuration (set during configure())
private config?: {ModuleName}Config;
readonly manifest: ModuleManifest = {MODULE_NAME}_MANIFEST;
readonly routes = [];
/**
* Configure module with runtime settings
*/
configure<T = {ModuleName}Config>(config: T): Observable<void> {
this.config = config as {ModuleName}Config;
this.logger.info('Configuring {module-name} module...', {
service: '{ModuleName}Module',
moduleId: this.manifest.id,
version: this.manifest.version,
tags: ['module', 'lifecycle', 'configure'],
});
return of(void 0);
}
/**
* Initialize module
*/
initialize(): Observable<void> {
this.logger.info('Initializing {module-name} module...', {
service: '{ModuleName}Module',
moduleId: this.manifest.id,
tags: ['module', 'lifecycle', 'initialize'],
});
return of(void 0);
}
/**
* Start module (activate services)
*/
start(): Observable<void> {
this.logger.info('Starting {module-name} module...', {
service: '{ModuleName}Module',
moduleId: this.manifest.id,
tags: ['module', 'lifecycle', 'start'],
});
return of(void 0);
}
/**
* Stop module (pause services)
*/
stop(): Observable<void> {
this.logger.info('Stopping {module-name} module...', {
service: '{ModuleName}Module',
moduleId: this.manifest.id,
tags: ['module', 'lifecycle', 'stop'],
});
return of(void 0);
}
/**
* Destroy module (cleanup)
*/
destroy(): Observable<void> {
this.logger.info('Destroying {module-name} module...', {
service: '{ModuleName}Module',
moduleId: this.manifest.id,
tags: ['module', 'lifecycle', 'destroy'],
});
this.config = undefined;
return of(void 0);
}
}
Step 6: Create Index Exports
// modules/structural/{module-name}/src/index.ts
export * from './lib/{module-name}.module';
export * from './lib/{module-name}.manifest';
export * from './lib/components';
export * from './lib/models';
// modules/structural/{module-name}/src/lib/components/index.ts
export * from './{component-name}/{component-name}.component';
// modules/structural/{module-name}/src/lib/models/index.ts
export * from './{module-name}-config.types';
Step 7: Create NX Library Configuration Files
package.json:
{
"name": "@module/{module-name}",
"version": "1.0.0",
"type": "commonjs",
"main": "./index.js",
"typings": "./index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js",
"types": "./index.d.ts"
}
}
}
project.json:
{
"name": "{module-name}",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "modules/structural/{module-name}/src",
"projectType": "library",
"tags": ["type:module", "scope:structural"],
"targets": {
"build": {
"executor": "@nx/angular:ng-packagr-lite",
"outputs": ["{workspaceRoot}/dist/modules/structural/{module-name}"],
"options": {
"project": "modules/structural/{module-name}/ng-package.json"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/modules/structural/{module-name}"]
}
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
Step 8: Register Module in App
// apps/gograb-space/src/app/core/modules.ts
import { {ModuleName}Module } from '@module/{module-name}';
// Add to module registry
export const MODULE_REGISTRY = [
// ... existing modules
{ModuleName}Module,
];
Step 9: Update Imports
Before (using widget directly):
import { {ComponentName}Component } from '@ux-ui-core';
After (using module):
import { {ModuleName}Module } from '@module/{module-name}';
// Component is available via module.manifest.embeddableComponents[]
Step 10: Update Documentation
- Update component audit document
- Add module to module index
- Create module documentation
- Update architecture diagrams
Example: Converting Sidebar Widget to Module
See modules/structural/sidebar-nav/ for a complete example of this migration pattern.
Checklist
- Module directory structure created
- Widget component moved/copied
- Configuration types defined
- Module manifest created
- Module class implemented
- Index exports created
- NX configuration files created
- Module registered in app
- Imports updated
- Documentation updated
- Tests written
- Linter errors fixed
Common Pitfalls
- Don't forget to expose widget: Always add component to
embeddableComponents[] - Don't skip lifecycle methods: All methods must return
Observable<void> - Don't forget logging: Use LoggerService for all lifecycle events
- Don't skip configuration: Use
configure()for runtime settings - Don't forget registration: Register module in
apps/gograb-space/src/app/core/modules.ts
This template ensures consistent, production-ready module creation! 🚀