@andreaswissel/uiflow
Version:
Adaptive UI density management library with progressive disclosure, element dependencies, A/B testing, and intelligent behavior-based adaptation
448 lines (384 loc) • 11.5 kB
text/typescript
/**
* UIFlow Angular Adapter
* Angular services, directives, and components for UIFlow integration
*/
import {
Injectable,
Directive,
Component,
Input,
Output,
EventEmitter,
ElementRef,
OnInit,
OnDestroy,
Inject,
InjectionToken,
TemplateRef,
ViewContainerRef,
ChangeDetectorRef,
Provider,
EnvironmentProviders,
makeEnvironmentProviders
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { UIFlow } from '../../index.js';
// Injection token for UIFlow configuration
export const UIFLOW_CONFIG = new InjectionToken<any>('UIFlow Configuration');
/**
* UIFlow Service - Core Angular service for UIFlow integration
*/
export class UIFlowService implements OnDestroy {
private uiflow: UIFlow | null = null;
private readonly isReady$ = new BehaviorSubject<boolean>(false);
private readonly destroy$ = new Subject<void>();
private readonly densitySubjects = new Map<string, BehaviorSubject<number>>();
private readonly overrideSubjects = new Map<string, BehaviorSubject<boolean>>();
constructor( private config: any = {}) {
this.initialize();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
if (this.uiflow) {
this.uiflow.destroy();
}
}
private async initialize(): Promise<void> {
try {
this.uiflow = new UIFlow();
await this.uiflow.init(this.config);
this.isReady$.next(true);
this.setupEventListeners();
} catch (error) {
console.error('UIFlow initialization failed:', error);
}
}
private setupEventListeners(): void {
// Listen for density changes
document.addEventListener('uiflow:density-changed', (event: any) => {
const { area, density } = event.detail;
this.getDensitySubject(area).next(density);
});
// Listen for adaptation events
document.addEventListener('uiflow:adaptation', (event: any) => {
const { area, newDensity } = event.detail;
this.getDensitySubject(area).next(newDensity);
});
// Listen for override events
document.addEventListener('uiflow:override-applied', (event: any) => {
const { area } = event.detail;
this.getOverrideSubject(area).next(true);
});
document.addEventListener('uiflow:override-cleared', (event: any) => {
const { area } = event.detail;
this.getOverrideSubject(area).next(false);
});
}
private getDensitySubject(area: string): BehaviorSubject<number> {
if (!this.densitySubjects.has(area)) {
const initialDensity = this.uiflow ? this.uiflow.getDensityLevel(area) : 0.3;
this.densitySubjects.set(area, new BehaviorSubject(initialDensity));
}
return this.densitySubjects.get(area)!;
}
private getOverrideSubject(area: string): BehaviorSubject<boolean> {
if (!this.overrideSubjects.has(area)) {
const hasOverride = this.uiflow ? this.uiflow.hasOverride(area) : false;
this.overrideSubjects.set(area, new BehaviorSubject(hasOverride));
}
return this.overrideSubjects.get(area)!;
}
// Public API methods
get isReady(): Observable<boolean> {
return this.isReady$.asObservable();
}
get instance(): UIFlow | null {
return this.uiflow;
}
categorize(element: Element, category: string, area: string = 'default', options: any = {}): void {
if (this.uiflow) {
this.uiflow.categorize(element, category, area, options);
}
}
getDensityLevel(area: string = 'default'): number {
return this.uiflow ? this.uiflow.getDensityLevel(area) : 0.3;
}
getDensity$(area: string = 'default'): Observable<number> {
return this.getDensitySubject(area).asObservable();
}
setDensityLevel(level: number, area: string = 'default'): void {
if (this.uiflow) {
this.uiflow.setDensityLevel(level, area);
}
}
hasOverride(area: string): boolean {
return this.uiflow ? this.uiflow.hasOverride(area) : false;
}
hasOverride$(area: string): Observable<boolean> {
return this.getOverrideSubject(area).asObservable();
}
shouldShowElement(category: string, area: string = 'default'): boolean {
return this.uiflow ? this.uiflow.shouldShowElement(category, area) : true;
}
highlightElement(elementId: string, style: string = 'default', options: any = {}): void {
if (this.uiflow) {
this.uiflow.highlightElement(elementId, style, options);
}
}
flagAsNew(elementId: string, helpText?: string, duration?: number): void {
if (this.uiflow) {
this.uiflow.flagAsNew(elementId, helpText, duration);
}
}
simulateUsage(area: string, interactions: any[], days: number = 7): void {
if (this.uiflow) {
this.uiflow.simulateUsage(area, interactions, days);
}
}
}
/**
* Directive for automatic element categorization
*/
export class UIFlowElementDirective implements OnInit, OnDestroy {
uiflowElement!: string; // category
uiflowArea: string = 'default';
uiflowHelpText?: string;
uiflowIsNew: boolean = false;
private destroy$ = new Subject<void>();
constructor(
private elementRef: ElementRef,
private uiflowService: UIFlowService
) {}
ngOnInit(): void {
this.uiflowService.isReady
.pipe(takeUntil(this.destroy$))
.subscribe(ready => {
if (ready) {
this.categorizeElement();
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private categorizeElement(): void {
const options = {
helpText: this.uiflowHelpText,
isNew: this.uiflowIsNew
};
this.uiflowService.categorize(
this.elementRef.nativeElement,
this.uiflowElement,
this.uiflowArea,
options
);
}
}
/**
* Structural directive for conditional rendering based on density
*/
export class UIFlowIfDirective implements OnInit, OnDestroy {
uiflowIf!: string; // category
uiflowIfArea: string = 'default';
private destroy$ = new Subject<void>();
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private uiflowService: UIFlowService
) {}
ngOnInit(): void {
this.uiflowService.isReady
.pipe(takeUntil(this.destroy$))
.subscribe(ready => {
if (ready) {
this.updateView();
this.listenForDensityChanges();
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private listenForDensityChanges(): void {
this.uiflowService.getDensity$(this.uiflowIfArea)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.updateView();
});
}
private updateView(): void {
const shouldShow = this.uiflowService.shouldShowElement(this.uiflowIf, this.uiflowIfArea);
if (shouldShow && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (!shouldShow && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
/**
* Component for density level display
*/
export class UIFlowDensityIndicatorComponent implements OnInit, OnDestroy {
area: string = 'default';
showOverride: boolean = true;
density: number = 30;
hasOverride: boolean = false;
private destroy$ = new Subject<void>();
constructor(private uiflowService: UIFlowService) {}
ngOnInit(): void {
this.uiflowService.getDensity$(this.area)
.pipe(takeUntil(this.destroy$))
.subscribe(density => {
this.density = Math.round(density * 100);
});
this.uiflowService.hasOverride$(this.area)
.pipe(takeUntil(this.destroy$))
.subscribe(hasOverride => {
this.hasOverride = hasOverride;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
get indicatorStyle(): any {
return {
padding: '4px 8px',
backgroundColor: this.hasOverride ? '#fbbf24' : '#3b82f6',
color: 'white',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
};
}
}
/**
* Component for density control slider
*/
export class UIFlowDensityControlComponent implements OnInit, OnDestroy {
area: string = 'default';
densityChange = new EventEmitter<number>();
density: number = 30;
hasOverride: boolean = false;
private destroy$ = new Subject<void>();
constructor(private uiflowService: UIFlowService) {}
ngOnInit(): void {
this.uiflowService.getDensity$(this.area)
.pipe(takeUntil(this.destroy$))
.subscribe(density => {
this.density = Math.round(density * 100);
});
this.uiflowService.hasOverride$(this.area)
.pipe(takeUntil(this.destroy$))
.subscribe(hasOverride => {
this.hasOverride = hasOverride;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
onDensityChange(event: any): void {
const density = parseInt(event.target.value) / 100;
this.uiflowService.setDensityLevel(density, this.area);
this.densityChange.emit(density);
}
}
/**
* Standalone components and directives for UIFlow
*/
export const UIFLOW_COMPONENTS = [
UIFlowElementDirective,
UIFlowIfDirective,
UIFlowDensityIndicatorComponent,
UIFlowDensityControlComponent
] as const;
/**
* Provide UIFlow services for Angular applications using standalone APIs
*/
export function provideUIFlow(config: any = {}): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: UIFLOW_CONFIG, useValue: config },
UIFlowService
]);
}
/**
* Legacy NgModule for backwards compatibility (deprecated)
* @deprecated Use provideUIFlow() and standalone components instead
*/
export function provideUIFlowModule() {
return {
providers: [UIFlowService]
};
}
// Export Angular version for compatibility check
export const ANGULAR_ADAPTER_VERSION = '1.0.0';