Skip to content

Commit 0e41e4a

Browse files
committed
feat: Display a div/fieldset/details element
Allows a div, fieldset or details element to be created inside the form via json. The element will then hold various widgets based on the "items" property set in the layout
1 parent 7844e73 commit 0e41e4a

22 files changed

+396
-33
lines changed

projects/demo-e2e/src/pages/app.po.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export class AppPage {
1313

1414
private readonly forms: Map<string, typeof Form> = new Map<string, typeof Form>([
1515
['Hidden', Form],
16-
['Buttons', ButtonsForm]
16+
['Buttons', ButtonsForm],
17+
['Containers', Form]
1718
]);
1819

1920
private static async insertConsoleTracing(): Promise<void> {

projects/demo-e2e/src/pages/form.po.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { by, element, ElementFinder } from 'protractor';
22

33
export class Form {
44
private readonly formSelector = by.css('jsf-json-schema-form');
5-
private readonly widgetSelector = by.css('jsf-select-widget:first-child');
5+
private readonly widgetSelector = by.css('jsf-select-widget > *:first-child');
66
private readonly controlSelector = by.css('input, button, textarea, select');
77

88
async getWidgetCount(): Promise<number> {
@@ -37,6 +37,14 @@ export class Form {
3737
return this.getControl(controlName).getText();
3838
}
3939

40+
async getContainerAttribute(containerType: string, attribute: string): Promise<string> {
41+
return this.getContainer(containerType).getAttribute(attribute);
42+
}
43+
44+
getContainer(containerType: string): ElementFinder {
45+
return element(this.formSelector).element(by.css(`jsf-container > ${ containerType }`));
46+
}
47+
4048
private getControl(controlName: string): ElementFinder {
4149
return element(this.formSelector).element(by.name(controlName));
4250
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { AppPage, Form } from '@demo-e2e';
2+
3+
import { by } from 'protractor';
4+
5+
import { verifyElementAttributes } from '../../../utils';
6+
7+
describe('container example', () => {
8+
let page: AppPage;
9+
10+
beforeEach(async () => {
11+
page = new AppPage();
12+
await page.navigateTo();
13+
});
14+
15+
it('should have 3 widgets and 0 controls', async () => {
16+
const form: Form = await page.selectExample('Containers');
17+
const numWidgets = 3;
18+
await expect(form.getWidgetCount()).toEqual(numWidgets);
19+
await expect(form.getControlCount()).toEqual(0);
20+
});
21+
22+
it('should have a div', async () => {
23+
const form: Form = await page.selectExample('Containers');
24+
await Promise.all([
25+
expect(form.getContainerAttribute('div', 'id')).toBeDefined(),
26+
verifyElementAttributes(form.getContainer('div'), {
27+
accesskey: 'd',
28+
description: 'This is a div container',
29+
htmlClass: 'div',
30+
style: 'color: red;',
31+
tabindex: 3
32+
})
33+
]);
34+
});
35+
36+
/**
37+
* @todo Verify disabled and form properties
38+
*/
39+
it('should have a fieldset/legend', async () => {
40+
const form: Form = await page.selectExample('Containers');
41+
await Promise.all([
42+
expect(form.getContainerAttribute('fieldset', 'id')).toBeDefined(),
43+
expect(form.getContainerAttribute('fieldset', 'name')).toBe('fields'),
44+
expect(form.getContainer('fieldset').element(by.tagName('legend')).getText()).toBe('Fields'),
45+
verifyElementAttributes(form.getContainer('fieldset'), {
46+
accesskey: 'f',
47+
description: 'This is a fieldset container',
48+
htmlClass: 'fieldset',
49+
style: 'color: green;',
50+
tabindex: 2
51+
})
52+
]);
53+
});
54+
55+
it('should have a details/summary', async () => {
56+
const form: Form = await page.selectExample('Containers');
57+
await Promise.all([
58+
expect(form.getContainerAttribute('details', 'id')).toBeDefined(),
59+
expect(form.getContainerAttribute('details', 'open')).toBeDefined(),
60+
expect(form.getContainer('details').element(by.tagName('summary')).getText()).toBe('Details'),
61+
verifyElementAttributes(form.getContainer('details'), {
62+
accesskey: 's',
63+
description: 'This is a details container',
64+
htmlClass: 'details',
65+
style: 'color: blue;',
66+
tabindex: 1
67+
})
68+
]);
69+
});
70+
});

projects/demo-e2e/src/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ElementFinder } from 'protractor';
2+
3+
export async function verifyElementAttributes(elementFinder: ElementFinder, expectedAttributes: {
4+
accesskey: string;
5+
description: string;
6+
htmlClass: string;
7+
style: string;
8+
tabindex: number;
9+
}): Promise<void> {
10+
const tabIndex = await elementFinder.getAttribute('tabindex');
11+
const expectationPromise = expectedAttributes.tabindex === undefined
12+
? expect(tabIndex).toBeNull()
13+
: expect(tabIndex).toEqual(expectedAttributes.tabindex.toString());
14+
15+
await Promise.all([
16+
expectationPromise,
17+
expect(await elementFinder.getAttribute('style')).toEqual(expectedAttributes.style),
18+
expect(await elementFinder.getAttribute('title')).toEqual(expectedAttributes.description),
19+
expect(await elementFinder.getAttribute('accesskey')).toEqual(expectedAttributes.accesskey),
20+
expect((await elementFinder.getAttribute('class')).split(' ')).toContain(expectedAttributes.htmlClass)
21+
]);
22+
}

projects/demo/src/assets/examples/examples.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
"name": "Buttons",
1717
"file": "buttons"
1818
},
19+
{
20+
"name": "Containers",
21+
"file": "containers"
22+
},
1923
{
2024
"name": "Hidden",
2125
"file": "hidden"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"schema": {
3+
"type": "object",
4+
"properties": {
5+
"items": {
6+
"type": "array",
7+
"items": {
8+
"type": "string",
9+
"title": "Item",
10+
"default": "New Item"
11+
}
12+
}
13+
}
14+
},
15+
"layout": [{
16+
"type": "div",
17+
"items": [],
18+
"accesskey": "d",
19+
"style": {
20+
"color": "red"
21+
},
22+
"htmlClass": "div",
23+
"description": "This is a div container",
24+
"tabindex": 3
25+
}, {
26+
"type": "fieldset",
27+
"title": "Fields",
28+
"items": [],
29+
"accesskey": "f",
30+
"style": {
31+
"color": "green"
32+
},
33+
"htmlClass": "fieldset",
34+
"description": "This is a fieldset container",
35+
"tabindex": 2,
36+
"name": "fields"
37+
}, {
38+
"type": "details",
39+
"title": "Details",
40+
"expandable": true,
41+
"items": [],
42+
"accesskey": "s",
43+
"style": {
44+
"color": "blue"
45+
},
46+
"htmlClass": "details",
47+
"description": "This is a details container",
48+
"tabindex": 1
49+
}]
50+
}

projects/ngx-json-schema-form/src/docs/pages/introduction.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ There are numerous attributes that can be set on a widget which are set via the
174174
"htmlClass": "input-sm",
175175
"name": "name",
176176
"readonly": true,
177-
"style": "color: #333;",
177+
"style": {
178+
"color": "#333"
179+
},
178180
"tabindex": 2,
179181
"title": "Name"
180182
}]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<ng-container [ngSwitch]="layoutNode.type">
2+
<ng-container *ngSwitchCase="'div'">
3+
<div
4+
[attr.accesskey]="options.accesskey"
5+
[attr.class]="options.htmlClass"
6+
[attr.tabindex]="options.tabindex"
7+
[attr.title]="options.description"
8+
[ngStyle]="options.style"
9+
>
10+
<ng-container *ngTemplateOutlet="widgets"></ng-container>
11+
</div>
12+
</ng-container>
13+
<ng-container *ngSwitchCase="'fieldset'">
14+
<fieldset
15+
[attr.accesskey]="options.accesskey"
16+
[attr.class]="options.htmlClass"
17+
[attr.tabindex]="options.tabindex"
18+
[attr.title]="options.description"
19+
[ngStyle]="options.style"
20+
[name]="layoutNode.name"
21+
>
22+
<legend>{{ layoutNode.options.title }}</legend>
23+
<ng-container *ngTemplateOutlet="widgets"></ng-container>
24+
</fieldset>
25+
</ng-container>
26+
<ng-container *ngSwitchCase="'details'">
27+
<details
28+
[attr.accesskey]="options.accesskey"
29+
[attr.class]="options.htmlClass"
30+
[attr.tabindex]="options.tabindex"
31+
[attr.title]="options.description"
32+
[ngStyle]="options.style"
33+
>
34+
<summary>{{ layoutNode.options.title }}</summary>
35+
<ng-container *ngTemplateOutlet="widgets"></ng-container>
36+
</details>
37+
</ng-container>
38+
</ng-container>
39+
<ng-template #widgets>
40+
<jsf-select-widget
41+
*ngFor="let _layoutNode of layoutNode.items; trackBy: trackByFn;"
42+
[layoutNode]="_layoutNode"
43+
></jsf-select-widget>
44+
</ng-template>

projects/ngx-json-schema-form/src/lib/container/container.component.scss

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
4+
import { LayoutNode } from '../core/models/layout-node';
5+
6+
import { JsonSchemaFormService } from '../form/services/json-schema-form.service';
7+
8+
import { ContainerComponent } from './container.component';
9+
10+
describe('ContainerComponent', () => {
11+
let component: ContainerComponent;
12+
let fixture: ComponentFixture<ContainerComponent>;
13+
14+
beforeEach(async () => {
15+
const mockFormService: JsonSchemaFormService = jasmine.createSpyObj('JsonSchemaFormService', {
16+
initializeControl: true
17+
});
18+
(<jasmine.Spy>mockFormService.initializeControl).and.callFake((comp) => {
19+
comp.options = comp.layoutNode.options;
20+
});
21+
22+
await TestBed.configureTestingModule({
23+
declarations: [ ContainerComponent ],
24+
providers: [{
25+
provide: JsonSchemaFormService,
26+
useValue: mockFormService
27+
}],
28+
schemas: [ NO_ERRORS_SCHEMA ]
29+
})
30+
.compileComponents();
31+
});
32+
33+
beforeEach(() => {
34+
fixture = TestBed.createComponent(ContainerComponent);
35+
component = fixture.componentInstance;
36+
component.layoutNode = {id: '0', options: {}, type: 'div', items: []} as any as LayoutNode;
37+
fixture.detectChanges();
38+
});
39+
40+
it('should create', () => {
41+
expect(component).toBeTruthy();
42+
});
43+
44+
it('should return index for tracking', () => {
45+
expect(component.trackByFn(1)).toEqual(1);
46+
});
47+
});

0 commit comments

Comments
 (0)