Skip to content

Commit baf290c

Browse files
Merge pull request #138 from stalsma/master
sets up basic bootstrap 4 integration; tested with bootstrap 4, beta 2
2 parents d077f94 + 5be5fa3 commit baf290c

File tree

6 files changed

+352
-4
lines changed

6 files changed

+352
-4
lines changed

src/demo/app/demo.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import { JsonPointer } from '../../lib/src/shared';
2727
})
2828
export class DemoComponent implements OnInit {
2929
examples: any = Examples;
30-
frameworkList: any = ['material-design', 'bootstrap-3', 'no-framework'];
30+
frameworkList: any = ['material-design', 'bootstrap-3', 'bootstrap-4', 'no-framework'];
3131
frameworks: any = {
3232
'material-design': 'Material Design',
3333
'bootstrap-3': 'Bootstrap 3',
34+
'bootstrap-4': 'Bootstrap 4',
3435
'no-framework': 'None (plain HTML)',
3536
};
3637
selectedSet = 'ng-jsf';

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export { MaterialDesignFrameworkModule } from './src/framework-library/material-
8181
export { NoFrameworkComponent } from './src/framework-library/no-framework.component';
8282

8383
export { Bootstrap3FrameworkComponent } from './src/framework-library/bootstrap-3-framework/bootstrap-3-framework.component';
84+
export { Bootstrap4FrameworkComponent } from './src/framework-library/bootstrap-4-framework/bootstrap-4-framework.component';
8485

8586
export { FrameworkLibraryService } from './src/framework-library/framework-library.service';
8687
export { FrameworkLibraryModule } from './src/framework-library/framework-library.module';
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import { ChangeDetectorRef, Component, Input, OnChanges, OnInit } from '@angular/core';
2+
3+
import * as _ from 'lodash';
4+
5+
import { JsonSchemaFormService } from '../../json-schema-form.service';
6+
import {
7+
addClasses, hasOwn, inArray, isArray, JsonPointer, toTitleCase
8+
} from '../../shared';
9+
10+
/**
11+
* Bootstrap 3 framework for Angular JSON Schema Form.
12+
*
13+
*/
14+
@Component({
15+
selector: 'bootstrap-3-framework',
16+
template: `
17+
<div
18+
[class]="options?.htmlClass"
19+
[class.has-feedback]="options?.feedback && options?.isInputWidget &&
20+
(formControl?.dirty || options?.feedbackOnRender)"
21+
[class.has-error]="options?.enableErrorState && formControl?.errors &&
22+
(formControl?.dirty || options?.feedbackOnRender)"
23+
[class.has-success]="options?.enableSuccessState && !formControl?.errors &&
24+
(formControl?.dirty || options?.feedbackOnRender)">
25+
26+
<button *ngIf="showRemoveButton"
27+
class="close pull-right"
28+
type="button"
29+
(click)="removeItem()">
30+
<span aria-hidden="true">&times;</span>
31+
<span class="sr-only">Close</span>
32+
</button>
33+
<div *ngIf="options?.messageLocation === 'top'">
34+
<p *ngIf="options?.helpBlock"
35+
class="help-block"
36+
[innerHTML]="options?.helpBlock"></p>
37+
</div>
38+
39+
<label *ngIf="options?.title && layoutNode?.type !== 'tab'"
40+
[attr.for]="'control' + layoutNode?._id"
41+
[class]="options?.labelHtmlClass"
42+
[class.sr-only]="options?.notitle"
43+
[innerHTML]="options?.title"></label>
44+
<p *ngIf="layoutNode?.type === 'submit' && jsf?.formOptions?.fieldsRequired">
45+
<strong class="text-danger">*</strong> = required fields
46+
</p>
47+
<div [class.input-group]="options?.fieldAddonLeft || options?.fieldAddonRight">
48+
<span *ngIf="options?.fieldAddonLeft"
49+
class="input-group-addon"
50+
[innerHTML]="options?.fieldAddonLeft"></span>
51+
52+
<select-widget-widget
53+
[layoutNode]="widgetLayoutNode"
54+
[dataIndex]="dataIndex"
55+
[layoutIndex]="layoutIndex"></select-widget-widget>
56+
57+
<span *ngIf="options?.fieldAddonRight"
58+
class="input-group-addon"
59+
[innerHTML]="options?.fieldAddonRight"></span>
60+
</div>
61+
62+
<span *ngIf="options?.feedback && options?.isInputWidget &&
63+
!options?.fieldAddonRight && !layoutNode.arrayItem &&
64+
(formControl?.dirty || options?.feedbackOnRender)"
65+
[class.glyphicon-ok]="options?.enableSuccessState && !formControl?.errors"
66+
[class.glyphicon-remove]="options?.enableErrorState && formControl?.errors"
67+
aria-hidden="true"
68+
class="form-control-feedback glyphicon"></span>
69+
<div *ngIf="options?.messageLocation !== 'top'">
70+
<p *ngIf="options?.helpBlock"
71+
class="help-block"
72+
[innerHTML]="options?.helpBlock"></p>
73+
</div>
74+
</div>
75+
76+
<div *ngIf="debug && debugOutput">debug: <pre>{{debugOutput}}</pre></div>
77+
`,
78+
styles: [`
79+
:host /deep/ .list-group-item .form-control-feedback { top: 40px; }
80+
:host /deep/ .checkbox,
81+
:host /deep/ .radio { margin-top: 0; margin-bottom: 0; }
82+
:host /deep/ .checkbox-inline,
83+
:host /deep/ .checkbox-inline + .checkbox-inline,
84+
:host /deep/ .checkbox-inline + .radio-inline,
85+
:host /deep/ .radio-inline,
86+
:host /deep/ .radio-inline + .radio-inline,
87+
:host /deep/ .radio-inline + .checkbox-inline { margin-left: 0; margin-right: 10px; }
88+
:host /deep/ .checkbox-inline:last-child,
89+
:host /deep/ .radio-inline:last-child { margin-right: 0; }
90+
`],
91+
})
92+
export class Bootstrap4FrameworkComponent implements OnInit, OnChanges {
93+
frameworkInitialized = false;
94+
widgetOptions: any; // Options passed to child widget
95+
widgetLayoutNode: any; // layoutNode passed to child widget
96+
options: any; // Options used in this framework
97+
formControl: any = null;
98+
debugOutput: any = '';
99+
debug: any = '';
100+
parentArray: any = null;
101+
isOrderable = false;
102+
@Input() layoutNode: any;
103+
@Input() layoutIndex: number[];
104+
@Input() dataIndex: number[];
105+
106+
constructor(
107+
public changeDetector: ChangeDetectorRef,
108+
public jsf: JsonSchemaFormService
109+
) { }
110+
111+
get showRemoveButton(): boolean {
112+
if (!this.options.removable || this.options.readonly ||
113+
this.layoutNode.type === '$ref'
114+
) { return false; }
115+
if (this.layoutNode.recursiveReference) { return true; }
116+
if (!this.layoutNode.arrayItem || !this.parentArray) { return false; }
117+
// If array length <= minItems, don't allow removing any items
118+
return this.parentArray.items.length - 1 <= this.parentArray.options.minItems ? false :
119+
// For removable list items, allow removing any item
120+
this.layoutNode.arrayItemType === 'list' ? true :
121+
// For removable tuple items, only allow removing last item in list
122+
this.layoutIndex[this.layoutIndex.length - 1] === this.parentArray.items.length - 2;
123+
}
124+
125+
ngOnInit() {
126+
this.initializeFramework();
127+
if (this.layoutNode.arrayItem && this.layoutNode.type !== '$ref') {
128+
this.parentArray = this.jsf.getParentNode(this);
129+
if (this.parentArray) {
130+
this.isOrderable = this.layoutNode.arrayItemType === 'list' &&
131+
!this.options.readonly && this.parentArray.options.orderable;
132+
}
133+
}
134+
}
135+
136+
ngOnChanges() {
137+
if (!this.frameworkInitialized) { this.initializeFramework(); }
138+
}
139+
140+
initializeFramework() {
141+
if (this.layoutNode) {
142+
this.options = _.cloneDeep(this.layoutNode.options);
143+
this.widgetLayoutNode = {
144+
...this.layoutNode,
145+
options: _.cloneDeep(this.layoutNode.options)
146+
};
147+
this.widgetOptions = this.widgetLayoutNode.options;
148+
this.formControl = this.jsf.getFormControl(this);
149+
150+
this.options.isInputWidget = inArray(this.layoutNode.type, [
151+
'button', 'checkbox', 'checkboxes-inline', 'checkboxes', 'color',
152+
'date', 'datetime-local', 'datetime', 'email', 'file', 'hidden',
153+
'image', 'integer', 'month', 'number', 'password', 'radio',
154+
'radiobuttons', 'radios-inline', 'radios', 'range', 'reset', 'search',
155+
'select', 'submit', 'tel', 'text', 'textarea', 'time', 'url', 'week'
156+
]);
157+
158+
this.options.title = this.setTitle();
159+
160+
this.options.htmlClass =
161+
addClasses(this.options.htmlClass, 'schema-form-' + this.layoutNode.type);
162+
this.options.htmlClass =
163+
this.layoutNode.type === 'array' ?
164+
addClasses(this.options.htmlClass, 'list-group') :
165+
this.layoutNode.arrayItem && this.layoutNode.type !== '$ref' ?
166+
addClasses(this.options.htmlClass, 'list-group-item') :
167+
addClasses(this.options.htmlClass, 'form-group');
168+
this.widgetOptions.htmlClass = '';
169+
this.options.labelHtmlClass =
170+
addClasses(this.options.labelHtmlClass, 'control-label');
171+
this.widgetOptions.activeClass =
172+
addClasses(this.widgetOptions.activeClass, 'active');
173+
this.options.fieldAddonLeft =
174+
this.options.fieldAddonLeft || this.options.prepend;
175+
this.options.fieldAddonRight =
176+
this.options.fieldAddonRight || this.options.append;
177+
178+
// Add asterisk to titles if required
179+
if (this.options.title && this.layoutNode.type !== 'tab' &&
180+
!this.options.notitle && this.options.required &&
181+
!this.options.title.includes('*')
182+
) {
183+
this.options.title += ' <strong class="text-danger">*</strong>';
184+
}
185+
// Set miscelaneous styles and settings for each control type
186+
switch (this.layoutNode.type) {
187+
// Checkbox controls
188+
case 'checkbox': case 'checkboxes':
189+
this.widgetOptions.htmlClass = addClasses(
190+
this.widgetOptions.htmlClass, 'checkbox');
191+
break;
192+
case 'checkboxes-inline':
193+
this.widgetOptions.htmlClass = addClasses(
194+
this.widgetOptions.htmlClass, 'checkbox');
195+
this.widgetOptions.itemLabelHtmlClass = addClasses(
196+
this.widgetOptions.itemLabelHtmlClass, 'checkbox-inline');
197+
break;
198+
// Radio controls
199+
case 'radio': case 'radios':
200+
this.widgetOptions.htmlClass = addClasses(
201+
this.widgetOptions.htmlClass, 'radio');
202+
break;
203+
case 'radios-inline':
204+
this.widgetOptions.htmlClass = addClasses(
205+
this.widgetOptions.htmlClass, 'radio');
206+
this.widgetOptions.itemLabelHtmlClass = addClasses(
207+
this.widgetOptions.itemLabelHtmlClass, 'radio-inline');
208+
break;
209+
// Button sets - checkboxbuttons and radiobuttons
210+
case 'checkboxbuttons': case 'radiobuttons':
211+
this.widgetOptions.htmlClass = addClasses(
212+
this.widgetOptions.htmlClass, 'btn-group');
213+
this.widgetOptions.itemLabelHtmlClass = addClasses(
214+
this.widgetOptions.itemLabelHtmlClass, 'btn');
215+
this.widgetOptions.itemLabelHtmlClass = addClasses(
216+
this.widgetOptions.itemLabelHtmlClass, this.options.style || 'btn-default');
217+
this.widgetOptions.fieldHtmlClass = addClasses(
218+
this.widgetOptions.fieldHtmlClass, 'sr-only');
219+
break;
220+
// Single button controls
221+
case 'button': case 'submit':
222+
this.widgetOptions.fieldHtmlClass = addClasses(
223+
this.widgetOptions.fieldHtmlClass, 'btn');
224+
this.widgetOptions.fieldHtmlClass = addClasses(
225+
this.widgetOptions.fieldHtmlClass, this.options.style || 'btn-info');
226+
break;
227+
// Containers - arrays and fieldsets
228+
case 'array': case 'fieldset': case 'section': case 'conditional':
229+
case 'advancedfieldset': case 'authfieldset':
230+
case 'selectfieldset': case 'optionfieldset':
231+
this.options.messageLocation = 'top';
232+
break;
233+
case 'tabarray': case 'tabs':
234+
this.widgetOptions.htmlClass = addClasses(
235+
this.widgetOptions.htmlClass, 'tab-content');
236+
this.widgetOptions.fieldHtmlClass = addClasses(
237+
this.widgetOptions.fieldHtmlClass, 'tab-pane');
238+
this.widgetOptions.labelHtmlClass = addClasses(
239+
this.widgetOptions.labelHtmlClass, 'nav nav-tabs');
240+
break;
241+
// 'Add' buttons - references
242+
case '$ref':
243+
this.widgetOptions.fieldHtmlClass = addClasses(
244+
this.widgetOptions.fieldHtmlClass, 'btn pull-right');
245+
this.widgetOptions.fieldHtmlClass = addClasses(
246+
this.widgetOptions.fieldHtmlClass, this.options.style || 'btn-default');
247+
this.options.icon = 'glyphicon glyphicon-plus';
248+
break;
249+
// Default - including regular inputs
250+
default:
251+
this.widgetOptions.fieldHtmlClass = addClasses(
252+
this.widgetOptions.fieldHtmlClass, 'form-control');
253+
}
254+
255+
if (this.formControl) {
256+
this.updateHelpBlock(this.formControl.status);
257+
this.formControl.statusChanges.subscribe(status => this.updateHelpBlock(status));
258+
259+
if (this.options.debug) {
260+
let vars: any[] = [];
261+
this.debugOutput = _.map(vars, thisVar => JSON.stringify(thisVar, null, 2)).join('\n');
262+
}
263+
}
264+
this.frameworkInitialized = true;
265+
}
266+
267+
}
268+
269+
updateHelpBlock(status) {
270+
this.options.helpBlock = status === 'INVALID' &&
271+
this.options.enableErrorState && this.formControl.errors &&
272+
(this.formControl.dirty || this.options.feedbackOnRender) ?
273+
this.jsf.formatErrors(this.formControl.errors, this.options.errorMessages) :
274+
this.options.description || this.options.help || null;
275+
}
276+
277+
setTitle(): string {
278+
switch (this.layoutNode.type) {
279+
case 'button': case 'checkbox': case 'help': case 'msg':
280+
case 'message': case 'submit': case 'tabarray': case '$ref':
281+
return null;
282+
case 'advancedfieldset':
283+
this.widgetOptions.expandable = true;
284+
this.widgetOptions.title = 'Advanced options';
285+
return null;
286+
case 'authfieldset':
287+
this.widgetOptions.expandable = true;
288+
this.widgetOptions.title = 'Authentication settings';
289+
return null;
290+
case 'tabs': case 'section':
291+
return null;
292+
default:
293+
let thisTitle = this.options.title ||
294+
(isNaN(this.layoutNode.name) && this.layoutNode.name !== '-' ?
295+
toTitleCase(this.layoutNode.name) : null);
296+
this.widgetOptions.title = null;
297+
return !thisTitle ? null :
298+
thisTitle.indexOf('{{') === -1 || !this.formControl || !this.dataIndex ?
299+
thisTitle :
300+
this.jsf.parseText(
301+
thisTitle,
302+
this.jsf.getFormControlValue(this),
303+
this.jsf.getFormControlGroup(this).value,
304+
this.dataIndex[this.dataIndex.length - 1]
305+
);
306+
}
307+
}
308+
309+
removeItem() {
310+
this.jsf.removeItem(this);
311+
}
312+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
4+
import { WidgetLibraryModule } from '../../widget-library/widget-library.module';
5+
6+
import { WidgetLibraryService } from '../../widget-library/widget-library.service';
7+
import { FrameworkLibraryService } from '../framework-library.service';
8+
9+
import { Bootstrap4FrameworkComponent } from './bootstrap-4-framework.component';
10+
11+
@NgModule({
12+
imports: [CommonModule, WidgetLibraryModule],
13+
declarations: [Bootstrap4FrameworkComponent],
14+
exports: [Bootstrap4FrameworkComponent],
15+
entryComponents: [Bootstrap4FrameworkComponent],
16+
providers: [WidgetLibraryService, FrameworkLibraryService]
17+
})
18+
export class Bootstrap4FrameworkModule {
19+
}

src/lib/src/framework-library/framework-library.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ import { Bootstrap3FrameworkModule } from './bootstrap-3-framework/bootstrap-3-f
1010
import { MaterialDesignFrameworkModule } from './material-design-framework/material-design-framework.module';
1111

1212
import { NoFrameworkComponent } from './no-framework.component';
13+
import { Bootstrap4FrameworkModule } from './bootstrap-4-framework/bootstrap-4-framework.module';
1314

1415
@NgModule({
1516
imports: [
1617
CommonModule, WidgetLibraryModule,
17-
Bootstrap3FrameworkModule, MaterialDesignFrameworkModule
18+
Bootstrap3FrameworkModule, MaterialDesignFrameworkModule, Bootstrap4FrameworkModule
1819
],
1920
declarations: [ NoFrameworkComponent ],
2021
exports: [
2122
NoFrameworkComponent,
22-
Bootstrap3FrameworkModule, MaterialDesignFrameworkModule
23+
Bootstrap3FrameworkModule, MaterialDesignFrameworkModule, Bootstrap4FrameworkModule
2324
],
2425
entryComponents: [ NoFrameworkComponent ],
2526
providers: [ WidgetLibraryService, FrameworkLibraryService ]

0 commit comments

Comments
 (0)