@bbq-chat/widgets-angular 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,120 +0,0 @@
1
- import { Type, TemplateRef } from '@angular/core';
2
- import { ChatWidget } from '@bbq-chat/widgets';
3
-
4
- /**
5
- * Context provided to template-based custom widget renderers
6
- */
7
- export interface WidgetTemplateContext {
8
- /**
9
- * The widget instance being rendered
10
- */
11
- $implicit: ChatWidget;
12
-
13
- /**
14
- * The widget instance (alternative access)
15
- */
16
- widget: ChatWidget;
17
-
18
- /**
19
- * Emit a widget action
20
- */
21
- emitAction: (actionName: string, payload: unknown) => void;
22
- }
23
-
24
- /**
25
- * Interface for component-based custom widget renderers
26
- */
27
- export interface CustomWidgetComponent {
28
- /**
29
- * The widget instance to render
30
- */
31
- widget: ChatWidget;
32
-
33
- /**
34
- * Event emitter for widget actions (optional, will be set by the renderer)
35
- */
36
- widgetAction?: (actionName: string, payload: unknown) => void;
37
- }
38
-
39
- /**
40
- * Type for custom widget renderer functions that return HTML strings
41
- */
42
- export type CustomWidgetHtmlRenderer = (widget: ChatWidget) => string;
43
-
44
- /**
45
- * Type for custom widget renderer configurations
46
- */
47
- export type CustomWidgetRenderer =
48
- | CustomWidgetHtmlRenderer
49
- | Type<CustomWidgetComponent>
50
- | TemplateRef<WidgetTemplateContext>;
51
-
52
- /**
53
- * Configuration for registering a custom widget renderer
54
- */
55
- export interface CustomWidgetRendererConfig {
56
- /**
57
- * The widget type identifier
58
- */
59
- type: string;
60
-
61
- /**
62
- * The renderer: can be a function returning HTML, an Angular Component class, or a TemplateRef
63
- */
64
- renderer: CustomWidgetRenderer;
65
- }
66
-
67
- /**
68
- * Type guard to check if a renderer is a TemplateRef
69
- */
70
- export function isTemplateRenderer(
71
- renderer: CustomWidgetRenderer
72
- ): renderer is TemplateRef<WidgetTemplateContext> {
73
- return (
74
- renderer !== null &&
75
- typeof renderer === 'object' &&
76
- 'createEmbeddedView' in renderer
77
- );
78
- }
79
-
80
- /**
81
- * Type guard to check if a renderer is an Angular Component
82
- *
83
- * Note: This uses a heuristic check based on the following assumptions:
84
- * 1. Components are constructor functions
85
- * 2. Components have a prototype with a constructor property
86
- * 3. Components typically use dependency injection (no required constructor params)
87
- *
88
- * Limitation: This may not detect components with required constructor parameters.
89
- * For edge cases, explicitly check your component's constructor signature.
90
- *
91
- * Alternative: You can always register a wrapper component that has no required params.
92
- */
93
- export function isComponentRenderer(
94
- renderer: CustomWidgetRenderer
95
- ): renderer is Type<CustomWidgetComponent> {
96
- // Check if it's a function (constructor) but not a regular function renderer
97
- if (typeof renderer !== 'function') {
98
- return false;
99
- }
100
-
101
- // Check for Angular component characteristics
102
- // Components typically have prototype with constructor property
103
- return (
104
- renderer.prototype !== undefined &&
105
- renderer.prototype.constructor === renderer &&
106
- renderer.length === 0 // Constructor with no required params (Angular DI)
107
- );
108
- }
109
-
110
- /**
111
- * Type guard to check if a renderer is an HTML function
112
- *
113
- * Note: This should be checked AFTER checking for component and template renderers
114
- * since components are also functions but with additional properties.
115
- */
116
- export function isHtmlRenderer(
117
- renderer: CustomWidgetRenderer
118
- ): renderer is CustomWidgetHtmlRenderer {
119
- return typeof renderer === 'function';
120
- }
package/src/index.ts DELETED
@@ -1,65 +0,0 @@
1
- /**
2
- * @bbq-chat/widgets-angular
3
- *
4
- * Angular components and services for BbQ ChatWidgets
5
- *
6
- * This package provides Angular-native components and services that wrap
7
- * the core @bbq-chat/widgets library, making it easy to integrate chat
8
- * widgets into Angular applications.
9
- *
10
- * @packageDocumentation
11
- */
12
-
13
- // Export components
14
- export { WidgetRendererComponent } from './widget-renderer.component';
15
-
16
- // Export services
17
- export { WidgetRegistryService } from './widget-registry.service';
18
-
19
- // Export custom widget renderer types
20
- export type {
21
- CustomWidgetComponent,
22
- CustomWidgetRenderer,
23
- CustomWidgetHtmlRenderer,
24
- CustomWidgetRendererConfig,
25
- WidgetTemplateContext,
26
- } from './custom-widget-renderer.types';
27
-
28
- export {
29
- isHtmlRenderer,
30
- isComponentRenderer,
31
- isTemplateRenderer,
32
- } from './custom-widget-renderer.types';
33
-
34
- // Re-export commonly used types and classes from core package
35
- export {
36
- ChatWidget,
37
- } from '@bbq-chat/widgets';
38
-
39
- export type {
40
- ButtonWidget,
41
- CardWidget,
42
- FormWidget,
43
- InputWidget,
44
- TextAreaWidget,
45
- DropdownWidget,
46
- SliderWidget,
47
- ToggleWidget,
48
- FileUploadWidget,
49
- DatePickerWidget,
50
- MultiSelectWidget,
51
- ProgressBarWidget,
52
- ThemeSwitcherWidget,
53
- ImageWidget,
54
- ImageCollectionWidget,
55
- } from '@bbq-chat/widgets';
56
-
57
- // Re-export utilities
58
- export {
59
- SsrWidgetRenderer,
60
- WidgetEventManager,
61
- customWidgetRegistry,
62
- } from '@bbq-chat/widgets';
63
-
64
- // Version
65
- export const VERSION = '1.0.0';
@@ -1,128 +0,0 @@
1
- import { Injectable } from '@angular/core';
2
- import { customWidgetRegistry, ChatWidget } from '@bbq-chat/widgets';
3
- import { CustomWidgetRenderer } from './custom-widget-renderer.types';
4
-
5
- /**
6
- * Service for registering custom widget factories and renderers
7
- *
8
- * This service provides a centralized way to register custom widget types
9
- * that extend the base widget functionality, including support for
10
- * Angular components and templates as custom renderers.
11
- *
12
- * @example
13
- * ```typescript
14
- * constructor(private widgetRegistry: WidgetRegistryService) {
15
- * // Register a widget factory
16
- * this.widgetRegistry.registerFactory('myWidget', (obj) => {
17
- * if (obj.type === 'myWidget') {
18
- * return new MyCustomWidget(obj.label, obj.action);
19
- * }
20
- * return null;
21
- * });
22
- *
23
- * // Register a component-based renderer
24
- * this.widgetRegistry.registerRenderer('myWidget', MyWidgetComponent);
25
- * }
26
- * ```
27
- */
28
- @Injectable({
29
- providedIn: 'root',
30
- })
31
- export class WidgetRegistryService {
32
- private readonly customRenderers = new Map<string, CustomWidgetRenderer>();
33
- /**
34
- * Register a custom widget factory function
35
- *
36
- * @param type - The widget type identifier
37
- * @param factory - Factory function that creates widget instances from plain objects
38
- */
39
- registerFactory(
40
- type: string,
41
- factory: (obj: unknown) => ChatWidget | null
42
- ): void {
43
- customWidgetRegistry.registerFactory(type, factory);
44
- }
45
-
46
- /**
47
- * Register a widget class with automatic factory creation
48
- *
49
- * @param type - The widget type identifier
50
- * @param ctor - Widget class constructor
51
- */
52
- registerClass(type: string, ctor: any): void {
53
- customWidgetRegistry.registerClass(type, ctor);
54
- }
55
-
56
- /**
57
- * Get a factory for a specific widget type
58
- *
59
- * @param type - The widget type identifier
60
- * @returns The factory function if registered, undefined otherwise
61
- */
62
- getFactory(type: string): ((obj: any) => ChatWidget | null) | undefined {
63
- return customWidgetRegistry.getFactory(type);
64
- }
65
-
66
- /**
67
- * Register a custom renderer for a specific widget type
68
- *
69
- * The renderer can be:
70
- * - A function that returns HTML string
71
- * - An Angular Component class
72
- * - An Angular TemplateRef
73
- *
74
- * @param type - The widget type identifier
75
- * @param renderer - The custom renderer (function, Component, or TemplateRef)
76
- *
77
- * @example
78
- * ```typescript
79
- * // HTML function renderer
80
- * widgetRegistry.registerRenderer('weather', (widget) => `<div>${widget.label}</div>`);
81
- *
82
- * // Component renderer
83
- * widgetRegistry.registerRenderer('weather', WeatherWidgetComponent);
84
- *
85
- * // Template renderer (from @ViewChild or elsewhere)
86
- * widgetRegistry.registerRenderer('weather', this.weatherTemplate);
87
- * ```
88
- */
89
- registerRenderer(type: string, renderer: CustomWidgetRenderer): void {
90
- if (!type || typeof type !== 'string') {
91
- throw new Error('type must be a non-empty string');
92
- }
93
- if (!renderer) {
94
- throw new Error('renderer is required');
95
- }
96
- this.customRenderers.set(type, renderer);
97
- }
98
-
99
- /**
100
- * Get a custom renderer for a specific widget type
101
- *
102
- * @param type - The widget type identifier
103
- * @returns The custom renderer if registered, undefined otherwise
104
- */
105
- getRenderer(type: string): CustomWidgetRenderer | undefined {
106
- return this.customRenderers.get(type);
107
- }
108
-
109
- /**
110
- * Check if a custom renderer is registered for a widget type
111
- *
112
- * @param type - The widget type identifier
113
- * @returns True if a custom renderer is registered, false otherwise
114
- */
115
- hasRenderer(type: string): boolean {
116
- return this.customRenderers.has(type);
117
- }
118
-
119
- /**
120
- * Unregister a custom renderer for a widget type
121
- *
122
- * @param type - The widget type identifier
123
- * @returns True if a renderer was removed, false if none was registered
124
- */
125
- unregisterRenderer(type: string): boolean {
126
- return this.customRenderers.delete(type);
127
- }
128
- }
@@ -1,384 +0,0 @@
1
- import {
2
- Component,
3
- Input,
4
- Output,
5
- EventEmitter,
6
- ElementRef,
7
- AfterViewInit,
8
- OnInit,
9
- OnDestroy,
10
- OnChanges,
11
- SimpleChanges,
12
- ViewChild,
13
- ComponentRef,
14
- EmbeddedViewRef,
15
- TemplateRef,
16
- inject,
17
- Injector,
18
- createComponent,
19
- EnvironmentInjector,
20
- } from '@angular/core';
21
- import { CommonModule } from '@angular/common';
22
- import {
23
- SsrWidgetRenderer,
24
- WidgetEventManager,
25
- ChatWidget,
26
- } from '@bbq-chat/widgets';
27
- import { WidgetRegistryService } from './widget-registry.service';
28
- import {
29
- WidgetTemplateContext,
30
- isHtmlRenderer,
31
- isComponentRenderer,
32
- isTemplateRenderer,
33
- } from './custom-widget-renderer.types';
34
-
35
- /**
36
- * Angular component for rendering chat widgets
37
- *
38
- * This component handles rendering of chat widgets using the BbQ ChatWidgets library.
39
- * It manages widget lifecycle, event handling, and cleanup.
40
- *
41
- * Supports three types of custom widget renderers:
42
- * 1. HTML function renderers (return HTML strings)
43
- * 2. Angular Component renderers (render as dynamic components)
44
- * 3. Angular TemplateRef renderers (render as embedded views)
45
- *
46
- * @example
47
- * ```typescript
48
- * <bbq-widget-renderer
49
- * [widgets]="messageWidgets"
50
- * (widgetAction)="handleWidgetAction($event)">
51
- * </bbq-widget-renderer>
52
- * ```
53
- */
54
- @Component({
55
- selector: 'bbq-widget-renderer',
56
- standalone: true,
57
- imports: [CommonModule],
58
- template: `
59
- <div #widgetContainer class="bbq-widgets-container" (click)="handleClick($event)">
60
- @for (item of widgetItems; track item.index) {
61
- @if (item.isHtml) {
62
- <div class="bbq-widget" [innerHTML]="item.html"></div>
63
- } @else {
64
- <div class="bbq-widget" #dynamicWidget></div>
65
- }
66
- }
67
- </div>
68
- `,
69
- styles: [
70
- `
71
- .bbq-widgets-container {
72
- margin-top: 0.5rem;
73
- }
74
-
75
- .bbq-widget {
76
- margin-bottom: 0.5rem;
77
- }
78
- `,
79
- ],
80
- })
81
- export class WidgetRendererComponent
82
- implements OnInit, AfterViewInit, OnDestroy, OnChanges
83
- {
84
- /**
85
- * Array of widgets to render
86
- */
87
- @Input() widgets: ChatWidget[] | null | undefined;
88
-
89
- /**
90
- * Emits when a widget action is triggered
91
- */
92
- @Output() widgetAction = new EventEmitter<{
93
- actionName: string;
94
- payload: unknown;
95
- }>();
96
-
97
- @ViewChild('widgetContainer', { static: false })
98
- containerRef!: ElementRef<HTMLDivElement>;
99
-
100
- protected widgetItems: Array<{
101
- index: number;
102
- widget: ChatWidget;
103
- isHtml: boolean;
104
- html?: string;
105
- }> = [];
106
-
107
- protected renderer = new SsrWidgetRenderer();
108
- protected eventManager?: WidgetEventManager;
109
- protected isViewInitialized = false;
110
- protected widgetRegistry = inject(WidgetRegistryService);
111
- protected injector = inject(Injector);
112
- protected environmentInjector = inject(EnvironmentInjector);
113
- protected dynamicComponents: Array<ComponentRef<any>> = [];
114
- protected dynamicViews: Array<EmbeddedViewRef<WidgetTemplateContext>> = [];
115
-
116
- ngOnInit() {
117
- this.updateWidgetHtml();
118
- }
119
-
120
- ngOnChanges(changes: SimpleChanges) {
121
- if (changes['widgets']) {
122
- this.updateWidgetHtml();
123
- }
124
- }
125
-
126
- ngAfterViewInit() {
127
- this.isViewInitialized = true;
128
- this.setupEventHandlers();
129
- // Render dynamic components/templates after view init
130
- this.renderDynamicWidgets();
131
- }
132
-
133
- ngOnDestroy() {
134
- this.cleanup();
135
- }
136
-
137
- /**
138
- * Base implementation for updating the rendered HTML for the current widgets.
139
- *
140
- * Subclasses may override this method to customize how widgets are rendered
141
- * (for example, to inject additional markup or perform preprocessing).
142
- *
143
- * Since this is the base implementation, overriding implementations are not
144
- * required to call `super.updateWidgetHtml()`.
145
- */
146
- protected updateWidgetHtml() {
147
- if (!this.widgets || this.widgets.length === 0) {
148
- this.widgetItems = [];
149
- return;
150
- }
151
-
152
- this.widgetItems = this.widgets.map((widget, index) => {
153
- const customRenderer = this.widgetRegistry.getRenderer(widget.type);
154
-
155
- // Check template renderer first (most specific)
156
- if (customRenderer && isTemplateRenderer(customRenderer)) {
157
- return {
158
- index,
159
- widget,
160
- isHtml: false,
161
- };
162
- }
163
-
164
- // Check component renderer second
165
- if (customRenderer && isComponentRenderer(customRenderer)) {
166
- return {
167
- index,
168
- widget,
169
- isHtml: false,
170
- };
171
- }
172
-
173
- // Check HTML function renderer last (most general, matches any function)
174
- if (customRenderer && isHtmlRenderer(customRenderer)) {
175
- return {
176
- index,
177
- widget,
178
- isHtml: true,
179
- html: customRenderer(widget),
180
- };
181
- }
182
-
183
- // Default: render using the BbQ library renderer
184
- return {
185
- index,
186
- widget,
187
- isHtml: true,
188
- html: this.renderer.renderWidget(widget),
189
- };
190
- });
191
-
192
- // After view updates, reinitialize widgets only if view is already initialized
193
- if (this.isViewInitialized) {
194
- setTimeout(() => {
195
- this.setupEventHandlers();
196
- this.renderDynamicWidgets();
197
- }, 0);
198
- }
199
- }
200
-
201
- /**
202
- * Render dynamic components and templates for custom widgets
203
- */
204
- protected renderDynamicWidgets() {
205
- if (!this.containerRef?.nativeElement) return;
206
-
207
- // Use microtask to ensure Angular has completed change detection
208
- Promise.resolve().then(() => {
209
- if (!this.containerRef?.nativeElement) return;
210
-
211
- // Clean up existing dynamic components and views
212
- this.cleanupDynamicWidgets();
213
-
214
- const container = this.containerRef.nativeElement;
215
- // Query all widget divs without the data-rendered filter
216
- const dynamicWidgetDivs = Array.from(
217
- container.querySelectorAll('.bbq-widget')
218
- ) as HTMLElement[];
219
-
220
- let dynamicIndex = 0;
221
- this.widgetItems.forEach((item) => {
222
- if (!item.isHtml) {
223
- const customRenderer = this.widgetRegistry.getRenderer(item.widget.type);
224
-
225
- if (!customRenderer) return;
226
-
227
- const targetDiv = dynamicWidgetDivs[dynamicIndex];
228
- if (!targetDiv) return;
229
-
230
- // Clear the div content before rendering
231
- targetDiv.innerHTML = '';
232
-
233
- if (isComponentRenderer(customRenderer)) {
234
- this.renderComponent(customRenderer, item.widget, targetDiv);
235
- } else if (isTemplateRenderer(customRenderer)) {
236
- this.renderTemplate(customRenderer, item.widget, targetDiv);
237
- }
238
-
239
- dynamicIndex++;
240
- }
241
- });
242
- });
243
- }
244
-
245
- /**
246
- * Render an Angular component for a custom widget
247
- *
248
- * Note: This method safely assigns properties to component instances
249
- * by checking for property existence at runtime. This approach is necessary
250
- * because we cannot statically verify that all components implement
251
- * the CustomWidgetComponent interface.
252
- */
253
- protected renderComponent(
254
- componentType: any,
255
- widget: ChatWidget,
256
- targetElement: HTMLElement
257
- ) {
258
- // Create the component using Angular's createComponent API
259
- const componentRef = createComponent(componentType, {
260
- environmentInjector: this.environmentInjector,
261
- elementInjector: this.injector,
262
- });
263
-
264
- // Safely set component inputs if they exist
265
- const instance = componentRef.instance;
266
- if (instance && typeof instance === 'object') {
267
- // Set widget property if it exists in the prototype chain
268
- if ('widget' in instance) {
269
- (instance as any).widget = widget;
270
- }
271
-
272
- // Set widgetAction callback if it exists in the prototype chain
273
- if ('widgetAction' in instance) {
274
- (instance as any).widgetAction = (actionName: string, payload: unknown) => {
275
- this.widgetAction.emit({ actionName, payload });
276
- };
277
- }
278
- }
279
-
280
- // Attach the component's host view to the target element
281
- targetElement.appendChild(componentRef.location.nativeElement);
282
-
283
- // Store reference for cleanup
284
- this.dynamicComponents.push(componentRef);
285
-
286
- // Trigger change detection (use optional chaining for safety)
287
- componentRef.changeDetectorRef?.detectChanges();
288
- }
289
-
290
- /**
291
- * Render an Angular template for a custom widget
292
- */
293
- protected renderTemplate(
294
- templateRef: TemplateRef<WidgetTemplateContext>,
295
- widget: ChatWidget,
296
- targetElement: HTMLElement
297
- ) {
298
- const context: WidgetTemplateContext = {
299
- $implicit: widget,
300
- widget: widget,
301
- emitAction: (actionName: string, payload: unknown) => {
302
- this.widgetAction.emit({ actionName, payload });
303
- },
304
- };
305
-
306
- const viewRef = templateRef.createEmbeddedView(context);
307
-
308
- // Attach the view's DOM nodes to the target element
309
- viewRef.rootNodes.forEach((node: Node) => {
310
- targetElement.appendChild(node);
311
- });
312
-
313
- // Store reference for cleanup
314
- this.dynamicViews.push(viewRef);
315
-
316
- // Trigger change detection
317
- viewRef.detectChanges();
318
- }
319
-
320
- /**
321
- * Cleanup dynamic components and views
322
- */
323
- protected cleanupDynamicWidgets() {
324
- this.dynamicComponents.forEach((componentRef) => {
325
- componentRef.destroy();
326
- });
327
- this.dynamicComponents = [];
328
-
329
- this.dynamicViews.forEach((viewRef) => {
330
- viewRef.destroy();
331
- });
332
- this.dynamicViews = [];
333
- }
334
-
335
- private setupEventHandlers() {
336
- if (!this.containerRef?.nativeElement) return;
337
-
338
- // Cleanup old resources before setting up new ones
339
- this.cleanup();
340
-
341
- const container = this.containerRef.nativeElement;
342
-
343
- // Create a custom action handler that emits events
344
- const actionHandler = {
345
- handle: async (action: string, payload: any) => {
346
- this.widgetAction.emit({ actionName: action, payload });
347
- },
348
- };
349
-
350
- // Attach event handlers using WidgetEventManager
351
- this.eventManager = new WidgetEventManager(actionHandler);
352
- this.eventManager.attachHandlers(container);
353
- }
354
-
355
- handleClick(event: MouseEvent) {
356
- const target = event.target as HTMLElement;
357
- // Only trigger actions on non-form buttons and clickable elements (cards)
358
- // Don't trigger on input elements or form buttons (let WidgetEventManager handle those)
359
- const button = target.tagName === 'BUTTON' ? target : target.closest('button');
360
- if (button && !button.closest('[data-widget-type="form"]')) {
361
- const actionName = button.getAttribute('data-action');
362
- if (actionName) {
363
- try {
364
- const payloadStr = button.getAttribute('data-payload');
365
- const payload = payloadStr ? JSON.parse(payloadStr) : {};
366
- this.widgetAction.emit({ actionName, payload });
367
- } catch (err) {
368
- console.error('Failed to parse widget action payload:', err);
369
- }
370
- }
371
- }
372
- }
373
-
374
- /**
375
- * Cleanup all resources including event listeners.
376
- */
377
- private cleanup() {
378
- // Cleanup dynamic widgets first
379
- this.cleanupDynamicWidgets();
380
-
381
- // Cleanup event manager
382
- this.eventManager = undefined;
383
- }
384
- }