diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a3e0ce..3cecb966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.5.7 (2020-08-17) + +- fix #360: VisibleIf-oneOf with 2+ conditions has same property name is not working (daniele-pecora) +- fix #331: visibleIf stopped working in 2.5.2 (daniele-pecora) +- fix #329: visibleIf "allOf" Edge case BUG (daniele-pecora) + # 2.5.6 (2020-07-16) - Fix #358 Completing Public API diff --git a/README.md b/README.md index bb341a3d..3b333b7a 100644 --- a/README.md +++ b/README.md @@ -522,9 +522,16 @@ export class AppComponent { ``` ### Conditional fields -It is possible to make the presence of a field depends on another field's value. -To achieve this you just have to add a `visibleIf` property to a field's definition. -Adding the value `$ANY$` to the array of conditional values,will make the field visible for any value inserted. +It is possible to make the presence of a field depends on another field's value. +To achieve this you just have to add a `visibleIf` property to a field's definition. + +**Value** +The value to match is set as array item. +Setting multiple items will make the visiblity condition `true` if one of the values matches. +If it is required to match all values head over to the section `visibleIf` with `allOf` condition. + +**$ANY$** +Adding the value `$ANY$` to the array of conditional values, will make the field visible for any value inserted. ```js @Component({ @@ -564,7 +571,9 @@ export class AppComponent { } } ``` +**$EMPTY$** Assigning an empty Object to 'visibleIf' is interpreted as _visibleIf_ nothing, thereby the widget is hidden and not present in model. + ```js mySchema = { "properties": { diff --git a/package.json b/package.json index fdb07ce3..62add4cf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "copy:schema": "node -e \"var src='./schema/ngx-schema-form-schema.json'; var dest='./dist/schema-form/ngx-schema-form-schema.json'; var fs = require('fs'); if (fs.existsSync(src)) { var data = fs.readFileSync(src, 'utf-8'); fs.writeFileSync(dest, data);}\"", "build:lib": "ng build --prod schema-form && npm run copy:schema && cp ./README.md ./dist/schema-form/", "build-demo": "ng build --prod --base-href /ngx-schema-form/dist/ngx-schema-form/", - "test": "ng test schema-form --watch=false", + "test:lib": "ng test schema-form --watch=false", + "test": "ng test --watch=false", "lint": "ng lint", "get_version": "cat ./projects/schema-form/package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]'" }, diff --git a/projects/schema-form/package.json b/projects/schema-form/package.json index 302be0b4..533c2dd8 100644 --- a/projects/schema-form/package.json +++ b/projects/schema-form/package.json @@ -1,6 +1,6 @@ { "name": "ngx-schema-form", - "version": "2.5.6", + "version": "2.5.7", "repository": { "type": "git", "url": "git+https://github.com/guillotinaweb/ngx-schema-form" diff --git a/projects/schema-form/src/lib/model/formproperty.ts b/projects/schema-form/src/lib/model/formproperty.ts index c08b1249..75475a6f 100644 --- a/projects/schema-form/src/lib/model/formproperty.ts +++ b/projects/schema-form/src/lib/model/formproperty.ts @@ -233,7 +233,7 @@ export abstract class FormProperty { targetProperty: FormProperty, dependencyPath: string, value: any = '', - expression: string|string[]|number = ''): boolean { + expression: string | string[] | number | number[] | boolean | boolean[]): boolean { try { let valid = false if (isNaN(expression as number) && (expression as string).indexOf('$ANY$') !== -1) { @@ -252,8 +252,13 @@ export abstract class FormProperty { } } } else { - valid = !!value ? expression.toString() === value.toString() : false; - + const expArray = Array.isArray(expression) ? expression : (expression ? [expression] : []) + for (const expString of expArray) { + valid = !!value ? `${expString}` === `${value}` : false; + if (valid) { + break + } + } } return valid } catch (error) { @@ -266,7 +271,11 @@ export abstract class FormProperty { } } - private __bindVisibility(): boolean { + /** + * binds visibility conditions of type `oneOf` and `allOf`. + * @returns `true` if any visibility binding of type `oneOf` or `allOf` has been processed. Otherwise `false`. + */ + private __bindVisibility_oneOf_or_allOf(): boolean { /** *
* "oneOf":[{ @@ -299,9 +308,19 @@ export abstract class FormProperty { if (property) { let valueCheck; if (this.schema.visibleIf.oneOf) { - valueCheck = property.valueChanges.pipe(map( - value => this.__evaluateVisibilityIf(this, property, dependencyPath, value, visibleIf[dependencyPath]) - )); + const _chk = (value) => { + for (const item of this.schema.visibleIf.oneOf) { + for (const depPath of Object.keys(item)) { + const prop = this.searchProperty(depPath); + const propVal = prop.value; + if (this.__evaluateVisibilityIf(this, prop, dependencyPath, propVal, item[depPath])) { + return true + } + } + } + return false; + }; + valueCheck = property.valueChanges.pipe(map(_chk)); } else if (this.schema.visibleIf.allOf) { const _chk = (value) => { for (const item of this.schema.visibleIf.allOf) { @@ -332,8 +351,11 @@ export abstract class FormProperty { } combineLatest(propertiesBinding, (...values: boolean[]) => { + if (this.schema.visibleIf.allOf) { + return values.indexOf(false) === -1; + } return values.indexOf(true) !== -1; - }).pipe(distinctUntilChanged()).subscribe((visible) => { + }).subscribe((visible) => { this.setVisible(visible); }); } @@ -344,7 +366,7 @@ export abstract class FormProperty { // A field is visible if AT LEAST ONE of the properties it depends on is visible AND has a value in the list public _bindVisibility() { - if (this.__bindVisibility()) + if (this.__bindVisibility_oneOf_or_allOf()) return; let visibleIf = this.schema.visibleIf; if (typeof visibleIf === 'object' && Object.keys(visibleIf).length === 0) { diff --git a/src/app/json-schema-example/json-schema-example.component.spec.ts b/src/app/json-schema-example/json-schema-example.component.spec.ts index 7da5cda7..4066a1f1 100644 --- a/src/app/json-schema-example/json-schema-example.component.spec.ts +++ b/src/app/json-schema-example/json-schema-example.component.spec.ts @@ -19,7 +19,7 @@ describe('JsonSchemaExampleComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - SchemaFormModule, + SchemaFormModule.forRoot(), HttpClientModule ], declarations: [ JsonSchemaExampleComponent ], diff --git a/src/app/json-schema-example/json-schema-example.component.ts b/src/app/json-schema-example/json-schema-example.component.ts index 8e56ddeb..752ee1f9 100644 --- a/src/app/json-schema-example/json-schema-example.component.ts +++ b/src/app/json-schema-example/json-schema-example.component.ts @@ -14,6 +14,7 @@ import binding_sample_schema from './binding_sample_schema.json'; import binding_sample_model from './binding_sample_model.json'; import binding_sample_bindings from './binding_sample_bindings'; import visibility_binding_example from './visibility-binding-example-schema.json'; +import visibility_binding_example2 from './visibility-binding-example-schema2.json'; import {AppService, AppData} from '../app.service'; import {ISchema} from 'ngx-schema-form'; @@ -39,6 +40,7 @@ export class JsonSchemaExampleComponent implements OnInit, OnDestroy { {label: 'Sample 2 - Custom bindings', event: this.changeSchemaWithBindings, selected: false}, {label: 'Sample 3 - Otherschema', event: this.changeSchemaOtherschema, selected: false}, {label: 'Sample 4 - Visibility binding', event: this.changeSchemaVisibilityBinding, selected: false}, + {label: 'Sample 5 - Visibility binding 2', event: this.changeSchemaVisibilityBinding2, selected: false}, ]; constructor( @@ -212,6 +214,14 @@ export class JsonSchemaExampleComponent implements OnInit, OnDestroy { this.actions = {}; } + changeSchemaVisibilityBinding2() { + this.schema = visibility_binding_example2 as unknown as ISchema; + this.model = {}; + this.fieldBindings = {}; + this.fieldValidators = {}; + this.actions = {}; + } + disableAll() { Object.keys(this.schema.properties).map(prop => { this.schema.properties[prop].readOnly = true; diff --git a/src/app/json-schema-example/json-schema-example.component.visibleIf.spec.ts b/src/app/json-schema-example/json-schema-example.component.visibleIf.spec.ts new file mode 100644 index 00000000..2513c782 --- /dev/null +++ b/src/app/json-schema-example/json-schema-example.component.visibleIf.spec.ts @@ -0,0 +1,679 @@ +// - - - - - - - - - - - - - - - - - - +// Running only this test is possible on Angular9 with: +// ng test --include='**/json-schema-example/*.visibleIf.spec.ts' --watch=true` +// - - - - - - - - - - - - - - - - - - + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientModule } from '@angular/common/http'; +import { + SchemaFormModule, + SchemaValidatorFactory, + ZSchemaValidatorFactory, + WidgetRegistry, + DefaultWidgetRegistry +} from '../../../projects/schema-form/src/public_api'; + + +import { JsonSchemaExampleComponent } from './json-schema-example.component'; +import { By } from '@angular/platform-browser'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +describe('JsonSchemaExampleComponent', () => { + let component: JsonSchemaExampleComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SchemaFormModule.forRoot(), + HttpClientModule, + FormsModule, + ReactiveFormsModule + ], + declarations: [ JsonSchemaExampleComponent ], + providers: [ + {provide: WidgetRegistry, useClass: DefaultWidgetRegistry}, + { + provide: SchemaValidatorFactory, + useClass: ZSchemaValidatorFactory + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(JsonSchemaExampleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + + +describe('JsonSchemaExampleComponent - visibleIf - data-types', () => { + let component: JsonSchemaExampleComponent; + let fixture: ComponentFixture ; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SchemaFormModule.forRoot(), + HttpClientModule, + FormsModule, + ReactiveFormsModule + ], + declarations: [ JsonSchemaExampleComponent ], + providers: [ + {provide: WidgetRegistry, useClass: DefaultWidgetRegistry}, + { + provide: SchemaValidatorFactory, + useClass: ZSchemaValidatorFactory + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(JsonSchemaExampleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +/* + it('should create', () => { + expect(component).toBeTruthy(); + }); +*/ + beforeEach(() => { + + // select demo sample + const select: HTMLSelectElement = fixture.debugElement.query(By.css('#samples')).nativeElement; + select.value = select.options[4].value; // <-- select a new value + select.dispatchEvent(new Event('change')); + fixture.detectChanges(); + }); + + it(`# 1. Test boolean as boolean`, async(() => { + // Visible component shows up if a boolean value `true` is provided + + const app = component + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect selected sample schema to be loaded + expect(app.schema.properties.demo.properties.typeTest.fieldsets[0].description).toEqual('# 1. Test boolean'); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_boolean_check = fixture.debugElement.query(By.css('#demo\\.typeTest\\.checkbool')); + expect(_test_boolean_check).toBeTruthy() + + let _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testbool')); + expect(_test_boolean_visible).toBeNull() + + // positive state + _test_boolean_check.nativeElement.checked = true + _test_boolean_check.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testbool')); + expect(_test_boolean_visible).toBeTruthy() + + // negative state + _test_boolean_check.nativeElement.checked = false + _test_boolean_check.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testbool')); + expect(_test_boolean_visible).toBeNull() + }); + + })); + + it(`# 1. Test boolean as string`, async(() => { + // Visible component shows up if a boolean value `"true"` as string is provided + + const app = component + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect selected sample schema to be loaded + expect(app.schema.properties.demo.properties.typeTest.fieldsets[0].description).toEqual('# 1. Test boolean'); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_boolean_check_true = fixture.debugElement.query(By.css('#demo\\.typeTest\\.checkboolstring\\.true')); + expect(_test_boolean_check_true).toBeTruthy() + let _test_boolean_check_false = fixture.debugElement.query(By.css('#demo\\.typeTest\\.checkboolstring\\.false')); + expect(_test_boolean_check_false).toBeTruthy() + + let _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testboolstring')); + expect(_test_boolean_visible).toBeNull() + + // positive state + _test_boolean_check_true.nativeElement.checked = true + _test_boolean_check_true.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testboolstring')); + expect(_test_boolean_visible).toBeTruthy() + + // negative state + _test_boolean_check_false.nativeElement.checked = false + _test_boolean_check_false.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testboolstring')); + expect(_test_boolean_visible).toBeNull() + }); + })); + + it(`# 2. Test number`, async(() => { + // Visible component shows up if a number value `1` is provided + + const app = component + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect selected sample schema to be loaded + expect(app.schema.properties.demo.properties.typeTest.fieldsets[1].description).toEqual('# 2. Test number'); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_number_input = fixture.debugElement.query(By.css('#demo\\.typeTest\\.checknum')); + expect(_test_number_input).toBeTruthy() + + let _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testnum')); + expect(_test_number_visible).toBeNull() + + // positive state + _test_number_input.nativeElement.value = '1' + _test_number_input.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testnum')); + expect(_test_number_visible).toBeTruthy() + + // negative state + _test_number_input.nativeElement.value = '2' + _test_number_input.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testnum')); + expect(_test_number_visible).toBeNull() + }); + })); + + it(`# 2. Test number as string`, async(() => { + // Visible component shows up if a number value `"1"` as string is provided + + const app = component + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect selected sample schema to be loaded + expect(app.schema.properties.demo.properties.typeTest.fieldsets[1].description).toEqual('# 2. Test number'); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_number_select = fixture.debugElement.query(By.css('#demo\\.typeTest\\.checknumstring')); + expect(_test_number_select).toBeTruthy() + + let _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testnumstring')); + expect(_test_number_visible).toBeNull() + + // positive state + _test_number_select.nativeElement.value = _test_number_select.nativeElement.options[1].value; // set to '1' + _test_number_select.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testnumstring')); + expect(_test_number_visible).toBeTruthy() + + // negative state + _test_number_select.nativeElement.value = _test_number_select.nativeElement.options[2].value; // set to '2' + _test_number_select.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.testnumstring')); + expect(_test_number_visible).toBeNull() + }); + })); + + it(`# 3. Test string`, async(() => { + // Visible component shows up if a string value `"a"` is provided + + const app = component + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect selected sample schema to be loaded + expect(app.schema.properties.demo.properties.typeTest.fieldsets[2].description).toEqual('# 3. Test string'); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_string_input = fixture.debugElement.query(By.css('#demo\\.typeTest\\.checkstring')); + expect(_test_string_input).toBeTruthy() + + let _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.teststring')); + expect(_test_number_visible).toBeNull() + + // positive state + _test_string_input.nativeElement.value = 'a' + _test_string_input.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.teststring')); + expect(_test_number_visible).toBeTruthy() + + // negative state + _test_string_input.nativeElement.value = 'z' + _test_string_input.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + _test_number_visible = fixture.debugElement.query(By.css('#demo\\.typeTest\\.teststring')); + expect(_test_number_visible).toBeNull() + }); + })); + + +}); + + +describe('JsonSchemaExampleComponent - visibleIf - condition-types', () => { + let component: JsonSchemaExampleComponent; + let fixture: ComponentFixture ; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SchemaFormModule.forRoot(), + HttpClientModule, + FormsModule, + ReactiveFormsModule + ], + declarations: [ JsonSchemaExampleComponent ], + providers: [ + {provide: WidgetRegistry, useClass: DefaultWidgetRegistry}, + { + provide: SchemaValidatorFactory, + useClass: ZSchemaValidatorFactory + } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(JsonSchemaExampleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +/* + it('should create', () => { + expect(component).toBeTruthy(); + }); +*/ + beforeEach(() => { + + // select demo sample + const select: HTMLSelectElement = fixture.debugElement.query(By.css('#samples')).nativeElement; + select.value = select.options[4].value; // <-- select a new value + select.dispatchEvent(new Event('change')); + fixture.detectChanges(); + }); + + it(`# 4. Test 'VisibleIf' with default 'one-of' with multiple values`, async(() => { + // Visible component shows up if status value is 'Warn' or 'Fail' + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_boolean_check_pass = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.status1a\\.Pass')); + expect(_test_boolean_check_pass).toBeTruthy() + let _test_boolean_check_warn = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.status1a\\.Warn')); + expect(_test_boolean_check_warn).toBeTruthy() + let _test_boolean_check_fail = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.status1a\\.Fail')); + expect(_test_boolean_check_fail).toBeTruthy() + + let _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.visibleComponent1a')); + expect(_test_boolean_visible).toBeNull() + + // negative state 'Pass' + _test_boolean_check_pass.nativeElement.checked = true + _test_boolean_check_pass.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.visibleComponent1a')); + expect(_test_boolean_visible).toBeNull() + + // positive state 'Warn' + _test_boolean_check_warn.nativeElement.checked = true + _test_boolean_check_warn.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.visibleComponent1a')); + expect(_test_boolean_visible).toBeTruthy() + + // negative state 'Pass' + _test_boolean_check_pass.nativeElement.checked = true + _test_boolean_check_pass.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.visibleComponent1a')); + expect(_test_boolean_visible).toBeNull() + + // positive state 'Fail' + _test_boolean_check_fail.nativeElement.checked = true + _test_boolean_check_fail.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.visibleComponent1a')); + expect(_test_boolean_visible).toBeTruthy() + + }); + + })); + + + it(`# 5. Test 'VisibleIf' with 'oneOf' condition`, async(() => { + // Visible component shows up if status value is 'Warn' or 'Fail' + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_boolean_check_pass = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.status1b\\.Pass')); + expect(_test_boolean_check_pass).toBeTruthy() + let _test_boolean_check_warn = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.status1b\\.Warn')); + expect(_test_boolean_check_warn).toBeTruthy() + let _test_boolean_check_fail = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.status1b\\.Fail')); + expect(_test_boolean_check_fail).toBeTruthy() + + let _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1a\\.visibleComponent1b')); + expect(_test_boolean_visible).toBeNull() + + // negative state 'Pass' + _test_boolean_check_pass.nativeElement.checked = true + _test_boolean_check_pass.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.visibleComponent1b')); + expect(_test_boolean_visible).toBeNull() + + // positive state 'Warn' + _test_boolean_check_warn.nativeElement.checked = true + _test_boolean_check_warn.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.visibleComponent1b')); + expect(_test_boolean_visible).toBeTruthy() + + // negative state 'Pass' + _test_boolean_check_pass.nativeElement.checked = true + _test_boolean_check_pass.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.visibleComponent1b')); + expect(_test_boolean_visible).toBeNull() + + // positive state 'Fail' + _test_boolean_check_fail.nativeElement.checked = true + _test_boolean_check_fail.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding1b\\.visibleComponent1b')); + expect(_test_boolean_visible).toBeTruthy() + + }); + + })); + + it(`# 6. Test Boolean 'VisibleIf' with 'allOf' condition (check 'Warn' and 'Fail')`, async(() => { + // Visible component shows up if status 'Warn' and 'Fail' are checked + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_checkbox_pass = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2a\\.status2a')); + expect(_test_checkbox_pass).toBeTruthy() + let _test_checkbox_warn = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2a\\.status2b')); + expect(_test_checkbox_warn).toBeTruthy() + let _test_checkbox_fail = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2a\\.status2c')); + expect(_test_checkbox_fail).toBeTruthy() + + let _test_boolean_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2a\\.visibleComponent2a')); + expect(_test_boolean_visible).toBeNull() + + const visibleComponent = '#demo\\.visibleIfBinding2a\\.visibleComponent2a' + const checkboxes = [ + _test_checkbox_pass, + _test_checkbox_warn, + _test_checkbox_fail + ] + + let combinations = [ + // 'Pass', 'Warn', 'Fail', 'Should component show up?' + { values: [false, false, false], visible: false, emit:false }, // the initial state + { values: [true, false, false], visible: false, emit:false }, + { values: [false, true, false], visible: false, emit:false }, + { values: [false, false, true], visible: false, emit:false }, + { values: [true, true, false], visible: false, emit:false }, + { values: [true, true, true], visible: true, emit:false }, + { values: [false, true, true], visible: true, emit:false } + ] + combinations = combinations.concat( + // same as above but this forces emitting the change event + combinations.map(item => { item.emit = true; return item })) + + for (const combination of combinations) { + for (let i = 0; i < combination.values.length; i++) { + if (checkboxes[i].nativeElement.checked !== combination.values[i] || combination.emit) { + checkboxes[i].nativeElement.checked = combination.values[i] + checkboxes[i].nativeElement.dispatchEvent(new Event('change')); + } + fixture.detectChanges(); + + const _test_boolean_visible_el = fixture.debugElement.query(By.css(visibleComponent)); + const errorOut=`Expected visibility ${combination.visible} | emits: ${combination.emit||false} | checked: pass:${combination.values[0]}/native:${checkboxes[0].nativeElement.checked}, warn:${combination.values[1]}/native:${checkboxes[1].nativeElement.checked}, fail:${combination.values[2]}/native:${checkboxes[2].nativeElement.checked}` + if(_test_checkbox_warn.nativeElement.checked && _test_checkbox_fail.nativeElement.checked){ + expect(_test_boolean_visible_el).toBeTruthy(errorOut) + } else { + expect(_test_boolean_visible_el).toBeNull(errorOut) + } + } + } + + }); + + })); + + it(`# 7. Test String 'VisibleIf' with 'allOf' condition (select 'Warn' and 'Fail')`, async(() => { + // Visible component shows up if status 'Warn' and 'Fail' are checked + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_select_pass = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2b\\.status2a')); + expect(_test_select_pass).toBeTruthy() + let _test_select_warn = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2b\\.status2b')); + expect(_test_select_warn).toBeTruthy() + let _test_select_fail = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2b\\.status2c')); + expect(_test_select_fail).toBeTruthy() + + let _test_select_visible = fixture.debugElement.query(By.css('#demo\\.visibleIfBinding2b\\.visibleComponent2b')); + expect(_test_select_visible).toBeNull() + + const visibleComponent = '#demo\\.visibleIfBinding2b\\.visibleComponent2b' + const dropdowns = [ + _test_select_pass, + _test_select_warn, + _test_select_fail + ] + + let combinations = [ + // 'Pass', 'Warn', 'Fail', 'Should component show up?' + { values: ['', '', ''], visible: false, emit:false }, // the initial state + + { values: ['Pass', '', ''], visible: false, emit:false }, + { values: ['', 'Warn', ''], visible: false, emit:false }, + { values: ['', '', 'Fail'], visible: false, emit:false }, + { values: ['', 'Pass', 'Fail'], visible: true, emit:false }, + { values: ['Pass', 'Warn', 'Pass'], visible: true, emit:false }, + { values: ['', 'Warn', 'Fail'], visible: true, emit:false }, + { values: ['Pass', 'Warn', 'Fail'], visible: true, emit:false } + + ] + combinations = combinations.concat( + // same as above but this forces emitting the change event + combinations.map(item => { item.emit = true; return item })) + + for (const combination of combinations) { + for (let i = 0; i < combination.values.length; i++) { + if (dropdowns[i].nativeElement.value !== combination.values[i] || combination.emit) { + dropdowns[i].nativeElement.value = combination.values[i] + dropdowns[i].nativeElement.dispatchEvent(new Event('change')); + } + fixture.detectChanges(); + + const _test_select_visible_el = fixture.debugElement.query(By.css(visibleComponent)); + const errorOut=`Expected visibility ${combination.visible} | emits: ${combination.emit||false} | checked: pass:${combination.values[0]}/native:${dropdowns[0].nativeElement.value}, warn:${combination.values[1]}/native:${dropdowns[1].nativeElement.value}, fail:${combination.values[2]}/native:${dropdowns[2].nativeElement.value}` + if (_test_select_warn.nativeElement.value === 'Warn' && _test_select_fail.nativeElement.value === 'Fail') { + expect(_test_select_visible_el).toBeTruthy(errorOut) + } else { + expect(_test_select_visible_el).toBeNull(errorOut) + } + } + } + + }); + + })); + + it(`# 8. Test oneOf - Set age to 15, set last name to 'aaa'`, async(() => { + // Visible component shows up if age is set to 15 and last name to 'aaa' + + fixture.whenStable().then(() => { + + fixture.detectChanges(); + + // expect page containing a sf-form element + let sf_form = fixture.debugElement.query(By.css('sf-form')) + expect(sf_form).toBeTruthy() + + + // initial state + let _test_input_age = fixture.debugElement.query(By.css('#demo\\.updateVisibiltyTest\\.age')); + expect(_test_input_age).toBeTruthy() + let _test_select_lastname = fixture.debugElement.query(By.css('#demo\\.updateVisibiltyTest\\.lastName')); + expect(_test_select_lastname).toBeTruthy() + + let _test_visible_component = fixture.debugElement.query(By.css('#demo\\.updateVisibiltyTest\\.firstName')); + expect(_test_visible_component).toBeNull() + + let visibleComponent = '#demo\\.updateVisibiltyTest\\.firstName' + let elements = [ + _test_input_age, + _test_select_lastname + ] + + let combinations = [ + // 'Pass', 'Warn', 'Fail', 'Should component show up?' + { values: ['', ''], visible: false, emit:false }, // the initial state + { values: [0, ''], visible: false, emit:false }, + { values: [0, 'bbb'], visible: false, emit:false }, + { values: [0, 'aaa'], visible: false, emit:false }, + { values: [15, 'aaa'], visible: true, emit:false }, + { values: [15, 'bbb'], visible: false, emit:false }, + { values: [155, 'aaa'], visible: false, emit:false }, + + ] + combinations = combinations.concat( + // same as above but this forces emitting the change event + combinations.map(item => { item.emit = true; return item })) + + for (const combination of combinations) { + for (let i = 0; i < combination.values.length; i++) { + if (elements[i].nativeElement.value !== combination.values[i] || combination.emit) { + elements[i].nativeElement.value = combination.values[i] + elements[i].nativeElement.dispatchEvent(new Event('change')); + } + fixture.detectChanges(); + + const _test_select_visible_el = fixture.debugElement.query(By.css(visibleComponent)); + const errorOut=`Expected visibility ${combination.visible} | emits: ${combination.emit||false} | checked: age:${combination.values[0]}/native:${elements[0].nativeElement.value}, firstname:${combination.values[1]}/native:${elements[1].nativeElement.value}` + if (_test_input_age.nativeElement.value === 15 && _test_select_lastname.nativeElement.value === 'aaa') { + expect(_test_select_visible_el).toBeTruthy(errorOut) + } else { + expect(_test_select_visible_el).toBeNull(errorOut) + } + } + } + + }); + + })); + +}); + diff --git a/src/app/json-schema-example/visibility-binding-example-schema2.json b/src/app/json-schema-example/visibility-binding-example-schema2.json new file mode 100644 index 00000000..386c1b76 --- /dev/null +++ b/src/app/json-schema-example/visibility-binding-example-schema2.json @@ -0,0 +1,442 @@ +{ + "properties": { + "demo": { + "type": "object", + "properties": { + "typeTest": { + "fieldsets": [ + { + "id": "bool", + "title": "", + "description": "# 1. Test boolean", + "name": "", + "fields": [ + "checkbool", + "testbool", + "checkboolstring", + "testboolstring" + ] + }, + { + "id": "num", + "title": "", + "description": "# 2. Test number", + "name": "", + "fields": [ + "checknum", + "testnum", + "checknumstring", + "testnumstring" + ] + }, + { + "id": "num", + "title": "", + "description": "# 3. Test string", + "name": "", + "fields": [ + "checkstring", + "teststring" + ] + } + ], + "type": "object", + "properties": { + "checkbool": { + "type": "boolean", + "description": "Boolean test (true) as boolean" + }, + "testbool": { + "type": "string", + "description": "Visible if value is 'true' as boolean", + "visibleIf": { + "checkbool": true + } + }, + "checkboolstring": { + "type": "string", + "widget": "radio", + "description": "Boolean test (\"true\") as string", + "oneOf": [ + { + "description": "String 'true'", + "enum": [ + "true" + ] + }, + { + "description": "String 'false'", + "enum": [ + "false" + ] + } + ] + }, + "testboolstring": { + "type": "string", + "description": "Visible if value is 'true' as string", + "visibleIf": { + "checkboolstring": "true" + } + }, + "checknum": { + "type": "number", + "description": "Number test (1)" + }, + "testnum": { + "type": "string", + "description": "Visible if value is '1' as number", + "visibleIf": { + "checknum": 1 + } + }, + "checknumstring": { + "type": "string", + "description": "Number test (\"1\") as string", + "widget": "select", + "oneOf": [ + { + "description": "Select a number", + "enum": [ + "" + ] + }, + { + "description": "Number 1", + "enum": [ + "1" + ] + }, + { + "description": "Number 2", + "enum": [ + "2" + ] + } + ] + }, + "testnumstring": { + "type": "string", + "description": "Visible if value is '1' as string", + "visibleIf": { + "checknumstring": "1" + } + }, + "checkstring": { + "type": "string", + "description": "String test (a)" + }, + "teststring": { + "type": "string", + "description": "Visible if value is 'a' as string", + "visibleIf": { + "checkstring": "a" + } + } + } + }, + "visibleIfBinding1a": { + "description": "# 4. Test 'VisibleIf' with default 'one-of' with multiple values", + "type": "object", + "properties": { + "status1a": { + "type": "string", + "description": "Visible component shows up if status is 'Warn' or 'Fail'", + "oneOf": [ + { + "description": "Pass", + "enum": [ + "Pass" + ] + }, + { + "description": "Warn", + "enum": [ + "Warn" + ] + }, + { + "description": "Fail", + "enum": [ + "Fail" + ] + } + ], + "widget": "radio" + }, + "visibleComponent1a": { + "type": "string", + "description": "Visible component if status is 'Warn' or 'Fail'", + "visibleIf": { + "/demo/visibleIfBinding1a/status1a": [ + "Warn", + "Fail" + ] + } + } + } + }, + "visibleIfBinding1b": { + "description": "# 5. Test 'VisibleIf' with 'oneOf' condition", + "type": "object", + "properties": { + "status1b": { + "type": "string", + "description": "Visible component shows up if status is 'Warn' or 'Fail'", + "oneOf": [ + { + "description": "Pass", + "enum": [ + "Pass" + ] + }, + { + "description": "Warn", + "enum": [ + "Warn" + ] + }, + { + "description": "Fail", + "enum": [ + "Fail" + ] + } + ], + "widget": "radio" + }, + "visibleComponent1b": { + "type": "string", + "description": "Visible component if status is 'Warn' or 'Fail'", + "visibleIf": { + "oneOf": [ + { + "/demo/visibleIfBinding1b/status1b": [ + "Warn" + ] + }, + { + "/demo/visibleIfBinding1b/status1b": [ + "Fail" + ] + } + ] + } + } + } + }, + "visibleIfBinding2a": { + "description": "# 6. Test Boolean 'VisibleIf' with 'allOf' condition (check 'Warn' and 'Fail')", + "type": "object", + "properties": { + "status2a": { + "type": "boolean", + "description": "Pass", + "widget": "checkbox" + }, + "status2b": { + "type": "boolean", + "description": "Warn", + "widget": "checkbox" + }, + "status2c": { + "type": "boolean", + "description": "Fail", + "widget": "checkbox" + }, + "visibleComponent2a": { + "type": "string", + "description": "Visible component if status 'Warn' and 'Fail' are checked", + "visibleIf": { + "allOf": [ + { + "/demo/visibleIfBinding2a/status2b": [ + true + ] + }, + { + "/demo/visibleIfBinding2a/status2c": [ + true + ] + } + ] + } + } + } + }, + "visibleIfBinding2b": { + "description": "# 7. Test String 'VisibleIf' with 'allOf' condition (select 'Warn' and 'Fail')", + "type": "object", + "properties": { + "status2a": { + "type": "string", + "oneOf": [ + { + "description": "...", + "enum": [ + "" + ] + }, + { + "description": "Pass", + "enum": [ + "Pass" + ] + }, + { + "description": "Warn", + "enum": [ + "Warn" + ] + }, + { + "description": "Fail", + "enum": [ + "Fail" + ] + } + ], + "widget": "select" + }, + "status2b": { + "type": "string", + "oneOf": [ + { + "description": "Select 'Warn' here", + "enum": [ + "" + ] + }, + { + "description": "Pass", + "enum": [ + "Pass" + ] + }, + { + "description": "Warn", + "enum": [ + "Warn" + ] + }, + { + "description": "Fail", + "enum": [ + "Fail" + ] + } + ], + "widget": "select" + }, + "status2c": { + "type": "string", + "oneOf": [ + { + "description": "Select 'Fail' here", + "enum": [ + "" + ] + }, + { + "description": "Pass", + "enum": [ + "Pass" + ] + }, + { + "description": "Warn", + "enum": [ + "Warn" + ] + }, + { + "description": "Fail", + "enum": [ + "Fail" + ] + } + ], + "widget": "select" + }, + "visibleComponent2b": { + "type": "string", + "description": "Visible component if status 'Warn' and 'Fail' are checked", + "visibleIf": { + "allOf": [ + { + "/demo/visibleIfBinding2b/status2b": [ + "Warn" + ] + }, + { + "/demo/visibleIfBinding2b/status2c": [ + "Fail" + ] + } + ] + } + } + } + }, + "updateVisibiltyTest": { + "type": "object", + "description": "Test oneOf - Set age to 15, set last name to 'aaa'", + "properties": { + "age": { + "id": "age", + "name": "age", + "title": "Age", + "type": "string", + "widget": { + "id": "string" + } + }, + "firstName": { + "id": "firstName", + "name": "firstName", + "title": "First Name", + "type": "string", + "visibleIfOperator": "and", + "widget": { + "id": "string" + }, + "visibleIf": { + "allOf": [ + { + "lastName": "aaa" + }, + { + "age": 15 + } + ] + } + }, + "lastName": { + "id": "lastName", + "name": "lastName", + "title": "Last Name", + "type": "string", + "widget": { + "id": "select" + }, + "oneOf": [ + { + "description": "AAA", + "enum": [ + "aaa" + ] + }, + { + "description": "BBB", + "enum": [ + "bbb" + ] + } + ] + } + } + } + } + } + } +} diff --git a/src/app/template-schema-example/template-schema-example.component.html b/src/app/template-schema-example/template-schema-example.component.html index 9ee8d623..3f326505 100644 --- a/src/app/template-schema-example/template-schema-example.component.html +++ b/src/app/template-schema-example/template-schema-example.component.html @@ -2,7 +2,10 @@ diff --git a/src/app/template-schema-example/template-schema-example.component.spec.ts b/src/app/template-schema-example/template-schema-example.component.spec.ts index 911df53d..a2e539d3 100644 --- a/src/app/template-schema-example/template-schema-example.component.spec.ts +++ b/src/app/template-schema-example/template-schema-example.component.spec.ts @@ -21,7 +21,7 @@ describe('TemplateSchemaExampleComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - SchemaFormModule, + SchemaFormModule.forRoot(), TemplateSchemaModule, HttpClientModule, FormsModule diff --git a/src/app/template-schema-example/template-schema-example.component.ts b/src/app/template-schema-example/template-schema-example.component.ts index 56553239..acd68c0c 100644 --- a/src/app/template-schema-example/template-schema-example.component.ts +++ b/src/app/template-schema-example/template-schema-example.component.ts @@ -8,6 +8,11 @@ import { Component, OnInit } from '@angular/core'; export class TemplateSchemaExampleComponent implements OnInit { model: any = {}; + /** + * Using a separate variable for showing the model prevents from: + * `Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value:` + */ + value; constructor() { } @@ -18,4 +23,18 @@ export class TemplateSchemaExampleComponent implements OnInit { ngOnInit() { } + setValue(value) { + if (undefined === this.value) { + /** + * If the first time the variable is set, then setting timeout will prevents error: + * `Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value:` + */ + setTimeout(() => { + this.value = value; + }, 0); + return + } + this.value = value; + } + }Form:
-+ Part 1 - Recipient Template: Model:
-{{model | json}}+{{value | json}}