Skip to content

Widget Creation Guide

Complete step-by-step guide to creating widgets for the Barcoding system.

Overview

There are two primary methods for creating widgets:

  1. Using Schematics (Recommended) - Automated widget generation with proper structure
  2. Using Claude Code Command - Integrated workflow with database migration creation

Both methods create instance-specific widgets. For shared/package widgets, see Creating Shared Widgets.

Prerequisites

Before creating a widget, ensure you have:

  • Development Environment: Web development server running (yarn start-back, yarn start-front, or yarn start-dashboard)
  • API Access: API server running on port 3001
  • Database Access: MySQL database accessible for migration
  • Widget Definition: Clear understanding of what your widget will display/do
  • Instance Selection: Know which instance (backend/front/dashboard) will use the widget

Method 1: Using Schematics

Step 1: Run the Schematic

Navigate to the web directory and run the widget creation schematic:

bash
cd /workspace/web
schematics ./widget-create/:widget-create --no-dry-run --name="YourWidgetName" --instance="backend"

Interactive Prompts:

  • Widget Name? → Enter PascalCase name (e.g., "OrderList", "UserActivity", "SalesChart")
  • Instance of Barcoding? → Enter instance name: backend, front, or dashboard

Step 2: What the Schematic Creates

The schematic automatically generates:

File Structure:

web/projects/{instance}/src/widgets/widget-{name}/
├── widget-{name}/
│   ├── widget-{name}.component.ts     # Main component
│   ├── widget-{name}.component.html   # Template
│   └── widget-{name}.component.scss   # Styles (if needed)
└── widget-{name}.module.ts             # Widget module

Component Template:

typescript
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { WidgetInterface, WidgetParams } from '@barcoding/gridster-core';
import { WidgetConfigWithRelations } from '@barcoding/sdk';

@Component({
  selector: 'widget-order-list',
  templateUrl: './widget-order-list.component.html'
})
export class WidgetOrderListComponent implements OnInit, WidgetInterface {
  public item: WidgetConfigWithRelations;
  public params: WidgetParams;

  constructor(private store: Store) {}

  ngOnInit() {
    // Widget initialization logic
  }
}

Module Template:

typescript
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WidgetOrderListComponent } from './widget-order-list/widget-order-list.component';
import { DynamicComponentLoaderModule } from '@barcoding/gridster-core';

@NgModule({
  declarations: [WidgetOrderListComponent],
  imports: [
    CommonModule,
    DynamicComponentLoaderModule.forChild(WidgetOrderListComponent),
  ],
})
export class WidgetOrderListModule {}

Step 3: Automatic Registration

The schematic automatically registers your widget in the instance's widget manifest:

Location: web/projects/{instance}/src/widgets/widgets.ts

Registration Entry:

typescript
export const WidgetsManifests = [
  // ... existing widgets
  {
    componentId: 'WidgetOrderList',
    path: 'WidgetOrderList',
    loadChildren: () =>
      import('./widget-order-list/widget-order-list.module').then(
        (m) => m.WidgetOrderListModule
      ),
  },
];

Step 4: Create Database Migration

CRITICAL: You must create a Knex migration to register the widget in the database.

Navigate to the API directory:

bash
cd /workspace/api/b3api
npx knex migrate:make add_new_widget_order_list

This creates a migration file at:

/workspace/api/b3api/src/knex_migrations/{timestamp}_add_new_widget_order_list.ts

Migration Template:

typescript
import {Knex} from 'knex';

const widgetName = `WidgetOrderList`;
const widgetDesc = `Order List Widget`;

export async function up(knex: Knex): Promise<void> {
  await knex.raw(`
    set @x := (SELECT 1 FROM Widget WHERE controller = '${widgetName}' LIMIT 1);
    set @sql := if( @x = 1, 'select ''Index exists.''', 'INSERT INTO Widget (controller,dashboard,category_id,general_name,status,duplication_option) VALUES("${widgetName}","0","2","${widgetDesc}","1","0")');
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
  `);
}

export async function down(knex: Knex): Promise<void> {
  await knex.raw(`DELETE FROM Widget WHERE controller = "${widgetName}"`);
}

Migration Parameters Explained:

  • controller: Must match the component class name (e.g., "WidgetOrderList")
  • dashboard: "0" = available in all instances, "1" = dashboard-only
  • category_id: "2" = default category (adjust based on your categorization)
  • general_name: User-facing display name
  • status: "1" = active, "0" = inactive
  • duplication_option: "0" = single instance per dashboard, "1" = allow multiple

Step 5: Run the Migration

Execute the migration to register the widget in the database:

bash
cd /workspace/api/b3api
npm run migrate

