Skip to content

feat(dashboards-ng): support widgets as standalone component#1036

Merged
spike-rabbit merged 1 commit intomainfrom
feat/dashboards/support-standalone-widgets
Dec 19, 2025
Merged

feat(dashboards-ng): support widgets as standalone component#1036
spike-rabbit merged 1 commit intomainfrom
feat/dashboards/support-standalone-widgets

Conversation

@chintankavathia
Copy link
Member

@chintankavathia chintankavathia commented Nov 18, 2025

Support widgets as standalone Angular components. This change maintains backward compatibility with existing module-based widgets while enabling modern standalone component architecture.

Usage:

For standalone components, use componentLoader in your widget descriptor:

const loaderFunction = async (name: string): Promise<any> => {
  if (name === 'MyWidgetComponent' || name === 'MyWidgetEditorComponent') {
    return import('./index').then(m => m[name as keyof typeof m]);
  }
  throw new Error(`Unknown component: ${name}`);
};

export const MY_WIDGET: Widget = {
  name: 'My Widget',
  id: '@myapp/my-widget',
  componentFactory: {
    componentName: 'MyWidgetComponent',
    editorComponentName: 'MyWidgetEditorComponent',
    componentLoader: loaderFunction
  },
  ...
};

Summary by CodeRabbit

  • New Features

    • Added support for standalone components in widget definitions, enabling developers to provision widgets without module wrappers.
  • Refactor

    • Updated widget loading architecture to support both module-based and standalone component provisioning strategies.
  • Documentation

    • Updated widget development guidelines to document dual provisioning approaches and updated requirements.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Walkthrough

The PR migrates the dashboards widget system from module-based loading to dual provisioning, supporting both Angular modules and standalone components. Modules are removed, widget descriptors are refactored to use component loaders, the loader service is updated with conditional routing logic, and type definitions enforce mutually exclusive configurations.

Changes

Cohort / File(s) Summary
Charts Widget Module Removal
projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts, projects/dashboards-demo/src/app/widgets/charts/index.ts, projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts
Deleted ChartsWidgetModule; removed module re-export from barrel; updated widget descriptors to use async componentLoader with dynamic import().then(m => m[name]) pattern instead of moduleName/moduleLoader
Contact Widget Module Removal
projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts, projects/dashboards-demo/src/app/widgets/contact-widget/index.ts, projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts
Deleted ContactWidgetModule; removed module re-export from barrel; refactored loader function to async with specific named export resolution for componentLoader
Hello Widget Module Removal
projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts, projects/dashboards-demo/src/app/widgets/hello-widget/index.ts, projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts
Deleted HelloWidgetModule; removed module re-export from barrel; updated loader to async with relative import path and named export extraction for componentLoader
Type System Updates
projects/dashboards-ng/src/model/widgets.model.ts
Added Type import from @angular/core; introduced WidgetComponentTypeBaseConfig, ModuleOptions (with moduleName/moduleLoader), and StandaloneOptions (with componentLoader); updated WidgetComponentTypeFactory as union to enforce mutually exclusive configurations
Widget Loader Refactoring
projects/dashboards-ng/src/widget-loader.ts
Added loadModuleBasedComponent (module import, ngModuleRef creation, component instantiation) and loadStandaloneComponent (direct component instantiation); refactored loadAndAttachComponent to route conditionally based on moduleName vs componentLoader presence
Test Infrastructure
projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts, projects/dashboards-ng/test/test-widget/test-widget.ts
Converted test suite to data-driven variant loop for TEST_WIDGET and TEST_WIDGET_STANDALONE; added TEST_WIDGET_STANDALONE with loaderFunctionStandalone and componentLoader; each variant has isolated scaffolding
Documentation
projects/dashboards-ng/README.md
Updated widget provisioning to support both Angular modules and standalone components; clarified dual loader provisioning (moduleName/moduleLoader OR componentLoader); strengthened interface requirement language

Sequence Diagram(s)

