@bbq-chat/widgets-angular 1.0.7 → 1.0.9

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.
Files changed (40) hide show
  1. package/.angular/cache/21.0.5/ng-packagr/97cbacd0e5e4cb18d1fead4d7f3aee1c3863ba3ffbe7cb7dd7780f237a848a5c +1 -0
  2. package/.angular/cache/21.0.5/ng-packagr/tsbuildinfo/index.tsbuildinfo +1 -0
  3. package/.eslintrc.json +23 -0
  4. package/.prettierrc.json +8 -0
  5. package/EXAMPLES.md +484 -0
  6. package/README.md +119 -0
  7. package/angular.json +36 -0
  8. package/ng-package.json +9 -0
  9. package/package.json +4 -25
  10. package/src/angular-widget-renderer.spec.ts +157 -0
  11. package/src/components/button.component.ts +35 -0
  12. package/src/components/card.component.ts +52 -0
  13. package/src/components/datepicker.component.ts +63 -0
  14. package/src/components/dropdown.component.ts +65 -0
  15. package/src/components/fileupload.component.ts +71 -0
  16. package/src/components/form.component.ts +433 -0
  17. package/src/components/image.component.ts +33 -0
  18. package/src/components/imagecollection.component.ts +39 -0
  19. package/src/components/index.ts +20 -0
  20. package/src/components/input.component.ts +63 -0
  21. package/src/components/multiselect.component.ts +67 -0
  22. package/src/components/progressbar.component.ts +50 -0
  23. package/src/components/slider.component.ts +67 -0
  24. package/src/components/textarea.component.ts +63 -0
  25. package/src/components/themeswitcher.component.ts +46 -0
  26. package/src/components/toggle.component.ts +63 -0
  27. package/src/custom-widget-renderer.types.ts +120 -0
  28. package/src/examples/form-validation-listener.component.ts +41 -0
  29. package/src/public_api.ts +107 -0
  30. package/src/renderers/AngularWidgetRenderer.ts +100 -0
  31. package/src/renderers/built-in-components.ts +41 -0
  32. package/src/renderers/index.ts +7 -0
  33. package/src/services/form-validation.service.ts +21 -0
  34. package/src/widget-di.tokens.ts +95 -0
  35. package/src/widget-registry.service.ts +128 -0
  36. package/src/widget-renderer.component.ts +421 -0
  37. package/tsconfig.json +37 -0
  38. package/tsconfig.lib.json +18 -0
  39. package/tsconfig.lib.prod.json +11 -0
  40. package/tsconfig.spec.json +13 -0