Verify the widget was registered:

bash
mysql -h mysqldb -P 3306 -u root -pYpp01o#3 lb4 -e "SELECT * FROM Widget WHERE controller = 'WidgetOrderList';"

Step 6: Test the Widget

Start the instance app:

bash
cd /workspace/web
yarn start-back    # For backend instance
# OR
yarn start-front   # For front instance
# OR
yarn start-dashboard  # For dashboard instance

The widget is now available in the gridster dashboard system and can be added to dashboards through the management interface.

Method 2: Using Claude Code Command

The .claude/commands/create-widget.md command provides an integrated workflow.

Usage

bash
/create-widget OrderList "Order List Widget for Tool Usage"

Or interactive mode:

bash
/create-widget

This command:

  1. Prompts for widget name, description, and instance
  2. Runs the schematic (as described in Method 1)
  3. Automatically creates the database migration file
  4. Provides a summary with next steps

When to Use This Method

  • You want an integrated workflow
  • You prefer Claude Code to handle both schematic and migration
  • You need guidance through the entire process

Creating Shared Widgets

Shared/package widgets are reusable across all three instances.

Step 1: Generate Library Package

bash
cd /workspace/web
ng generate library @barcoding/widget-calendar --prefix=app

Step 2: Define Widget Component

Location: web/projects/packages/@barcoding/widgets/widget-calendar/src/lib/

typescript
import { Component, OnInit } from '@angular/core';
import { WidgetInterface, WidgetParams } from '@barcoding/gridster-core';
import { WidgetConfigWithRelations } from '@barcoding/sdk';

@Component({
  selector: 'app-widget-calendar',
  templateUrl: './widget-calendar.component.html',
  styleUrls: ['./widget-calendar.component.scss']
})
export class WidgetCalendarComponent implements OnInit, WidgetInterface {
  public item: WidgetConfigWithRelations;
  public params: WidgetParams;

  constructor() {}

  ngOnInit() {
    // Widget initialization
  }
}

Step 3: Create Widget Module

typescript
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WidgetCalendarComponent } from './widget-calendar.component';
import { DynamicComponentLoaderModule } from '@barcoding/gridster-core';

@NgModule({
  declarations: [WidgetCalendarComponent],
  imports: [
    CommonModule,
    DynamicComponentLoaderModule.forChild(WidgetCalendarComponent),
  ],
  exports: [WidgetCalendarComponent]
})
export class WidgetCalendarModule {}

Step 4: Configure Package Dependencies

package.json:

json
{
  "name": "@barcoding/widget-calendar",
  "version": "0.0.1",
  "peerDependencies": {
    "@angular/common": "^17.0.0",
    "@angular/core": "^17.0.0",
    "@barcoding/core": "*",
    "@barcoding/sdk": "*",
    "@barcoding/gridster-core": "*"
  }
}

Step 5: Build the Package

bash
cd /workspace/web
ng build @barcoding/widget-calendar

Step 6: Register in Each Instance

Each instance that wants to use the shared widget must register it in its widgets.ts manifest:

Backend (web/projects/backend/src/widgets/widgets.ts):

typescript
{
  componentId: 'WidgetCalendar',
  path: 'WidgetCalendar',
  loadChildren: () =>
    import('@barcoding/widget-calendar').then(
      (m) => m.WidgetCalendarModule
    ),
}

Front (web/projects/front/src/widgets/widgets.ts):

typescript
{
  componentId: 'WidgetCalendar',
  path: 'WidgetCalendar',
  loadChildren: () =>
    import('@barcoding/widget-calendar').then(
      (m) => m.WidgetCalendarModule
    ),
}

Step 7: Database Migration

Create the migration (same as instance-specific widgets):

bash
cd /workspace/api/b3api
npx knex migrate:make add_new_widget_calendar
typescript
import {Knex} from 'knex';

const widgetName = `WidgetCalendar`;
const widgetDesc = `Calendar Widget`;

export async function up(knex: Knex): Promise<void> {
  await knex.raw(`
    set @x := (SELECT 1 FROM Widget WHERE controller = '${widgetName}' LIMIT 1);
    set @sql := if( @x = 1, 'select ''Index exists.''', 'INSERT INTO Widget (controller,dashboard,category_id,general_name,status,duplication_option,package_name) VALUES("${widgetName}","0","2","${widgetDesc}","1","0","@barcoding/widget-calendar")');
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
  `);
}

export async function down(knex: Knex): Promise<void> {
  await knex.raw(`DELETE FROM Widget WHERE controller = "${widgetName}"`);
}

Note the package_name field which identifies this as a shared package widget.

Widget Naming Conventions

