UNPKG

@pega/dx-component-builder-sdk

Version:

Utility for building custom UI components

362 lines (314 loc) 12.3 kB
import { Component, OnInit, Input, ChangeDetectorRef, forwardRef, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatOptionModule } from '@angular/material/core'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { interval, Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { AngularPConnectData, AngularPConnectService } from '@pega/angular-sdk-components'; import { Utils } from '@pega/angular-sdk-components'; import { ComponentMapperComponent } from '@pega/angular-sdk-components'; import { DatapageService } from '@pega/angular-sdk-components'; import { handleEvent } from '@pega/angular-sdk-components'; import { PConnFieldProps } from '@pega/angular-sdk-components'; interface IOption { key: string; value: string; } interface AutoCompleteProps extends PConnFieldProps { // If any, enter additional props that only exist on AutoComplete here deferDatasource?: boolean; datasourceMetadata?: any; onRecordChange?: any; additionalProps?: object; listType: string; parameters?: any; datasource: any; columns: any[]; } @Component({ selector: 'app-auto-complete', templateUrl: './auto-complete.component.html', styleUrls: ['./auto-complete.component.scss'], standalone: true, imports: [ CommonModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatAutocompleteModule, MatOptionModule, forwardRef(() => ComponentMapperComponent) ] }) export class AutoCompleteComponent implements OnInit, OnDestroy { @Input() pConn$: typeof PConnect; @Input() formGroup$: FormGroup; // Used with AngularPConnect angularPConnectData: AngularPConnectData = {}; configProps$: AutoCompleteProps; label$ = ''; value$ = ''; bRequired$ = false; bReadonly$ = false; bDisabled$ = false; bVisible$ = true; displayMode$?: string = ''; controlName$: string; bHasForm$ = true; options$: any[]; componentReference = ''; testId: string; listType: string; columns = []; helperText: string; placeholder: string; fieldControl = new FormControl('', null); parameters: {}; hideLabel: boolean; filteredOptions: Observable<any[]>; filterValue = ''; actionsApi: Object; propName: string; constructor( private angularPConnect: AngularPConnectService, private cdRef: ChangeDetectorRef, private utils: Utils, private dataPageService: DatapageService ) {} async ngOnInit(): Promise<void> { // First thing in initialization is registering and subscribing to the AngularPConnect service this.angularPConnectData = this.angularPConnect.registerAndSubscribeComponent(this, this.onStateChange); this.controlName$ = this.angularPConnect.getComponentID(this); // Then, continue on with other initialization // call updateSelf when initializing // this.updateSelf(); await this.checkAndUpdate(); if (this.formGroup$) { // add control to formGroup this.formGroup$.addControl(this.controlName$, this.fieldControl); this.fieldControl.setValue(this.value$); this.bHasForm$ = true; } else { this.bReadonly$ = true; this.bHasForm$ = false; } this.filteredOptions = this.fieldControl.valueChanges.pipe( startWith(''), map(value => this._filter((value as string) || '')) ); } setOptions(options: IOption[]) { this.options$ = options; const index = this.options$?.findIndex(element => element.key === this.configProps$.value); this.value$ = index > -1 ? this.options$[index].value : this.configProps$.value; this.fieldControl.setValue(this.value$); } ngOnDestroy(): void { if (this.formGroup$) { this.formGroup$.removeControl(this.controlName$); } if (this.angularPConnectData.unsubscribeFn) { this.angularPConnectData.unsubscribeFn(); } } private _filter(value: string): string[] { const filterVal = (value || this.filterValue).toLowerCase(); return this.options$?.filter(option => option.value?.toLowerCase().includes(filterVal)); } // Callback passed when subscribing to store change async onStateChange() { await this.checkAndUpdate(); } async checkAndUpdate() { // Should always check the bridge to see if the component should // update itself (re-render) const bUpdateSelf = this.angularPConnect.shouldComponentUpdate(this); // ONLY call updateSelf when the component should update if (bUpdateSelf) { await this.updateSelf(); } } // updateSelf async updateSelf(): Promise<void> { // starting very simple... // moved this from ngOnInit() and call this from there instead... this.configProps$ = this.pConn$.resolveConfigProps(this.pConn$.getConfigProps()) as AutoCompleteProps; if (this.configProps$.value != undefined) { const index = this.options$?.findIndex(element => element.key === this.configProps$.value); this.value$ = index > -1 ? this.options$[index].value : this.configProps$.value; this.fieldControl.setValue(this.value$); } this.setPropertyValuesFromProps(); this.actionsApi = this.pConn$.getActionsApi(); this.propName = this.pConn$.getStateProps().value; const context = this.pConn$.getContextName(); const { columns, datasource } = this.generateColumnsAndDataSource(); if (columns) { this.columns = this.preProcessColumns(columns); } // timeout and detectChanges to avoid ExpressionChangedAfterItHasBeenCheckedError setTimeout(() => { if (this.configProps$.required != null) { this.bRequired$ = this.utils.getBooleanValue(this.configProps$.required); } this.cdRef.detectChanges(); }); if (this.configProps$.visibility != null) { this.bVisible$ = this.utils.getBooleanValue(this.configProps$.visibility); } // disabled if (this.configProps$.disabled != undefined) { this.bDisabled$ = this.utils.getBooleanValue(this.configProps$.disabled); } if (this.bDisabled$) { this.fieldControl.disable(); } else { this.fieldControl.enable(); } if (this.configProps$.readOnly != null) { this.bReadonly$ = this.utils.getBooleanValue(this.configProps$.readOnly); } this.componentReference = this.pConn$.getStateProps().value; if (this.listType === 'associated') { const optionsList = this.utils.getOptionList(this.configProps$, this.pConn$.getDataObject('')); // 1st arg empty string until typedef marked correctly this.setOptions(optionsList); } if (!this.displayMode$ && this.listType !== 'associated') { const results = await this.dataPageService.getDataPageData(datasource, this.parameters, context); this.fillOptions(results); } // trigger display of error message with field control if (this.angularPConnectData.validateMessage != null && this.angularPConnectData.validateMessage != '') { const timer = interval(100).subscribe(() => { this.fieldControl.setErrors({ message: true }); this.fieldControl.markAsTouched(); timer.unsubscribe(); }); } } setPropertyValuesFromProps() { this.testId = this.configProps$.testId; this.label$ = this.configProps$.label; this.placeholder = this.configProps$.placeholder || ''; this.displayMode$ = this.configProps$.displayMode; this.listType = this.configProps$.listType; this.hideLabel = this.configProps$.hideLabel; this.helperText = this.configProps$.helperText; this.parameters = this.configProps$?.parameters; } generateColumnsAndDataSource() { let datasource = this.configProps$.datasource; let columns = this.configProps$.columns; // const { deferDatasource, datasourceMetadata } = this.configProps$; const { deferDatasource, datasourceMetadata } = this.pConn$.getConfigProps(); // convert associated to datapage listtype and transform props // Process deferDatasource when datapage name is present. WHhen tableType is promptList / localList if (deferDatasource && datasourceMetadata?.datasource?.name) { this.listType = 'datapage'; datasource = datasourceMetadata.datasource.name; const { parameters, propertyForDisplayText, propertyForValue } = datasourceMetadata.datasource; this.parameters = this.flattenParameters(parameters); const displayProp = propertyForDisplayText?.startsWith('@P') ? propertyForDisplayText.substring(3) : propertyForDisplayText; const valueProp = propertyForValue?.startsWith('@P') ? propertyForValue.substring(3) : propertyForValue; columns = [ { key: 'true', setProperty: 'Associated property', value: valueProp }, { display: 'true', primary: 'true', useForSearch: true, value: displayProp } ]; } return { columns, datasource }; } fillOptions(results: any) { const optionsData: any[] = []; const displayColumn = this.getDisplayFieldsMetaData(this.columns); results?.forEach(element => { const obj = { key: element[displayColumn.key] || element.pyGUID, value: element[displayColumn.primary]?.toString() }; optionsData.push(obj); }); this.setOptions(optionsData); } flattenParameters(params = {}) { const flatParams = {}; Object.keys(params).forEach(key => { const { name, value: theVal } = params[key]; flatParams[name] = theVal; }); return flatParams; } getDisplayFieldsMetaData(columnList) { const displayColumns = columnList.filter(col => col.display === 'true'); const metaDataObj: any = { key: '', primary: '', secondary: [] }; const keyCol = columnList.filter(col => col.key === 'true'); metaDataObj.key = keyCol.length > 0 ? keyCol[0].value : 'auto'; for (let index = 0; index < displayColumns.length; index += 1) { if (displayColumns[index].primary === 'true') { metaDataObj.primary = displayColumns[index].value; } else { metaDataObj.secondary.push(displayColumns[index].value); } } return metaDataObj; } preProcessColumns(columnList) { return columnList?.map(col => { const tempColObj = { ...col }; tempColObj.value = col.value && col.value.startsWith('.') ? col.value.substring(1) : col.value; return tempColObj; }); } fieldOnChange(event: Event) { // this works - this.pConn$.setValue( this.componentReference, `property: ${this.componentReference}`); // this works - this.pConn$.setValue( this.componentReference, this.fieldControl.value); // PConnect wants to use changeHandler for onChange // this.angularPConnect.changeHandler( this, event); const value = (event.target as HTMLInputElement).value; this.filterValue = value; handleEvent(this.actionsApi, 'change', this.propName, value); } optionChanged(event: any) { const value = event?.option?.value; handleEvent(this.actionsApi, 'change', this.propName, value); } fieldOnBlur(event: Event) { let key = ''; const el = event?.target as HTMLInputElement; if (el?.value) { const index = this.options$?.findIndex(element => element.value === el.value); key = index > -1 ? (key = this.options$[index].key) : el.value; } const value = key; handleEvent(this.actionsApi, 'changeNblur', this.propName, value); if (this.configProps$?.onRecordChange) { el.value = value; this.configProps$.onRecordChange(event); } } getErrorMessage() { let errMessage = ''; // look for validation messages for json, pre-defined or just an error pushed from workitem (400) if (this.fieldControl.hasError('message')) { errMessage = this.angularPConnectData.validateMessage ?? ''; return errMessage; } if (this.fieldControl.hasError('required')) { errMessage = 'You must enter a value'; } else if (this.fieldControl.errors) { errMessage = this.fieldControl.errors.toString(); } return errMessage; } }