mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-04-16 22:24:49 +00:00
AAE-41699 Evaluate variables in form display text (#11620)
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ChangeDetectorRef, Component, inject, AfterViewInit, DestroyRef, InjectionToken, Optional, Inject } from '@angular/core';
|
||||
import { debounceTime, filter, isObservable, Observable } from 'rxjs';
|
||||
import { FormRulesEvent } from '../../../events';
|
||||
import { FormExpressionService } from '../../../services/form-expression.service';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
export interface DisplayTextWidgetSettings {
|
||||
enableExpressionEvaluation: boolean;
|
||||
// a setting for a /juel API can be added here for full expression support
|
||||
}
|
||||
|
||||
export const ADF_DISPLAY_TEXT_SETTINGS = new InjectionToken<DisplayTextWidgetSettings>('adf-display-text-settings');
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: true
|
||||
})
|
||||
export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent implements AfterViewInit {
|
||||
private readonly formExpressionService = inject(FormExpressionService);
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
private enableExpressionEvaluation: boolean = false;
|
||||
protected originalFieldValue?: string;
|
||||
|
||||
constructor(@Optional() @Inject(ADF_DISPLAY_TEXT_SETTINGS) settings: Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings) {
|
||||
super();
|
||||
if (isObservable(settings)) {
|
||||
settings.pipe(takeUntilDestroyed()).subscribe((data: DisplayTextWidgetSettings) => {
|
||||
this.updateSettingsBasedProperties(data);
|
||||
});
|
||||
} else {
|
||||
this.updateSettingsBasedProperties(settings);
|
||||
}
|
||||
}
|
||||
|
||||
override ngAfterViewInit() {
|
||||
if (this.enableExpressionEvaluation) {
|
||||
this.storeOriginalValue();
|
||||
this.setupFieldDependencies();
|
||||
this.applyExpressions();
|
||||
}
|
||||
super.ngAfterViewInit();
|
||||
}
|
||||
|
||||
protected abstract storeOriginalValue(): void;
|
||||
protected abstract evaluateExpressions(): void;
|
||||
protected abstract reevaluateExpressions(): void;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected resolveExpressions(text: string): string {
|
||||
return this.formExpressionService.resolveExpressions(this.field.form, text);
|
||||
}
|
||||
|
||||
private applyExpressions() {
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.evaluateExpressions();
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
private setupFieldDependencies() {
|
||||
if (!this.field?.form || !this.originalFieldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dependencies = this.formExpressionService.getFieldDependencies(this.originalFieldValue);
|
||||
if (dependencies.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formService.formRulesEvent
|
||||
.pipe(
|
||||
filter((event: FormRulesEvent) => event.type === 'fieldValueChanged' && event.field && dependencies.includes(event.field.id)),
|
||||
debounceTime(300),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reapplyExpressions();
|
||||
});
|
||||
}
|
||||
|
||||
private reapplyExpressions() {
|
||||
if (!this.field || !this.originalFieldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reevaluateExpressions();
|
||||
this.fieldChanged.emit(this.field);
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
private updateSettingsBasedProperties(data: DisplayTextWidgetSettings): void {
|
||||
this.enableExpressionEvaluation = data?.enableExpressionEvaluation ?? false;
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,24 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormFieldModel, FormModel } from '../core';
|
||||
import { DisplayTextWidgetComponent } from './display-text.widget';
|
||||
import { ADF_DISPLAY_TEXT_SETTINGS } from '../base-display-text/base-display-text.widget';
|
||||
import { FormService } from '../../../services/form.service';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('DisplayTextWidgetComponent', () => {
|
||||
let fixture: ComponentFixture<DisplayTextWidgetComponent>;
|
||||
let widget: DisplayTextWidgetComponent;
|
||||
let formService: FormService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayTextWidgetComponent]
|
||||
imports: [DisplayTextWidgetComponent],
|
||||
providers: [FormService]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
formService = TestBed.inject(FormService);
|
||||
});
|
||||
|
||||
describe('event tracking', () => {
|
||||
@@ -49,4 +55,189 @@ describe('DisplayTextWidgetComponent', () => {
|
||||
expect(eventSpy).toHaveBeenCalledWith(clickEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expression evaluation', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: { enableExpressionEvaluation: true }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
formService = TestBed.inject(FormService);
|
||||
});
|
||||
|
||||
it('should resolve field expressions in text value', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: 'Hello ${field.name}' },
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should resolve variable expressions in text value', () => {
|
||||
const form = new FormModel({
|
||||
fields: [{ id: 'displayText1', type: 'display-text', value: 'Status: ${variable.status}' }],
|
||||
variables: [{ id: 'status', name: 'status', type: 'string', value: 'Active' }]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Status: Active');
|
||||
});
|
||||
|
||||
it('should resolve multiple expressions in text value', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: '${field.firstName} ${field.lastName}' },
|
||||
{ id: 'firstName', type: 'text', value: 'John' },
|
||||
{ id: 'lastName', type: 'text', value: 'Doe' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('should update display text when dependent field value changes', (done) => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: 'Hello ${field.name}' },
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
const nameField = form.getFieldById('name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Hello John');
|
||||
|
||||
nameField.value = 'Jane';
|
||||
formService.formRulesEvent.next({ type: 'fieldValueChanged', field: nameField } as any);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(widget.field.value).toBe('Hello Jane');
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
it('should handle non-string field values by stringifying them', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: 'Count: ${field.count}' },
|
||||
{ id: 'count', type: 'number', value: 42 }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Count: 42');
|
||||
});
|
||||
|
||||
it('should handle missing field references with empty string', () => {
|
||||
const form = new FormModel({
|
||||
fields: [{ id: 'displayText1', type: 'display-text', value: 'Hello ${field.nonExistent}' }]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Hello ');
|
||||
});
|
||||
|
||||
it('should not resolve expressions when enableExpressionEvaluation is false', () => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: { enableExpressionEvaluation: false }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: 'Hello ${field.name}' },
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Hello ${field.name}');
|
||||
});
|
||||
|
||||
it('should preserve original value for re-evaluation', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: 'Hello ${field.name}' },
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe('Hello John');
|
||||
expect(widget['originalFieldValue']).toBe('Hello ${field.name}');
|
||||
});
|
||||
|
||||
it('should support observable settings', (done) => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: of({ enableExpressionEvaluation: true })
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'displayText1', type: 'display-text', value: 'Hello ${field.name}' },
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('displayText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(widget.field.value).toBe('Hello John');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
import { BaseDisplayTextWidgetComponent } from '../base-display-text/base-display-text.widget';
|
||||
|
||||
@Component({
|
||||
selector: 'display-text-widget',
|
||||
@@ -39,4 +39,22 @@ import { WidgetComponent } from '../widget.component';
|
||||
imports: [TranslatePipe],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class DisplayTextWidgetComponent extends WidgetComponent {}
|
||||
export class DisplayTextWidgetComponent extends BaseDisplayTextWidgetComponent {
|
||||
protected storeOriginalValue(): void {
|
||||
if (this.field) {
|
||||
this.originalFieldValue = this.field.value;
|
||||
}
|
||||
}
|
||||
|
||||
protected evaluateExpressions(): void {
|
||||
if (this.field) {
|
||||
this.field.value = this.resolveExpressions(this.field.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected reevaluateExpressions(): void {
|
||||
if (this.field && this.originalFieldValue) {
|
||||
this.field.value = this.resolveExpressions(this.originalFieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ButtonWidgetComponent } from './button/button.widget';
|
||||
// core
|
||||
export * from './widget.component';
|
||||
export * from './reactive-widget.interface';
|
||||
export * from './base-display-text/base-display-text.widget';
|
||||
export * from './core';
|
||||
|
||||
// primitives
|
||||
|
||||
@@ -27,6 +27,7 @@ export * from './components/helpers/buttons-visibility';
|
||||
|
||||
export * from './services/form-rendering.service';
|
||||
export * from './services/form.service';
|
||||
export * from './services/form-expression.service';
|
||||
export * from './services/form-validation-service.interface';
|
||||
export * from './services/widget-visibility.service';
|
||||
|
||||
|
||||
351
lib/core/src/lib/form/services/form-expression.service.spec.ts
Normal file
351
lib/core/src/lib/form/services/form-expression.service.spec.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { FormExpressionService } from './form-expression.service';
|
||||
import { FormModel } from '../components/widgets/core';
|
||||
|
||||
describe('FormExpressionService', () => {
|
||||
let service: FormExpressionService;
|
||||
let formModel: FormModel;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [FormExpressionService]
|
||||
});
|
||||
service = TestBed.inject(FormExpressionService);
|
||||
formModel = new FormModel();
|
||||
});
|
||||
|
||||
describe('resolveExpressions', () => {
|
||||
it('should return the original string if there are no expressions', () => {
|
||||
const input = 'plain text without expressions';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
|
||||
it('should return empty string for null input', () => {
|
||||
const result = service.resolveExpressions(formModel, null);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string for undefined input', () => {
|
||||
const result = service.resolveExpressions(formModel, undefined);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should resolve field expression with value', () => {
|
||||
const mockField = {
|
||||
id: 'testField',
|
||||
value: 'test value'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.testField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(formModel.getFieldById).toHaveBeenCalledWith('testField');
|
||||
expect(result).toBe('test value');
|
||||
});
|
||||
|
||||
it('should resolve variable expression with value', () => {
|
||||
spyOn(formModel, 'getProcessVariableValue').and.returnValue('variable value');
|
||||
|
||||
const input = '${variable.myVar}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(formModel.getProcessVariableValue).toHaveBeenCalledWith('myVar');
|
||||
expect(result).toBe('variable value');
|
||||
});
|
||||
|
||||
it('should replace expression with empty string when field value is null', () => {
|
||||
const mockField = {
|
||||
id: 'testField',
|
||||
value: null
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.testField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should replace expression with empty string when field value is undefined', () => {
|
||||
const mockField = {
|
||||
id: 'testField',
|
||||
value: undefined
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.testField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should replace expression with empty string when field is not found', () => {
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(undefined);
|
||||
|
||||
const input = '${field.nonExistentField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should stringify non-string field values', () => {
|
||||
const mockField = {
|
||||
id: 'numericField',
|
||||
value: 42
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.numericField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('42');
|
||||
});
|
||||
|
||||
it('should stringify object field values', () => {
|
||||
const mockField = {
|
||||
id: 'objectField',
|
||||
value: { key: 'value', num: 123 }
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.objectField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('{"key":"value","num":123}');
|
||||
});
|
||||
|
||||
it('should stringify array field values', () => {
|
||||
const mockField = {
|
||||
id: 'arrayField',
|
||||
value: [1, 2, 3]
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.arrayField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('[1,2,3]');
|
||||
});
|
||||
|
||||
it('should resolve multiple expressions in the same string', () => {
|
||||
const mockField1 = {
|
||||
id: 'field1',
|
||||
value: 'value1'
|
||||
};
|
||||
const mockField2 = {
|
||||
id: 'field2',
|
||||
value: 'value2'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.callFake((id) => {
|
||||
if (id === 'field1') {
|
||||
return mockField1 as any;
|
||||
}
|
||||
if (id === 'field2') {
|
||||
return mockField2 as any;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const input = 'Hello ${field.field1} and ${field.field2}!';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('Hello value1 and value2!');
|
||||
});
|
||||
|
||||
it('should resolve mixed field and variable expressions', () => {
|
||||
const mockField = {
|
||||
id: 'myField',
|
||||
value: 'field value'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
spyOn(formModel, 'getProcessVariableValue').and.returnValue('var value');
|
||||
|
||||
const input = 'Field: ${field.myField}, Variable: ${variable.myVar}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('Field: field value, Variable: var value');
|
||||
});
|
||||
|
||||
it('should not resolve expressions with whitespace', () => {
|
||||
const mockField = {
|
||||
id: 'testField',
|
||||
value: 'test value'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${ field.testField }';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('${ field.testField }');
|
||||
});
|
||||
|
||||
it('should handle field names with underscores', () => {
|
||||
const mockField = {
|
||||
id: 'test_field_name',
|
||||
value: 'underscore value'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.test_field_name}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('underscore value');
|
||||
});
|
||||
|
||||
it('should handle field names with numbers', () => {
|
||||
const mockField = {
|
||||
id: 'field123',
|
||||
value: 'numeric value'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.field123}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('numeric value');
|
||||
});
|
||||
|
||||
it('should not resolve expressions with multiple variables', () => {
|
||||
const mockField1 = {
|
||||
id: 'field1',
|
||||
value: 'value1'
|
||||
};
|
||||
const mockField2 = {
|
||||
id: 'field2',
|
||||
value: 'value2'
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.callFake((id) => {
|
||||
if (id === 'field1') {
|
||||
return mockField1 as any;
|
||||
}
|
||||
if (id === 'field2') {
|
||||
return mockField2 as any;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const input = '${field.field1 field.field2}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
|
||||
it('should not resolve expressions without valid variable names', () => {
|
||||
const input = '${sometext}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle boolean field values', () => {
|
||||
const mockField = {
|
||||
id: 'boolField',
|
||||
value: true
|
||||
};
|
||||
spyOn(formModel, 'getFieldById').and.returnValue(mockField as any);
|
||||
|
||||
const input = '${field.boolField}';
|
||||
const result = service.resolveExpressions(formModel, input);
|
||||
|
||||
expect(result).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldDependencies', () => {
|
||||
it('should return empty array for string without expressions', () => {
|
||||
const input = 'plain text without expressions';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should extract single field dependency', () => {
|
||||
const input = '${field.testField}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['testField']);
|
||||
});
|
||||
|
||||
it('should extract multiple field dependencies', () => {
|
||||
const input = '${field.field1} and ${field.field2}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['field1', 'field2']);
|
||||
});
|
||||
|
||||
it('should not include duplicate field dependencies', () => {
|
||||
const input = '${field.testField} and ${field.testField}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['testField']);
|
||||
});
|
||||
|
||||
it('should not include variable dependencies', () => {
|
||||
const input = '${variable.myVar}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should extract only field dependencies when mixed with variables', () => {
|
||||
const input = '${field.myField} and ${variable.myVar}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['myField']);
|
||||
});
|
||||
|
||||
it('should handle field names with underscores', () => {
|
||||
const input = '${field.test_field_name}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['test_field_name']);
|
||||
});
|
||||
|
||||
it('should handle field names with numbers', () => {
|
||||
const input = '${field.field123}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['field123']);
|
||||
});
|
||||
|
||||
it('should handle multiple different fields', () => {
|
||||
const input = '${field.firstName} ${field.lastName} ${field.email}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['firstName', 'lastName', 'email']);
|
||||
});
|
||||
|
||||
it('should handle complex expressions with text', () => {
|
||||
const input = 'Hello ${field.firstName}, your email is ${field.email}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual(['firstName', 'email']);
|
||||
});
|
||||
|
||||
it('should return empty array for expressions without field prefix', () => {
|
||||
const input = '${sometext}';
|
||||
const result = service.getFieldDependencies(input);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
110
lib/core/src/lib/form/services/form-expression.service.ts
Normal file
110
lib/core/src/lib/form/services/form-expression.service.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FormModel } from '../components/widgets/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FormExpressionService {
|
||||
private readonly GLOBAL_EXPRESSION_REGEX = /\$\{[a-zA-Z0-9_.]+\}/g;
|
||||
private readonly FIELD_PREFIX = 'field.';
|
||||
private readonly VARIABLE_PREFIX = 'variable.';
|
||||
private readonly VARIABLES_REGEX = /(?:field|variable)\.[a-zA-Z_$][a-zA-Z0-9_$]*/g;
|
||||
|
||||
resolveExpressions(form: FormModel, formField: string): string {
|
||||
let result = formField || '';
|
||||
|
||||
const matches = result.match(this.GLOBAL_EXPRESSION_REGEX);
|
||||
|
||||
if (!matches) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
let expressionResult = this.resolveExpression(form, match);
|
||||
if (expressionResult === null || expressionResult === undefined) {
|
||||
expressionResult = '';
|
||||
} else if (typeof expressionResult !== 'string') {
|
||||
expressionResult = JSON.stringify(expressionResult);
|
||||
}
|
||||
result = result.replace(match, expressionResult);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private resolveExpression(form: FormModel, expression: any): any {
|
||||
if (expression === undefined || expression === null) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const expressionString = String(expression).trim();
|
||||
if (!expressionString.startsWith('${') || !expressionString.endsWith('}')) {
|
||||
return expressionString;
|
||||
}
|
||||
|
||||
const variableNames = expressionString.match(this.VARIABLES_REGEX);
|
||||
if (!variableNames || variableNames.length === 0) {
|
||||
return expressionString;
|
||||
}
|
||||
|
||||
if (variableNames.length === 1 && variableNames[0].length === expressionString.length - 3) {
|
||||
return this.resolveVariable(form, variableNames[0]);
|
||||
}
|
||||
|
||||
return expressionString;
|
||||
}
|
||||
|
||||
private resolveVariable(form: FormModel, variableName: string): any {
|
||||
if (variableName.startsWith(this.FIELD_PREFIX)) {
|
||||
const field = variableName.slice(this.FIELD_PREFIX.length);
|
||||
return form.getFieldById(field)?.value;
|
||||
} else if (variableName.startsWith(this.VARIABLE_PREFIX)) {
|
||||
const variable = variableName.slice(this.VARIABLE_PREFIX.length);
|
||||
return form.getProcessVariableValue(variable);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getFieldDependencies(expression: string): string[] {
|
||||
const dependencies: string[] = [];
|
||||
const matches = expression.match(this.GLOBAL_EXPRESSION_REGEX);
|
||||
|
||||
if (!matches) {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
const variableNames = match.match(this.VARIABLES_REGEX);
|
||||
if (variableNames) {
|
||||
for (const variableName of variableNames) {
|
||||
if (variableName.startsWith(this.FIELD_PREFIX)) {
|
||||
const fieldId = variableName.slice(this.FIELD_PREFIX.length);
|
||||
if (!dependencies.includes(fieldId)) {
|
||||
dependencies.push(fieldId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DisplayRichTextWidgetComponent, RICH_TEXT_PARSER_TOKEN } from './display-rich-text.widget';
|
||||
import { RichTextParserService } from '../../../services/rich-text-parser.service';
|
||||
import { FormFieldModel, FormModel } from '@alfresco/adf-core';
|
||||
import { ADF_DISPLAY_TEXT_SETTINGS, FormFieldModel, FormModel, FormService } from '@alfresco/adf-core';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('DisplayRichTextWidgetComponent', () => {
|
||||
let widget: DisplayRichTextWidgetComponent;
|
||||
let fixture: ComponentFixture<DisplayRichTextWidgetComponent>;
|
||||
let debugEl: DebugElement;
|
||||
let mockRichTextParserService: jasmine.SpyObj<RichTextParserService>;
|
||||
let formService: FormService;
|
||||
|
||||
const cssSelector = {
|
||||
parsedHTML: '.adf-display-rich-text-widget-parsed-html'
|
||||
@@ -89,11 +91,12 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService }]
|
||||
providers: [FormService, { provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService }]
|
||||
});
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
debugEl = fixture.debugElement;
|
||||
formService = TestBed.inject(FormService);
|
||||
widget.field = fakeFormField;
|
||||
});
|
||||
|
||||
@@ -119,7 +122,7 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalledWith(fakeFormField.value);
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalledTimes(1);
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should parse editorjs data to html', async () => {
|
||||
@@ -142,4 +145,325 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
const parsedHtmlEl = debugEl.query(By.css(cssSelector.parsedHTML));
|
||||
expect(parsedHtmlEl.nativeElement.innerHTML.includes('<img src="x" onerror="alert(\'XSS\')">')).toBe(false);
|
||||
});
|
||||
|
||||
describe('expression evaluation', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService },
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: { enableExpressionEvaluation: true }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
debugEl = fixture.debugElement;
|
||||
formService = TestBed.inject(FormService);
|
||||
mockRichTextParserService = TestBed.inject(RICH_TEXT_PARSER_TOKEN) as jasmine.SpyObj<RichTextParserService>;
|
||||
});
|
||||
|
||||
it('should resolve field expressions in rich text blocks', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should resolve expressions in multiple blocks', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'header',
|
||||
data: {
|
||||
text: 'User: ${field.firstName}',
|
||||
level: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Status: ${variable.status}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'firstName', type: 'text', value: 'Jane' }
|
||||
],
|
||||
variables: [{ id: 'status', name: 'status', type: 'string', value: 'Active' }]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('User: Jane');
|
||||
expect(widget.field.value.blocks[1].data.text).toBe('Status: Active');
|
||||
});
|
||||
|
||||
it('should update rich text when dependent field value changes', (done) => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
const nameField = form.getFieldById('name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
|
||||
nameField.value = 'Jane';
|
||||
formService.formRulesEvent.next({ type: 'fieldValueChanged', field: nameField } as any);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello Jane');
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
it('should preserve original value structure for re-evaluation', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
const originalValue = JSON.parse(widget['originalFieldValue']);
|
||||
expect(originalValue.blocks[0].data.text).toBe('Hello ${field.name}');
|
||||
});
|
||||
|
||||
it('should handle missing field references with empty string', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.nonExistent}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello ');
|
||||
});
|
||||
|
||||
it('should not resolve expressions when enableExpressionEvaluation is false', () => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService },
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: { enableExpressionEvaluation: false }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello ${field.name}');
|
||||
});
|
||||
|
||||
it('should re-parse HTML after expressions are evaluated', () => {
|
||||
mockRichTextParserService.parse.and.returnValue('<p>Test HTML</p>');
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalled();
|
||||
const lastCall = mockRichTextParserService.parse.calls.mostRecent();
|
||||
expect(lastCall.args[0].blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should support observable settings', (done) => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService },
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: of({ enableExpressionEvaluation: true })
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
import { Component, inject, InjectionToken, OnInit, SecurityContext, ViewEncapsulation } from '@angular/core';
|
||||
import { WidgetComponent } from '@alfresco/adf-core';
|
||||
import { Component, inject, InjectionToken, OnDestroy, OnInit, SecurityContext, ViewEncapsulation } from '@angular/core';
|
||||
import { BaseDisplayTextWidgetComponent } from '@alfresco/adf-core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RichTextParserService } from '../../../services/rich-text-parser.service';
|
||||
|
||||
export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>('RichTextParserService', {
|
||||
@@ -43,13 +44,58 @@ export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>(
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class DisplayRichTextWidgetComponent extends WidgetComponent implements OnInit {
|
||||
export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetComponent implements OnInit, OnDestroy {
|
||||
parsedHTML: string | Error;
|
||||
|
||||
private readonly richTextParserService = inject(RICH_TEXT_PARSER_TOKEN);
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
private fieldChangedSubscription?: Subscription;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.parseAndSanitize();
|
||||
|
||||
// Re-parse when field changes (after expressions are evaluated)
|
||||
this.fieldChangedSubscription = this.fieldChanged.subscribe(() => {
|
||||
this.parseAndSanitize();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.fieldChangedSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
protected storeOriginalValue(): void {
|
||||
if (this.field) {
|
||||
this.originalFieldValue = JSON.stringify(this.field.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected evaluateExpressions(): void {
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = JSON.parse(JSON.stringify(this.field.value));
|
||||
this.applyExpressionsToBlocks(value);
|
||||
}
|
||||
|
||||
protected reevaluateExpressions(): void {
|
||||
if (!this.field || !this.originalFieldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = JSON.parse(this.originalFieldValue);
|
||||
this.applyExpressionsToBlocks(value);
|
||||
}
|
||||
|
||||
private applyExpressionsToBlocks(value: any): void {
|
||||
for (const block of value.blocks) {
|
||||
block.data.text = this.resolveExpressions(block.data.text);
|
||||
}
|
||||
this.field.value = value;
|
||||
}
|
||||
|
||||
private parseAndSanitize(): void {
|
||||
this.parsedHTML = this.richTextParserService.parse(this.field.value);
|
||||
|
||||
if (this.parsedHTML instanceof Error) {
|
||||
|
||||
Reference in New Issue
Block a user