Follow these naming conventions for consistency:

Component Names

  • Format: Widget{Name}Component
  • Examples: WidgetCommentsComponent, WidgetOrderListComponent, WidgetNcrInfoComponent
  • Selector: widget-{name} (e.g., widget-comments, widget-order-list)

Module Names

  • Format: Widget{Name}Module
  • Examples: WidgetCommentsModule, WidgetOrderListModule

File Names

  • Component: widget-{name}.component.ts
  • Template: widget-{name}.component.html
  • Styles: widget-{name}.component.scss
  • Module: widget-{name}.module.ts

Directory Names

  • Format: widget-{name}/
  • Examples: widget-comments/, widget-order-list/, widget-ncr-info/

Database Controller Names

  • Format: Widget{Name} (PascalCase, no "Component" suffix)
  • Examples: WidgetComments, WidgetOrderList, WidgetNcrInfo
  • CRITICAL: Must match componentId in manifest and class name prefix

Common Issues and Troubleshooting

Widget Not Appearing in Dashboard

Symptom: Widget doesn't appear in the widget selection interface

Solutions:

  1. Verify widget is registered in the database:
    bash
    mysql -h mysqldb -u root -pYpp01o#3 lb4 -e "SELECT * FROM Widget WHERE controller = 'WidgetYourName';"
  2. Check status field is set to "1" (active)
  3. Verify widget is registered in instance's widgets.ts manifest
  4. Ensure componentId in manifest matches database controller field exactly

Widget Fails to Load Dynamically

Symptom: Console error "WIDGET_NOT_EXISTS" or widget area is blank

Solutions:

  1. Check componentId in manifest matches exactly with controller name
  2. Verify module is properly exported and implements DynamicComponentLoaderModule.forChild()
  3. Ensure widget component implements WidgetInterface
  4. Check browser console for import/module resolution errors

Migration Already Exists Error

Symptom: Migration fails with "Index exists" or duplicate entry error

Solutions:

  1. Check if widget already exists in database:
    bash
    mysql -h mysqldb -u root -pYpp01o#3 lb4 -e "SELECT * FROM Widget WHERE controller = 'WidgetYourName';"
  2. If exists, either use existing entry or delete and re-run migration
  3. Ensure widget name is unique (controller field is unique index)

Widget Component Not Receiving State/Params

Symptom: Widget's state or params properties are undefined

Solutions:

  1. Verify component implements WidgetInterface correctly
  2. Check that properties are declared as public:
    typescript
    public item: WidgetConfigWithRelations;
    public params: WidgetParams;
    public state: Observable<any>;
  3. Ensure WidgetWrapper is correctly passing props (should be automatic)

Best Practices

1. Widget Design Principles

  • Single Responsibility: Each widget should do one thing well
  • Configurable: Use widget config for customizable behavior
  • Self-Contained: Minimize external dependencies
  • Responsive: Design for different grid sizes (cols/rows)

2. State Management

  • Subscribe to state Observable: Always handle state changes reactively
  • Unsubscribe properly: Clean up subscriptions in ngOnDestroy()
  • Use NGXS for complex state: Leverage stores for shared state
typescript
ngOnInit() {
  this.objectSubscription = this.state.subscribe(object => {
    // Handle state changes
  });
}

ngOnDestroy() {
  if (this.objectSubscription) {
    this.objectSubscription.unsubscribe();
  }
}

3. API Integration

  • Always use SDK services: Never construct HTTP requests manually
  • Handle errors gracefully: Use try/catch or error callbacks
  • Show loading states: Provide visual feedback during data fetching
typescript
constructor(private myApiService: MyApiControllerService) {}

ngOnInit() {
  this.myApiService.apiMyControllerGet$Response().subscribe(
    response => {
      // Handle success
      this.data = response.body;
    },
    error => {
      // Handle error
      console.error('Failed to load data:', error);
    }
  );
}

4. Performance Optimization

  • Lazy load widgets: Use dynamic imports (already handled by manifest)
  • Debounce rapid updates: Use RxJS debounceTime() for frequent changes
  • OnPush change detection: Consider using ChangeDetectionStrategy.OnPush
  • Minimize DOM operations: Use trackBy with *ngFor

5. Testing

  • Unit tests: Test widget logic in isolation
  • Integration tests: Test widget in gridster context
  • E2E tests: Test widget in full application flow

Next Steps

After creating your widget:

  1. Implement Widget Logic - See Widget Development Guide
  2. Add Configuration UI - See Widget Configuration Guide
  3. Test Your Widget - See Widget Testing Guide
  4. Deploy Widget - See Widget Deployment Guide

Syneo/Barcoding Documentation