Skip to content

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:

dart
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

ParameterTypeRequiredDefaultDescription
schemaTypeStringYes--Unique schema type identifier (e.g., 'lms.stat_card')
componentKeyStringYes--Registry key for lookup
displayNameStringYes--Human-readable name for the palette
descriptionStringNo''Longer description for tooltips
iconIconDataNoIcons.widgetsIcon for palette and toolbar
categoryComponentCategoryNoComponentCategory.generalCategory for palette grouping
isPrimitiveboolNofalseWhether this is a framework-provided primitive
defaultSizeSizeNoSize(2, 2)Default grid size (columns, rows)
minSizeSizeNoSize(1, 1)Minimum allowed size
maxSizeSizeNonullMaximum allowed size (null = no limit)
isResizableboolNotrueWhether users can resize this component
minCellHeightdoubleNonullMinimum row height when this component is placed
propertiesPropertiesFactoryYes--Factory function returning a fresh PropertyCollection
renderComponentBuilderYes--Build function receiving context and properties
configPanelConfigPanelBuilderNonullConfig panel builder (null uses auto-generated panel)
thumbnailThumbnailBuilderNonullThumbnail builder for the component palette

Type Aliases

dart
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:

dart
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 TypeDescriptor system for JSON polymorphism

ComponentCategory

Categories organize components in the editor palette:

dart
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:

dart
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.

dart
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

dart
// 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.

dart
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:

dart
// 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

dart
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

dart
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

dart
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

dart
final registry = DashboardEditorRegistry([
  DashboardDescriptor(
    name: 'LMS',
    description: 'Learning Management System dashboards',
    tags: ['education', 'analytics', 'enrollment'],
    components: [
      lmsStatCard,
      lmsKpiWidget,
      lmsEnrollmentChart,
    ],
  ),
);

Next Steps