sequenceDiagram
    participant WidgetHost
    participant Loader as Widget Loader Service
    participant ModuleLoader as Module Loader
    participant ComponentLoader as Component Loader
    participant ModuleRef as NgModuleRef
    participant CompFactory as Component Factory

    WidgetHost->>Loader: loadAndAttachComponent(config)
    alt Has moduleName
        Loader->>ModuleLoader: moduleLoader()
        ModuleLoader->>ModuleRef: createNgModuleRef()
        ModuleRef->>CompFactory: componentFactoryResolver.get(component)
        CompFactory-->>Loader: ComponentRef
    else Has componentLoader
        Loader->>ComponentLoader: componentLoader(name)
        ComponentLoader-->>Loader: ComponentClass
        Loader->>CompFactory: createComponent(ComponentClass)
        CompFactory-->>Loader: ComponentRef
    else Error
        Loader-->>WidgetHost: Invalid factory config
    end
    Loader-->>WidgetHost: ComponentRef (mounted)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

  • Type union enforcement: Verify ModuleOptions and StandaloneOptions mutual exclusivity is correctly enforced across all widget definitions
  • Conditional loading paths: Ensure both loadModuleBasedComponent and loadStandaloneComponent handle error cases, edge cases, and async/await semantics correctly
  • Widget descriptor consistency: Confirm all descriptor migrations (charts, contact, hello) follow the same pattern and loader function signatures
  • Test coverage: Validate that data-driven test harness adequately exercises both module-based and standalone variants
  • Dynamic import resolution: Check that componentLoader functions correctly extract named exports via .then(m => m[name as keyof typeof m]) pattern

Suggested reviewers

  • timowolf
  • spike-rabbit
  • fh1ch

Poem

🐰 Modules removed, standalone now shines,
Dual-path loading: components intertwine,
Type unions guide which route to take,
Loaders divide—no breaks to make!
Widgets evolve, freedom in the line!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature addition: enabling widgets to work as standalone Angular components, which aligns with the core change throughout the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dashboards/support-standalone-widgets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chintankavathia chintankavathia force-pushed the feat/dashboards/support-standalone-widgets branch 4 times, most recently from 35c191a to df66eba Compare November 18, 2025 05:09
@github-actions
Copy link

github-actions bot commented Nov 18, 2025

@chintankavathia chintankavathia marked this pull request as ready for review November 18, 2025 05:47
@chintankavathia chintankavathia requested a review from a team as a code owner November 18, 2025 05:47
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (1)

7-13: Migration to async component loading looks good.

The loader function correctly implements dynamic component loading with appropriate error handling.

Consider two optional improvements:

  1. Type safety: Replace Promise<any> with a more specific return type to preserve type information.

  2. Code reuse: This loader pattern is duplicated in contact-descriptors.ts and charts/widget-descriptors.ts. Consider extracting a shared factory function:

function createComponentLoader(modulePath: string, allowedComponents: string[]) {
  return async (name: string): Promise<any> => {
    if (allowedComponents.includes(name)) {
      return import(modulePath).then(m => m[name as keyof typeof m]);
    }
    throw new Error(`Unknown component to be loaded ${name}`);
  };
}

const loaderFunction = createComponentLoader('./index', [
  'HelloWidgetComponent',
  'HelloWidgetEditorComponent'
]);
projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (1)

7-13: Duplicate loader pattern detected.

This loader function is nearly identical to the one in hello-widget/widget-descriptors.ts (lines 7-13). Consider the shared factory approach suggested in that file's review to eliminate this duplication.

projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (1)

7-21: Loader function follows established pattern but scales poorly.

The implementation is correct, but this file demonstrates why the duplicated loader pattern creates maintenance overhead - the component list must be manually kept in sync with exports.

The shared factory approach suggested in hello-widget/widget-descriptors.ts would be especially beneficial here given the larger number of components to maintain.

projects/dashboards-ng/src/model/widgets.model.ts (1)

5-5: Widget factory typing for module vs standalone looks consistent; consider tightening componentLoader typing

The new WidgetComponentTypeBaseConfig + ModuleOptions/StandaloneOptions union correctly enforces mutual exclusivity of moduleLoader and componentLoader and matches the loader logic. You could optionally:

  • Narrow componentLoader’s return type away from Promise<Type<unknown>>/any toward a more specific generic (e.g., Type<WidgetInstance | WidgetInstanceEditor>) to improve editor/compile‑time help.
  • Reconsider the [index: string]: any on the base config if you want stronger type safety for future consumers; right now it hides many mistakes at compile time.

These are refinements rather than blockers; the current shape is functionally fine.

Also applies to: 59-79

projects/dashboards-ng/src/widget-loader.ts (1)

117-151: Add a defensive guard when neither moduleLoader nor componentLoader is defined

The fallback const loader = factory.moduleLoader ?? factory.componentLoader; is correct for well‑typed factories, but if a consumer misconfigures a widget (e.g. JS usage or any casts), loader can be undefined, causing a hard runtime error when invoked instead of a clear message.

You can cheaply harden this with a guard and slightly simplify error formatting:

