#967 rework container model structure

- now FormFieldModel provides information about columns and child fields
- greatly simplified models
- visibility engine fixes
This commit is contained in:
Denys Vuika 2016-11-15 15:13:05 +00:00 committed by Mario Romano
parent e114534bc5
commit ee56a1faa6
17 changed files with 97 additions and 135 deletions

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, OnInit, ViewChild, ViewContainerRef, Input, ComponentRef, ComponentFactoryResolver, Output, EventEmitter/*, Injector*/ } from '@angular/core'; import { Component, OnInit, ViewChild, ViewContainerRef, Input, ComponentRef, ComponentFactoryResolver/*,Output, EventEmitter, Injector*/ } from '@angular/core';
import { WidgetVisibilityService } from './../../services/widget-visibility.service'; import { WidgetVisibilityService } from './../../services/widget-visibility.service';
import { FormRenderingService } from './../../services/form-rendering.service'; import { FormRenderingService } from './../../services/form-rendering.service';
import { WidgetComponent } from './../widgets/widget.component'; import { WidgetComponent } from './../widgets/widget.component';
@ -23,7 +23,11 @@ import { FormFieldModel/*, FormWidgetModel*/ } from './../widgets/core/index';
@Component({ @Component({
selector: 'form-field', selector: 'form-field',
template: `<div #container></div>` template: `
<div [hidden]="!field.isVisible">
<div #container></div>
</div>
`
}) })
export class FormFieldComponent implements OnInit { export class FormFieldComponent implements OnInit {
@ -33,10 +37,6 @@ export class FormFieldComponent implements OnInit {
@Input() @Input()
field: FormFieldModel = null; field: FormFieldModel = null;
/** @deprecated component handles visibilty itself */
@Output()
fieldChanged: EventEmitter<FormFieldModel> = new EventEmitter<FormFieldModel>();
private componentRef: ComponentRef<{}>; private componentRef: ComponentRef<{}>;
constructor( constructor(
@ -54,12 +54,10 @@ export class FormFieldComponent implements OnInit {
this.componentRef = this.container.createComponent(factory/*, 0, this.injector*/); this.componentRef = this.container.createComponent(factory/*, 0, this.injector*/);
let instance = <WidgetComponent>this.componentRef.instance; let instance = <WidgetComponent>this.componentRef.instance;
instance.field = this.field; instance.field = this.field;
instance.fieldChanged.subscribe(args => { instance.fieldChanged.subscribe(field => {
if (this.field && this.field.form) { if (field && field.form) {
this.visibilityService.refreshVisibility(this.field.form); this.visibilityService.refreshVisibility(field.form);
} }
/** @deprecated */
this.fieldChanged.emit(args);
}); });
} }
} }

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { ContainerColumnModel } from './container-column.model'; import { ContainerColumnModel } from './../core/container-column.model';
import { FormModel } from './../core/form.model'; import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model'; import { FormFieldModel } from './../core/form-field.model';

View File

@ -1,3 +1,4 @@
<div>{{content?.isVisible}}</div>
<div class="container-widget"> <div class="container-widget">
<div *ngIf="content?.isGroup() && content?.isVisible" class="container-widget__header"> <div *ngIf="content?.isGroup() && content?.isVisible" class="container-widget__header">
<h4 class="container-widget__header-text" id="container-header" <h4 class="container-widget__header-text" id="container-header"
@ -15,7 +16,7 @@
<div *ngFor="let col of content.columns" class="mdl-cell mdl-cell--{{col.size}}-col"> <div *ngFor="let col of content.columns" class="mdl-cell mdl-cell--{{col.size}}-col">
<div class="mdl-grid" *ngIf="col.hasFields()"> <div class="mdl-grid" *ngIf="col.hasFields()">
<div *ngFor="let field of col.fields" class="mdl-cell mdl-cell--12-col"> <div *ngFor="let field of col.fields" class="mdl-cell mdl-cell--12-col">
<form-field [field]="field" [hidden]="!field.isVisible"></form-field> <form-field [field]="field"></form-field>
</div> </div>
</div> </div>
</div> </div>

View File

@ -17,52 +17,26 @@
import { ContainerWidgetModel } from './container.widget.model'; import { ContainerWidgetModel } from './container.widget.model';
import { FormModel } from './../core/form.model'; import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model';
import { FormFieldTypes } from './../core/form-field-types'; import { FormFieldTypes } from './../core/form-field-types';
describe('ContainerWidgetModel', () => { describe('ContainerWidgetModel', () => {
it('should store the form reference', () => { it('should store the form reference', () => {
let form = new FormModel(); let form = new FormModel();
let model = new ContainerWidgetModel(form); let field = new FormFieldModel(form);
let model = new ContainerWidgetModel(field);
expect(model.form).toBe(form); expect(model.form).toBe(form);
}); });
it('should store original json', () => {
let json = {};
let model = new ContainerWidgetModel(null, json);
expect(model.json).toBe(json);
});
it('should have 1 column layout by default', () => {
let container = new ContainerWidgetModel(null, null);
expect(container.numberOfColumns).toBe(1);
});
it('should be expanded by default', () => { it('should be expanded by default', () => {
let container = new ContainerWidgetModel(null, null); let container = new ContainerWidgetModel(null);
expect(container.isExpanded).toBeTruthy(); expect(container.isExpanded).toBeTruthy();
}); });
/*
it('should setup with json config', () => {
let json = {
fieldType: '<type>',
id: '<id>',
name: '<name>',
type: '<type>',
tab: '<tab>',
numberOfColumns: 2,
params: {}
};
let container = new ContainerWidgetModel(null, json);
Object.keys(json).forEach(key => {
expect(container[key]).toEqual(json[key]);
});
});
*/
it('should wrap fields into columns on setup', () => { it('should wrap fields into columns on setup', () => {
let form = new FormModel(); let form = new FormModel();
let json = { let json = {
fieldType: '<type>', fieldType: '<type>',
id: '<id>', id: '<id>',
@ -83,7 +57,10 @@ describe('ContainerWidgetModel', () => {
'3': null '3': null
} }
}; };
let container = new ContainerWidgetModel(form, json);
let field = new FormFieldModel(form, json);
let container = new ContainerWidgetModel(field);
expect(container.columns.length).toBe(3); expect(container.columns.length).toBe(3);
let col1 = container.columns[0]; let col1 = container.columns[0];
@ -100,47 +77,47 @@ describe('ContainerWidgetModel', () => {
}); });
it('should allow collapsing only when of a group type', () => { it('should allow collapsing only when of a group type', () => {
let container = new ContainerWidgetModel(new FormModel(), { let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.CONTAINER, type: FormFieldTypes.CONTAINER,
params: { params: {
allowCollapse: true allowCollapse: true
} }
}); }));
expect(container.isCollapsible()).toBeFalsy(); expect(container.isCollapsible()).toBeFalsy();
container = new ContainerWidgetModel(new FormModel(), { container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
params: { params: {
allowCollapse: true allowCollapse: true
} }
}); }));
expect(container.isCollapsible()).toBeTruthy(); expect(container.isCollapsible()).toBeTruthy();
}); });
it('should allow collapsing only when explicitly defined in params', () => { it('should allow collapsing only when explicitly defined in params', () => {
let container = new ContainerWidgetModel(new FormModel(), { let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
params: {} params: {}
}); }));
expect(container.isCollapsible()).toBeFalsy(); expect(container.isCollapsible()).toBeFalsy();
container = new ContainerWidgetModel(new FormModel(), { container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
params: { params: {
allowCollapse: true allowCollapse: true
} }
}); }));
expect(container.isCollapsible()).toBeTruthy(); expect(container.isCollapsible()).toBeTruthy();
}); });
it('should be collapsed by default', () => { it('should be collapsed by default', () => {
let container = new ContainerWidgetModel(new FormModel(), { let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
params: { params: {
allowCollapse: true, allowCollapse: true,
collapseByDefault: true collapseByDefault: true
} }
}); }));
expect(container.isCollapsedByDefault()).toBeTruthy(); expect(container.isCollapsedByDefault()).toBeTruthy();
}); });

