A TypeScript widget system built on Handlebars that provides reusable components for generating Markdown documentation. The framework allows you to create custom widgets that can transform data into formatted output using Handlebars templates.
Renders data as Markdown tables with support for nested objects and column filtering.
Options:
headers(boolean): Show/hide table headers (default: true)columns(string): Comma-separated list of columns to includekey(string): Property to use as unique identifier (default: "unique-id")
Renders arrays as Markdown lists (ordered or unordered).
Options:
ordered(boolean): Create numbered list (default: false)property(string): Extract specific property from objects
Renders data as formatted JSON blocks.
Create a widget by implementing the CalmWidget interface:
// src/widgets/my-widget/index.ts
import { CalmWidget } from '@finos/calm-widgets';
export interface MyWidgetContext {
title: string;
items: string[];
}
export interface MyWidgetOptions {
showCount?: boolean;
prefix?: string;
}
export interface MyWidgetViewModel {
title: string;
items: string[];
count?: number;
prefix: string;
}
export const MyWidget: CalmWidget<
MyWidgetContext,
MyWidgetOptions,
MyWidgetViewModel
> = {
id: 'my-widget',
templatePartial: 'my-widget-template.html',
// Optional: additional template partials
partials: ['item-template.html'],
// Transform input data to view model
transformToViewModel: (context, options) => {
const showCount = options?.hash?.showCount ?? false;
const prefix = options?.hash?.prefix ?? '•';
return {
title: context.title,
items: context.items,
count: showCount ? context.items.length : undefined,
prefix
};
},
// Validate input context
validateContext: (context): context is MyWidgetContext => {
return (
typeof context === 'object' &&
context !== null &&
typeof (context as any).title === 'string' &&
Array.isArray((context as any).items) &&
(context as any).items.every((item: any) => typeof item === 'string')
);
},
// Optional: register custom helpers
registerHelpers: () => ({
upperCase: (str: string) => str.toUpperCase(),
repeat: (str: string, count: number) => str.repeat(count)
})
};Create Handlebars templates for your widget:
Create comprehensive tests for your widget:
// src/widgets/my-widget/index.spec.ts
import { describe, it, expect } from 'vitest';
import { MyWidget } from './index';
describe('MyWidget', () => {
describe('validateContext', () => {
it('accepts valid context', () => {
const context = {
title: 'Test Title',
items: ['item1', 'item2']
};
expect(MyWidget.validateContext(context)).toBe(true);
});
it('rejects invalid context', () => {
expect(MyWidget.validateContext(null)).toBe(false);
expect(MyWidget.validateContext({ title: 123 })).toBe(false);
});
});
describe('transformToViewModel', () => {
it('transforms context correctly', () => {
const context = { title: 'Test', items: ['a', 'b'] };
const options = { hash: { showCount: true, prefix: '-' } };
const result = MyWidget.transformToViewModel!(context, options);
expect(result).toEqual({
title: 'Test',
items: ['a', 'b'],
count: 2,
prefix: '-'
});
});
});
});Create test fixtures to verify widget output:
// test-fixtures/my-widget/basic-example/context.json
{
"title": "My Items",
"items": ["First Item", "Second Item", "Third Item"]
}<!-- test-fixtures/my-widget/basic-example/expected.md -->
## My Items
*Total items: 3*
→ FIRST ITEM
→ SECOND ITEM
→ THIRD ITEMAdd your widget to the engine:
import { MyWidget } from './widgets/my-widget';
// Register individual widget
engine.setupWidgets([{
widget: MyWidget,
folder: __dirname + '/widgets/my-widget'
}]);
// Or extend registerDefaultWidgets
class MyWidgetEngine extends WidgetEngine {
registerDefaultWidgets() {
super.registerDefaultWidgets();
this.setupWidgets([{
widget: MyWidget,
folder: __dirname + '/widgets/my-widget'
}]);
}
}The framework includes comprehensive testing utilities:
# Run all tests
npm test
# Run specific widget tests
npm test -- my-widget
# Run with coverage
npm run test:coverageUse the fixture system for consistent testing:
import { FixtureLoader } from './test-utils/fixture-loader';
const fixtures = new FixtureLoader();
const { context, template, expected } = fixtures.loadFixture('my-widget', 'basic-example');
const compiledTemplate = handlebars.compile(template);
const result = compiledTemplate(context);
expect(result.trim()).toBe(expected);Use the fixture update script to regenerate expected outputs:
npx tsx src/scripts/update-fixtures.ts- WidgetEngine: Orchestrates widget registration and setup
- WidgetRegistry: Manages widget storage and Handlebars partial registration
- WidgetRenderer: Handles widget rendering with context validation
- Widget Helpers: Global Handlebars helpers available to all widgets
The framework provides built-in helpers:
eq,ne: Equality comparisonslookup: Property accessjson: JSON stringificationkebabToTitleCase: Convert "api-service" → "Api Service"kebabCase: Convert "API Service" → "api-service"isObject,isArray: Type checkingnotEmpty: Check for non-empty valuesor: Logical OR operationscurrentTimestamp,currentDate: Date utilitiesinstanceOf: Constructor name checkingeachInMap: Object iteration
The framework uses TypeScript generics for type-safe widgets:
CalmWidget<TContext, TOptions, TViewModel>TContext: Input data typeTOptions: Handlebars options/parametersTViewModel: Transformed data for template
- Keep widgets focused: Each widget should have a single responsibility
- Validate inputs: Always implement robust
validateContext - Transform data: Use
transformToViewModelto prepare data for templates - Handle errors gracefully: Provide meaningful error messages
- Test thoroughly: Include unit tests and integration fixtures
- Use semantic markup: Generate clean, readable Markdown
- Handle empty data: Gracefully handle missing or empty inputs
- Be consistent: Follow established patterns from built-in widgets
- Optimize performance: Avoid complex logic in templates
- Unit test widget logic: Test
validateContextandtransformToViewModel - Integration test output: Use fixtures to verify rendered output
- Test edge cases: Handle null, undefined, and malformed data
- Maintain fixtures: Keep expected outputs up to date
- Create your widget following the structure above
- Add comprehensive tests including fixtures
- Update documentation if adding new concepts
- Follow code style using the project's ESLint configuration
- Test thoroughly with
npm test