-  if (factory[componentName]) {
-    const loader = factory.moduleLoader ?? factory.componentLoader;
-    loader(factory[componentName]!).then(
+  if (factory[componentName]) {
+    const loader = factory.moduleLoader ?? factory.componentLoader;
+    if (!loader) {
+      result.error(
+        `Provided component factory has neither moduleLoader nor componentLoader configured`
+      );
+      return result;
+    }
+    loader(factory[componentName]!).then(
@@
-        const msg = rejection
-          ? `${errorMsg} with ${JSON.stringify(rejection.toString())}`
-          : `${errorMsg}`;
+        const msg = rejection
+          ? `${errorMsg} with ${String(rejection)}`
+          : errorMsg;

Functionality stays the same for valid configs but failure modes become clearer and less brittle.

Also applies to: 137-144

projects/dashboards-ng/README.md (1)

167-175: Widget development section matches implementation; consider adding a tiny example

The description that widgets “have to be provided by an Angular module or as standalone” and that you “must provide either a module loader function with module name or a component loader function for standalone components” is in line with the new WidgetComponentFactory/loader behavior. For extra clarity, you could add a short module‑based vs standalone componentFactory snippet so users see both concrete shapes immediately.

projects/dashboards-ng/test/test-widget/test-widget.ts (1)

15-23: Standalone loader aligns with new componentLoader contract; type could be made more specific

loaderFunctionStandalone correctly returns the concrete component export and matches the componentLoader(componentName: string) => Promise<...> usage in widget-loader.ts. You could optionally tighten the signature from Promise<any> to the same Type<unknown> (or a more specific generic) used in StandaloneOptions to keep type information consistent end‑to‑end.

projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)

35-160: Nice parametric tests over both widget variants; decouple config from TEST_WIDGET id

Looping over { widget, name } to run the same suite for TEST_WIDGET and TEST_WIDGET_STANDALONE is a good way to guarantee behavior parity between module‑based and standalone widgets. The only small coupling is that TEST_WIDGET_CONFIG_0.widgetId is hard‑coded to TEST_WIDGET.id, so the standalone tests rely on both widgets sharing the same id string.

You could make the tests more future‑proof by overriding widgetId per variant when setting up the input:

-        gridService.widgetCatalog.set([widget]);
-        fixture.componentRef.setInput('widgetConfig', TEST_WIDGET_CONFIG_0);
+        gridService.widgetCatalog.set([widget]);
+        fixture.componentRef.setInput('widgetConfig', {
+          ...TEST_WIDGET_CONFIG_0,
+          widgetId: widget.id
+        });

That way, the tests stay correct even if you later give the standalone widget a distinct id.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5231513 and df66eba.

📒 Files selected for processing (14)
  • projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/charts/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (9 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (2 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (2 hunks)
  • projects/dashboards-ng/README.md (2 hunks)
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (2 hunks)
  • projects/dashboards-ng/src/model/widgets.model.ts (2 hunks)
  • projects/dashboards-ng/src/widget-loader.ts (1 hunks)
  • projects/dashboards-ng/test/test-widget/test-widget.ts (2 hunks)
💤 Files with no reviewable changes (6)
  • projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/charts/index.ts
  • projects/dashboards-demo/src/app/widgets/hello-widget/index.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/index.ts
  • projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts
🧰 Additional context used
🧬 Code graph analysis (2)
projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)
projects/dashboards-ng/test/test-widget/test-widget.ts (3)
  • TEST_WIDGET (25-43)
  • TEST_WIDGET_STANDALONE (45-62)
  • TEST_WIDGET_CONFIG_0 (64-74)
projects/dashboards-ng/test/test-widget/test-widget.ts (1)
projects/dashboards-ng/src/model/widgets.model.ts (1)
  • Widget (16-44)
🔇 Additional comments (6)
projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (1)

20-25: LGTM! ComponentFactory correctly migrated to standalone pattern.

The switch from moduleName/moduleLoader to componentLoader aligns with the PR's objective to support widgets as standalone components.

projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (1)

20-24: ComponentFactory migration looks correct.

The transition to componentLoader maintains consistency with the standalone component pattern.

projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (1)

23-179: All widget descriptors correctly migrated to componentLoader.

All seven widget descriptors (LINE_CHART_DESC, BAR_CHART_DESC, CIRCLE_CHART_DESC, GAUGE_CHART_DESC, VALUE_WIDGET, LIST_WIDGET, TIMELINE_WIDGET) have been consistently updated to use componentLoader, maintaining the standalone component pattern across the board.

projects/dashboards-ng/README.md (1)

19-19: Docs note about standalone and modules is accurate

“The library supports standalone components and modules.” correctly reflects the current API surface; no change needed.

projects/dashboards-ng/test/test-widget/test-widget.ts (1)

45-62: TEST_WIDGET_STANDALONE definition mirrors TEST_WIDGET and looks correct

The standalone test widget reuses the same id, defaults, and payload as TEST_WIDGET while switching to componentLoader: loaderFunctionStandalone, which is exactly what you need to exercise both loading paths without duplicating test data. No issues from a test perspective.

projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)

17-21: Import of TEST_WIDGET_STANDALONE is consistent with new test widget

Adding TEST_WIDGET_STANDALONE alongside TEST_WIDGET and TEST_WIDGET_CONFIG_0 is consistent with the new standalone widget definition and keeps all test fixtures in one place.

@chintankavathia chintankavathia force-pushed the feat/dashboards/support-standalone-widgets branch from df66eba to 4f86474 Compare November 18, 2025 12:57
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (1)

7-21: Consider simplifying the async function and addressing code duplication.

The async function uses .then() instead of await, which is less idiomatic. Additionally, this pattern is duplicated in test-widget.ts (loaderFunctionStandalone). Consider:

  1. Using await for cleaner async code
  2. Creating a shared utility function to reduce duplication

Apply this diff to simplify the function:

-const loaderFunction = async (name: string): Promise<any> => {
+const loaderFunction = async (name: string): Promise<any> => {
   if (
     name === 'CircleComponent' ||
     name === 'CartesianComponent' ||
     name === 'GaugeComponent' ||
     name === 'ValueWidgetComponent' ||
     name === 'ValueWidgetEditorComponent' ||
     name === 'ListWidgetComponent' ||
     name === 'TimelineWidgetComponent'
   ) {
-    return import('../../widgets/charts/index').then(m => m[name as keyof typeof m]);
+    const m = await import('../../widgets/charts/index');
+    return m[name as keyof typeof m];
   } else {
     throw new Error(`Unknown component to be loaded ${name}`);
   }
 };
projects/dashboards-ng/test/test-widget/test-widget.ts (1)

15-23: Code duplication with widget-descriptors.ts loaderFunction.

The loaderFunctionStandalone follows the same pattern as the loaderFunction in projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts. While the duplication is currently limited to test code, consider creating a shared utility function if this pattern is used in additional places to improve maintainability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df66eba and 4f86474.

📒 Files selected for processing (14)
  • projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/charts/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (9 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (2 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (2 hunks)
  • projects/dashboards-ng/README.md (2 hunks)
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (2 hunks)
  • projects/dashboards-ng/src/model/widgets.model.ts (2 hunks)
  • projects/dashboards-ng/src/widget-loader.ts (1 hunks)
  • projects/dashboards-ng/test/test-widget/test-widget.ts (2 hunks)
💤 Files with no reviewable changes (6)
  • projects/dashboards-demo/src/app/widgets/charts/index.ts
  • projects/dashboards-demo/src/app/widgets/hello-widget/index.ts
  • projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/index.ts
  • projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts
  • projects/dashboards-ng/src/widget-loader.ts
  • projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts
  • projects/dashboards-ng/src/model/widgets.model.ts
🧰 Additional context used
🧬 Code graph analysis (2)
projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)
projects/dashboards-ng/test/test-widget/test-widget.ts (3)
  • TEST_WIDGET (25-43)
  • TEST_WIDGET_STANDALONE (45-62)
  • TEST_WIDGET_CONFIG_0 (64-74)
projects/dashboards-ng/test/test-widget/test-widget.ts (1)
projects/dashboards-ng/src/model/widgets.model.ts (1)
  • Widget (16-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
projects/dashboards-ng/README.md (2)

19-19: LGTM!

The updated documentation clearly states that the library supports both standalone components and modules, which accurately reflects the PR changes.


167-174: Documentation accurately reflects type definitions.

The type system enforces the documented requirement perfectly. WidgetComponentTypeFactory uses a discriminated union of ModuleOptions | StandaloneOptions, where each option forbids the other's fields via ?: never. This prevents providing both loaders simultaneously and guarantees compliance with the documented "either/or" requirement.

projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (1)

23-179: LGTM!

The widget descriptors have been consistently migrated from module-based loading (moduleName + moduleLoader) to component-based loading (componentLoader). The pattern is uniform across all seven descriptors, maintaining backward compatibility with existing descriptor properties.

projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)

35-160: Excellent test coverage for both widget loading strategies!

The test suite has been properly refactored to use a data-driven approach that validates both module-based (TEST_WIDGET) and standalone (TEST_WIDGET_STANDALONE) widget loading. All test cases are executed for both variants, ensuring backward compatibility while validating the new standalone component support.

projects/dashboards-ng/test/test-widget/test-widget.ts (1)

45-62: LGTM!

The TEST_WIDGET_STANDALONE provides essential test infrastructure for validating standalone component loading. The descriptor correctly uses componentLoader instead of moduleName/moduleLoader, enabling the test suite to verify both loading strategies work correctly.

@chintankavathia chintankavathia force-pushed the feat/dashboards/support-standalone-widgets branch from 4f86474 to c2692ad Compare December 17, 2025 05:59
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (1)

7-13: Add runtime validation for the imported export.

The dynamic export selection m[name as keyof typeof m] will return undefined if the export doesn't exist in the module, bypassing the error handling on line 11. This could cause cryptic failures downstream when the widget loader tries to instantiate undefined as a component.

Apply this diff to add validation:

-const loaderFunction = async (name: string): Promise<any> => {
+const loaderFunction = async (name: string): Promise<any> => {
   if (name === 'HelloWidgetComponent' || name === 'HelloWidgetEditorComponent') {
-    return import('./index').then(m => m[name as keyof typeof m]);
+    return import('./index').then(m => {
+      const component = m[name as keyof typeof m];
+      if (!component) {
+        throw new Error(`Component ${name} not found in module exports`);
+      }
+      return component;
+    });
   } else {
     throw new Error(`Unknown component to be loaded ${name}`);
   }
 };
🧹 Nitpick comments (6)
projects/dashboards-ng/README.md (1)

167-174: Add example code for standalone widget development.

The documentation now correctly mentions that widgets can be provided as standalone components with a component loader function, but unlike the setup sections (which show both standalone and module examples with code), the widget development guidance lacks a concrete example of the standalone pattern.

Consider adding a code example demonstrating:

  • The component loader function signature and how it imports/returns named components
  • A sample widget descriptor using componentFactory, componentName, editorComponentName, and componentLoader

This would parallel the clarity of the existing setup examples and help developers understand the new pattern.

Example addition after line 174:

// Standalone widget loader example
export const myWidgetLoader: WidgetComponentLoader = async () => ({
  MyWidgetComponent: (await import('./my-widget.component')).MyWidgetComponent,
  MyWidgetEditorComponent: (await import('./my-widget-editor.component')).MyWidgetEditorComponent,
});

// Widget descriptor
const myWidget: Widget = {
  id: 'my-widget',
  title: 'My Widget',
  componentFactory: {
    componentName: 'MyWidgetComponent',
    editorComponentName: 'MyWidgetEditorComponent',
    componentLoader: myWidgetLoader,
  },
};
projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (1)

7-21: Consider using await for idiomatic async/await pattern.

The function is declared async but uses .then() for the dynamic import. While this works correctly, using await would be more consistent with modern async/await patterns.

Apply this diff to use await:

-const loaderFunction = async (name: string): Promise<any> => {
+const loaderFunction = async (name: string): Promise<any> => {
   if (
     name === 'CircleComponent' ||
     name === 'CartesianComponent' ||
     name === 'GaugeComponent' ||
     name === 'ValueWidgetComponent' ||
     name === 'ValueWidgetEditorComponent' ||
     name === 'ListWidgetComponent' ||
     name === 'TimelineWidgetComponent'
   ) {
-    return import('../../widgets/charts/index').then(m => m[name as keyof typeof m]);
+    const m = await import('../../widgets/charts/index');
+    return m[name as keyof typeof m];
   } else {
     throw new Error(`Unknown component to be loaded ${name}`);
   }
 };

Optional: Consider stronger return typing.

The return type Promise<any> loses type safety. If the component types are available, consider using a union type of all component classes for better type checking.

projects/dashboards-ng/src/model/widgets.model.ts (1)

59-62: Consider narrowing the index signature for better type safety.

The [index: string]: any; index signature permits arbitrary properties of any type, which weakens type safety. If additional properties are only needed for specific use cases, consider using a more restrictive type or requiring explicit extension.

projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (1)

7-13: Consider improving type safety for the loader function.

The return type Promise<any> is permissive and loses type information. Since the function loads specific component types, consider using a more specific return type (e.g., Promise<Type<any>> if using Angular's Type, or a union of the expected component types) to improve type safety and IDE support.

projects/dashboards-ng/src/widget-loader.ts (2)

128-133: Redundant result.complete() after result.error().

In RxJS, calling error() on a Subject terminates it; the subsequent complete() call is a no-op. Additionally, JSON.stringify(rejection.toString()) double-wraps the string in quotes.

     rejection => {
       const errorMsg = `Loading widget module ${factory.moduleName} failed`;
-      const msg = rejection ? `${errorMsg} with ${JSON.stringify(rejection.toString())}` : errorMsg;
+      const msg = rejection ? `${errorMsg} with ${rejection.toString()}` : errorMsg;
       result.error(msg);
-      result.complete();
     }

153-158: Same issues as loadModuleBasedComponent: redundant completion and double-stringification.

     rejection => {
       const errorMsg = `Loading widget component ${componentKey} failed`;
-      const msg = rejection ? `${errorMsg} with ${JSON.stringify(rejection.toString())}` : errorMsg;
+      const msg = rejection ? `${errorMsg} with ${rejection.toString()}` : errorMsg;
       result.error(msg);
-      result.complete();
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f86474 and c2692ad.

📒 Files selected for processing (14)
  • projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/charts/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (9 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (2 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/contact-widget/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/index.ts (0 hunks)
  • projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (2 hunks)
  • projects/dashboards-ng/README.md (2 hunks)
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (2 hunks)
  • projects/dashboards-ng/src/model/widgets.model.ts (2 hunks)
  • projects/dashboards-ng/src/widget-loader.ts (2 hunks)
  • projects/dashboards-ng/test/test-widget/test-widget.ts (2 hunks)
💤 Files with no reviewable changes (6)
  • projects/dashboards-demo/src/app/widgets/hello-widget/index.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/index.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/charts/charts-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/hello-widget/hello-widget.module.ts
  • projects/dashboards-demo/src/app/widgets/charts/index.ts
🧰 Additional context used
🧠 Learnings (13)
📚 Learning: 2025-12-17T04:34:55.597Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1173
File: src/app/examples/si-charts/interactive/interactive.ts:5-5
Timestamp: 2025-12-17T04:34:55.597Z
Learning: Guideline: In Angular projects using OnPush change detection (zoneless mode), you only need to call ChangeDetectorRef.markForCheck() for asynchronous operations (e.g., setTimeout, setInterval, Promises, Observables) where a change is not automatically detected. Event handlers triggered by Angular template bindings (click, input, etc.) will trigger change detection automatically and do not require explicit markForCheck() calls. Apply this broadly to TS files within Angular components/services that use OnPush; use markForCheck() selectively for async work where change detection wouldn’t run otherwise.

Applied to files:

  • projects/dashboards-ng/src/model/widgets.model.ts
  • projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts
  • projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts
  • projects/dashboards-ng/test/test-widget/test-widget.ts
  • projects/dashboards-ng/src/widget-loader.ts
  • projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-15T07:17:06.981Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1191
File: projects/dashboards-ng/src/components/widget-instance-editor-dialog/si-widget-instance-editor-dialog.component.spec.ts:37-49
Timestamp: 2025-12-15T07:17:06.981Z
Learning: In widget-instance-editor-dialog component tests (projects/dashboards-ng/src/components/widget-instance-editor-dialog/si-widget-instance-editor-dialog.component.spec.ts), real setTimeout waits (e.g., `await new Promise(resolve => setTimeout(resolve, 0))`) must be used instead of `fixture.whenStable()` when testing component initialization. This is likely due to the component's lifecycle behavior or modal initialization timing that fixture.whenStable() cannot properly wait for in zoneless mode.

Applied to files:

  • projects/dashboards-ng/test/test-widget/test-widget.ts
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-15T07:16:32.082Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1191
File: projects/dashboards-ng/src/components/gridstack-wrapper/si-gridstack-wrapper.component.spec.ts:69-80
Timestamp: 2025-12-15T07:16:32.082Z
Learning: In gridstack-wrapper component tests (projects/dashboards-ng/src/components/gridstack-wrapper/si-gridstack-wrapper.component.spec.ts), real setTimeout waits (e.g., `await new Promise(resolve => setTimeout(resolve, 0))`) must be used instead of `fixture.whenStable()` when testing grid item rendering and initialization. This is likely due to gridstack library's initialization timing or lifecycle behavior that fixture.whenStable() cannot properly wait for in zoneless mode.

Applied to files:

  • projects/dashboards-ng/test/test-widget/test-widget.ts
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-15T07:16:53.762Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1191
File: projects/dashboards-ng/src/components/gridstack-wrapper/si-gridstack-wrapper.component.spec.ts:92-105
Timestamp: 2025-12-15T07:16:53.762Z
Learning: In gridstack-wrapper component tests (projects/dashboards-ng/src/components/gridstack-wrapper/si-gridstack-wrapper.component.spec.ts), real setTimeout waits must be used instead of fixture.whenStable() to avoid injector destroyed errors during mount/unmount operations in zoneless mode. The GridStack timing-dependent operations require actual async delays.

Applied to files:

  • projects/dashboards-ng/test/test-widget/test-widget.ts
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-04T11:54:31.132Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1133
File: projects/element-ng/tour/si-tour.service.spec.ts:21-26
Timestamp: 2025-12-04T11:54:31.132Z
Learning: In the siemens/element repository, all components are standalone by default and do not require the explicit `standalone: true` flag. Components should be added to the `imports` array in TestBed configuration, not the `declarations` array.

Applied to files:

  • projects/dashboards-ng/test/test-widget/test-widget.ts
  • projects/dashboards-ng/README.md
  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-09T14:19:34.846Z
Learnt from: Killusions
Repo: siemens/element PR: 967
File: package.json:153-153
Timestamp: 2025-12-09T14:19:34.846Z
Learning: In library projects like element-ng, optional features that are provided through callback/function parameters (e.g., syntaxHighlighter in markdown renderer) should not have their implementation dependencies (e.g., highlight.js) listed in the library's dependencies. These should remain in devDependencies for demo/example purposes, and consuming applications should install them directly if they choose to use those features.

Applied to files:

  • projects/dashboards-ng/README.md
📚 Learning: 2025-12-11T10:09:01.564Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1151
File: projects/element-ng/auto-collapsable-list/si-auto-collapsable-list.directive.spec.ts:83-104
Timestamp: 2025-12-11T10:09:01.564Z
Learning: In the auto-collapsable-list directive tests (projects/element-ng/auto-collapsable-list/si-auto-collapsable-list.directive.spec.ts), jasmine.clock() successfully controls the directive's setTimeout-based layout logic in zoneless mode. Mock timers work fine here, unlike in select component overflow tests where real setTimeout waits are required.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-08T11:25:51.584Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1163
File: projects/element-ng/select/si-select.component.spec.ts:511-516
Timestamp: 2025-12-08T11:25:51.584Z
Learning: In the select component overflow tests (projects/element-ng/select/si-select.component.spec.ts), jasmine.clock() cannot be used to control the timing for overflow item detection. Real setTimeout waits must be used instead, likely due to ResizeObserver or debounced layout calculations that jasmine.clock() cannot control in zoneless mode.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-05T08:00:38.407Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1151
File: projects/element-ng/typeahead/si-typeahead.directive.spec.ts:114-121
Timestamp: 2025-12-05T08:00:38.407Z
Learning: In the typeahead test suite (projects/element-ng/typeahead/si-typeahead.directive.spec.ts), jasmine.clock() must be installed in beforeAll and uninstalled in afterAll (not beforeEach/afterEach) because using per-test clock installation causes schedulerFunction-related issues in these specific tests.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-01T04:01:27.365Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1099
File: projects/element-ng/accordion/si-accordion.component.spec.ts:39-43
Timestamp: 2025-12-01T04:01:27.365Z
Learning: With Angular 20 and esbuild, synchronous `TestBed.configureTestingModule(...).compileComponents()` calls work correctly even when components use external templates (templateUrl), because esbuild pre-compiles templates at build time. The async/await pattern is not required in this scenario.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-04T05:50:17.637Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1135
File: projects/element-ng/tree-view/si-tree-view.component.spec.ts:953-964
Timestamp: 2025-12-04T05:50:17.637Z
Learning: In the siemens/element repository, when making tree-view tests zoneless-ready, jasmine.clock() should be used to mimic setTimeouts even in zoneless mode, as the component implementation relies on setTimeout calls that need to be controlled in tests.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-04T05:50:38.735Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1135
File: projects/element-ng/tree-view/si-tree-view.component.spec.ts:1244-1245
Timestamp: 2025-12-04T05:50:38.735Z
Learning: In zoneless Angular tests with OnPush components, calling `await runOnPushChangeDetection(fixture)` followed by `await fixture.whenStable()` is NOT redundant. The `runOnPushChangeDetection` helper forces change detection to run for OnPush components, while `whenStable()` waits for all async operations to complete. Both are necessary and serve different purposes.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
📚 Learning: 2025-12-15T10:05:59.100Z
Learnt from: chintankavathia
Repo: siemens/element PR: 1193
File: projects/element-ng/search-bar/si-search-bar.component.spec.ts:21-28
Timestamp: 2025-12-15T10:05:59.100Z
Learning: In tests that rely on Jasmine's fake clock to control time-based RxJS operators (e.g., debounceTime), use jasmine.clock().install() together with jasmine.clock().mockDate() to enable precise timing control. Without mockDate, scheduler-based operators may not respond to the fake clock. Apply this guidance to spec files (e.g., any file named *.spec.ts or *.spec.js) that test time-sensitive behavior, ensuring you install and mockDate before advancing time in tests.

Applied to files:

  • projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts
🧬 Code graph analysis (3)
projects/dashboards-ng/test/test-widget/test-widget.ts (1)
projects/dashboards-ng/src/model/widgets.model.ts (1)
  • Widget (16-44)
projects/dashboards-ng/src/widget-loader.ts (1)
projects/dashboards-ng/src/model/widgets.model.ts (1)
  • WidgetComponentTypeFactory (78-78)
projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)
projects/dashboards-ng/test/test-widget/test-widget.ts (3)
  • TEST_WIDGET (25-43)
  • TEST_WIDGET_STANDALONE (45-62)
  • TEST_WIDGET_CONFIG_0 (64-74)
🔇 Additional comments (7)
projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts (1)

23-179: LGTM! Widget descriptors successfully migrated to componentLoader pattern.

All seven widget descriptors have been correctly updated to use componentLoader instead of the previous moduleName/moduleLoader approach. The component names referenced in each descriptor match those validated in the loaderFunction, and the VALUE_WIDGET properly specifies both the widget and editor components.

This migration aligns perfectly with the PR objectives of supporting standalone components while maintaining backward compatibility.

projects/dashboards-ng/src/model/widgets.model.ts (1)

73-73: Verify componentLoader signature matches intended design.

The current signature includes a componentName parameter: <T = unknown>(componentName: string) => Promise<Type<T>>. A past review comment suggested a simpler signature without parameters: () => Promise<Type<unknown>>. The current implementation in test-widget.ts (line 15-23) uses the componentName parameter, so this appears intentional. However, please confirm this signature change was deliberate.

projects/dashboards-ng/test/test-widget/test-widget.ts (1)

45-62: Verify that duplicate widget IDs won't cause catalog conflicts.

Both TEST_WIDGET (line 27) and TEST_WIDGET_STANDALONE share the same id value '@siemens/dashboards-ng/TestWidgetComponent'. If both widgets are registered in the widget catalog simultaneously, this could cause key conflicts or unexpected behavior. The parameterized tests in si-widget-host.component.spec.ts appear to use one variant at a time, but verify this design is intentional and won't cause issues in other test scenarios.

projects/dashboards-ng/src/components/widget-host/si-widget-host.component.spec.ts (1)

35-160: Excellent test refactoring for dual-variant coverage.

The parameterized test structure effectively eliminates duplication while ensuring both module-based (TEST_WIDGET) and standalone (TEST_WIDGET_STANDALONE) widgets are tested consistently. The per-variant setup maintains proper test isolation, and the shared test logic follows the DRY principle without sacrificing readability.

projects/dashboards-demo/src/app/widgets/contact-widget/contact-descriptors.ts (1)

20-24: Migration to componentLoader pattern is correct and complete.

The shift from module-based loading (moduleName/moduleLoader) to component-based loading (componentLoader) properly supports standalone Angular components. The configuration correctly specifies both component names, and the ./index module exports both ContactWidgetComponent and ContactWidgetEditorComponent as expected. The loaderFunction correctly handles dynamic imports using the component names as keys, with proper error handling for unknown components.

projects/dashboards-demo/src/app/widgets/hello-widget/widget-descriptors.ts (1)

20-25: The implementation correctly supports dynamic loading of standalone components.

The migration to componentLoader is properly implemented:

  • The ./index import path is correct and resolves to the hello-widget directory's index file
  • Both HelloWidgetComponent and HelloWidgetEditorComponent are correctly exported from index.ts
  • The loaderFunction properly handles dynamic imports via the componentLoader pattern
  • The widget-loader service correctly consumes the componentLoader configuration

No changes needed.

projects/dashboards-ng/src/widget-loader.ts (1)

109-160: Good separation of loading strategies into dedicated functions.

The extraction of loadModuleBasedComponent and loadStandaloneComponent improves readability and addresses the prior feedback about excessive conditionals. The pattern is consistent across both loaders.

@chintankavathia chintankavathia force-pushed the feat/dashboards/support-standalone-widgets branch 6 times, most recently from 33b2669 to 0bfd5a1 Compare December 19, 2025 08:05
@chintankavathia chintankavathia force-pushed the feat/dashboards/support-standalone-widgets branch from 0bfd5a1 to ea74260 Compare December 19, 2025 12:55
Copy link
Member

@spike-rabbit spike-rabbit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Support widgets as standalone Angular components. This change maintains backward compatibility with existing module-based widgets while enabling modern standalone component architecture.

Usage:

For standalone components, use `componentLoader` in your widget descriptor:
```ts
const loaderFunction = async (name: string): Promise<any> => {
  if (name === 'MyWidgetComponent' || name === 'MyWidgetEditorComponent') {
    return import('./index').then(m => m[name as keyof typeof m]);
  }
  throw new Error(`Unknown component: ${name}`);
};

export const MY_WIDGET: Widget = {
  name: 'My Widget',
  id: '@myapp/my-widget',
  componentFactory: {
    componentName: 'MyWidgetComponent',
    editorComponentName: 'MyWidgetEditorComponent',
    componentLoader: loaderFunction
  },
  ...
};
```
@spike-rabbit spike-rabbit force-pushed the feat/dashboards/support-standalone-widgets branch from ea74260 to 0ec2f96 Compare December 19, 2025 14:21
@spike-rabbit spike-rabbit enabled auto-merge (rebase) December 19, 2025 14:21
@github-actions
Copy link

Code Coverage

@spike-rabbit spike-rabbit merged commit 5dad1d4 into main Dec 19, 2025
10 checks passed
@spike-rabbit spike-rabbit deleted the feat/dashboards/support-standalone-widgets branch December 19, 2025 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants