UNPKG

@delon/form

Version:

Angular form generation based on JSON-Schema.

1,372 lines (1,355 loc) 182 kB
import { Platform } from '@angular/cdk/platform'; import * as i0 from '@angular/core'; import { NgZone, Injectable, inject, ViewContainerRef, Component, ViewEncapsulation, Input, ViewChild, ElementRef, Renderer2, numberAttribute, Directive, ChangeDetectorRef, EventEmitter, booleanAttribute, Injector, ChangeDetectionStrategy, Output, TemplateRef, HostBinding, NgModule, provideEnvironmentInitializer, makeEnvironmentProviders } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DomSanitizer } from '@angular/platform-browser'; import { map, of, BehaviorSubject, Observable, take, combineLatest, distinctUntilChanged, Subject, merge, filter, takeUntil, debounceTime, switchMap, catchError } from 'rxjs'; import { ACLService } from '@delon/acl'; import { DelonLocaleService, ALAIN_I18N_TOKEN, DelonLocaleModule } from '@delon/theme'; import * as i1$1 from '@delon/util/config'; import { AlainConfigService } from '@delon/util/config'; import { deepCopy } from '@delon/util/other'; import { NzFormStatusService } from 'ng-zorro-antd/core/form'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { REGEX } from '@delon/util/format'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i1$2 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import * as i4 from 'ng-zorro-antd/button'; import { NzButtonModule } from 'ng-zorro-antd/button'; import * as i2 from 'ng-zorro-antd/core/transition-patch'; import * as i6 from 'ng-zorro-antd/core/wave'; import * as i5 from 'ng-zorro-antd/grid'; import { NzGridModule } from 'ng-zorro-antd/grid'; import * as i8 from 'ng-zorro-antd/form'; import { NzFormModule } from 'ng-zorro-antd/form'; import * as i9 from 'ng-zorro-antd/icon'; import { NzIconModule } from 'ng-zorro-antd/icon'; import { helpMotion } from 'ng-zorro-antd/core/animation'; import * as i5$1 from 'ng-zorro-antd/tooltip'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; import * as i4$1 from 'ng-zorro-antd/card'; import { NzCardModule } from 'ng-zorro-antd/card'; import * as i4$2 from 'ng-zorro-antd/checkbox'; import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'; import * as i2$2 from 'ng-zorro-antd/date-picker'; import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; import * as i4$4 from 'ng-zorro-antd/input'; import { NzInputModule } from 'ng-zorro-antd/input'; import * as i2$3 from 'ng-zorro-antd/input-number'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; import { NzModalModule } from 'ng-zorro-antd/modal'; import * as i2$4 from 'ng-zorro-antd/radio'; import { NzRadioModule } from 'ng-zorro-antd/radio'; import * as i4$3 from 'ng-zorro-antd/select'; import { NzSelectModule } from 'ng-zorro-antd/select'; import * as i2$1 from 'ng-zorro-antd/switch'; import { NzSwitchModule } from 'ng-zorro-antd/switch'; import { format } from 'date-fns'; import { toDate } from '@delon/util/date-time'; import { ArrayService } from '@delon/util/array'; const SF_DEFAULT_CONFIG = { formatMap: { 'date-time': { widget: 'date', showTime: true, format: `yyyy-MM-dd'T'HH:mm:ss.SSSxxx` }, date: { widget: 'date', format: 'yyyy-MM-dd' }, 'full-date': { widget: 'date', format: 'yyyy-MM-dd' }, time: { widget: 'time', format: 'HH:mm:ss.SSSxxx' }, 'full-time': { widget: 'time' }, week: { widget: 'date', mode: 'week', format: 'yyyy-ww' }, month: { widget: 'date', mode: 'month', format: 'yyyy-MM' }, uri: { widget: 'upload' }, email: { widget: 'autocomplete', type: 'email' }, color: { widget: 'string', type: 'color' }, '': { widget: 'string' } }, ingoreKeywords: ['type', 'enum'], liveValidate: true, autocomplete: null, firstVisual: false, onlyVisual: false, errors: {}, ui: {}, button: { submit_type: 'primary', reset_type: 'default' }, uiDateStringFormat: 'yyyy-MM-dd HH:mm:ss', uiDateNumberFormat: 'T', uiTimeStringFormat: 'HH:mm:ss', uiTimeNumberFormat: 'T', uiEmailSuffixes: ['qq.com', '163.com', 'gmail.com', '126.com', 'aliyun.com'], delay: false }; function mergeConfig(srv) { return srv.merge('sf', SF_DEFAULT_CONFIG); } const SF_SEQ = '/'; function isBlank(o) { return o == null; } function toBool(value, defaultValue) { return value == null ? defaultValue : `${value}` !== 'false'; } function di(ui, ...args) { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (ui.debug) { console.warn(...args); } } } /** 根据 `$ref` 查找 `definitions` */ function findSchemaDefinition($ref, definitions) { const match = /^#\/definitions\/(.*)$/.exec($ref); if (match && match[1]) { // parser JSON Pointer const parts = match[1].split(SF_SEQ); let current = definitions; for (let part of parts) { part = part.replace(/~1/g, SF_SEQ).replace(/~0/g, '~'); if (Object.prototype.hasOwnProperty.call(current, part)) { current = current[part]; } else { throw new Error(`Could not find a definition for ${$ref}.`); } } return current; } throw new Error(`Could not find a definition for ${$ref}.`); } /** * 取回Schema,并处理 `$ref` 的关系 */ function retrieveSchema(schema, definitions = {}) { if (Object.prototype.hasOwnProperty.call(schema, '$ref')) { const $refSchema = findSchemaDefinition(schema.$ref, definitions); // remove $ref property // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $ref, ...localSchema } = schema; return retrieveSchema({ ...$refSchema, ...localSchema }, definitions); } return schema; } function resolveIfSchema(_schema, _ui) { const fn = (schema, ui) => { resolveIf(schema, ui); Object.keys(schema.properties).forEach(key => { const property = schema.properties[key]; const uiKey = `$${key}`; if (property.items) { fn(property.items, ui[uiKey].$items); } if (property.properties) { fn(property, ui[uiKey]); } }); }; fn(_schema, _ui); } function resolveIf(schema, ui) { if (!(Object.prototype.hasOwnProperty.call(schema, 'if') && Object.prototype.hasOwnProperty.call(schema, 'then'))) return null; if (!schema.if.properties) throw new Error(`if: does not contain 'properties'`); const allKeys = Object.keys(schema.properties); const ifKeys = Object.keys(schema.if.properties); detectKey(allKeys, ifKeys); detectKey(allKeys, schema.then.required); schema.required = schema.required.concat(schema.then.required); const hasElse = Object.prototype.hasOwnProperty.call(schema, 'else'); if (hasElse) { detectKey(allKeys, schema.else.required); schema.required = schema.required.concat(schema.else.required); } const visibleIf = {}; const visibleElse = {}; ifKeys.forEach(key => { const cond = schema.if.properties[key].enum; visibleIf[key] = cond; if (hasElse) visibleElse[key] = (value) => !cond.includes(value); }); schema.then.required.forEach(key => (ui[`$${key}`].visibleIf = visibleIf)); if (hasElse) { schema.else.required.forEach(key => (ui[`$${key}`].visibleIf = visibleElse)); } return schema; } function detectKey(keys, detectKeys) { detectKeys.forEach(key => { if (!keys.includes(key)) { throw new Error(`if: properties does not contain '${key}'`); } }); } function orderProperties(properties, order) { if (!Array.isArray(order)) return properties; const arrayToHash = (arr) => arr.reduce((prev, curr) => { prev[curr] = true; return prev; }, {}); const errorPropList = (arr) => `property [${arr.join(`', '`)}]`; const propertyHash = arrayToHash(properties); const orderHash = arrayToHash(order); const extraneous = order.filter(prop => prop !== '*' && !propertyHash[prop]); if (extraneous.length) { throw new Error(`ui schema order list contains extraneous ${errorPropList(extraneous)}`); } const rest = properties.filter(prop => !orderHash[prop]); const restIndex = order.indexOf('*'); if (restIndex === -1) { if (rest.length) { throw new Error(`ui schema order list does not contain ${errorPropList(rest)}`); } return order; } if (restIndex !== order.lastIndexOf('*')) { throw new Error('ui schema order list contains more than one wildcard item'); } const complete = [...order]; complete.splice(restIndex, 1, ...rest); return complete; } function getEnum(list, formData, readOnly) { if (isBlank(list) || !Array.isArray(list) || list.length === 0) return []; if (typeof list[0] !== 'object') { list = list.map((item) => { return { label: item, value: item }; }); } if (formData) { if (!Array.isArray(formData)) formData = [formData]; list.forEach((item) => { if (~formData.indexOf(item.value)) item.checked = true; }); } // fix disabled status if (readOnly) { list.forEach((item) => (item.disabled = true)); } return list; } function getCopyEnum(list, formData, readOnly) { return getEnum(deepCopy(list || []), formData, readOnly); } function getData(schema, ui, formData, asyncArgs) { if (typeof ui.asyncData === 'function') { return ui.asyncData(asyncArgs).pipe(map((list) => getCopyEnum(list, formData, schema.readOnly))); } return of(getCopyEnum(schema.enum, formData, schema.readOnly)); } /** * Whether to using date-fns to format a date */ function isDateFns(srv) { if (!srv) return false; const data = srv.getDateLocale(); // Compatible date-fns v1.x & v2.x return data != null && !!data.formatDistance; // (!!data.distanceInWords || !!data.formatDistance); } class FormProperty { injector; _options; _errors = null; _valueChanges = new BehaviorSubject({ path: null, pathValue: null, value: null }); _errorsChanges = new BehaviorSubject(null); _visible = true; _visibilityChanges = new BehaviorSubject(true); _root; _parent; _objErrors = {}; schemaValidator; schema; ui; formData; _value = null; widget; path; propertyId; constructor(injector, schemaValidatorFactory, schema, ui, formData, parent, path, _options) { this.injector = injector; this._options = _options; this.schema = schema; this.ui = ui; this.schemaValidator = schemaValidatorFactory.createValidatorFn(schema, { ingoreKeywords: this.ui.ingoreKeywords, debug: ui.debug }); this.formData = formData || schema.default; this._parent = parent; if (parent) { this._root = parent.root; } else { this._root = this; } this.path = path; } get valueChanges() { return this._valueChanges; } get errorsChanges() { return this._errorsChanges; } get type() { return this.schema.type; } get parent() { return this._parent; } get root() { return this._root; } get value() { return this._value; } get errors() { return this._errors; } get visible() { return this._visible; } get valid() { return this._errors === null || this._errors.length === 0; } get options() { return this._options; } cd(onlySelf = false) { this.widget?.detectChanges(onlySelf); } /** * 更新值且校验数据 */ updateValueAndValidity(options) { options = { onlySelf: false, emitValidator: true, emitValueEvent: true, updatePath: '', updateValue: null, ...options }; this._updateValue(); if (options.emitValueEvent) { options.updatePath = options.updatePath || this.path; options.updateValue = options.updateValue == null ? this.value : options.updateValue; this.valueChanges.next({ value: this.value, path: options.updatePath, pathValue: options.updateValue }); } // `emitValidator` 每一次数据变更已经包含完整错误链路,后续父节点数据变更无须再触发校验 if (options.emitValidator && this.ui.liveValidate === true) { this._runValidation(); } if (this.parent && !options.onlySelf) { this.parent.updateValueAndValidity({ ...options, emitValidator: false }); } } /** 根据路径搜索表单属性 */ searchProperty(path) { // eslint-disable-next-line @typescript-eslint/no-this-alias let prop = this; let base = null; let result = null; if (path[0] === SF_SEQ) { base = this.findRoot(); result = base.getProperty(path.substring(1)); } else { while (result === null && prop.parent !== null) { prop = base = prop.parent; result = base.getProperty(path); } } return result; } /** 查找根表单属性 */ findRoot() { // eslint-disable-next-line @typescript-eslint/no-this-alias let property = this; while (property.parent !== null) { property = property.parent; } return property; } // #region process errors isEmptyData(value) { if (isBlank(value)) return true; switch (this.type) { case 'string': return `${value}`.length === 0; } return false; } /** * @internal */ _runValidation() { let errors; // The definition of some rules: // 1. Should not ajv validator when is empty data and required fields // 2. Should not ajv validator when is empty data const isEmpty = this.isEmptyData(this._value); if (isEmpty && this.ui._required) { errors = [{ keyword: 'required' }]; } else if (isEmpty) { errors = []; } else { errors = this.schemaValidator(this._value) || []; } const customValidator = this.ui.validator; if (typeof customValidator === 'function') { const customErrors = customValidator(this.value, this, this.findRoot()); if (customErrors instanceof Observable) { customErrors.subscribe(res => { this.setCustomErrors(errors, res); this.cd(false); }); return; } this.setCustomErrors(errors, customErrors); return; } this._errors = errors; this.setErrors(this._errors); } setCustomErrors(errors, list) { const hasCustomError = Array.isArray(list) && list.length > 0; if (hasCustomError) { list.forEach(err => { if (!err.message) { throw new Error(`The custom validator must contain a 'message' attribute to viewed error text`); } err.keyword = null; }); } this._errors = hasCustomError ? errors.concat(...list) : errors; this.setErrors(this._errors); } /** * Set the current error message * * 设置当前错误消息 * * @param emitFormat 若提供的消息带有 `{xx}` 会自动根据参数进行转化,包含自定义函数 * * @example * * this.sf.getProperty('/name')?.setErrors({ keyword: 'required' }); * this.sf.getProperty('/name')?.setErrors({ message: 'Please input your username!' }); * this.sf.getProperty('/name')?.setErrors(); // Clean error */ setErrors(errors = [], emitFormat = true) { let arrErrs = Array.isArray(errors) ? errors : [errors]; if (emitFormat && arrErrs && !this.ui.onlyVisual) { const l = (this.widget && this.widget.l.error) || {}; arrErrs = arrErrs.map((err) => { let message = err.keyword == null && err.message ? err.message : (this.ui.errors || {})[err.keyword] || this._options.errors[err.keyword] || l[err.keyword] || ``; if (message && typeof message === 'function') { message = message(err); } if (message) { if (~message.indexOf('{') && err.params) { message = message.replace(/{([.a-zA-Z0-9]+)}/g, (_v, key) => err.params[key] || ''); } err.message = message; } return err; }); } this._errors = arrErrs; this._errorsChanges.next(arrErrs); // Should send errors to parent field if (this._parent) { this._parent.setParentAndPlatErrors(arrErrs, this.path); } } setParentAndPlatErrors(errors, path) { this._objErrors[path] = errors; const platErrors = []; Object.keys(this._objErrors).forEach(p => { const property = this.searchProperty(p); if (property && !property.visible) return; platErrors.push(...this._objErrors[p]); }); this.setErrors(platErrors, false); } // #endregion // #region condition /** * Set the hide or display of widget * 设置小部件的隐藏或显示 */ setVisible(visible) { this._visible = visible; this._visibilityChanges.next(visible); // 渲染时需要重新触发 reset if (visible) { this.injector .get(NgZone) .onStable.pipe(take(1)) .subscribe(() => { this.resetValue(this.value, true); }); } return this; } _bindVisibility() { const visibleIf = this.ui.visibleIf; if (typeof visibleIf === 'object' && Object.keys(visibleIf).length === 0) { this.setVisible(false); } else if (visibleIf != null) { const propertiesBinding = []; for (const dependencyPath in visibleIf) { if (Object.prototype.hasOwnProperty.call(visibleIf, dependencyPath)) { const property = this.searchProperty(dependencyPath); if (property) { const valueCheck = property.valueChanges.pipe(map(res => { const vi = visibleIf[dependencyPath]; if (typeof vi === 'function') { const viFnRes = vi(res.value, property); // 同步更新 required if (typeof viFnRes === 'object') { const fixViFnRes = { show: false, required: false, ...viFnRes }; const parentRequired = this.parent?.schema.required; if (parentRequired && this.propertyId) { const idx = parentRequired.findIndex(w => w === this.propertyId); if (fixViFnRes.required) { if (idx === -1) parentRequired.push(this.propertyId); } else { if (idx !== -1) parentRequired.splice(idx, 1); } this.ui._required = fixViFnRes.required; } return fixViFnRes.show; } return viFnRes; } if (vi.indexOf('$ANY$') !== -1) { return res.value && res.value.length > 0; } else { return vi.indexOf(res.value) !== -1; } })); const visibilityCheck = property._visibilityChanges; const and = combineLatest([valueCheck, visibilityCheck]).pipe(map(results => results[0] && results[1])); propertiesBinding.push(and); } else { if (typeof ngDevMode === 'undefined' || ngDevMode) { console.warn(`Can't find property ${dependencyPath} for visibility check of ${this.path}`); } } } } combineLatest(propertiesBinding) .pipe(map(values => (this.ui.visibleIfLogical === 'and' ? values.every(v => v) : values.some(v => v))), distinctUntilChanged()) .subscribe(visible => this.setVisible(visible)); } } // #endregion updateFeedback(status = '') { this.ui.feedback = status; this.widget?.injector.get(NzFormStatusService).formStatusChanges.next({ status, hasFeedback: !!status }); this.cd(true); } } class PropertyGroup extends FormProperty { properties = null; getProperty(path) { const subPathIdx = path.indexOf(SF_SEQ); const propertyId = subPathIdx !== -1 ? path.substring(0, subPathIdx) : path; let property = this.properties[propertyId]; if (property !== null && subPathIdx !== -1 && property instanceof PropertyGroup) { const subPath = path.substring(subPathIdx + 1); property = property.getProperty(subPath); } return property; } forEachChild(fn) { // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const propertyId in this.properties) { if (Object.prototype.hasOwnProperty.call(this.properties, propertyId)) { const property = this.properties[propertyId]; fn(property, propertyId); } } } forEachChildRecursive(fn) { this.forEachChild(child => { fn(child); if (child instanceof PropertyGroup) { child.forEachChildRecursive(fn); } }); } _bindVisibility() { super._bindVisibility(); this._bindVisibilityRecursive(); } _bindVisibilityRecursive() { this.forEachChildRecursive(property => { property._bindVisibility(); }); } isRoot() { return this === this.root; } } class ObjectProperty extends PropertyGroup { formPropertyFactory; _propertiesId = []; get propertiesId() { return this._propertiesId; } constructor(injector, formPropertyFactory, schemaValidatorFactory, schema, ui, formData, parent, path, options) { super(injector, schemaValidatorFactory, schema, ui, formData, parent, path, options); this.formPropertyFactory = formPropertyFactory; this.createProperties(); } createProperties() { this.properties = {}; this._propertiesId = []; let orderedProperties; try { orderedProperties = orderProperties(Object.keys(this.schema.properties), this.ui.order); } catch (e) { console.error(`Invalid ${this.schema.title || 'root'} object field configuration:`, e); } orderedProperties.forEach(propertyId => { this.properties[propertyId] = this.formPropertyFactory.createProperty(this.schema.properties[propertyId], this.ui[`$${propertyId}`], (this.formData || {})[propertyId], this, propertyId); this._propertiesId.push(propertyId); }); } setValue(value, onlySelf) { const properties = this.properties; for (const propertyId in value) { if (Object.prototype.hasOwnProperty.call(value, propertyId) && properties[propertyId]) { properties[propertyId].setValue(value[propertyId], true); } } this.cd(onlySelf); this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); } resetValue(value, onlySelf) { value = value || this.schema.default || {}; const properties = this.properties; for (const propertyId in this.schema.properties) { if (Object.prototype.hasOwnProperty.call(this.schema.properties, propertyId)) { properties[propertyId].resetValue(value[propertyId], true); } } this.cd(onlySelf); this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); } _hasValue() { return this.value != null && !!Object.keys(this.value).length; } _updateValue() { const value = {}; this.forEachChild((property, propertyId) => { if (property.visible && property._hasValue()) { value[propertyId] = property.value; } }); this._value = value; } } class ArrayProperty extends PropertyGroup { formPropertyFactory; constructor(injector, formPropertyFactory, schemaValidatorFactory, schema, ui, formData, parent, path, options) { super(injector, schemaValidatorFactory, schema, ui, formData, parent, path, options); this.formPropertyFactory = formPropertyFactory; this.properties = []; } getProperty(path) { const subPathIdx = path.indexOf(SF_SEQ); const pos = +(subPathIdx !== -1 ? path.substring(0, subPathIdx) : path); const list = this.properties; if (isNaN(pos) || pos >= list.length) { return undefined; } const subPath = path.substring(subPathIdx + 1); return list[pos].getProperty(subPath); } setValue(value, onlySelf) { this.properties = []; this.clearErrors(); this.resetProperties(value); this.cd(onlySelf); this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); } resetValue(value, onlySelf) { this._value = value || this.schema.default || []; this.setValue(this._value, onlySelf); } _hasValue() { return true; } _updateValue() { const value = []; this.forEachChild((property) => { if (property.visible) { value.push({ ...(this.widget?.cleanValue ? null : property.formData), ...property.value }); } }); this._value = value; } addProperty(formData) { const newProperty = this.formPropertyFactory.createProperty(deepCopy(this.schema.items), deepCopy(this.ui.$items), formData, this); this.properties.push(newProperty); return newProperty; } resetProperties(formDatas) { for (const item of formDatas) { const property = this.addProperty(item); property.resetValue(item, true); } } clearErrors(property) { (property || this)._objErrors = {}; } // #region actions add(formData) { const newProperty = this.addProperty(formData); newProperty.resetValue(formData, false); return newProperty; } remove(index) { const list = this.properties; this.clearErrors(); list.splice(index, 1); list.forEach((property, idx) => { property.path = [property.parent.path, idx].join(SF_SEQ); this.clearErrors(property); // TODO: 受限于 sf 的设计思路,对于移除数组项需要重新对每个子项进行校验,防止错误被父级合并后引起始终是错误的现象 if (property instanceof ObjectProperty) { property.forEachChild(p => { p.updateValueAndValidity({ emitValueEvent: false }); }); } }); if (list.length === 0) { this.updateValueAndValidity(); } } } class AtomicProperty extends FormProperty { setValue(value, onlySelf) { this._value = value; this.cd(onlySelf); this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); } resetValue(value, onlySelf) { if (value == null) { value = this.schema.default !== undefined ? this.schema.default : this.fallbackValue(); } this._value = value; this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); if (this.widget) { this.widget.reset(value); this.cd(onlySelf); } } _hasValue() { return this.fallbackValue() !== this.value; } _updateValue() { } } class BooleanProperty extends AtomicProperty { fallbackValue() { return null; } } class NumberProperty extends AtomicProperty { fallbackValue() { return null; } setValue(value, onlySelf) { if (typeof value === 'string') { if (value.length) { value = value.indexOf('.') > -1 ? parseFloat(value) : parseInt(value, 10); } else { value = undefined; } } this._value = value; this.cd(onlySelf); this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); } } class StringProperty extends AtomicProperty { fallbackValue() { return null; } setValue(value, onlySelf) { this._value = value == null ? '' : value; this.cd(onlySelf); this.updateValueAndValidity({ onlySelf, emitValueEvent: true }); } } class FormPropertyFactory { injector; schemaValidatorFactory; options; constructor(injector, schemaValidatorFactory, cogSrv) { this.injector = injector; this.schemaValidatorFactory = schemaValidatorFactory; this.options = mergeConfig(cogSrv); } createProperty(schema, ui, formData, parent = null, propertyId) { let newProperty = null; let path = ''; if (parent) { path += parent.path; if (parent.parent !== null) { path += SF_SEQ; } switch (parent.type) { case 'object': path += propertyId; break; case 'array': path += parent.properties.length; break; default: throw new Error(`Instanciation of a FormProperty with an unknown parent type: ${parent.type}`); } } else { path = SF_SEQ; } if (schema.$ref) { const refSchema = retrieveSchema(schema, parent.root.schema.definitions); newProperty = this.createProperty(refSchema, ui, formData, parent, path); } else { // fix required if ((propertyId && parent.schema.required.indexOf(propertyId.split(SF_SEQ).pop()) !== -1) || ui.showRequired === true) { ui._required = true; } // fix title if (schema.title == null) { schema.title = propertyId; } // fix date if ((schema.type === 'string' || schema.type === 'number') && !schema.format && !ui.format) { if (ui.widget === 'date') ui._format = schema.type === 'string' ? this.options.uiDateStringFormat : this.options.uiDateNumberFormat; else if (ui.widget === 'time') ui._format = schema.type === 'string' ? this.options.uiTimeStringFormat : this.options.uiTimeNumberFormat; } else { ui._format = ui.format; } switch (schema.type) { case 'integer': case 'number': newProperty = new NumberProperty(this.injector, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options); break; case 'string': newProperty = new StringProperty(this.injector, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options); break; case 'boolean': newProperty = new BooleanProperty(this.injector, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options); break; case 'object': newProperty = new ObjectProperty(this.injector, this, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options); break; case 'array': newProperty = new ArrayProperty(this.injector, this, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options); break; default: throw new TypeError(`Undefined type ${schema.type}`); } } newProperty.propertyId = propertyId; if (newProperty instanceof PropertyGroup) { this.initializeRoot(newProperty); } return newProperty; } initializeRoot(rootProperty) { // rootProperty.init(); rootProperty._bindVisibility(); } } class TerminatorService { onDestroy; constructor() { this.onDestroy = new Subject(); } destroy() { this.onDestroy.next(true); } } class SchemaValidatorFactory { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SchemaValidatorFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SchemaValidatorFactory }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SchemaValidatorFactory, decorators: [{ type: Injectable }] }); class AjvSchemaValidatorFactory extends SchemaValidatorFactory { ngZone = inject(NgZone); cogSrv = inject(AlainConfigService); ajv; options; constructor() { super(); if (!(typeof document === 'object' && !!document)) { return; } this.options = mergeConfig(this.cogSrv); const customOptions = this.options.ajv || {}; this.ngZone.runOutsideAngular(() => { this.ajv = new Ajv({ allErrors: true, loopEnum: 50, ...customOptions, formats: { 'data-url': /^data:([a-z]+\/[a-z0-9-+.]+)?;name=(.*);base64,(.*)$/, color: REGEX.color, mobile: REGEX.mobile, 'id-card': REGEX.idCard, ...customOptions.formats } }); addFormats(this.ajv); }); } createValidatorFn(schema, extraOptions) { const ingoreKeywords = [ ...this.options.ingoreKeywords, ...(extraOptions.ingoreKeywords || []) ]; return (value) => { try { this.ngZone.runOutsideAngular(() => this.ajv.validate(schema, value)); } catch (e) { if (typeof ngDevMode === 'undefined' || ngDevMode) { // swallow errors thrown in ajv due to invalid schemas, these // still get displayed if (extraOptions.debug) { console.warn(e); } } } let errors = this.ajv.errors; if (this.options && ingoreKeywords && errors) { errors = errors.filter(w => ingoreKeywords.indexOf(w.keyword) === -1); } return errors; }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AjvSchemaValidatorFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AjvSchemaValidatorFactory }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AjvSchemaValidatorFactory, decorators: [{ type: Injectable }], ctorParameters: () => [] }); class WidgetRegistry { _widgets = {}; defaultWidget; get widgets() { return this._widgets; } setDefault(widget) { this.defaultWidget = widget; } register(type, widget) { this._widgets[type] = widget; } has(type) { return Object.prototype.hasOwnProperty.call(this._widgets, type); } getType(type) { if (this.has(type)) { return this._widgets[type]; } return this.defaultWidget; } } class WidgetFactory { registry = inject(WidgetRegistry); createWidget(container, type) { if (!this.registry.has(type)) { if (typeof ngDevMode === 'undefined' || ngDevMode) { console.warn(`No widget for type "${type}"`); } } const componentClass = this.registry.getType(type); return container.createComponent(componentClass); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: WidgetFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: WidgetFactory }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: WidgetFactory, decorators: [{ type: Injectable }] }); let nextUniqueId = 0; class SFItemComponent { widgetFactory = inject(WidgetFactory); terminator = inject(TerminatorService); ref; destroy$ = new Subject(); widget = null; formProperty; footer = null; container; onWidgetInstanciated(widget) { this.widget = widget; const id = `_sf-${nextUniqueId++}`; const ui = this.formProperty.ui; this.widget.formProperty = this.formProperty; this.widget.schema = this.formProperty.schema; this.widget.ui = ui; this.widget.id = id; this.formProperty.widget = widget; } ngOnInit() { this.terminator.onDestroy.subscribe(() => this.ngOnDestroy()); } ngOnChanges() { const p = this.formProperty; this.ref = this.widgetFactory.createWidget(this.container, (p.ui.widget || p.schema.type)); this.onWidgetInstanciated(this.ref.instance); } ngOnDestroy() { const { destroy$ } = this; destroy$.next(); destroy$.complete(); this.ref.destroy(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SFItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: SFItemComponent, isStandalone: false, selector: "sf-item", inputs: { formProperty: "formProperty", footer: "footer" }, host: { properties: { "class.sf__item": "true" } }, providers: [NzFormStatusService], viewQueries: [{ propertyName: "container", first: true, predicate: ["target"], descendants: true, read: ViewContainerRef, static: true }], exportAs: ["sfItem"], usesOnChanges: true, ngImport: i0, template: ` <ng-template #target /> <ng-container *ngTemplateOutlet="footer" /> `, isInline: true, dependencies: [{ kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SFItemComponent, decorators: [{ type: Component, args: [{ selector: 'sf-item', exportAs: 'sfItem', host: { '[class.sf__item]': 'true' }, template: ` <ng-template #target /> <ng-container *ngTemplateOutlet="footer" /> `, preserveWhitespaces: false, encapsulation: ViewEncapsulation.None, providers: [NzFormStatusService], // eslint-disable-next-line @angular-eslint/prefer-standalone standalone: false }] }], propDecorators: { formProperty: [{ type: Input }], footer: [{ type: Input }], container: [{ type: ViewChild, args: ['target', { read: ViewContainerRef, static: true }] }] } }); class SFFixedDirective { el = inject(ElementRef).nativeElement; render = inject(Renderer2); _inited = false; num; init() { if (!this._inited || this.num == null || this.num <= 0) return; const el = this.el; const widgetEl = el.querySelector('.ant-row') || el; this.render.addClass(widgetEl, 'sf__fixed'); const labelEl = widgetEl.querySelector('.ant-form-item-label'); const controlEl = widgetEl.querySelector('.ant-form-item-control-wrapper,.ant-form-item-control'); const unit = `${this.num}px`; if (labelEl) { this.render.setStyle(labelEl, 'flex', `0 0 ${unit}`); this.render.setStyle(controlEl, 'max-width', `calc(100% - ${unit})`); } else { this.render.setStyle(controlEl, 'margin-left', unit); } } ngAfterViewInit() { this._inited = true; this.init(); } ngOnChanges() { if (this._inited) this.init(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SFFixedDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.6", type: SFFixedDirective, isStandalone: false, selector: "[fixed-label]", inputs: { num: ["fixed-label", "num", (v) => numberAttribute(v, 0)] }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: SFFixedDirective, decorators: [{ type: Directive, args: [{ selector: '[fixed-label]', // eslint-disable-next-line @angular-eslint/prefer-standalone standalone: false }] }], propDecorators: { num: [{ type: Input, args: [{ alias: 'fixed-label', transform: (v) => numberAttribute(v, 0) }] }] } }); function useFactory(injector, schemaValidatorFactory, cogSrv) { return new FormPropertyFactory(injector, schemaValidatorFactory, cogSrv); } class SFComponent { formPropertyFactory = inject(FormPropertyFactory); terminator = inject(TerminatorService); dom = inject(DomSanitizer); cdr = inject(ChangeDetectorRef); localeSrv = inject(DelonLocaleService); aclSrv = inject(ACLService); i18nSrv = inject(ALAIN_I18N_TOKEN); platform = inject(Platform); _renders = new Map(); _item; _valid = true; _defUi; options; _inited = false; locale = {}; rootProperty = null; _formData; _btn; _schema; _ui; get btnGrid() { return this._btn.render.grid; } // #region fields /** 表单布局,等同 `nzLayout`,默认:horizontal */ layout = 'horizontal'; /** JSON Schema */ schema; /** UI Schema */ ui; /** 表单默认值 */ formData; /** * 按钮 * - 值为 `null` 或 `undefined` 表示手动添加按钮,但保留容器 * - 值为 `none` 表示手动添加按钮,且不保留容器 * - 使用 `spanLabelFixed` 固定标签宽度时,若无 `render.class` 则默认为居中状态 */ button = {}; /** * 是否实时校验,默认:`true` * - `true` 每一次都校验 * - `false` 提交时校验 */ liveValidate = true; /** 指定表单 `autocomplete` 值 */ autocomplete; /** * Whether to display error visuals immediately * * 是否立即显示错误视觉 */ firstVisual = true; /** * Whether to only display error visuals but not error text * * 是否只展示错误视觉不显示错误文本 */ onlyVisual = false; compact = false; /** * Form default mode, will force override `layout`, `firstVisual`, `liveValidate` parameters * * 表单预设模式,会强制覆盖 `layout`,`firstVisual`,`liveValidate` 参数 */ set mode(value) { switch (value) { case 'search': this.layout = 'inline'; this.firstVisual = false; this.liveValidate = false; if (this._btn) { this._btn.submit = this._btn.search; } break; case 'edit': this.layout = 'horizontal'; this.firstVisual = false; this.liveValidate = true; if (this._btn) { this._btn.submit = this._btn.edit; } break; } this._mode = value; } get mode() { return this._mode; } _mode; /** * Whether to load status,when `true` reset button is disabled status, submit button is loading status */ loading = false; disabled = false; noColon = false; cleanValue = false; delay = false; formValueChange = new EventEmitter(); formChange = new EventEmitter(); formSubmit = new EventEmitter(); formReset = new EventEmitter(); formError = new EventEmitter(); // #endregion /** * Whether the form is valid * * 表单是否有效 */ get valid() { return this._valid; } /** * The value of the form * * 表单值 */ get value() { return this._item; } /** * Get form element property based on [path](https://ng-alain.com/form/qa#path) * * 根据[路径](https://ng-alain.com/form/qa#path)获取表单元素属性 */ getProperty(path) { return this.rootProperty?.searchProperty(path); } /** * Get element value based on [path](https://ng-alain.com/form/qa#path) * * 根据[路径](https://ng-alain.com/form/qa#path)获取表单元素值 */ getValue(path) { return this.getProperty(path)?.value; } /** * Set form element new value based on [path](https://ng-alain.com/form/qa#path) * * 根据[路径](https://ng-alain.com/form/qa#path)设置某个表单元素属性值 */ setValue(path, value) { const item = this.getProperty(path); if (!item) { throw new Error(`Invalid path: ${path}`); } item.resetValue(value, false); return this; } /** * Set form element new `disabled` based on [path](https://ng-alain.com/form/qa#path) * * 根据[路径](https://ng-alain.com/form/qa#path)设置某个表单元素 `disabled` 状态 */ setDisabled(path, status) { const property = this.getProperty(path); if (!property) { throw new Error(`Invalid path: ${path}`); } property.schema.readOnly = status; property.widget.detectChanges(); return this; } /** * Set form element new `required` based on [path](https://ng-alain.com/form/qa#path) * * 根据[路径](https://ng-alain.com/form/qa#path)设置某个表单元素 `required` 状态 */ setRequired(path, status) { const property = this.getProperty(path); if (!property) { throw new Error(`Invalid path: ${path}`); } const key = path.split(SF_SEQ).pop(); const parentRequired = property.parent?.schema.required || []; const idx = parentRequired.findIndex(w => w === key); if (status) { if (idx === -1) parentRequired.push(key); } else { if (idx !== -1) parentRequired.splice(idx, 1); } property.parent.schema.required = parentRequired; property.ui._required = status; property.widget.detectChanges(); this.validator({ onlyRoot: false }); return this; } /** * Update the feedback status of the widget * * 更新小部件的反馈状态 * * ```ts * // Validate status of the widget * this.sf.updateFeedback('/name', 'validating'); * // Clean validate status of the widget * this.sf.updateFeedback('/name'); * ``` */ updateFeedback(path, status = '') { this.getProperty(path)?.updateFeedback(status); return this; } onSubmit(e) { e.preventDefault(); e.stopPropagation(); if (!this.liveValidate) this.validator(); if (!this.valid) return; this.formSubmit.emit(this.value); } constructor(cogSrv) { this.options = mergeConfig(cogSrv); this.liveValidate = this.options.liveValidate; this.firstVisual = this.options.firstVisual; this.autocomplete = this.options.autocomplete; this.delay = this.options.delay; this.localeSrv.change.pipe(takeUntilDestroyed()).subscribe(() => { this.locale = this.localeSrv.getData('sf'); if (this._inited) { this.validator({ emitError: false, onlyRoot: false }); this.coverButtonProperty(); this.cdr.markForCheck(); } }); merge(this.aclSrv.change, this.i18nSrv.change) .pipe(filter(() => this._inited), takeUntilDestroyed()) .subsc