UNPKG

ngx-schema-forms

Version:

New features: - Ajv schema validator. - Angular forms compatible: Property tree is created using FormGroup, FormArray and FormControl classes. - Array now properly loads initial data from model. - WidgetTyep: WidgetRegistry now supports WidgetType, now wo

1,791 lines (1,755 loc) 287 kB
import { ComponentFactoryResolver, Injectable, EventEmitter, Component, Input, forwardRef, ChangeDetectorRef, ViewEncapsulation, TemplateRef, Directive, ViewContainerRef, Injector, NgModule, Output, ElementRef, ContentChildren, SimpleChange } from '@angular/core'; import { BehaviorSubject, combineLatest, merge } from 'rxjs'; import { map, startWith, filter, distinctUntilChanged } from 'rxjs/operators'; import { FormControl, FormArray, FormGroup, NG_VALUE_ACCESSOR, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { __decorate, __metadata } from 'tslib'; import * as ZSchema from 'z-schema'; import * as Ajv from 'ajv'; import { CommonModule } from '@angular/common'; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class ActionRegistry { constructor() { this.actions = {}; } /** * @return {?} */ clear() { this.actions = {}; } /** * @param {?} actionId * @param {?} action * @return {?} */ register(actionId, action) { this.actions[actionId] = action; } /** * @param {?} actionId * @return {?} */ get(actionId) { return this.actions[actionId]; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class ValidatorRegistry { constructor() { this.validators = {}; } /** * @param {?} path * @param {?} validator * @return {?} */ register(path, validator) { this.validators[path] = validator; } /** * @param {?} path * @return {?} */ get(path) { return this.validators[path]; } /** * @return {?} */ clear() { this.validators = {}; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** @enum {string} */ const SchemaPropertyType = { String: 'string', Object: 'object', Array: 'array', Boolean: 'boolean', Integer: 'integer', Number: 'number', }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @abstract */ class SchemaValidatorFactory { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** @enum {string} */ const WidgetType = { Field: 'field', Fieldset: 'fieldset', Button: 'button', }; class WidgetRegistry { constructor() { this.widgets = {}; this.defaultWidget = {}; } /** * @param {?} widget * @param {?=} type * @return {?} */ setDefaultWidget(widget, type = WidgetType.Field) { this.defaultWidget[type] = widget; } /** * @param {?=} type * @return {?} */ getDefaultWidget(type = WidgetType.Field) { return this.defaultWidget[type]; } /** * @param {?} id * @param {?=} type * @return {?} */ hasWidget(id, type = WidgetType.Field) { if (!this.widgets.hasOwnProperty(type)) { return false; } return this.widgets[type].hasOwnProperty(id); } /** * @param {?} id * @param {?} widget * @param {?=} type * @return {?} */ register(id, widget, type = WidgetType.Field) { if (!this.widgets.hasOwnProperty(type)) { this.widgets[type] = {}; } this.widgets[type][id] = widget; } /** * @template T * @param {?} id * @param {?=} type * @return {?} */ getWidgetType(id, type = WidgetType.Field) { if (this.hasWidget(id, type)) { return this.widgets[type][id]; } return this.getDefaultWidget(type); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class WidgetFactory { /** * @param {?} widgetRegistry * @param {?} factoryResolver */ constructor(widgetRegistry, factoryResolver) { this.widgetRegistry = widgetRegistry; this.factoryResolver = factoryResolver; } /** * @template T * @param {?} container * @param {?} id * @param {?=} opts * @return {?} */ createWidget(container, id, opts = { type: WidgetType.Field }) { /** @type {?} */ const componentClass = this.widgetRegistry.getWidgetType(id, opts.type); /** @type {?} */ const componentFactory = this.factoryResolver .resolveComponentFactory(componentClass); return container.createComponent(componentFactory, undefined, // index // index opts.injector); } } WidgetFactory.decorators = [ { type: Injectable } ]; /** @nocollapse */ WidgetFactory.ctorParameters = () => [ { type: WidgetRegistry }, { type: ComponentFactoryResolver } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class FormPropertyErrors { /** * @param {?} errors */ constructor(errors) { this.errors = errors; } /** * @return {?} */ getMessages() { /** @type {?} */ const errorsPaths = Object.keys(this.errors); if (!errorsPaths.length) { return []; } return errorsPaths .reduce((messages, path) => { /** @type {?} */ const message = this.errors[path]["message"]; if (!message) { messages.push('Missing validation error "message" for property ' + path); return messages; } messages.push(message); return messages; }, []); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @template T * @param {?} Base * @return {?} */ function ControlProperty(Base) { /** * @abstract */ class Property extends Base { /** * @param {...?} args */ constructor(...args) { super(...args); this.nonEmptyValueChanges = new EventEmitter(); this.visibilityChanges = new BehaviorSubject(true); this._visible = true; } /** * @return {?} */ get id() { return this.path.toLowerCase().slice(1).replace(/\//g, '-'); } /** * @return {?} */ get isRoot() { return this === this.root; } /** * @return {?} */ get name() { return this.path.split('/').pop(); } /** * @return {?} */ get visible() { return this._visible; } /** * @return {?} */ getErrors() { /** @type {?} */ const errors = this.errors; if (!errors) { return null; } return new FormPropertyErrors({ [this.path]: errors }); } /** * @param {?} visible * @param {?=} opts * @return {?} */ setVisible(visible, opts = { disable: false }) { this._visible = visible; if (opts.disable) { if (this.visible) { this.enable(); } else { this.disable(); } } this.visibilityChanges.next(this.visible); } /** * @return {?} */ bindVisibility() { /** @type {?} */ const visibleIf = this.schema["visibleIf"]; if (visibleIf === undefined) { return; } /** @type {?} */ const paths = Object.keys(visibleIf); if (typeof visibleIf === 'object' && paths.length === 0) { this.setVisible(false); return; } /** @type {?} */ const observables = []; for (const path of paths) { if (!visibleIf.hasOwnProperty(path)) { continue; } /** @type {?} */ const property = this.root.get(path); if (!property) { console.warn(`Couldn't find property ${path} for visibility check of ` + this.path); continue; } /** @type {?} */ const values = visibleIf[path]; /** @type {?} */ const observable = property.valueChanges.pipe(startWith(values.includes(property.value)), map((value) => { return values.includes('$ANY$') || values.includes(value); })); observables.push(observable); } // TODO unsubscribe combineLatest(observables) .subscribe((values) => { this.setVisible(values.includes(true)); }); } /** * @param {?} path * @return {?} */ get(path) { if (typeof path === 'string' && path.includes('/')) { path = this.normalizePath(path); } return super.get(path); } /** * @param {?} path * @return {?} */ normalizePath(path) { if (path[0] === '/') { path = path.slice(1); } return path.split('/'); } } return Property; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class GenericProperty extends ControlProperty(FormControl) { /** * @param {?} path * @param {?} schema */ constructor(path, schema) { super(schema["default"]); this.path = path; this.schema = schema; } /** * @return {?} */ _updateValue() { if (this.value === null || this.value === '') { this.nonEmptyValue = undefined; return; } this.nonEmptyValue = this.value; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class NumberProperty extends GenericProperty { /** * @param {?} value * @param {?=} options * @return {?} */ setValue(value, options = {}) { super.setValue(+value, options); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class BooleanProperty extends GenericProperty { /** * @param {?} value * @param {?=} options * @return {?} */ setValue(value, options = {}) { if (typeof value !== 'boolean') { value = Boolean(value); } super.setValue(value, options); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class StringProperty extends GenericProperty { /** * @param {?} value * @param {?=} options * @return {?} */ setValue(value, options = {}) { if (typeof value !== 'string') { value = `${value}`; } super.setValue(value, options); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class ArrayProperty extends ControlProperty(FormArray) { /** * @param {?} formPropertyFactory * @param {?} path * @param {?} schema */ constructor(formPropertyFactory, path, schema) { super([]); this.formPropertyFactory = formPropertyFactory; this.path = path; this.schema = schema; } /** * @return {?} */ _updateValue() { // to avoid ts complaints super['_updateValue'](); this.nonEmptyValue = this.controls .filter((control) => { /** @type {?} */ const enabled = control.enabled || this.disabled; return control.nonEmptyValue !== undefined && enabled; }) .map((control) => control.value); } /** * @return {?} */ getErrors() { /** @type {?} */ const aggregatedErrors = this.controls .reduce((errors, property) => { /** @type {?} */ const propertyErrors = property.getErrors(); if (!propertyErrors) { return errors; } return Object.assign(errors, propertyErrors.errors); }, {}); if (this.errors) { aggregatedErrors[this.path] = this.errors; } if (!Object.keys(aggregatedErrors).length) { return null; } return new FormPropertyErrors(aggregatedErrors); } /** * @param {?} value * @param {?=} options * @return {?} */ patchValue(value, options = {}) { value.forEach((newValue, index) => { this.addPropertyAt(index); if (this.at(index)) { this.at(index).patchValue(newValue, { onlySelf: true, emitEvent: options.emitEvent }); } }); this.updateValueAndValidity(options); } /** * @return {?} */ addProperty() { /** @type {?} */ const property = this.getPropertyFromSchemaItems(); super.push(property); property.bindVisibility(); } /** * @param {?} index * @return {?} */ addPropertyAt(index) { /** @type {?} */ const property = this.getPropertyFromSchemaItems(); this.insert(index, property); property.bindVisibility(); } /** * @return {?} */ bindVisibility() { super.bindVisibility(); this.controls.forEach((control) => { control.bindVisibility(); }); } /** * @param {?} fn * @param {?=} opts * @return {?} */ forEach(fn, opts = { includeSelf: true }) { if (opts.includeSelf) { fn(this); } for (const control of this.controls) { /** @type {?} */ const property = /** @type {?} */ (control); if (property.forEach instanceof Function) { property.forEach(fn, { includeSelf: true }); continue; } fn(property); } } /** * @return {?} */ getPropertyFromSchemaItems() { return this.formPropertyFactory.createProperty(this.schema["items"], this); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class ObjectProperty extends ControlProperty(FormGroup) { /** * @param {?} path * @param {?} schema */ constructor(path, schema) { super({}); this.path = path; this.schema = schema; } /** * @return {?} */ _updateValue() { // to avoid ts complaints super['_updateValue'](); this.nonEmptyValue = this['_reduceChildren']({}, (result, control, name) => { if (control.nonEmptyValue === undefined) { return result; } if (control.enabled || this.disabled) { result[name] = control.nonEmptyValue; } return result; }); } /** * @return {?} */ getErrors() { /** @type {?} */ const aggregatedErrors = Object.keys(this.controls) .reduce((errors, key) => { /** @type {?} */ const property = /** @type {?} */ (this.controls[key]); /** @type {?} */ const propertyErrors = property.getErrors(); if (!propertyErrors) { return errors; } return Object.assign(errors, propertyErrors.errors); }, {}); if (this.errors) { aggregatedErrors[this.path] = this.errors; } if (!Object.keys(aggregatedErrors).length) { return null; } return new FormPropertyErrors(aggregatedErrors); } /** * @return {?} */ bindVisibility() { super.bindVisibility(); for (const key in this.controls) { if (this.controls.hasOwnProperty(key)) { (/** @type {?} */ (this.controls[key])).bindVisibility(); } } } /** * @param {?} fn * @param {?=} opts * @return {?} */ forEach(fn, opts = { includeSelf: true }) { if (opts.includeSelf) { fn(this); } for (const key in this.controls) { if (this.controls.hasOwnProperty(key)) { /** @type {?} */ const property = (/** @type {?} */ (this.controls[key])); if (property.forEach instanceof Function) { property.forEach(fn, { includeSelf: true }); continue; } fn(property); } } } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @param {?} o * @return {?} */ function isBlank(o) { return o === null || o === undefined; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @param {?} message * @param {?} path * @return {?} */ function formatMessage(message, path) { return `Parsing error on ${path}: ${message}`; } /** * @param {?} message * @param {?} path * @return {?} */ function schemaError(message, path) { /** @type {?} */ const mesg = formatMessage(message, path); throw new Error(mesg); } /** * @param {?} message * @param {?} path * @return {?} */ function schemaWarning(message, path) { /** @type {?} */ const mesg = formatMessage(message, path); throw new Error(mesg); } class SchemaPreprocessor { /** * @param {?} jsonSchema * @param {?=} path * @return {?} */ static preprocess(jsonSchema, path = '/') { jsonSchema = jsonSchema || {}; if (jsonSchema.type === 'object') { SchemaPreprocessor.checkProperties(jsonSchema, path); SchemaPreprocessor.checkAndCreateFieldsets(jsonSchema, path); } else if (jsonSchema.type === 'array') { SchemaPreprocessor.checkItems(jsonSchema, path); } SchemaPreprocessor.normalizeWidget(jsonSchema); SchemaPreprocessor.recursiveCheck(jsonSchema, path); } /** * @param {?} jsonSchema * @param {?} path * @return {?} */ static checkProperties(jsonSchema, path) { if (isBlank(jsonSchema.properties)) { jsonSchema.properties = {}; schemaWarning('Provided json schema does not contain a \'properties\' entry. Output schema will be empty', path); } } /** * @param {?} jsonSchema * @param {?} path * @return {?} */ static checkAndCreateFieldsets(jsonSchema, path) { if (jsonSchema.fieldsets === undefined) { if (jsonSchema.order !== undefined) { SchemaPreprocessor.replaceOrderByFieldsets(jsonSchema); } else { SchemaPreprocessor.createFieldsets(jsonSchema); } } SchemaPreprocessor.checkFieldsUsage(jsonSchema, path); } /** * @param {?} jsonSchema * @param {?} path * @return {?} */ static checkFieldsUsage(jsonSchema, path) { /** @type {?} */ const fieldsId = Object.keys(jsonSchema.properties); /** @type {?} */ const usedFields = {}; for (const fieldset of jsonSchema.fieldsets) { for (const fieldId of fieldset.fields) { if (usedFields[fieldId] === undefined) { usedFields[fieldId] = []; } usedFields[fieldId].push(fieldset.id); } } for (const fieldId of fieldsId) { if (usedFields.hasOwnProperty(fieldId)) { if (usedFields[fieldId].length > 1) { schemaError(`${fieldId} is referenced by more than one fieldset: ${usedFields[fieldId]}`, path); } delete usedFields[fieldId]; } else if (jsonSchema.required.indexOf(fieldId) > -1) { schemaError(`${fieldId} is a required field but it is not referenced as part of a 'order' or a 'fieldset' property`, path); } else { delete jsonSchema[fieldId]; schemaWarning(`Removing unreferenced field ${fieldId}`, path); } } for (const remainingfieldsId in usedFields) { if (usedFields.hasOwnProperty(remainingfieldsId)) { schemaWarning(`Referencing non-existent field ${remainingfieldsId} in one or more fieldsets`, path); } } } /** * @param {?} jsonSchema * @return {?} */ static createFieldsets(jsonSchema) { jsonSchema.order = Object.keys(jsonSchema.properties); SchemaPreprocessor.replaceOrderByFieldsets(jsonSchema); } /** * @param {?} jsonSchema * @return {?} */ static replaceOrderByFieldsets(jsonSchema) { jsonSchema.fieldsets = [{ id: 'fieldset-default', title: jsonSchema.title || '', description: jsonSchema.description || '', name: jsonSchema.name || '', fields: jsonSchema.order }]; delete jsonSchema.order; } /** * @param {?} fieldSchema * @return {?} */ static normalizeWidget(fieldSchema) { /** @type {?} */ let widget = fieldSchema.widget; if (widget === undefined) { widget = { 'id': fieldSchema.type }; } else if (typeof widget === 'string') { widget = { 'id': widget }; } fieldSchema.widget = widget; } /** * @param {?} jsonSchema * @param {?} path * @return {?} */ static checkItems(jsonSchema, path) { if (jsonSchema.items === undefined) { schemaError('No \'items\' property in array', path); } } /** * @param {?} jsonSchema * @param {?} path * @return {?} */ static recursiveCheck(jsonSchema, path) { if (jsonSchema.type === 'object') { /* for (const fieldId in jsonSchema.properties) { if (jsonSchema.properties.hasOwnProperty(fieldId)) { const fieldSchema = jsonSchema.properties[fieldId]; SchemaPreprocessor.preprocess(fieldSchema, path + fieldId + '/'); } } */ if (jsonSchema.hasOwnProperty('definitions')) { for (const fieldId in jsonSchema.definitions) { if (jsonSchema.definitions.hasOwnProperty(fieldId)) { /** @type {?} */ const fieldSchema = jsonSchema.definitions[fieldId]; SchemaPreprocessor.removeRecursiveRefProperties(fieldSchema, `#/definitions/${fieldId}`); // formPropertyFactory recursive is used instead // SchemaPreprocessor.preprocess(fieldSchema, path + fieldId + '/'); } } } } // else if (jsonSchema.type === 'array') { // formPropertyFactory recursive is used instead // SchemaPreprocessor.preprocess(jsonSchema.items, path + '*/'); // } } /** * @param {?} jsonSchema * @param {?} definitionPath * @return {?} */ static removeRecursiveRefProperties(jsonSchema, definitionPath) { // to avoid infinite loop if (jsonSchema.type === 'object') { for (const fieldId in jsonSchema.properties) { if (jsonSchema.properties.hasOwnProperty(fieldId)) { if (jsonSchema.properties[fieldId].$ref && jsonSchema.properties[fieldId].$ref === definitionPath) { delete jsonSchema.properties[fieldId]; } else if (jsonSchema.properties[fieldId].type === 'object') { SchemaPreprocessor.removeRecursiveRefProperties(jsonSchema.properties[fieldId], definitionPath); } } } } } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class FormPropertyFactory { /** * @param {?} schemaValidatorFactory * @param {?} validatorRegistry */ constructor(schemaValidatorFactory, validatorRegistry) { this.schemaValidatorFactory = schemaValidatorFactory; this.validatorRegistry = validatorRegistry; } /** * @param {?} schema * @param {?=} propertyParent * @param {?=} propertyKey * @return {?} */ createProperty(schema, propertyParent, propertyKey) { /** @type {?} */ let property; /** @type {?} */ const path = this.generatePath(propertyParent, propertyKey); SchemaPreprocessor.preprocess(schema, path); // TODO test for parsing for reference schema if (schema["$ref"]) { /** @type {?} */ const refSchema = this.schemaValidatorFactory.getSchema((/** @type {?} */ (propertyParent.root)).schema, schema["$ref"]); property = this.createProperty(refSchema, propertyParent, propertyKey || path); } else { switch (schema["type"]) { case SchemaPropertyType.Integer: case SchemaPropertyType.Number: property = new NumberProperty(path, schema); break; case SchemaPropertyType.String: property = new StringProperty(path, schema); break; case SchemaPropertyType.Boolean: property = new BooleanProperty(path, schema); break; case SchemaPropertyType.Object: property = new ObjectProperty(path, schema); break; case SchemaPropertyType.Array: if (schema["widget"].id === 'array') { property = new ArrayProperty(this, path, schema); } else { schema["default"] = []; property = new GenericProperty(path, schema); } break; default: throw new TypeError(`Undefined type ${schema["type"]}`); } } this.initializeFormProperty(property, propertyParent); return property; } /** * @param {?} property * @param {?=} propertyParent * @return {?} */ initializeFormProperty(property, propertyParent) { if (propertyParent) { property.setParent(propertyParent); } this.bindCustomValidator(property); if (property instanceof ObjectProperty) { for (const key in property.schema["properties"]) { if (property.schema["properties"].hasOwnProperty(key)) { /** @type {?} */ const _schema = property.schema["properties"][key]; /** @type {?} */ const _property = this.createProperty(_schema, property, key); property.addControl(key, _property); } } } if (property.isRoot) { this.bindSchemaValidator(property); // needs to run after entire property tree is built property.bindVisibility(); } } /** * @param {?} property * @return {?} */ bindSchemaValidator(property) { /** @type {?} */ const validate = this.schemaValidatorFactory.createValidatorFn(property.schema); // TODO use pipe startWith to do initial run property.valueChanges .pipe(startWith(null)) .subscribe(() => { /** @type {?} */ const value = property.nonEmptyValue; property.nonEmptyValueChanges.emit(value); /** @type {?} */ const errors = validate(value); if (!errors) { return; } Object.keys(errors).forEach((path) => { /** @type {?} */ const control = property.get(path); if (control) { // set error to specific control control.setErrors(errors[path], { emitEvent: true }); } }); }); } /** * @param {?} property * @return {?} */ bindCustomValidator(property) { /** @type {?} */ const validators = this.validatorRegistry.get(property.path); if (validators) { property.setValidators(validators); } } /** * @param {?=} propertyParent * @param {?=} propertyKey * @return {?} */ generatePath(propertyParent, propertyKey) { if (!propertyParent) { return '/'; } /** @type {?} */ let path = ''; path += propertyParent.path; if (propertyParent.parent !== undefined) { path += '/'; } switch (propertyParent.schema["type"]) { case SchemaPropertyType.Object: path += propertyKey; break; case SchemaPropertyType.Array: path += (/** @type {?} */ (propertyParent)).controls.length; break; default: // TODO move to class throw new Error('Instantiation of a FormProperty with an unknown parent type: ' + propertyParent.schema["type"]); } return path; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** @enum {string} */ const TemplateElementType = { Field: 'field', Button: 'button', }; class TemplateSchemaElementRegistry { constructor() { this.elements = {}; } /** * @param {?} id * @param {?=} type * @return {?} */ hasElement(id, type = TemplateElementType.Field) { if (!this.elements.hasOwnProperty(type)) { return false; } return this.elements[type].hasOwnProperty(id); } /** * @param {?} id * @param {?} element * @param {?=} type * @return {?} */ register(id, element, type = TemplateElementType.Field) { if (!this.elements.hasOwnProperty(type)) { this.elements[type] = {}; } this.elements[type][id] = element; } /** * @template T * @param {?} id * @param {?=} type * @return {?} */ getElement(id, type = TemplateElementType.Field) { if (this.hasElement(id, type)) { return this.elements[type][id]; } } /** * @return {?} */ clear() { this.elements = {}; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @param {?} schemaValidatorFactory * @param {?} validatorRegistry * @return {?} */ function useFactory(schemaValidatorFactory, validatorRegistry) { return new FormPropertyFactory(schemaValidatorFactory, validatorRegistry); } class FormComponent { /** * @param {?} changeDetectorRef * @param {?} formPropertyFactory * @param {?} actionRegistry * @param {?} validatorRegistry */ constructor(changeDetectorRef, formPropertyFactory, actionRegistry, validatorRegistry) { this.changeDetectorRef = changeDetectorRef; this.formPropertyFactory = formPropertyFactory; this.actionRegistry = actionRegistry; this.validatorRegistry = validatorRegistry; this.schema = null; this.actions = {}; this.validators = {}; this.rootFormProperty = null; } /** * @param {?} value * @return {?} */ writeValue(value) { // value should be object if (this.rootFormProperty && value) { this.rootFormProperty.patchValue(value); } } /** * @param {?} fn * @return {?} */ registerOnChange(fn) { this.onChangeCallback = fn; if (this.rootFormProperty) { this.rootFormProperty.nonEmptyValueChanges.subscribe(fn); } } /** * @param {?} fn * @return {?} */ registerOnTouched(fn) { } /** * @param {?} isDisabled * @return {?} */ setDisabledState(isDisabled) { if (!this.rootFormProperty) { return; } if (isDisabled) { this.rootFormProperty.disable(); } else { this.rootFormProperty.enable(); } } /** * @param {?} changes * @return {?} */ ngOnChanges(changes) { if (changes["validators"]) { this.registerValidators(); } if (changes["actions"]) { this.registerActions(); } if (this.schema && !this.schema.type) { this.schema.type = SchemaPropertyType.Object; } if (this.schema && changes["schema"]) { /** @type {?} */ let value; if (this.rootFormProperty) { // TODO validate model against schema value = this.rootFormProperty.nonEmptyValue; } // force component destruction this.rootFormProperty = null; this.changeDetectorRef.detectChanges(); /** @type {?} */ const rootFormProperty = this.formPropertyFactory.createProperty(this.schema); // registerOnChange for changes after init if (this.onChangeCallback) { rootFormProperty.nonEmptyValueChanges.subscribe(this.onChangeCallback); if (value) { rootFormProperty.patchValue(value); } } this.rootFormProperty = rootFormProperty; } } /** * @return {?} */ ngOnInit() { } /** * @return {?} */ registerValidators() { this.validatorRegistry.clear(); if (!this.validators) { return; } for (const propertyPath in this.validators) { if (this.validators.hasOwnProperty(propertyPath)) { this.validatorRegistry.register(propertyPath, this.validators[propertyPath]); } } } /** * @return {?} */ registerActions() { this.actionRegistry.clear(); if (!this.actions) { return; } for (const actionId in this.actions) { if (this.actions.hasOwnProperty(actionId)) { this.actionRegistry.register(actionId, this.actions[actionId]); } } } } FormComponent.decorators = [ { type: Component, args: [{ selector: 'sf-form', template: ` <form #form="ngForm"> <sf-form-element *ngIf="rootFormProperty; else noSchema" [formProperty]="rootFormProperty"> </sf-form-element> <ng-template #noSchema> You need to provide a json or a template schema! </ng-template> </form> `, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FormComponent), multi: true }, ActionRegistry, ValidatorRegistry, WidgetFactory, { provide: FormPropertyFactory, useFactory: useFactory, deps: [SchemaValidatorFactory, ValidatorRegistry] }, TemplateSchemaElementRegistry ], encapsulation: ViewEncapsulation.None }] } ]; /** @nocollapse */ FormComponent.ctorParameters = () => [ { type: ChangeDetectorRef }, { type: FormPropertyFactory }, { type: ActionRegistry }, { type: ValidatorRegistry } ]; FormComponent.propDecorators = { schema: [{ type: Input }], actions: [{ type: Input }], validators: [{ type: Input }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @abstract */ class FormElementTemplateRef extends TemplateRef { } class FormElementComponent { } FormElementComponent.decorators = [ { type: Component, args: [{ selector: 'sf-form-element', template: `<div *ngIf="formProperty.visible && formProperty.schema.widget?.id !== 'none'" [class.has-error]="!formProperty.hasOwnProperty('controls') && !formProperty.valid" [class.has-success]="!formProperty.hasOwnProperty('controls') && formProperty.valid"> <ng-template sfFormPropertyWidgetChooser [formProperty]="formProperty"> </ng-template> <ng-container *ngIf="formProperty.schema.buttons as buttons"> <div class="button-container" > <ng-template sfFormButtonWidgetChooser *ngFor="let button of buttons" [button]="button" [formProperty]="formProperty"> </ng-template> </div> </ng-container> </div>`, encapsulation: ViewEncapsulation.None }] } ]; FormElementComponent.propDecorators = { formProperty: [{ type: Input }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @return {?} */ function Unsubscriber() { return function (target, propertyKey) { /** @type {?} */ const _propertyKey = '__' + propertyKey; Object.defineProperty(target, propertyKey, { get: function () { return this[_propertyKey]; }, set: function (subs) { // replace anything the property holds with subscriptions list if (!this[_propertyKey]) { this[_propertyKey] = /** @type {?} */ ([]); } this[_propertyKey].push(subs); }, enumerable: true, configurable: true }); /** @type {?} */ const componentOnDestroy = target.ngOnDestroy; target.ngOnDestroy = function ngOnDestroy() { if (componentOnDestroy) { componentOnDestroy.call(target); } if (this[_propertyKey] && this[_propertyKey].length) { // unsubscribe to all subscriptions added to unsubscriber while (this[_propertyKey].length) { /** @type {?} */ const subscription = this[_propertyKey].pop(); if (subscription && subscription.unsubscribe) { subscription.unsubscribe(); } } this[_propertyKey] = undefined; } }; }; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @abstract */ class Widget { } /** * @template T */ class FieldsetLayoutWidget extends Widget { } // unsupported: template constraints. /** * @abstract * @template T */ class ButtonLayoutWidget extends Widget { } // unsupported: template constraints. // unsupported: template constraints. /** * @abstract * @template T, U */ class PropertyWidget extends Widget { } // unsupported: template constraints. /** * @template T */ class ArrayPropertyWidget extends PropertyWidget { } // unsupported: template constraints. /** * @template T */ class ObjectPropertyWidget extends PropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class ArrayWidget extends ArrayPropertyWidget { /** * @return {?} */ addItem() { this.formProperty.addProperty(); } /** * @param {?} index * @return {?} */ removeItem(index) { this.formProperty.removeAt(index); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class CheckboxWidget extends PropertyWidget { constructor() { super(...arguments); this.checked = {}; } /** * @return {?} */ ngOnInit() { if (this.schema["type"] === 'array') { this.formProperty.valueChanges.subscribe((values) => { values.forEach((value) => { if (!this.checked[value]) { this.checked[value] = true; } }); }); } } /** * @param {?} checked * @param {?} value * @return {?} */ check(checked, value) { if (checked) { this.checked[value] = true; } else { delete this.checked[value]; } this.formProperty.patchValue(Object.keys(this.checked).filter((key) => this.checked[key])); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class FileWidget extends PropertyWidget { constructor() { super(...arguments); this.reader = new FileReader(); this.filedata = {}; this.fileName = new FormControl(); } /** * @return {?} */ ngAfterViewInit() { this.reader.onloadend = () => { this.filedata.data = btoa(this.reader.result); this.formProperty.setValue(this.filedata); }; } /** * @param {?} $event * @return {?} */ onFileChange($event) { /** @type {?} */ const file = $event.target.files[0]; this.filedata.filename = file.name; this.filedata.size = file.size; this.filedata['content-type'] = file.type; this.filedata.encoding = 'base64'; this.reader.readAsBinaryString(file); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class IntegerWidget extends PropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class ObjectWidget extends ObjectPropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class RadioWidget extends PropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class RangeWidget extends PropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class SelectWidget extends PropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class StringWidget extends PropertyWidget { /** * @return {?} */ getInputType() { if (!this.schema.widget.id || this.schema.widget.id === 'string') { return 'text'; } return this.schema.widget.id; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class TextAreaWidget extends PropertyWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class ButtonWidgetOptions { constructor() { this.onInvalidFormProperty = { disable: false, preventClick: false }; } } // unsupported: template constraints. /** * @abstract * @template T */ class ButtonWidget extends ButtonLayoutWidget { constructor() { super(...arguments); this.options = new ButtonWidgetOptions(); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ // unsupported: template constraints. /** * @abstract * @template T */ class FieldsetWidget extends FieldsetLayoutWidget { } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class FormButtonWidgetChooserDirective { /** * @param {?} viewContainerRef * @param {?} widgetFactory * @param {?} actionRegistry * @param {?} templateRegistry */ constructor(viewContainerRef, widgetFactory, actionRegistry, templateRegistry) { this.viewContainerRef = viewContainerRef; this.widgetFactory = widgetFactory; this.actionRegistry = actionRegistry; this.templateRegistry = templateRegistry; } /** * @return {?} */ getWidget() { /** @type {?} */ const id = 'button'; if (!this.button.widget) { return { id }; } if (!this.button.widget.id) { this.button.widget.id = id; } return this.button.widget; } /** * @param {?} widgetInstance * @return {?} */ getButtonAction(widgetInstance) { return (event, params) => { /** @type {?} */ const options = this.button.options; if (this.formProperty.invalid && options.onInvalidFormProperty.preventClick) { return; } /** @type {?} */ const action = this.actionRegistry.get(this.button.id); if (!action) { return; } action({ event, formProperty: this.formProperty }, params); if (event.hasOwnProperty('preventDefault')) { event.preventDefault(); } }; } /** * @return {?} */ bindTemplateChanges() { /** @type {?} */ const element = this.templateRegistry.getElement(this.button.id, TemplateElementType.Button); if (!element) { return; } // templateSchema button changes this.subs = element.changes.subscribe((button) => { /** @type {?} */ const instance = this.componentRef.instance; // TODO make sure widget id is not changed // TODO widget id change should trigger a form rebuild instance.label = button.label; if (typeof button.widget !== 'string') { Object.assign(instance.widget, button.widget); } Object.assign(instance.options, button.options); // TODO dont rebuild if there is no changes // rebuild action in case onInvalidProperty changed instance.action = this.getButtonAction(instance); this.componentRef.changeDetectorRef.detectChanges(); }); } /** * @return {?} */ ngOnInit() { /** @type {?} */ const widget = this.getWidget(); this.componentRef = this.widgetFactory.createWidget(this.viewContainerRef, widget.id, { type: WidgetType.Button }); /** @type {?} */ const instance = this.componentRef.instance; instance.label = this.button.label; instance.formProperty = this.formProperty; if (instance.widget) { Object.assign(instance.widget, widget); } else { instance.widget = widget; } // update instance options, with schema options Object.assign(instance.options, this.button.options); // after widget has been merged with defaults instance.action = this.getButtonAction(instance); // react to templateSchema button changes this.bindTemplateChanges(); } /** * @return {?} */ ngOnDestroy() { if (this.componentRef) { this.componentRef.destroy(); } if (this.viewContainerRef) { this.viewContainerRef.clear(); } } } FormButtonWidgetChooserDirective.decorators = [ { type: Directive, args: [{ selector: '[sfFormButtonWidgetChooser]' },] } ]; /** @nocollapse */ FormButtonWidgetChooserDirective.ctorParameters = () => [ { type: ViewContainerRef }, { type: WidgetFactory }, { type: ActionRegistry }, { type: TemplateSchemaElementRegistry } ]; FormButtonWidgetChooserDirective.propDecorators = { button: [{ type: Input }], formProperty: [{ type: Input }] }; __decorate([ Unsubscribe