Component System
The component system defines how dashboard components are declared, registered, and rendered. The central pattern is ComponentDescriptor -- a declarative, composition-based approach where all component behavior is passed as constructor parameters.
Overview
ComponentDescriptor
ComponentDescriptor is the recommended way to define dashboard components. Instead of creating a subclass of DashboardComponentConfiguration, you create an instance and pass all behavior as parameters:
final statCard = ComponentDescriptor(
schemaType: 'lms.stat_card',
componentKey: 'lms_stat_card',
displayName: 'Stat Card',
description: 'Displays a single metric with trend indicator',
icon: Icons.analytics,
category: lmsCategory,
defaultSize: const Size(2, 2),
minSize: const Size(1, 1),
isResizable: true,
minCellHeight: 100,
// Properties factory -- creates a fresh PropertyCollection
properties: () => (PropertyCollectionBuilder()
..group('content', 'Content')
..string('title', 'Title', defaultValue: 'Metric')
..integer('value', 'Value', defaultValue: 0)
..string('trend', 'Trend', defaultValue: '+0%')
..group('appearance', 'Appearance')
..enumeration('trendDirection', 'Trend Direction',
values: ['up', 'down', 'neutral'],
defaultValue: 'up',
)
).buildCollection(),
// Render callback -- builds the widget from properties
render: (context, props) {
final title = props.stringValue('title') ?? 'Metric';
final value = props.intValue('value') ?? 0;
final trend = props.stringValue('trend') ?? '';
return StatCardWidget(title: title, value: value, trend: trend);
},
// Config panel callback (optional) -- builds the settings UI
configPanel: (context, props) {
return StatCardConfigPanel(properties: props);
},
// Thumbnail callback (optional) -- builds a palette preview
thumbnail: (context) {
return const Icon(Icons.analytics, size: 32);
},
);Constructor Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
schemaType | String | Yes | -- | Unique schema type identifier (e.g., 'lms.stat_card') |
componentKey | String | Yes | -- | Registry key for lookup |
displayName | String | Yes | -- | Human-readable name for the palette |
description | String | No | '' | Longer description for tooltips |
icon | IconData | No | Icons.widgets | Icon for palette and toolbar |
category | ComponentCategory | No | ComponentCategory.general | Category for palette grouping |
isPrimitive | bool | No | false | Whether this is a framework-provided primitive |
defaultSize | Size | No | Size(2, 2) | Default grid size (columns, rows) |
minSize | Size | No | Size(1, 1) | Minimum allowed size |
maxSize | Size | No | null | Maximum allowed size (null = no limit) |
isResizable | bool | No | true | Whether users can resize this component |
minCellHeight | double | No | null | Minimum row height when this component is placed |
properties | PropertiesFactory | Yes | -- | Factory function returning a fresh PropertyCollection |
render | ComponentBuilder | Yes | -- | Build function receiving context and properties |
configPanel | ConfigPanelBuilder | No | null | Config panel builder (null uses auto-generated panel) |
thumbnail | ThumbnailBuilder | No | null | Thumbnail builder for the component palette |
Type Aliases
typedef ComponentBuilder = Widget Function(
BuildContext context,
PropertyCollection properties,
);
typedef PropertiesFactory = PropertyCollection Function();
typedef ConfigPanelBuilder = Widget? Function(
BuildContext context,
PropertyCollection properties,
);
typedef ThumbnailBuilder = Widget? Function(BuildContext context);Default Config Panel
If you do not provide a configPanel callback, the system uses TabbedPropertyCollectionEditor, which automatically generates a tabbed editor UI from the PropertyCollection's group structure. This works well for most components and requires zero custom UI code.
DashboardComponentConfiguration (Base Class)
For advanced cases where you need inheritance or complex initialization, you can extend DashboardComponentConfiguration directly:
abstract class DashboardComponentConfiguration {
final String schemaType;
// Override these getters
String get componentKey;
String get displayName;
String get description;
IconData get icon;
ComponentCategory get category;
bool get isPrimitive;
Size get defaultSize;
Size? get minSize;
Size? get maxSize;
bool get isResizable;
double? get minCellHeight;
// Implement these methods
PropertyCollection createProperties();
Widget build(BuildContext context, PropertyCollection properties);
Widget? buildConfigPanel(BuildContext context, PropertyCollection properties);
Widget? buildThumbnail(BuildContext context);
Map<String, dynamic> toJson();
}In practice, ComponentDescriptor is preferred because it avoids boilerplate. Use the base class only when you need:
- Shared behavior across multiple component types via a common superclass
- Complex initialization logic that does not fit in a single constructor call
- Integration with the framework's
TypeDescriptorsystem for JSON polymorphism
ComponentCategory
Categories organize components in the editor palette:
class ComponentCategory {
final String id; // Unique identifier
final String displayName; // Human-readable name
final IconData? icon; // Optional palette icon
final int sortOrder; // Sort priority (lower = first)
// Built-in categories
static const primitives; // id: 'primitives', sortOrder: 1
static const general; // id: 'general', sortOrder: 999
}Define custom categories for your domain:
const lmsCategory = ComponentCategory(
id: 'lms',
displayName: 'LMS',
icon: Icons.school,
sortOrder: 10,
);
const analyticsCategory = ComponentCategory(
id: 'analytics',
displayName: 'Analytics',
icon: Icons.bar_chart,
sortOrder: 20,
);DashboardDescriptor
A DashboardDescriptor groups related components for a specific product or domain. This follows the same "Descriptor" pattern used by FeatureDescriptor and EntityConfiguration in the Vyuh Framework.
class DashboardDescriptor {
final String name; // Unique group name (e.g., 'LMS', 'IPQC')
final String? description; // Human-readable description
final ComponentCategory? category;
final List<String> tags; // For filtering and searching
final List<DashboardComponentConfiguration> components;
}Registering Descriptors
// Define descriptors for each domain
final lmsDashboard = DashboardDescriptor(
name: 'LMS',
description: 'Learning Management System dashboards',
tags: ['education', 'analytics'],
components: [
statCardDescriptor,
kpiWidgetDescriptor,
enrollmentChartDescriptor,
completionsTableDescriptor,
],
);
// Create a scoped registry for the editor/view that needs these descriptors.
final registry = DashboardEditorRegistry([
PrimitiveComponents.descriptor,
lmsDashboard,
]);DashboardEditorRegistry
The DashboardEditorRegistry is a scoped index of dashboard descriptors and their components. Create it from a list of DashboardDescriptor values, then pass the same registry into DashboardEditorController, DashboardEngine, DashboardView, and any ComponentInstance.withDefaults calls that need to resolve a component key.
class DashboardEditorRegistry {
DashboardEditorRegistry(List<DashboardDescriptor> dashboards);
// Dashboard queries
DashboardDescriptor? getDashboard(String name);
List<DashboardDescriptor> get dashboards;
List<DashboardComponentConfiguration> getComponentsFor(String dashboardName);
bool hasDashboard(String name);
int get dashboardCount;
// Component queries (flat, global)
DashboardComponentConfiguration? get(String componentKey);
List<DashboardComponentConfiguration> getAll();
bool hasComponent(String componentKey);
int get componentCount;
List<DashboardComponentConfiguration> getByCategory(String categoryId);
List<DashboardComponentConfiguration> getPrimitives();
Map<String, List<DashboardComponentConfiguration>> getGroupedByCategory();
// Category access
ComponentCategoryRegistry get categories;
}Lookup Flow
When a ComponentInstance is deserialized or a component needs to be rendered, the system looks up the component by its componentKey:
// ComponentInstance references a component by key
final component = ComponentInstance(
id: 'component-1',
componentKey: 'lms_stat_card', // ← key used for lookup
properties: ...,
);
// The registry resolves the key to a ComponentDescriptor
final descriptor = registry.get('lms_stat_card');
// The descriptor provides properties, rendering, and config panel
final properties = descriptor!.createProperties();
final widget = descriptor.build(context, properties);LMS Example: Complete Component Set
Here is a complete set of LMS dashboard components:
Stat Card Component
final lmsStatCard = ComponentDescriptor(
schemaType: 'lms.stat_card',
componentKey: 'lms_stat_card',
displayName: 'Enrollment Stat',
description: 'Shows a single enrollment metric with trend',
icon: Icons.people,
category: lmsCategory,
minCellHeight: 100,
properties: () => (PropertyCollectionBuilder()
..string('title', 'Title', defaultValue: 'Metric')
..integer('value', 'Value', defaultValue: 0)
..string('trend', 'Trend', defaultValue: '+0%')
..enumeration('trendDirection', 'Direction',
values: ['up', 'down', 'neutral'],
defaultValue: 'up',
)
..string('icon', 'Icon Name', defaultValue: 'people')
).buildCollection(),
render: (context, props) => LmsStatCardView(properties: props),
);KPI Widget Component
final lmsKpiWidget = ComponentDescriptor(
schemaType: 'lms.kpi_widget',
componentKey: 'lms_kpi_widget',
displayName: 'KPI Progress',
description: 'Progress bar showing target vs actual',
icon: Icons.speed,
category: lmsCategory,
minCellHeight: 120,
properties: () => (PropertyCollectionBuilder()
..string('label', 'Label', defaultValue: 'Completion Rate')
..double_('target', 'Target', defaultValue: 100)
..double_('actual', 'Actual', defaultValue: 0)
..string('unit', 'Unit', defaultValue: '%')
..string('color', 'Color', defaultValue: '#2196F3')
).buildCollection(),
render: (context, props) => LmsKpiWidgetView(properties: props),
);Enrollment Chart Component
final lmsEnrollmentChart = ComponentDescriptor(
schemaType: 'lms.enrollment_chart',
componentKey: 'lms_enrollment_chart',
displayName: 'Enrollment Chart',
description: 'Bar chart showing enrollments per course',
icon: Icons.bar_chart,
category: lmsCategory,
minCellHeight: 250,
defaultSize: const Size(4, 3),
properties: () => (PropertyCollectionBuilder()
..group('data', 'Data Source')
..string('endpoint', 'API Endpoint',
defaultValue: '/api/enrollments/by-course',
)
..string('labelField', 'Label Field', defaultValue: 'courseName')
..string('valueField', 'Value Field', defaultValue: 'enrollmentCount')
..group('appearance', 'Appearance')
..enumeration('chartType', 'Chart Type',
values: ['bar', 'groupedBar', 'stackedBar'],
defaultValue: 'bar',
)
..boolean('showLegend', 'Show Legend', defaultValue: true)
).buildCollection(),
render: (context, props) => LmsEnrollmentChartView(properties: props),
);Registering the LMS Suite
final registry = DashboardEditorRegistry([
DashboardDescriptor(
name: 'LMS',
description: 'Learning Management System dashboards',
tags: ['education', 'analytics', 'enrollment'],
components: [
lmsStatCard,
lmsKpiWidget,
lmsEnrollmentChart,
],
),
);Next Steps
- Data Sources -- Connect components to live data
- Chart System -- Understand chart renderers and types
- Custom Components Guide -- Step-by-step component building