View File

@ -16,14 +16,12 @@
*/ */
import { ContainerModel } from './../core/container.model'; import { ContainerModel } from './../core/container.model';
import { FormModel } from './../core/form.model'; import { ContainerColumnModel } from './../core/container-column.model';
import { ContainerColumnModel } from './container-column.model';
import { FormFieldTypes } from './../core/form-field-types'; import { FormFieldTypes } from './../core/form-field-types';
import { FormFieldModel } from './../core/form-field.model'; import { FormFieldModel } from './../core/form-field.model';
export class ContainerWidgetModel extends ContainerModel { export class ContainerWidgetModel extends ContainerModel {
numberOfColumns: number = 1;
columns: ContainerColumnModel[] = []; columns: ContainerColumnModel[] = [];
isExpanded: boolean = true; isExpanded: boolean = true;
@ -51,48 +49,12 @@ export class ContainerWidgetModel extends ContainerModel {
return collapseByDefault; return collapseByDefault;
} }
constructor(form: FormModel, json?: any) { constructor(field: FormFieldModel) {
super(form, json); super(field);
if (json) {
this.numberOfColumns = <number> json.numberOfColumns;
let columnSize: number = 12;
if (this.numberOfColumns > 1) {
columnSize = 12 / this.numberOfColumns;
}
for (let i = 0; i < this.numberOfColumns; i++) {
let col = new ContainerColumnModel();
col.size = columnSize;
this.columns.push(col);
}
if (json.fields) {
Object.keys(json.fields).map(key => {
let fields = (json.fields[key] || []).map(f => new FormFieldModel(form, f));
let col = this.columns[parseInt(key, 10) - 1];
col.fields = fields;
});
}
if (this.field) {
this.columns = this.field.columns || [];
this.isExpanded = !this.isCollapsedByDefault(); this.isExpanded = !this.isCollapsedByDefault();
this.children = this.getFormFields();
} }
} }
private getFormFields(): FormFieldModel[] {
let result: FormFieldModel[] = [];
for (let j = 0; j < this.columns.length; j++) {
let column = this.columns[j];
for (let k = 0; k < column.fields.length; k++) {
let field = column.fields[k];
result.push(field);
}
}
return result;
}
} }

