angular2-json-schema-form
Version:
Angular 2 JSON Schema Form builder
350 lines (324 loc) • 14 kB
text/typescript
import {
ChangeDetectorRef, Component, Input, OnChanges, OnInit
} from '@angular/core';
import * as _ from 'lodash';
import { JsonSchemaFormService } from '../library/json-schema-form.service';
import {
addClasses, inArray, JsonPointer, toTitleCase
} from '../library/utilities/index';
/**
* Bootstrap 3 framework for Angular 2 JSON Schema Form.
*
*/
@Component({
selector: 'bootstrap-3-framework',
template: `
<div
[class]="options?.htmlClass"
[class.has-feedback]="options?.feedback &&
options?.isInputWidget && formControl?.dirty"
[class.has-error]="options?.enableErrorState &&
formControl?.errors && formControl?.dirty"
[class.has-success]="options?.enableSuccessState &&
!formControl?.errors && formControl?.dirty">
<button *ngIf="layoutNode?.arrayItem && options?.removable"
class="close pull-right"
style="position: relative; z-index: 20;"
type="button"
(click)="removeItem()">
<span aria-hidden="true">×</span>
<span class="sr-only">Close</span>
</button>
<div *ngIf="options?.messageLocation === 'top'">
<p *ngIf="options?.errorMessage"
[innerHTML]="options?.errorMessage" class="help-block"></p>
<p *ngIf="options?.feedback && formControl?.dirty"
class="sr-only"
[id]="'control' + layoutNode?._id + 'Status'"
[innerHTML]="options?.errorMessage ? '(error)' : '(success)'"></p>
<p *ngIf="options?.description"
class="help-block"
[innerHTML]="options?.description"></p>
<p *ngIf="options?.help"
class="help-block"
[innerHTML]="options?.help"></p>
</div>
<label *ngIf="options?.title && layoutNode?.type !== 'tab'"
[attr.for]="'control' + layoutNode?._id"
[class]="options?.labelHtmlClass"
[class.sr-only]="options?.notitle"
[innerHTML]="options?.title"></label>
<strong *ngIf="options?.title && options?.required"
class="text-danger">*</strong>
<p *ngIf="layoutNode?.type === 'submit' &&
jsf?.globalOptions?.fieldsRequired">
<strong class="text-danger">*</strong> = required fields
</p>
<div [class.input-group]="options?.fieldAddonLeft || options?.fieldAddonRight">
<span *ngIf="options?.fieldAddonLeft"
class="input-group-addon"
[innerHTML]="options?.fieldAddonLeft"></span>
<select-widget-widget
[formID]="formID"
[layoutNode]="widgetLayoutNode"
[dataIndex]="dataIndex"
[layoutIndex]="layoutIndex"></select-widget-widget>
<span *ngIf="options?.fieldAddonRight"
class="input-group-addon"
[innerHTML]="options?.fieldAddonRight"></span>
</div>
<span *ngIf="options?.feedback && options?.isInputWidget &&
!options?.fieldAddonRight && !layoutNode.arrayItem && formControl?.dirty"
[class.glyphicon-ok]="options?.enableSuccessState && !formControl?.errors"
[class.glyphicon-remove]="options?.enableErrorState && formControl?.errors"
aria-hidden="true"
class="form-control-feedback glyphicon"></span>
<div *ngIf="options?.messageLocation !== 'top'">
<p *ngIf="!options?.errorMessage"
class="help-block"
[innerHTML]="options?.errorMessage"></p>
<p *ngIf="options?.feedback && formControl?.dirty"
class="sr-only"
[id]="'control' + layoutNode?._id + 'Status'"
[innerHTML]="options?.errorMessage ? '(error)' : '(success)'"></p>
<p *ngIf="options?.description"
class="help-block"
[innerHTML]="options?.description"></p>
<p *ngIf="options?.help"
class="help-block"
[innerHTML]="options?.help"></p>
</div>
</div>
<div *ngIf="debug && debugOutput">debug: <pre>{{debugOutput}}</pre></div>
`,
styles: [`
:host /deep/ .list-group-item .form-control-feedback { top: 40; }
:host /deep/ .checkbox,
:host /deep/ .radio { margin-top: 0; margin-bottom: 0; }
:host /deep/ .checkbox-inline,
:host /deep/ .checkbox-inline + .checkbox-inline,
:host /deep/ .checkbox-inline + .radio-inline,
:host /deep/ .radio-inline,
:host /deep/ .radio-inline + .radio-inline,
:host /deep/ .radio-inline + .checkbox-inline { margin-left: 0; margin-right: 10px; }
:host /deep/ .checkbox-inline:last-child,
:host /deep/ .radio-inline:last-child { margin-right: 0; }
`],
})
export class Bootstrap3Component implements OnInit, OnChanges {
private controlInitialized: boolean = false;
private options: any; // Options used in this framework
private widgetLayoutNode: any; // layoutNode passed to child widget
private widgetOptions: any; // Options passed to child widget
private layoutPointer: string;
private formControl: any = null;
private debugOutput: any = '';
@Input() formID: number;
@Input() layoutNode: any;
@Input() layoutIndex: number[];
@Input() dataIndex: number[];
constructor(
public changeDetector: ChangeDetectorRef,
private jsf: JsonSchemaFormService
) { }
ngOnInit() {
this.initializeControl();
}
ngOnChanges() {
this.updateArrayItems();
if (!this.controlInitialized) { this.initializeControl(); }
}
private initializeControl() {
if (this.layoutNode) {
this.options = _.cloneDeep(this.layoutNode.options);
this.widgetLayoutNode = Object.assign(
{}, this.layoutNode, { options: _.cloneDeep(this.layoutNode.options) }
);
this.widgetOptions = this.widgetLayoutNode.options;
this.layoutPointer = this.jsf.getLayoutPointer(this);
this.formControl = this.jsf.getControl(this);
this.updateArrayItems();
this.options.isInputWidget = inArray(this.layoutNode.type, [
'button', 'checkbox', 'checkboxes-inline', 'checkboxes', 'color',
'date', 'datetime-local', 'datetime', 'email', 'file', 'hidden',
'image', 'integer', 'month', 'number', 'password', 'radio',
'radiobuttons', 'radios-inline', 'radios', 'range', 'reset', 'search',
'select', 'submit', 'tel', 'text', 'textarea', 'time', 'url', 'week'
]);
this.options.title = this.setTitle();
this.options.htmlClass =
addClasses(this.options.htmlClass, 'schema-form-' + this.layoutNode.type);
if (this.layoutNode.type === 'array') {
this.options.htmlClass =
addClasses(this.options.htmlClass, 'list-group');
} else if (this.layoutNode.arrayItem && this.layoutNode.type !== '$ref') {
this.options.htmlClass =
addClasses(this.options.htmlClass, 'list-group-item');
} else {
this.options.htmlClass =
addClasses(this.options.htmlClass, 'form-group');
}
this.widgetOptions.htmlClass = '';
this.options.labelHtmlClass =
addClasses(this.options.labelHtmlClass, 'control-label');
this.widgetOptions.activeClass =
addClasses(this.widgetOptions.activeClass, 'active');
this.options.fieldAddonLeft =
this.options.fieldAddonLeft || this.options.prepend;
this.options.fieldAddonRight =
this.options.fieldAddonRight || this.options.append;
// Set miscelaneous styles and settings for each control type
switch (this.layoutNode.type) {
// Checkbox controls
case 'checkbox': case 'checkboxes':
this.widgetOptions.htmlClass = addClasses(
this.widgetOptions.htmlClass, 'checkbox');
break;
case 'checkboxes-inline':
this.widgetOptions.htmlClass = addClasses(
this.widgetOptions.htmlClass, 'checkbox');
this.widgetOptions.itemLabelHtmlClass = addClasses(
this.widgetOptions.itemLabelHtmlClass, 'checkbox-inline');
break;
// Radio controls
case 'radio': case 'radios':
this.widgetOptions.htmlClass = addClasses(
this.widgetOptions.htmlClass, 'radio');
break;
case 'radios-inline':
this.widgetOptions.htmlClass = addClasses(
this.widgetOptions.htmlClass, 'radio');
this.widgetOptions.itemLabelHtmlClass = addClasses(
this.widgetOptions.itemLabelHtmlClass, 'radio-inline');
break;
// Button sets - checkboxbuttons and radiobuttons
case 'checkboxbuttons': case 'radiobuttons':
this.widgetOptions.htmlClass = addClasses(
this.widgetOptions.htmlClass, 'btn-group');
this.widgetOptions.itemLabelHtmlClass = addClasses(
this.widgetOptions.itemLabelHtmlClass, 'btn');
this.widgetOptions.itemLabelHtmlClass = addClasses(
this.widgetOptions.itemLabelHtmlClass, this.options.style || 'btn-default');
this.widgetOptions.fieldHtmlClass = addClasses(
this.widgetOptions.fieldHtmlClass, 'sr-only');
break;
// Single button controls
case 'button': case 'submit':
this.widgetOptions.fieldHtmlClass = addClasses(
this.widgetOptions.fieldHtmlClass, 'btn');
this.widgetOptions.fieldHtmlClass = addClasses(
this.widgetOptions.fieldHtmlClass, this.options.style || 'btn-info');
break;
// Containers - arrays and fieldsets
case 'array': case 'fieldset': case 'section': case 'conditional':
case 'advancedfieldset': case 'authfieldset':
case 'selectfieldset': case 'optionfieldset':
this.options.messageLocation = 'top';
if (this.options.title && this.options.required &&
this.options.title.indexOf('*') === -1
) {
this.options.title += ' <strong class="text-danger">*</strong>';
}
break;
case 'tabarray': case 'tabs':
this.widgetOptions.htmlClass = addClasses(
this.widgetOptions.htmlClass, 'tab-content');
this.widgetOptions.fieldHtmlClass = addClasses(
this.widgetOptions.fieldHtmlClass, 'tab-pane');
this.widgetOptions.labelHtmlClass = addClasses(
this.widgetOptions.labelHtmlClass, 'nav nav-tabs');
break;
// 'Add' buttons - references
case '$ref':
this.widgetOptions.fieldHtmlClass =
addClasses(this.widgetOptions.fieldHtmlClass, 'btn pull-right');
this.widgetOptions.fieldHtmlClass = addClasses(
this.widgetOptions.fieldHtmlClass, this.options.style || 'btn-default');
this.options.icon = 'glyphicon glyphicon-plus';
break;
// Default - including regular inputs
default:
this.widgetOptions.fieldHtmlClass = addClasses(
this.widgetOptions.fieldHtmlClass, 'form-control');
}
if (this.formControl) {
this.formControl.statusChanges.subscribe(value => {
if (this.options.enableErrorState &&
this.options.feedback && value === 'INVALID' &&
this.formControl.dirty && this.formControl.errors
) {
this.options.errorMessage = Object.keys(this.formControl.errors).map(
error => [error, Object.keys(this.formControl.errors[error]).map(
errorParameter => errorParameter + ': ' +
this.formControl.errors[error][errorParameter]
).join(', ')].filter(e => e).join(' - ')
).join('<br>');
} else {
this.options.errorMessage = null;
}
});
if (this.options.debug) {
let vars: any[] = [];
// vars.push(this.jsf.formGroup.value[this.options.name]);
// vars.push(this.jsf.formGroup.controls[this.options.name]['errors']);
this.debugOutput = _.map(vars, thisVar => JSON.stringify(thisVar, null, 2)).join('\n');
}
}
this.controlInitialized = true;
}
}
private updateArrayItems() {
if (this.layoutNode.arrayItem && this.options.removable &&
this.dataIndex && this.dataIndex.length
) {
const arrayIndex = this.dataIndex[this.dataIndex.length - 1];
const parentArray =
JsonPointer.get(this.jsf.layout, this.layoutPointer, 0, -2);
if (parentArray && parentArray.items && parentArray.items.length >= 2) {
const minItems = parentArray.minItems || 0;
const lastArrayItem = parentArray.items.length - 2;
const tupleItems = parentArray.tupleItems;
if (arrayIndex >= minItems && this.options.type !== '$ref' &&
(arrayIndex >= tupleItems || arrayIndex === lastArrayItem)
) {
this.options.removable = true;
}
}
}
}
private setTitle(): string {
switch (this.layoutNode.type) {
case 'array': case 'button': case 'checkbox': case 'conditional':
case 'fieldset': case 'help': case 'msg': case 'message':
case 'section': case 'submit': case 'tabarray': case '$ref':
return null;
case 'advancedfieldset':
this.widgetOptions.expandable = true;
this.widgetOptions.title = 'Advanced options';
return null;
case 'authfieldset':
this.widgetOptions.expandable = true;
this.widgetOptions.title = 'Authentication settings';
return null;
default:
let thisTitle = this.options.title || (
isNaN(this.layoutNode.name) && this.layoutNode.name !== '-' ?
toTitleCase(this.layoutNode.name) : null
);
this.widgetOptions.title = null;
if (!thisTitle) { return null; }
if (thisTitle.indexOf('{') === -1 || !this.formControl || !this.dataIndex) {
return thisTitle;
}
return this.jsf.parseText(
thisTitle,
this.jsf.getControlValue(this),
this.jsf.getControlGroup(this).value,
this.dataIndex[this.dataIndex.length - 1]
);
}
}
private removeItem() {
this.jsf.removeItem(this);
}
}