diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts
index ae527ea62d..5a480e7773 100644
--- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts
+++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts
@@ -18,6 +18,7 @@
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import {
+ FixedValueFieldValidator,
MaxLengthFieldValidator,
MaxValueFieldValidator,
MinLengthFieldValidator,
@@ -533,4 +534,57 @@ describe('FormFieldValidator', () => {
});
});
+
+ describe('FixedValueFieldValidator', () => {
+
+ let validator: FixedValueFieldValidator;
+
+ beforeEach(() => {
+ validator = new FixedValueFieldValidator();
+ });
+
+ it('should support only typeahead field', () => {
+ let field = new FormFieldModel(new FormModel(), {
+ type: FormFieldTypes.TEXT
+ });
+ expect(validator.isSupported(field)).toBeFalsy();
+
+ field = new FormFieldModel(new FormModel(), {
+ type: FormFieldTypes.TYPEAHEAD
+ });
+
+ expect(validator.isSupported(field)).toBeTruthy();
+ });
+
+ it('should allow empty values', () => {
+ let field = new FormFieldModel(new FormModel(), {
+ type: FormFieldTypes.TYPEAHEAD,
+ value: null,
+ regexPattern: 'pattern'
+ });
+
+ expect(validator.validate(field)).toBeTruthy();
+ });
+
+ it('should succeed for a valid input value in options', () => {
+ let field = new FormFieldModel(new FormModel(), {
+ type: FormFieldTypes.TYPEAHEAD,
+ value: '1',
+ options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}]
+ });
+
+ expect(validator.validate(field)).toBeTruthy();
+ });
+
+ it('should fail for an invalid input value in options', () => {
+ let field = new FormFieldModel(new FormModel(), {
+ type: FormFieldTypes.TYPEAHEAD,
+ value: 'Lean',
+ options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}]
+ });
+
+ expect(validator.validate(field)).toBeFalsy();
+ });
+
+ });
});
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts
index f1a7756303..51639b3ca6 100644
--- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts
+++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts
@@ -366,6 +366,47 @@ export class RegExFieldValidator implements FormFieldValidator {
}
+export class FixedValueFieldValidator implements FormFieldValidator {
+
+ private supportedTypes = [
+ FormFieldTypes.TYPEAHEAD
+ ];
+
+ isSupported(field: FormFieldModel): boolean {
+ return field && this.supportedTypes.indexOf(field.type) > -1;
+ }
+
+ hasValidNameOrValidId(field: FormFieldModel): boolean {
+ return this.hasValidName(field) || this.hasValidId(field);
+ }
+
+ hasValidName(field: FormFieldModel) {
+ return field.options.find(item => item.name && item.name.toLocaleLowerCase() === field.value.toLocaleLowerCase()) ? true : false;
+ }
+
+ hasValidId(field: FormFieldModel) {
+ return field.options[field.value - 1] ? true : false;
+ }
+
+ hasStringValue(field: FormFieldModel) {
+ return field.value && typeof field.value === 'string';
+ }
+
+ hasOptions(field: FormFieldModel) {
+ return field.options && field.options.length > 0;
+ }
+
+ validate(field: FormFieldModel): boolean {
+ if (this.isSupported(field)) {
+ if (this.hasStringValue(field) && this.hasOptions(field) && !this.hasValidNameOrValidId(field)) {
+ field.validationSummary = 'Invalid data inserted';
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
export const FORM_FIELD_VALIDATORS = [
new RequiredFieldValidator(),
new NumberFieldValidator(),
@@ -376,5 +417,6 @@ export const FORM_FIELD_VALIDATORS = [
new RegExFieldValidator(),
new DateFieldValidator(),
new MinDateFieldValidator(),
- new MaxDateFieldValidator()
+ new MaxDateFieldValidator(),
+ new FixedValueFieldValidator()
];
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html
index c5eea0646e..1289f51401 100644
--- a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html
+++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html
@@ -10,18 +10,16 @@
type="text"
[id]="field.id"
[(ngModel)]="value"
+ (ngModelChange)="validate()"
(keyup)="onKeyUp($event)"
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}"
[mdAutocomplete]="auto">
-
- {{item.name}}
+
+ {{item.name}}
-
- {{field.value}}
-
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts
index f159a7a3cb..681902efdb 100644
--- a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts
+++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
+import { OverlayContainer } from '@angular/cdk/overlay';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreModule, LogServiceMock } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx';
@@ -37,6 +38,7 @@ describe('TypeaheadWidgetComponent', () => {
let widget: TypeaheadWidgetComponent;
let visibilityService: WidgetVisibilityService;
let logService: LogServiceMock;
+ let overlayContainerElement: HTMLElement;
beforeEach(() => {
logService = new LogServiceMock();
@@ -108,23 +110,6 @@ describe('TypeaheadWidgetComponent', () => {
expect(widget.handleError).toHaveBeenCalledWith(err);
});
- it('should prevent default behaviour on option item click', () => {
- let event = jasmine.createSpyObj('event', ['preventDefault']);
- widget.onItemClick(null, event);
- expect(event.preventDefault).toHaveBeenCalled();
- });
-
- it('should update values on option item click', () => {
- let option: FormFieldOption = {
- id: '1',
- name: 'name'
- };
- spyOn(visibilityService, 'refreshVisibility').and.stub();
- widget.onItemClick(option, null);
- expect(widget.field.value).toBe(option.id);
- expect(widget.value).toBe(option.name);
- });
-
it('should setup initial value', () => {
spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => {
observer.next([
@@ -208,89 +193,6 @@ describe('TypeaheadWidgetComponent', () => {
expect(filtered[0]).toEqual(options[1]);
});
- it('should update form on value flush', () => {
- spyOn(widget.field, 'updateForm').and.callThrough();
- widget.flushValue();
- expect(widget.field.updateForm).toHaveBeenCalled();
- });
-
- it('should flush selected value', () => {
- let options: FormFieldOption[] = [
- { id: '1', name: 'Item one' },
- { id: '2', name: 'Item Two' }
- ];
-
- widget.field.options = options;
- widget.value = 'Item Two';
- widget.flushValue();
-
- expect(widget.value).toBe(options[1].name);
- expect(widget.field.value).toBe(options[1].id);
- });
-
- it('should be case insensitive when flushing value', () => {
- let options: FormFieldOption[] = [
- { id: '1', name: 'Item one' },
- { id: '2', name: 'iTEM TWo' }
- ];
-
- widget.field.options = options;
- widget.value = 'ITEM TWO';
- widget.flushValue();
-
- expect(widget.value).toBe(options[1].name);
- expect(widget.field.value).toBe(options[1].id);
- });
-
- it('should reset fields when flushing missing option value', () => {
- widget.field.options = [
- { id: '1', name: 'Item one' },
- { id: '2', name: 'Item two' }
- ];
- widget.value = 'Missing item';
- widget.flushValue();
-
- expect(widget.value).toBeNull();
- expect(widget.field.value).toBeNull();
- });
-
- it('should reset fields when flushing incorrect value', () => {
- widget.field.options = [
- { id: '1', name: 'Item one' },
- { id: '2', name: 'Item two' }
- ];
- widget.field.value = 'Item two';
- widget.value = 'Item two!';
- widget.flushValue();
-
- expect(widget.value).toBeNull();
- expect(widget.field.value).toBeNull();
- });
-
- it('should reset fields when flushing value having no options', () => {
- widget.field.options = null;
- widget.field.value = 'item 1';
- widget.value = 'new item';
- widget.flushValue();
-
- expect(widget.value).toBeNull();
- expect(widget.field.value).toBeNull();
- });
-
- it('should emit field change event on item click', () => {
- let event = jasmine.createSpyObj('event', ['preventDefault']);
- let fakeField = new FormFieldModel(new FormModel(), { id: 'fakeField', value: 'fakeValue' });
- widget.field = fakeField;
- let item = { id: 'fake-id-opt', name: 'fake-name-opt' };
- widget.onItemClick(item, event);
-
- widget.fieldChanged.subscribe((field) => {
- expect(field).toBeDefined();
- expect(field.id).toEqual('fakeField');
- expect(field.value).toEqual('fake-id-opt');
- });
- });
-
describe('when template is ready', () => {
let typeaheadWidgetComponent: TypeaheadWidgetComponent;
let fixture: ComponentFixture;
@@ -308,7 +210,19 @@ describe('TypeaheadWidgetComponent', () => {
TestBed.configureTestingModule({
imports: [CoreModule, MaterialModule],
declarations: [TypeaheadWidgetComponent, ErrorWidgetComponent],
- providers: [FormService, EcmModelService, WidgetVisibilityService]
+ providers: [
+ {provide: OverlayContainer, useFactory: () => {
+ overlayContainerElement = document.createElement('div');
+ overlayContainerElement.classList.add('cdk-overlay-container');
+
+ document.body.appendChild(overlayContainerElement);
+
+ // remove body padding to keep consistent cross-browser
+ document.body.style.padding = '0';
+ document.body.style.margin = '0';
+
+ return {getContainerElement: () => overlayContainerElement};
+ }}, FormService, EcmModelService, WidgetVisibilityService]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(TypeaheadWidgetComponent);
typeaheadWidgetComponent = fixture.componentInstance;
@@ -321,6 +235,33 @@ describe('TypeaheadWidgetComponent', () => {
TestBed.resetTestingModule();
});
+ describe ('and typeahead is in readonly mode', () => {
+
+ it('should show typeahead value with input disabled', async(() => {
+ typeaheadWidgetComponent.field = new FormFieldModel(
+ new FormModel({ processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), {
+ id: 'typeahead-id',
+ name: 'typeahead-name',
+ type: 'readonly',
+ params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } }
+ });
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ let readonlyInput: HTMLInputElement = element.querySelector('#typeahead-id');
+ expect(readonlyInput.disabled).toBeTruthy();
+ expect(readonlyInput).not.toBeNull();
+ expect(readonlyInput.value).toBe('FakeProcessValue');
+ });
+ }));
+
+ afterEach(() => {
+ fixture.destroy();
+ TestBed.resetTestingModule();
+ });
+
+ });
+
describe('and typeahead is populated via taskId', () => {
beforeEach(async(() => {
@@ -344,14 +285,58 @@ describe('TypeaheadWidgetComponent', () => {
}));
it('should show typeahead options', async(() => {
- let keyboardEvent = new KeyboardEvent('keypress');
+ let typeahedElement = fixture.debugElement.query(By.css('#typeahead-id'));
+ let typeahedHTMLElement: HTMLInputElement = typeahedElement.nativeElement;
+ typeahedHTMLElement.focus();
typeaheadWidgetComponent.value = 'F';
+ typeahedHTMLElement.value = 'F';
+ typeahedHTMLElement.dispatchEvent(new Event('keyup'));
+ typeahedHTMLElement.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).not.toBeNull();
+ expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_2"]'))).not.toBeNull();
+ expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_3"]'))).not.toBeNull();
+ });
+ }));
+
+ it('should hide the option when the value is empty', async(() => {
+ let typeahedElement = fixture.debugElement.query(By.css('#typeahead-id'));
+ let typeahedHTMLElement: HTMLInputElement = typeahedElement.nativeElement;
+ typeahedHTMLElement.focus();
+ typeaheadWidgetComponent.value = 'F';
+ typeahedHTMLElement.value = 'F';
+ typeahedHTMLElement.dispatchEvent(new Event('keyup'));
+ typeahedHTMLElement.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).not.toBeNull();
+ typeahedHTMLElement.focus();
+ typeaheadWidgetComponent.value = '';
+ typeahedHTMLElement.dispatchEvent(new Event('keyup'));
+ typeahedHTMLElement.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).toBeNull();
+ });
+ });
+ }));
+
+ it('should show error message when the value is not valid', async(() => {
+ typeaheadWidgetComponent.value = 'Fake Name';
+ typeaheadWidgetComponent.field.value = 'Fake Name';
+ typeaheadWidgetComponent.field.options = fakeOptionList;
+ expect(element.querySelector('.adf-error-text')).toBeNull();
+ let keyboardEvent = new KeyboardEvent('keypress');
typeaheadWidgetComponent.onKeyUp(keyboardEvent);
fixture.detectChanges();
fixture.whenStable().then(() => {
- expect(fixture.debugElement.queryAll(By.css('[id="md-option-1"]'))).toBeDefined();
- expect(fixture.debugElement.queryAll(By.css('[id="md-option-2"]'))).toBeDefined();
- expect(fixture.debugElement.queryAll(By.css('[id="md-option-3"]'))).toBeDefined();
+ fixture.detectChanges();
+ expect(element.querySelector('.adf-error-text')).not.toBeNull();
+ expect(element.querySelector('.adf-error-text').textContent).toContain('Invalid data inserted');
});
}));
@@ -362,27 +347,6 @@ describe('TypeaheadWidgetComponent', () => {
expect(element.querySelector('#typeahead-id')).toBeNull();
});
}));
-
- it('should show typeahead value when the type is readonly', async(() => {
- typeaheadWidgetComponent.field = new FormFieldModel(
- new FormModel({ taskId: 'fake-task-id', processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), {
- id: 'typeahead-id',
- name: 'typeahead-name',
- type: 'readonly',
- value: '9',
- params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } }
- });
- fixture.detectChanges();
- const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
- trigger.click();
- fixture.detectChanges();
- fixture.whenStable().then(() => {
- expect(element.querySelector('#typeahead-id')).not.toBeNull();
- let optionElement: HTMLElement = document.body.querySelector('.mat-option');
- expect(optionElement.innerText).toEqual('FakeProcessValue');
- });
- }));
-
});
describe('and typeahead is populated via processDefinitionId', () => {
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts
index 1d10fca7d5..0a35f61a8c 100644
--- a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts
+++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
- /* tslint:disable:component-selector */
+/* tslint:disable:component-selector */
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
@@ -23,7 +23,7 @@ import { LogService } from 'ng2-alfresco-core';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormService } from './../../../services/form.service';
import { FormFieldOption } from './../core/form-field-option';
-import { baseHost , WidgetComponent } from './../widget.component';
+import { baseHost, WidgetComponent } from './../widget.component';
@Component({
selector: 'typeahead-widget',
@@ -42,7 +42,7 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
constructor(public formService: FormService,
private visibilityService: WidgetVisibilityService,
private logService: LogService) {
- super(formService);
+ super(formService);
}
ngOnInit() {
@@ -51,101 +51,89 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
} else if (this.field.form.processDefinitionId) {
this.getValuesByProcessDefinitionId();
}
+ if (this.isReadOnlyType()) {
+ this.value = this.field.value;
+ }
}
getValuesByTaskId() {
this.formService
.getRestFieldValues(
- this.field.form.taskId,
- this.field.id
+ this.field.form.taskId,
+ this.field.id
)
.subscribe(
- (result: FormFieldOption[]) => {
- let options = result || [];
- this.field.options = options;
+ (result: FormFieldOption[]) => {
+ let options = result || [];
+ this.field.options = options;
- let fieldValue = this.field.value;
- if (fieldValue) {
- let toSelect = options.find(item => item.id === fieldValue);
- if (toSelect) {
- this.value = toSelect.name;
- }
+ let fieldValue = this.field.value;
+ if (fieldValue) {
+ let toSelect = options.find(item => item.id === fieldValue);
+ if (toSelect) {
+ this.value = toSelect.name;
}
- this.field.updateForm();
- this.visibilityService.refreshEntityVisibility(this.field);
- },
- err => this.handleError(err)
+ }
+ this.field.updateForm();
+ this.visibilityService.refreshEntityVisibility(this.field);
+ },
+ err => this.handleError(err)
);
}
getValuesByProcessDefinitionId() {
this.formService
.getRestFieldValuesByProcessId(
- this.field.form.processDefinitionId,
- this.field.id
+ this.field.form.processDefinitionId,
+ this.field.id
)
.subscribe(
- (result: FormFieldOption[]) => {
- let options = result || [];
- this.field.options = options;
+ (result: FormFieldOption[]) => {
+ let options = result || [];
+ this.field.options = options;
- let fieldValue = this.field.value;
- if (fieldValue) {
- let toSelect = options.find(item => item.id === fieldValue);
- if (toSelect) {
- this.value = toSelect.name;
- }
+ let fieldValue = this.field.value;
+ if (fieldValue) {
+ let toSelect = options.find(item => item.id === fieldValue);
+ if (toSelect) {
+ this.value = toSelect.name;
}
- this.field.updateForm();
- this.visibilityService.refreshEntityVisibility(this.field);
- },
- err => this.handleError(err)
+ }
+ this.field.updateForm();
+ this.visibilityService.refreshEntityVisibility(this.field);
+ },
+ err => this.handleError(err)
);
}
getOptions(): FormFieldOption[] {
- let val = this.value.toLocaleLowerCase();
+ let val = this.value.trim().toLocaleLowerCase();
return this.field.options.filter(item => {
let name = item.name.toLocaleLowerCase();
return name.indexOf(val) > -1;
});
}
+ isValidOptionName(optionName: string): boolean {
+ let option = this.field.options.find(item => item.name && item.name.toLocaleLowerCase() === optionName.toLocaleLowerCase());
+ return option ? true : false;
+ }
+
onKeyUp(event: KeyboardEvent) {
- if (this.value && this.value.length >= this.minTermLength && this.oldValue !== this.value) {
+ if (this.value && this.value.trim().length >= this.minTermLength && this.oldValue !== this.value) {
if (event.keyCode !== ESCAPE && event.keyCode !== ENTER) {
if (this.value.length >= this.minTermLength) {
this.options = this.getOptions();
this.oldValue = this.value;
+ if (this.isValidOptionName(this.value)) {
+ this.field.value = this.options[0].id;
+ }
}
}
}
- }
-
- flushValue() {
- let options = this.field.options || [];
- let lValue = this.value ? this.value.toLocaleLowerCase() : null;
-
- let field = options.find(item => item.name && item.name.toLocaleLowerCase() === lValue);
- if (field) {
- this.field.value = field.id;
- this.value = field.name;
- } else {
- this.field.value = null;
- this.value = null;
- }
-
- this.field.updateForm();
- }
-
- onItemClick(item: FormFieldOption, event: Event) {
- if (item) {
- this.field.value = item.id;
- this.value = item.name;
- this.checkVisibility();
- }
- if (event) {
- event.preventDefault();
+ if (this.isValueDefined() && this.value.trim().length === 0) {
+ this.oldValue = this.value;
+ this.options = [];
}
}
@@ -157,6 +145,14 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
}
}
+ validate() {
+ this.field.value = this.value;
+ }
+
+ isValueDefined() {
+ return this.value !== null && this.value !== undefined;
+ }
+
handleError(error: any) {
this.logService.error(error);
}