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:


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

  1. Update component audit document
  2. Add module to module index
  3. Create module documentation
  4. Update architecture diagrams

Example: Converting Sidebar Widget to Module

See modules/structural/sidebar-nav/ for a complete example of this migration pattern.


Checklist


Common Pitfalls

  1. Don't forget to expose widget: Always add component to embeddableComponents[]
  2. Don't skip lifecycle methods: All methods must return Observable<void>
  3. Don't forget logging: Use LoggerService for all lifecycle events
  4. Don't skip configuration: Use configure() for runtime settings
  5. Don't forget registration: Register module in apps/gograb-space/src/app/core/modules.ts

This template ensures consistent, production-ready module creation! 🚀