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
JavaScript
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