Canvas Template Module Pattern
Guide for creating layout template modules using BrowserModuleContract
Canvas Template Module Pattern
Status: ✅ Production Pattern - Use This!
First Implementation: Split Sidebar Template Module
Date: November 23, 2025
🎯 The Breakthrough
We've achieved true meta-modularity for layout templates!
Every layout template is now a full BrowserModuleContract module that:
- ✅ Self-describes via manifest
- ✅ Embeds renderers as components
- ✅ Exposes configuration via public actions
- ✅ Can be AI-generated
- ✅ Is marketplace-ready (export/import)
- ✅ Has exact Flutter/React Native equivalents documented
- ✅ Participates in full lifecycle management
📐 Template Module Structure
File Organization
modules/canvas-templates/{template-name}/
├── src/
│ ├── lib/
│ │ ├── {template-name}-template.module.ts # Main module class
│ │ └── components/
│ │ └── {template-name}-renderer.component.ts # Renderer
│ └── index.ts # Barrel export
├── project.json # NX configuration
├── tsconfig.lib.json # TypeScript config
└── README.md # Documentation
Module Class Pattern
import { Injectable, inject } from '@angular/core';
import { Observable, of } from 'rxjs';
import {
BrowserModuleContract,
ModuleManifest,
ModuleDecorator,
ModuleConfiguration
} from '@libs-core/module-base';
import { LoggerService } from '@libs-core/log-system';
@ModuleDecorator({
id: 'template-{name}',
version: '1.0.0',
displayName: '{Name} Template'
})
@Injectable({ providedIn: 'root' })
export class {Name}TemplateModule implements BrowserModuleContract {
private readonly logger = inject(LoggerService).child({ source: '{Name}Template' });
readonly manifest: ModuleManifest = {
id: 'template-{name}',
version: '1.0.0',
displayName: '{Name} Template',
description: '...',
icon: '...',
category: 'layout-template',
tags: ['template', 'layout', ...],
// Platform rendering instructions
metadata: {
type: 'layout-template',
responsive: true,
platforms: {
web: {
technology: 'CSS Grid',
gridColumns: '...',
gridRows: '...',
gridAreas: '...'
},
flutter: {
structure: '...',
widgets: [...]
}
}
},
// Template regions
regions: [
{
name: 'region-name',
displayName: 'Region Display Name',
description: '...',
type: 'module-container',
config: { width: '...', flex: true/false }
}
],
// Public actions API
publicActions: {
configure: (config) => {...},
exportJSON: () => {...},
getRenderingInstructions: (platform) => {...}
},
// Embeddable renderer
embeddableComponents: {
'template-renderer': {
component: {Name}RendererComponent,
inputs: ['regions', 'config'],
outputs: ['regionClicked', 'moduleDropped'],
description: '...'
}
}
};
// Lifecycle hooks
async configure(config: ModuleConfiguration): Promise<void> {}
async initialize(): Promise<void> {}
async start(): Promise<void> {}
async stop(): Promise<void> {}
async destroy(): Promise<void> {}
}
Renderer Component Pattern
import { Component, input, output, computed, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'lib-{name}-renderer',
standalone: true,
imports: [CommonModule],
template: `
<div class="{name}-template" [ngStyle]="containerStyles()">
<!-- Render each region -->
@for (region of regions() | keyvalue; track region.key) {
<div
class="template-region"
[attr.data-region]="region.key"
[ngStyle]="getRegionStyles(region.key)"
(click)="regionClicked.emit(region.key)">
<!-- Region label -->
<div class="region-label">
<span class="region-name">{{ region.value.displayName }}</span>
</div>
<!-- Region content (modules) -->
<div class="region-content">
@if (region.value.modules?.length) {
@for (module of region.value.modules; track module.instanceId) {
<div class="module-instance">{{ module.moduleId }}</div>
}
} @else {
<div class="region-empty">
<p>Drop modules here</p>
</div>
}
</div>
</div>
}
</div>
`,
styles: [`
/* Template-specific styles */
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class {Name}RendererComponent {
readonly regions = input.required<Record<string, TemplateRegion>>();
readonly config = input<TemplateConfig>();
readonly regionClicked = output<string>();
readonly moduleDropped = output<{ regionName: string; moduleId: string }>();
readonly containerStyles = computed(() => {
// Return CSS Grid styles based on config
});
}
✅ Completed Template: Split Sidebar
Location: modules/canvas-templates/split-sidebar
Module ID: template-split-sidebar
Status: ✅ Complete
Features:
- Fixed sidebar (300px, configurable)
- Flexible main content area
- CSS Grid (web):
300px 1fr - Flutter:
Row(children: [Container(width: 300), Expanded()]) - Full manifest with platform instructions
- Embeddable renderer component
- Public configuration API
📋 Remaining Templates to Create
1. Two-Column Equal (template-two-column-equal)
- Grid:
1fr 1fr - Flutter:
Row([Expanded(flex: 1), Expanded(flex: 1)]) - Use Case: Compare views, split editing
2. Three-Column Equal (template-three-column-equal)
- Grid:
1fr 1fr 1fr - Flutter:
Row([Expanded, Expanded, Expanded]) - Use Case: Multi-panel views
3. Header/Content/Footer (template-header-content-footer)
- Grid:
1fr / auto 1fr auto - Flutter:
Column([header, Expanded(content), footer]) - Use Case: Traditional page layout
4. Dashboard (template-dashboard)
- Grid:
250px 1fr 300px / 60px 1fr - Flutter:
Column([header, Expanded(Row([sidebar, main, aside]))]) - Use Case: Complex admin dashboards
5. Custom (template-custom)
- Grid: Configurable
- Flutter: Dynamic based on config
- Use Case: Advanced users, AI generation
🔧 Quick Creation Checklist
For each new template:
- Create module directory:
modules/canvas-templates/{name}/ - Create module class:
{name}-template.module.ts- Add
@ModuleDecorator - Implement
BrowserModuleContract - Define
manifestwith all metadata - Document web + Flutter equivalents
- Add public actions API
- Reference renderer component
- Add
- Create renderer component:
{name}-renderer.component.ts- Standalone component
- CSS Grid styles
- Region rendering logic
- Empty states
- Create
index.tsbarrel export - Create
project.json(NX configuration) - Create
tsconfig.lib.json - Add path mapping to
tsconfig.base.json - Update template registry (when ready)
- Test in canvas builder
🚀 Benefits of This Pattern
1. True Meta-Modularity 🧬
- Templates ARE modules
- Can query: "What templates are available?"
- Can invoke: "Configure this template"
- Can embed: "Render this template here"
2. AI-Generatable 🤖
const aiPrompt = "Create a blog layout template";
const module = await ai.generateModule(aiPrompt);
registry.registerModule(module); // Instantly available!
3. Marketplace-Ready 🏪
const exported = template.publicActions.exportJSON();
await marketplace.publish(exported);
4. Platform-Agnostic 🌍
// Same module works on:
- Web (CSS Grid)
- Flutter (Row/Column/Expanded)
- React Native (flexbox)
5. Discoverable 🔍
const templates = registry.getModulesByCategory('layout-template');
const responsive = templates.filter(t => t.metadata.responsive);
📊 Progress Tracking
| Template | Status | Module ID | Flutter Mapped |
|---|---|---|---|
| Split Sidebar | ✅ Complete | template-split-sidebar |
✅ Yes |
| Two-Column Equal | ⏳ Next | template-two-column-equal |
Pending |
| Three-Column Equal | ⏳ Pending | template-three-column-equal |
Pending |
| Header/Content/Footer | ⏳ Pending | template-header-content-footer |
Pending |
| Dashboard | ⏳ Pending | template-dashboard |
Pending |
| Custom | ⏳ Pending | template-custom |
Pending |
🎯 Next Steps
- ✅ Complete remaining 5 template modules
- ⏳ Create template registry service
- ⏳ Update canvas-builder to query templates from registry
- ⏳ Update layout-selector to show all registered templates
- ⏳ Implement region module containers
- ⏳ Enable drag-drop modules into regions
- ⏳ Add AI template generation
- ⏳ Build marketplace export/import
Last Updated: November 23, 2025 - 3:00 PM
Session ID: 11.25.1.1
Status: 🎯 Active Development - Template Module Pattern Established
Achievement: First template module complete, pattern documented, 5 more to build