View File

@ -53,12 +53,12 @@ describe('ContainerWidget', () => {
}); });
it('should toggle underlying group container', () => { it('should toggle underlying group container', () => {
let container = new ContainerWidgetModel(new FormModel(), { let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
params: { params: {
allowCollapse: true allowCollapse: true
} }
}); }));
let widget = new ContainerWidget(); let widget = new ContainerWidget();
widget.content = container; widget.content = container;
@ -71,9 +71,9 @@ describe('ContainerWidget', () => {
}); });
it('should toggle only collapsible container', () => { it('should toggle only collapsible container', () => {
let container = new ContainerWidgetModel(new FormModel(), { let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP type: FormFieldTypes.GROUP
}); }));
let widget = new ContainerWidget(); let widget = new ContainerWidget();
widget.content = container; widget.content = container;
@ -84,12 +84,12 @@ describe('ContainerWidget', () => {
}); });
it('should toggle only group container', () => { it('should toggle only group container', () => {
let container = new ContainerWidgetModel(new FormModel(), { let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.CONTAINER, type: FormFieldTypes.CONTAINER,
params: { params: {
allowCollapse: true allowCollapse: true
} }
}); }));
let widget = new ContainerWidget(); let widget = new ContainerWidget();
widget.content = container; widget.content = container;
@ -134,18 +134,18 @@ describe('ContainerWidget', () => {
beforeEach(() => { beforeEach(() => {
componentHandler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered', 'upgradeElement']); componentHandler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered', 'upgradeElement']);
window['componentHandler'] = componentHandler; window['componentHandler'] = componentHandler;
fakeContainerVisible = new ContainerWidgetModel(new FormModel(fakeFormJson), { fakeContainerVisible = new ContainerWidgetModel(new FormFieldModel(new FormModel(fakeFormJson), {
fieldType: FormFieldTypes.GROUP, fieldType: FormFieldTypes.GROUP,
id: 'fake-cont-id-1', id: 'fake-cont-id-1',
name: 'fake-cont-1-name', name: 'fake-cont-1-name',
type: FormFieldTypes.GROUP type: FormFieldTypes.GROUP
}); }));
fakeContainerInvisible = new ContainerWidgetModel(new FormModel(fakeFormJson), { fakeContainerInvisible = new ContainerWidgetModel(new FormFieldModel(new FormModel(fakeFormJson), {
fieldType: FormFieldTypes.GROUP, fieldType: FormFieldTypes.GROUP,
id: 'fake-cont-id-2', id: 'fake-cont-id-2',
name: 'fake-cont-2-name', name: 'fake-cont-2-name',
type: FormFieldTypes.GROUP type: FormFieldTypes.GROUP
}); }));
fakeContainerVisible.field.isVisible = true; fakeContainerVisible.field.isVisible = true;
fakeContainerInvisible.field.isVisible = false; fakeContainerInvisible.field.isVisible = false;
}); });

View File

@ -37,7 +37,7 @@ export class ContainerWidget extends WidgetComponent implements OnInit, AfterVie
ngOnInit() { ngOnInit() {
if (this.field) { if (this.field) {
this.content = new ContainerWidgetModel(this.field.form, this.field.json); this.content = new ContainerWidgetModel(this.field);
} }
} }

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { FormFieldModel } from './../core/form-field.model'; import { FormFieldModel } from './form-field.model';
export class ContainerColumnModel { export class ContainerColumnModel {

View File

@ -17,19 +17,14 @@
import { ContainerModel } from './container.model'; import { ContainerModel } from './container.model';
import { FormModel } from './form.model'; import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model';
describe('ContainerModel', () => { describe('ContainerModel', () => {
it('should store the form reference', () => { it('should store the form reference', () => {
let form = new FormModel(); let form = new FormModel();
let model = new ContainerModel(form); let model = new ContainerModel(new FormFieldModel(form));
expect(model.form).toBe(form); expect(model.form).toBe(form);
}); });
it('should store original json', () => {
let json = {};
let model = new ContainerModel(null, json);
expect(model.json).toBe(json);
});
}); });

View File

@ -16,23 +16,20 @@
*/ */
import { FormWidgetModel } from './form-widget.model'; import { FormWidgetModel } from './form-widget.model';
import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model'; import { FormFieldModel } from './form-field.model';
export class ContainerModel extends FormWidgetModel { export class ContainerModel extends FormWidgetModel {
field: FormFieldModel; field: FormFieldModel;
children: FormFieldModel[] = [];
get isVisible(): boolean { get isVisible(): boolean {
return this.field.isVisible; return this.field.isVisible;
} }
constructor(form: FormModel, json?: any) { constructor(field: FormFieldModel) {
super(form, json); if (field) {
super(field.form, field.json);
if (json) { this.field = field;
this.field = new FormFieldModel(form, json);
} }
} }

View File

@ -20,6 +20,7 @@ import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types'; import { FormFieldTypes } from './form-field-types';
import { FormFieldMetadata } from './form-field-metadata'; import { FormFieldMetadata } from './form-field-metadata';
import { FormModel } from './form.model'; import { FormModel } from './form.model';
import { ContainerColumnModel } from './container-column.model';
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model'; import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
import { import {
FormFieldValidator, FormFieldValidator,
@ -74,7 +75,12 @@ export class FormFieldModel extends FormWidgetModel {
enableFractions: boolean = false; enableFractions: boolean = false;
currency: string = null; currency: string = null;
// advanced members // container model members
numberOfColumns: number = 1;
fields: FormFieldModel[] = [];
columns: ContainerColumnModel[] = [];
// util members
emptyOption: FormFieldOption; emptyOption: FormFieldOption;
validationSummary: string; validationSummary: string;
validators: FormFieldValidator[] = []; validators: FormFieldValidator[] = [];
@ -150,6 +156,30 @@ export class FormFieldModel extends FormWidgetModel {
this.enableFractions = <boolean>json.enableFractions; this.enableFractions = <boolean>json.enableFractions;
this.currency = json.currency; this.currency = json.currency;
this._value = this.parseValue(json); this._value = this.parseValue(json);
// <container>
this.numberOfColumns = <number> json.numberOfColumns;
let columnSize: number = 12;
if (this.numberOfColumns > 1) {
columnSize = 12 / this.numberOfColumns;
}
for (let i = 0; i < this.numberOfColumns; i++) {
let col = new ContainerColumnModel();
col.size = columnSize;
this.columns.push(col);
}
if (json.fields) {
Object.keys(json.fields).map(key => {
let fields = (json.fields[key] || []).map(f => new FormFieldModel(form, f));
let col = this.columns[parseInt(key, 10) - 1];
col.fields = fields;
this.fields.push(...fields);
});
}
// </container>
} }
if (this.hasEmptyValue && this.options && this.options.length > 0) { if (this.hasEmptyValue && this.options && this.options.length > 0) {

View File

@ -125,7 +125,7 @@ export class FormModel {
if (field instanceof ContainerModel) { if (field instanceof ContainerModel) {
let container = <ContainerModel> field; let container = <ContainerModel> field;
result.push(container.field); result.push(container.field);
result.push(...container.children); result.push(...container.field.fields);
} }
} }
@ -172,11 +172,11 @@ export class FormModel {
if (field.params) { if (field.params) {
let originalField = field.params['field']; let originalField = field.params['field'];
if (originalField.type === FormFieldTypes.DYNAMIC_TABLE) { if (originalField.type === FormFieldTypes.DYNAMIC_TABLE) {
result.push(new ContainerModel(this, field)); result.push(new ContainerModel(new FormFieldModel(this, field)));
} }
} }
} else { } else {
result.push(new ContainerModel(this, field)); result.push(new ContainerModel(new FormFieldModel(this, field)));
} }
} }

View File

@ -53,7 +53,7 @@ describe('TabModel', () => {
model.fields = []; model.fields = [];
expect(model.hasContent()).toBeFalsy(); expect(model.hasContent()).toBeFalsy();
model.fields = [new ContainerModel(null, null)]; model.fields = [new ContainerModel(null)];
expect(model.hasContent()).toBeTruthy(); expect(model.hasContent()).toBeTruthy();
}); });

View File

@ -5,7 +5,7 @@
[attr.id]="field.id" [attr.id]="field.id"
[attr.required]="isRequired()" [attr.required]="isRequired()"
[(ngModel)]="field.value" [(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)" (ngModelChange)="onFieldChanged(field)"
[disabled]="field.readOnly"> [disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label> <label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span> <span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>

View File

@ -81,6 +81,7 @@ export class WidgetComponent implements AfterViewInit {
/** @deprecated use onFieldChanged instead */ /** @deprecated use onFieldChanged instead */
checkVisibility(field: FormFieldModel) { checkVisibility(field: FormFieldModel) {
console.log('checkVisibility is deprecated, use onFieldChanged instead');
this.fieldChanged.emit(field); this.fieldChanged.emit(field);
} }

View File

@ -667,13 +667,13 @@ describe('WidgetVisibilityService', () => {
visibilityObjTest.leftFormFieldId = 'FIELD_TEST'; visibilityObjTest.leftFormFieldId = 'FIELD_TEST';
visibilityObjTest.operator = '!='; visibilityObjTest.operator = '!=';
visibilityObjTest.rightFormFieldId = 'LEFT_FORM_FIELD_ID'; visibilityObjTest.rightFormFieldId = 'LEFT_FORM_FIELD_ID';
let contModel = new ContainerModel(fakeFormWithField, { let contModel = new ContainerModel(new FormFieldModel(fakeFormWithField, {
id: 'fake-container-id', id: 'fake-container-id',
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
name: 'fake-container-name', name: 'fake-container-name',
isVisible: true, isVisible: true,
visibilityCondition: visibilityObjTest visibilityCondition: visibilityObjTest
}); }));
fakeFormWithField.fields.push(contModel); fakeFormWithField.fields.push(contModel);
service.refreshVisibility(fakeFormWithField); service.refreshVisibility(fakeFormWithField);
@ -684,13 +684,13 @@ describe('WidgetVisibilityService', () => {
visibilityObjTest.leftFormFieldId = 'FIELD_TEST'; visibilityObjTest.leftFormFieldId = 'FIELD_TEST';
visibilityObjTest.operator = '!='; visibilityObjTest.operator = '!=';
visibilityObjTest.rightFormFieldId = 'RIGHT_FORM_FIELD_ID'; visibilityObjTest.rightFormFieldId = 'RIGHT_FORM_FIELD_ID';
let contModel = new ContainerModel(fakeFormWithField, { let contModel = new ContainerModel(new FormFieldModel(fakeFormWithField, {
id: 'fake-container-id', id: 'fake-container-id',
type: FormFieldTypes.GROUP, type: FormFieldTypes.GROUP,
name: 'fake-container-name', name: 'fake-container-name',
isVisible: true, isVisible: true,
visibilityCondition: visibilityObjTest visibilityCondition: visibilityObjTest
}); }));
service.refreshEntityVisibility(contModel.field); service.refreshEntityVisibility(contModel.field);
expect(contModel.isVisible).toBeFalsy(); expect(contModel.isVisible).toBeFalsy();
}); });

View File

@ -44,7 +44,8 @@ export class WidgetVisibilityService {
} }
refreshEntityVisibility(element: FormFieldModel | TabModel) { refreshEntityVisibility(element: FormFieldModel | TabModel) {
element.isVisible = this.evaluateVisibility(element.form, element.visibilityCondition); let visible = this.evaluateVisibility(element.form, element.visibilityCondition);
element.isVisible = visible;
} }
evaluateVisibility(form: FormModel, visibilityObj: WidgetVisibilityModel): boolean { evaluateVisibility(form: FormModel, visibilityObj: WidgetVisibilityModel): boolean {