@@ -0,0 +1,421 @@
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
+ Injector,
17
+ createComponent,
18
+ EnvironmentInjector,
19
+ Inject,
20
+ Optional,
21
+ ChangeDetectorRef,
22
+ } from '@angular/core';
23
+ import { CommonModule } from '@angular/common';
24
+ import {
25
+ SsrWidgetRenderer,
26
+ WidgetEventManager,
27
+ ChatWidget,
28
+ } from '@bbq-chat/widgets';
29
+ import { WidgetRegistryService } from './widget-registry.service';
30
+ import {
31
+ WidgetTemplateContext,
32
+ isHtmlRenderer,
33
+ isComponentRenderer,
34
+ isTemplateRenderer,
35
+ } from './custom-widget-renderer.types';
36
+ import {
37
+ WIDGET_EVENT_MANAGER_FACTORY,
38
+ SSR_WIDGET_RENDERER,
39
+ ANGULAR_WIDGET_RENDERER,
40
+ widgetEventManagerFactoryProvider,
41
+ ssrWidgetRendererFactory,
42
+ angularWidgetRendererFactory,
43
+ WidgetEventManagerFactory,
44
+ } from './widget-di.tokens';
45
+ import { AngularWidgetRenderer } from './renderers/AngularWidgetRenderer';
46
+
47
+ /**
48
+ * Angular component for rendering chat widgets
49
+ *
50
+ * This component handles rendering of chat widgets using the BbQ ChatWidgets library.
51
+ * It manages widget lifecycle, event handling, and cleanup.
52
+ *
53
+ * Supports three types of custom widget renderers:
54
+ * 1. HTML function renderers (return HTML strings)
55
+ * 2. Angular Component renderers (render as dynamic components)
56
+ * 3. Angular TemplateRef renderers (render as embedded views)
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * <bbq-widget-renderer
61
+ * [widgets]="messageWidgets"
62
+ * (widgetAction)="handleWidgetAction($event)">
63
+ * </bbq-widget-renderer>
64
+ * ```
65
+ */
66
+ @Component({
67
+ selector: 'bbq-widget-renderer',
68
+ standalone: true,
69
+ imports: [CommonModule],
70
+ providers: [
71
+ { provide: WIDGET_EVENT_MANAGER_FACTORY, useFactory: widgetEventManagerFactoryProvider },
72
+ { provide: SSR_WIDGET_RENDERER, useFactory: ssrWidgetRendererFactory },
73
+ { provide: ANGULAR_WIDGET_RENDERER, useFactory: angularWidgetRendererFactory },
74
+ ],
75
+ template: `
76
+ <div #widgetContainer class="bbq-widgets-container" (click)="handleClick($event)">
77
+ @for (item of widgetItems; track item.index) {
78
+ @if (item.isHtml) {
79
+ <div class="bbq-widget" [innerHTML]="item.html"></div>
80
+ } @else {
81
+ <div class="bbq-widget" #dynamicWidget></div>
82
+ }
83
+ }
84
+ </div>
85
+ `,
86
+ styles: [
87
+ `
88
+ .bbq-widgets-container {
89
+ margin-top: 0.5rem;
90
+ }
91
+
92
+ .bbq-widget {
93
+ margin-bottom: 0.5rem;
94
+ }
95
+ `,
96
+ ],
97
+ })
98
+ export class WidgetRendererComponent
99
+ implements OnInit, AfterViewInit, OnDestroy, OnChanges {
100
+ /**
101
+ * Array of widgets to render
102
+ */
103
+ @Input() widgets: ChatWidget[] | null | undefined;
104
+
105
+ /**
106
+ * Emits when a widget action is triggered
107
+ */
108
+ @Output() widgetAction = new EventEmitter<{
109
+ actionName: string;
110
+ payload: unknown;
111
+ }>();
112
+
113
+ @ViewChild('widgetContainer', { static: false })
114
+ containerRef!: ElementRef<HTMLDivElement>;
115
+
116
+ protected widgetItems: Array<{
117
+ index: number;
118
+ widget: ChatWidget;
119
+ isHtml: boolean;
120
+ html?: string;
121
+ }> = [];
122
+
123
+ protected eventManager?: WidgetEventManager;
124
+ protected isViewInitialized = false;
125
+ protected dynamicComponents: Array<ComponentRef<any>> = [];
126
+ protected dynamicViews: Array<EmbeddedViewRef<WidgetTemplateContext>> = [];
127
+
128
+ constructor(
129
+ @Inject(SSR_WIDGET_RENDERER) protected renderer: SsrWidgetRenderer,
130
+ @Optional() @Inject(ANGULAR_WIDGET_RENDERER) protected angularRenderer: AngularWidgetRenderer | null,
131
+ @Inject(WIDGET_EVENT_MANAGER_FACTORY) protected eventManagerFactory: WidgetEventManagerFactory,
132
+ protected widgetRegistry: WidgetRegistryService,
133
+ protected injector: Injector,
134
+ protected environmentInjector: EnvironmentInjector
135
+ ) { }
136
+
137
+ ngOnInit() {
138
+ // this.updateWidgetHtml();
139
+ }
140
+
141
+ ngOnChanges(changes: SimpleChanges) {
142
+ if (changes['widgets']) {
143
+ this.updateWidgetHtml();
144
+ }
145
+ }
146
+
147
+ ngAfterViewInit() {
148
+ this.updateWidgetHtml();
149
+ this.isViewInitialized = true;
150
+ this.setupEventHandlers();
151
+ // Render dynamic components/templates after view init
152
+ this.renderDynamicWidgets();
153
+ }
154
+
155
+ ngOnDestroy() {
156
+ this.cleanup();
157
+ }
158
+
159
+ /**
160
+ * Base implementation for updating the rendered HTML for the current widgets.
161
+ *
162
+ * Subclasses may override this method to customize how widgets are rendered
163
+ * (for example, to inject additional markup or perform preprocessing).
164
+ *
165
+ * Since this is the base implementation, overriding implementations are not
166
+ * required to call `super.updateWidgetHtml()`.
167
+ */
168
+ protected updateWidgetHtml() {
169
+ if (!this.widgets || this.widgets.length === 0) {
170
+ this.widgetItems = [];
171
+ return;
172
+ }
173
+
174
+ this.widgetItems = this.widgets.map((widget, index) => {
175
+ const customRenderer = this.widgetRegistry.getRenderer(widget.type);
176
+
177
+ // Check template renderer first (most specific)
178
+ if (customRenderer && isTemplateRenderer(customRenderer)) {
179
+ return {
180
+ index,
181
+ widget,
182
+ isHtml: false,
183
+ };
184
+ }
185
+
186
+ // Check component renderer second
187
+ if (customRenderer && isComponentRenderer(customRenderer)) {
188
+ return {
189
+ index,
190
+ widget,
191
+ isHtml: false,
192
+ };
193
+ }
194
+
195
+ // Check HTML function renderer last (most general, matches any function)
196
+ if (customRenderer && isHtmlRenderer(customRenderer)) {
197
+ return {
198
+ index,
199
+ widget,
200
+ isHtml: true,
201
+ html: customRenderer(widget),
202
+ };
203
+ }
204
+
205
+ // Try to use AngularWidgetRenderer for built-in widgets
206
+ if (this.angularRenderer) {
207
+ const componentType = this.angularRenderer.getComponentType(widget);
208
+ if (componentType) {
209
+ return {
210
+ index,
211
+ widget,
212
+ isHtml: false,
213
+ };
214
+ }
215
+ }
216
+
217
+ // Fallback: render using the SSR library renderer
218
+ return {
219
+ index,
220
+ widget,
221
+ isHtml: true,
222
+ html: this.renderer.renderWidget(widget),
223
+ };
224
+ });
225
+
226
+ // After view updates, reinitialize widgets only if view is already initialized
227
+ if (this.isViewInitialized) {
228
+ setTimeout(() => {
229
+ this.setupEventHandlers();
230
+ this.renderDynamicWidgets();
231
+ }, 0);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Render dynamic components and templates for custom widgets
237
+ */
238
+ protected renderDynamicWidgets() {
239
+ if (!this.containerRef?.nativeElement) return;
240
+
241
+ // Use microtask to ensure Angular has completed change detection
242
+ Promise.resolve().then(() => {
243
+ if (!this.containerRef?.nativeElement) return;
244
+
245
+ // Clean up existing dynamic components and views
246
+ this.cleanupDynamicWidgets();
247
+
248
+ const container = this.containerRef.nativeElement;
249
+ // Query all widget divs without the data-rendered filter
250
+ const dynamicWidgetDivs = Array.from(
251
+ container.querySelectorAll('.bbq-widget')
252
+ ) as HTMLElement[];
253
+
254
+ let dynamicIndex = 0;
255
+ this.widgetItems.forEach((item) => {
256
+ if (!item.isHtml) {
257
+ const customRenderer = this.widgetRegistry.getRenderer(item.widget.type);
258
+
259
+ const targetDiv = dynamicWidgetDivs[dynamicIndex];
260
+ if (!targetDiv) return;
261
+
262
+ // Clear the div content before rendering
263
+ targetDiv.innerHTML = '';
264
+
265
+ // Handle custom renderers first
266
+ if (customRenderer) {
267
+ if (isComponentRenderer(customRenderer)) {
268
+ this.renderComponent(customRenderer, item.widget, targetDiv);
269
+ } else if (isTemplateRenderer(customRenderer)) {
270
+ this.renderTemplate(customRenderer, item.widget, targetDiv);
271
+ }
272
+ } else if (this.angularRenderer) {
273
+ // Try to render using AngularWidgetRenderer for built-in widgets
274
+ const componentType = this.angularRenderer.getComponentType(item.widget);
275
+ if (componentType) {
276
+ this.renderComponent(componentType, item.widget, targetDiv);
277
+ }
278
+ }
279
+
280
+ dynamicIndex++;
281
+ }
282
+ });
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Render an Angular component for a custom widget
288
+ *
289
+ * Note: This method safely assigns properties to component instances
290
+ * by checking for property existence at runtime. This approach is necessary
291
+ * because we cannot statically verify that all components implement
292
+ * the CustomWidgetComponent interface.
293
+ */
294
+ protected renderComponent(
295
+ componentType: any,
296
+ widget: ChatWidget,
297
+ targetElement: HTMLElement
298
+ ) {
299
+ // Create the component using Angular's createComponent API
300
+ const componentRef = createComponent(componentType, {
301
+ environmentInjector: this.environmentInjector,
302
+ elementInjector: this.injector,
303
+ });
304
+
305
+ // Safely set component inputs if they exist
306
+ const instance = componentRef.instance as any;
307
+ // Set widget property if it exists in the prototype chain
308
+ if (!instance['widget']) {
309
+ instance['widget'] = widget;
310
+ }
311
+ // Set widgetAction property if it exists in the prototype chain
312
+ if(!instance['widgetAction']) {
313
+ instance['widgetAction'] = (actionName: string, payload: unknown) => {
314
+ this.widgetAction.emit({ actionName, payload });
315
+ };
316
+ }
317
+ // Attach the component's host view to the target element
318
+ targetElement.appendChild(componentRef.location.nativeElement);
319
+
320
+ // Store reference for cleanup
321
+ this.dynamicComponents.push(componentRef);
322
+
323
+ // Trigger change detection (use optional chaining for safety)
324
+ componentRef.changeDetectorRef?.detectChanges();
325
+ }
326
+
327
+ /**
328
+ * Render an Angular template for a custom widget
329
+ */
330
+ protected renderTemplate(
331
+ templateRef: TemplateRef<WidgetTemplateContext>,
332
+ widget: ChatWidget,
333
+ targetElement: HTMLElement
334
+ ) {
335
+ const context: WidgetTemplateContext = {
336
+ $implicit: widget,
337
+ widget: widget,
338
+ emitAction: (actionName: string, payload: unknown) => {
339
+ this.widgetAction.emit({ actionName, payload });
340
+ },
341
+ };
342
+
343
+ const viewRef = templateRef.createEmbeddedView(context);
344
+
345
+ // Attach the view's DOM nodes to the target element
346
+ viewRef.rootNodes.forEach((node: Node) => {
347
+ targetElement.appendChild(node);
348
+ });
349
+
350
+ // Store reference for cleanup
351
+ this.dynamicViews.push(viewRef);
352
+
353
+ // Trigger change detection
354
+ viewRef.detectChanges();
355
+ }
356
+
357
+ /**
358
+ * Cleanup dynamic components and views
359
+ */
360
+ protected cleanupDynamicWidgets() {
361
+ this.dynamicComponents.forEach((componentRef) => {
362
+ componentRef.destroy();
363
+ });
364
+ this.dynamicComponents = [];
365
+
366
+ this.dynamicViews.forEach((viewRef) => {
367
+ viewRef.destroy();
368
+ });
369
+ this.dynamicViews = [];
370
+ }
371
+
372
+ private setupEventHandlers() {
373
+ if (!this.containerRef?.nativeElement) return;
374
+
375
+ // Cleanup old resources before setting up new ones
376
+ this.cleanup();
377
+
378
+ const container = this.containerRef.nativeElement;
379
+
380
+ // Create a custom action handler that emits events
381
+ const actionHandler = {
382
+ handle: async (action: string, payload: any) => {
383
+ this.widgetAction.emit({ actionName: action, payload });
384
+ },
385
+ };
386
+
387
+ // Use the injected factory to create an event manager with the component-specific action handler
388
+ this.eventManager = this.eventManagerFactory(actionHandler);
389
+ this.eventManager.attachHandlers(container);
390
+ }
391
+
392
+ handleClick(event: MouseEvent) {
393
+ const target = event.target as HTMLElement;
394
+ // Only trigger actions on non-form buttons and clickable elements (cards)
395
+ // Don't trigger on input elements or form buttons (let WidgetEventManager handle those)
396
+ const button = target.tagName === 'BUTTON' ? target : target.closest('button');
397
+ if (button && !button.closest('[data-widget-type="form"]')) {
398
+ const actionName = button.getAttribute('data-action');
399
+ if (actionName) {
400
+ try {
401
+ const payloadStr = button.getAttribute('data-payload');
402
+ const payload = payloadStr ? JSON.parse(payloadStr) : {};
403
+ this.widgetAction.emit({ actionName, payload });
404
+ } catch (err) {
405
+ console.error('Failed to parse widget action payload:', err);
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Cleanup all resources including event listeners.
413
+ */
414
+ private cleanup() {
415
+ // Cleanup dynamic widgets first
416
+ this.cleanupDynamicWidgets();
417
+
418
+ // Cleanup event manager
419
+ this.eventManager = undefined;
420
+ }
421
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "outDir": "./dist/out-tsc",
6
+ "strict": true,
7
+ "noImplicitOverride": true,
8
+ "noPropertyAccessFromIndexSignature": true,
9
+ "paths": {
10
+ "@bbq-chat/widgets": ["../js/dist"]
11
+ },
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "skipLibCheck": true,
15
+ "isolatedModules": true,
16
+ "experimentalDecorators": true,
17
+ "importHelpers": true,
18
+ "target": "ES2022",
19
+ "module": "preserve",
20
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
21
+ },
22
+ "angularCompilerOptions": {
23
+ "enableI18nLegacyMessageIdFormat": false,
24
+ "strictInjectionParameters": true,
25
+ "strictInputAccessModifiers": true,
26
+ "strictTemplates": true,
27
+ },
28
+ "files": [],
29
+ "references": [
30
+ {
31
+ "path": "./tsconfig.lib.json"
32
+ },
33
+ {
34
+ "path": "./tsconfig.spec.json"
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/lib",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "types": []
8
+ },
9
+ "include": [
10
+ "src/**/*.ts",
11
+ "src/index.ts"
12
+ ],
13
+ "exclude": [
14
+ "**/*.spec.ts",
15
+ "src/test-setup.ts",
16
+ "**/*.test.ts"
17
+ ]
18
+ }
@@ -0,0 +1,11 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.lib.json",
5
+ "compilerOptions": {
6
+ "declarationMap": false
7
+ },
8
+ "angularCompilerOptions": {
9
+ "compilationMode": "partial"
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/spec",
5
+ "types": [
6
+ "vitest/globals"
7
+ ]
8
+ },
9
+ "include": [
10
+ "src/**/*.d.ts",
11
+ "src/**/*.spec.ts"
12
+ ]
13
+ }