Widget Creation Guide
Complete step-by-step guide to creating widgets for the Barcoding system.
Overview
There are two primary methods for creating widgets:
- Using Schematics (Recommended) - Automated widget generation with proper structure
- 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, oryarn 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:
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, ordashboard
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 moduleComponent Template:
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:
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:
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:
cd /workspace/api/b3api
npx knex migrate:make add_new_widget_order_listThis creates a migration file at:
/workspace/api/b3api/src/knex_migrations/{timestamp}_add_new_widget_order_list.tsMigration Template:
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-onlycategory_id: "2" = default category (adjust based on your categorization)general_name: User-facing display namestatus: "1" = active, "0" = inactiveduplication_option: "0" = single instance per dashboard, "1" = allow multiple
Step 5: Run the Migration
Execute the migration to register the widget in the database:
cd /workspace/api/b3api
npm run migrateVerify the widget was registered:
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:
cd /workspace/web
yarn start-back # For backend instance
# OR
yarn start-front # For front instance
# OR
yarn start-dashboard # For dashboard instanceThe 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
/create-widget OrderList "Order List Widget for Tool Usage"Or interactive mode:
/create-widgetThis command:
- Prompts for widget name, description, and instance
- Runs the schematic (as described in Method 1)
- Automatically creates the database migration file
- 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
cd /workspace/web
ng generate library @barcoding/widget-calendar --prefix=appStep 2: Define Widget Component
Location: web/projects/packages/@barcoding/widgets/widget-calendar/src/lib/
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
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:
{
"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
cd /workspace/web
ng build @barcoding/widget-calendarStep 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):
{
componentId: 'WidgetCalendar',
path: 'WidgetCalendar',
loadChildren: () =>
import('@barcoding/widget-calendar').then(
(m) => m.WidgetCalendarModule
),
}Front (web/projects/front/src/widgets/widgets.ts):
{
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):
cd /workspace/api/b3api
npx knex migrate:make add_new_widget_calendarimport {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
componentIdin 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:
- Verify widget is registered in the database:bash
mysql -h mysqldb -u root -pYpp01o#3 lb4 -e "SELECT * FROM Widget WHERE controller = 'WidgetYourName';" - Check
statusfield is set to "1" (active) - Verify widget is registered in instance's
widgets.tsmanifest - Ensure
componentIdin manifest matches databasecontrollerfield exactly
Widget Fails to Load Dynamically
Symptom: Console error "WIDGET_NOT_EXISTS" or widget area is blank
Solutions:
- Check
componentIdin manifest matches exactly with controller name - Verify module is properly exported and implements
DynamicComponentLoaderModule.forChild() - Ensure widget component implements
WidgetInterface - Check browser console for import/module resolution errors
Migration Already Exists Error
Symptom: Migration fails with "Index exists" or duplicate entry error
Solutions:
- Check if widget already exists in database:bash
mysql -h mysqldb -u root -pYpp01o#3 lb4 -e "SELECT * FROM Widget WHERE controller = 'WidgetYourName';" - If exists, either use existing entry or delete and re-run migration
- 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:
- Verify component implements
WidgetInterfacecorrectly - Check that properties are declared as public:typescript
public item: WidgetConfigWithRelations; public params: WidgetParams; public state: Observable<any>; - Ensure
WidgetWrapperis 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
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
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:
- Implement Widget Logic - See Widget Development Guide
- Add Configuration UI - See Widget Configuration Guide
- Test Your Widget - See Widget Testing Guide
- Deploy Widget - See Widget Deployment Guide