const internalGenerateUuid = (): string => { const tempURL = URL.createObjectURL(new Blob()) const uuidInUrl = tempURL URL.revokeObjectURL(tempURL) const uuid = uuidInUrl.slice(uuidInUrl.lastIndexOf("/") + 1) return uuid } /** * @description 这些属性是所有 Inputor 的共同属性。 */ export interface BaseInputorSchema { inputorTypeName: string id: string name: string placeholder: string value: ExternalValue isVoid: boolean isFocused: boolean /** * @description 只有 isFocused 发生了更新才会被设置为 true。 */ isTouched: boolean /** * @description 只要 value 发生了更新就会被设置为 true。 */ isDirty: boolean isDisabled: boolean isDebugMode: boolean } /** * @description Inputor 内部工作时的上下文,包含一些不对外暴露的属性和状态,扩展自 {@link BaseInputorSchema}。 */ export interface BaseInputorContext< ExternalValue, InternalValue, > extends BaseInputorSchema { internalValue: InternalValue /** * @description 是否可以开始进行交互。 */ isReady: boolean } export interface BaseInputorControllerOptions< ExternalValue, Schema extends BaseInputorSchema, > { id?: string | undefined name?: string | undefined placeholder?: string | undefined /** * @description 为了让所有的值(包括 undefined、null)都可以作为合法的 Inputor 的 Value, * 所以将 voidValue 交给 Inputor 的具体实现者来定义,于是此处就变成了必选。 * * 当然了,具体的 Inputor 为了使用方便,可以在处理好相关逻辑的基础上将其覆盖为可选。 */ value: ExternalValue /** * @default false */ isFocused?: boolean | undefined /** * @default false */ isTouched?: boolean | undefined /** * @default false */ isDirty?: boolean | undefined /** * @default false */ isDisabled?: boolean | undefined /** * @default false */ isDebugMode?: boolean | undefined /** * @description 在原生 type 为 text 的 input 元素中,input 和 change 是两个独立的事件, * 用户的所有修改操作都会实时触发 input 事件,但只有在失去焦点或确认(按回车等) * 时才会触发 change 事件。 * * 此参数用于控制是否在 input 事件发生后自动触发 change 事件,此行为受 * {@link inputDebounce} 影响。 * * @default false */ enableAutoChangeValueAfterInput?: boolean | undefined /** * @description 此参数用于控制 input 事件的触发频率,单位为毫秒,只有当 * {@link enableAutoChangeValueAfterInput} 为 true 时才会生效。 * * @default 0 */ inputDebounce?: number | undefined onInputChange?: InputChangeSubscriber | undefined onValueChange?: ValueChangeSubscriber | undefined onSchemaChange?: SchemaChangeSubscriber | undefined } export interface InputChangeContext { newInput: ExternalValue oldInput: ExternalValue oldValue: ExternalValue } export type InputChangeSubscriber = ( context: InputChangeContext, ) => void | Promise export interface InputChangeSubscriberEntry { subscriber: InputChangeSubscriber unsubscribe: () => void } export type InputChangeSubscriberMap = Map< InputChangeSubscriber, InputChangeSubscriberEntry > export interface ValueChangeContext { newValue: ExternalValue oldValue: ExternalValue } export type ValueChangeSubscriber = ( context: ValueChangeContext, ) => void | Promise export interface ValueChangeSubscriberEntry { subscriber: ValueChangeSubscriber unsubscribe: () => void } export type ValueChangeSubscriberMap = Map< ValueChangeSubscriber, ValueChangeSubscriberEntry > export interface SchemaChangeContext< ExternalValue, Schema extends BaseInputorSchema, > { newSchema: Schema oldSchema: Schema } export type SchemaChangeSubscriber< ExternalValue, Schema extends BaseInputorSchema, > = (context: SchemaChangeContext) => void | Promise export interface SchemaChangeSubscriberEntry< ExternalValue, Schema extends BaseInputorSchema, > { subscriber: SchemaChangeSubscriber unsubscribe: () => void } export type SchemaChangeSubscriberMap< ExternalValue, Schema extends BaseInputorSchema, > = Map< SchemaChangeSubscriber, SchemaChangeSubscriberEntry > export interface InputorControllerReadyState { isReady: boolean promise: Promise resolve: () => void } export interface InputorControllerInputState { isInputing: boolean previousInputValue: ExternalValue currentInputValue: ExternalValue timer: ReturnType | undefined } export interface InputorControllerInitializeHooks { onStart?: () => void | Promise onSchemaReady?: () => void | Promise onOptionsReady?: () => void | Promise onInternalStatesReady?: () => void | Promise onAllReady?: () => void | Promise onFinish?: () => void | Promise } export interface UpdateContext< ExternalValue, InternalValue, > extends BaseInputorSchema { internalValue: InternalValue } export type Update = ( context: UpdateContext, ) => | UpdateContext | Promise> /** * @description 假设一个 NumberTextInputor 由一个 `type="number"` 的 Input 元素实现, * 那么其 InternalValue 就是 number,ExternalValue 就是 string。 * * - ExternalValue - Inputor 对外界声明的值的类型。 * - InternalValue - Inputor 内部维护的值的类型。 * * 备忘: * * - 不要试图将 InternalValue 纳入 Schema,因为 InternalValue 和 ExternalValue 之间有特定的转换关系,将二者都放在 Schema * 中会提升使用难度和出错概率,换言之, InternalValue 叫 InternalValue 的原因之一就是因为我们决定将它隐藏在 Inputor 内部。 */ export abstract class BaseInputorController< ExternalValue, InternalValue, Schema extends BaseInputorSchema, Context extends BaseInputorContext, > { /** * @description 方便进行类型计算。 */ declare abstract schemaType: Schema /** * @description 方便进行类型计算。 */ declare abstract optionsType: BaseInputorControllerOptions /** * @description 方便进行类型计算。 */ declare abstract constructorType: typeof BaseInputorController< ExternalValue, InternalValue, Schema, Context > /** * @description 方便进行类型计算。 */ declare instanceType: this /** * @description 每个 Inputor 都应该有一个类型名称,用于将它与其它的 Inputor 区分开来。 */ abstract inputorTypeName: string protected abstract voidExternalValue: ExternalValue protected abstract voidInternalValue: InternalValue protected id!: string protected name!: string protected placeholder!: string protected value!: ExternalValue protected isVoid!: boolean protected isFocused!: boolean protected isTouched!: boolean protected isDirty!: boolean protected isDisabled!: boolean protected isDebugMode!: boolean protected enableAutoChangeValueAfterInput!: boolean protected inputDebounce!: number protected onInputChange?: InputChangeSubscriber | undefined protected onValueChange?: ValueChangeSubscriber | undefined protected onSchemaChange?: SchemaChangeSubscriber | undefined protected inputChangeSubscriberMap: InputChangeSubscriberMap protected valueChangeSubscriberMap: ValueChangeSubscriberMap protected schemaChangeSubscriberMap: SchemaChangeSubscriberMap protected readyState: InputorControllerReadyState protected internalValue!: InternalValue protected inputState!: InputorControllerInputState constructor(options: BaseInputorControllerOptions) { this.inputChangeSubscriberMap = new Map() this.valueChangeSubscriberMap = new Map() this.schemaChangeSubscriberMap = new Map() let _resolve = (): void => { // this is just a placeholder, the real resolve function will be assigned // below when initializing the readyState } this.readyState = { isReady: false, promise: new Promise((resolve) => { _resolve = () => { this.readyState.isReady = true resolve() } }), resolve: _resolve, } void this.initialize(options) } protected log(...args: unknown[]): void { if (this.isDebugMode === true) { console.log(...args) } } protected async initialize( options: BaseInputorControllerOptions, hooks?: InputorControllerInitializeHooks, ): Promise { const { onStart, onSchemaReady, onOptionsReady, onInternalStatesReady, onAllReady, onFinish } = hooks ?? {} await onStart?.() this.id = options.id ?? internalGenerateUuid() this.name = options.name ?? `${this.inputorTypeName}-${this.id}` this.placeholder = options.placeholder ?? "" this.value = await this.constrainExternalValue(options.value) this.isVoid = await this.isVoidExternalValue(this.value) this.isFocused = options.isFocused ?? false this.isTouched = options.isTouched ?? false this.isDirty = options.isDirty ?? false this.isDisabled = options.isDisabled ?? false this.isDebugMode = options.isDebugMode ?? false await onSchemaReady?.() this.enableAutoChangeValueAfterInput = options.enableAutoChangeValueAfterInput ?? false this.inputDebounce = options.inputDebounce ?? 0 this.onInputChange = options.onInputChange this.onValueChange = options.onValueChange this.onSchemaChange = options.onSchemaChange if (this.onInputChange !== undefined) { this.subscribeInputChange(this.onInputChange) } if (this.onValueChange !== undefined) { this.subscribeValueChange(this.onValueChange) } if (this.onSchemaChange !== undefined) { this.subscribeSchemaChange(this.onSchemaChange) } await onOptionsReady?.() this.internalValue = await this.externalValueToInternalValue(this.value) this.inputState = { isInputing: false, previousInputValue: this.value, currentInputValue: this.value, timer: undefined, } await onInternalStatesReady?.() // update ready states before emitting changes this.ready() await onAllReady?.() const currentValue = structuredClone(this.value) this.emitValueChange({ newValue: currentValue, oldValue: currentValue }) const currentSchema = await this.getSchema() this.emitSchemaChange({ newSchema: currentSchema, oldSchema: currentSchema }) await onFinish?.() } protected emitInputChange(context: InputChangeContext): void { const { newInput, oldInput, oldValue } = context this.log( "[emitInputChange] emit start", "newInput", newInput, "oldInput", oldInput, "oldValue", oldValue, ) this.log("[emitInputChange] count of subscribers", this.inputChangeSubscriberMap.size) for (const state of this.inputChangeSubscriberMap.values()) { void state.subscriber(context) } this.log("[emitInputChange] emit end") } subscribeInputChange(subscriber: InputChangeSubscriber): () => void { this.log("[subscribeInputChange] subscribe start", "subscriber", subscriber) const state: InputChangeSubscriberEntry = { unsubscribe: () => { this.log("[subscribeInputChange] unsubscribe start", "subscriber", subscriber) this.inputChangeSubscriberMap.delete(subscriber) this.log( "[subscribeInputChange] unsubscribe end", "count of subscribers", this.inputChangeSubscriberMap.size, ) }, subscriber, } this.inputChangeSubscriberMap.set(subscriber, state) this.log( "[subscribeInputChange] subscribe end", "count of subscribers", this.inputChangeSubscriberMap.size, ) return state.unsubscribe } unsubscribeInputChange(subscriber: InputChangeSubscriber): void { this.log("[unsubscribeInputChange] unsubscribe start", "subscriber", subscriber) this.inputChangeSubscriberMap.delete(subscriber) this.log( "[unsubscribeInputChange] unsubscribe end", "count of subscribers", this.inputChangeSubscriberMap.size, ) } clearInputChangeSubscribers(): void { this.log( "[clearInputChangeSubscribers] clear start, count of subscribers", this.inputChangeSubscriberMap.size, ) this.inputChangeSubscriberMap.clear() this.log( "[clearInputChangeSubscribers] clear end, count of subscribers", this.inputChangeSubscriberMap.size, ) } protected emitValueChange(context: ValueChangeContext): void { const { newValue, oldValue } = context this.log("[emitValueChange] emit start", "newValue", newValue, "oldValue", oldValue) this.log("[emitValueChange] count of subscribers", this.valueChangeSubscriberMap.size) for (const state of this.valueChangeSubscriberMap.values()) { void state.subscriber(context) } this.log("[emitValueChange] emit end") } subscribeValueChange(subscriber: ValueChangeSubscriber): () => void { this.log("[subscribeValueChange] subscribe start", "subscriber", subscriber) const state: ValueChangeSubscriberEntry = { unsubscribe: () => { this.log("[subscribeValueChange] unsubscribe start", "subscriber", subscriber) this.valueChangeSubscriberMap.delete(subscriber) this.log( "[subscribeValueChange] unsubscribe end", "count of subscribers", this.valueChangeSubscriberMap.size, ) }, subscriber, } this.valueChangeSubscriberMap.set(subscriber, state) this.log( "[subscribeValueChange] subscribe end", "count of subscribers", this.valueChangeSubscriberMap.size, ) return state.unsubscribe } unsubscribeValueChange(subscriber: ValueChangeSubscriber): void { this.log("[unsubscribeValueChange] unsubscribe start", "subscriber", subscriber) this.valueChangeSubscriberMap.delete(subscriber) this.log( "[unsubscribeValueChange] unsubscribe end", "count of subscribers", this.valueChangeSubscriberMap.size, ) } clearValueChangeSubscribers(): void { this.log( "[clearValueChangeSubscribers] clear start, count of subscribers", this.valueChangeSubscriberMap.size, ) this.valueChangeSubscriberMap.clear() this.log( "[clearValueChangeSubscribers] clear end, count of subscribers", this.valueChangeSubscriberMap.size, ) } protected emitSchemaChange(context: SchemaChangeContext): void { const { newSchema, oldSchema } = context this.log("[emitSchemaChange] emit start", "newSchema", newSchema, "oldSchema", oldSchema) this.log("[emitSchemaChange] count of subscribers", this.schemaChangeSubscriberMap.size) for (const state of this.schemaChangeSubscriberMap.values()) { void state.subscriber(context) } this.log("[emitSchemaChange] emit end") } subscribeSchemaChange(subscriber: SchemaChangeSubscriber): () => void { this.log("[subscribeSchemaChange] subscribe start", "subscriber", subscriber) const state: SchemaChangeSubscriberEntry = { unsubscribe: () => { this.log("[subscribeSchemaChange] unsubscribe start", "subscriber", subscriber) this.schemaChangeSubscriberMap.delete(subscriber) this.log( "[subscribeSchemaChange] unsubscribe end", "count of subscribers", this.schemaChangeSubscriberMap.size, ) }, subscriber, } this.schemaChangeSubscriberMap.set(subscriber, state) this.log( "[subscribeSchemaChange] subscribe end", "count of subscribers", this.schemaChangeSubscriberMap.size, ) return state.unsubscribe } unsubscribeSchemaChange(subscriber: SchemaChangeSubscriber): void { this.log("[unsubscribeSchemaChange] unsubscribe start", "subscriber", subscriber) this.schemaChangeSubscriberMap.delete(subscriber) this.log( "[unsubscribeSchemaChange] unsubscribe end", "count of subscribers", this.schemaChangeSubscriberMap.size, ) } clearSchemaChangeSubscribers(): void { this.log( "[clearSchemaChangeSubscribers] clear start, count of subscribers", this.schemaChangeSubscriberMap.size, ) this.schemaChangeSubscriberMap.clear() this.log( "[clearSchemaChangeSubscribers] clear end, count of subscribers", this.schemaChangeSubscriberMap.size, ) } protected ready(): void { this.log("[ready] ready start") this.readyState.resolve() this.log("[ready] ready end") } isReady(): boolean { return this.readyState.isReady } async waitForReady(): Promise { return await this.readyState.promise } getInternalValue(): InternalValue { return structuredClone(this.internalValue) } async getSchema(): Promise { await this.waitForReady() const schema: Schema = { inputorTypeName: this.inputorTypeName, id: this.id, name: this.name, placeholder: this.placeholder, value: this.value, isVoid: this.isVoid, isFocused: this.isFocused, isTouched: this.isTouched, isDirty: this.isDirty, isDisabled: this.isDisabled, isDebugMode: this.isDebugMode, } as unknown as Schema return structuredClone(schema) } async getContext(): Promise { const schema = await this.getSchema() const context: Context = { ...schema, internalValue: this.getInternalValue(), isReady: this.isReady(), } as unknown as Context return structuredClone(context) } protected async isVoidExternalValue(value: ExternalValue): Promise { const isSame = JSON.stringify(value) === JSON.stringify(this.voidExternalValue) return await Promise.resolve(isSame) } protected async isVoidInternalValue(value: InternalValue): Promise { const isSame = JSON.stringify(value) === JSON.stringify(this.voidInternalValue) return await Promise.resolve(isSame) } protected async isSameExternalValue( value1: ExternalValue, value2: ExternalValue, ): Promise { const isSame = JSON.stringify(value1) === JSON.stringify(value2) return await Promise.resolve(isSame) } protected async isSameInternalValue( value1: InternalValue, value2: InternalValue, ): Promise { const isSame = JSON.stringify(value1) === JSON.stringify(value2) return await Promise.resolve(isSame) } protected async isSameSchema(schema1: Schema, schema2: Schema): Promise { const isSame = JSON.stringify(schema1) === JSON.stringify(schema2) return await Promise.resolve(isSame) } /** * @description 给定任意可能是 Schema 的 Record,判断其是否与当前 Inputor 的 Schema 相同。 */ async isEqualSchema(schema: unknown): Promise { if (typeof schema !== "object" || schema === null) { return false } const externalSchema = schema as Schema const internalSchema = await this.getSchema() const partialExternalSchema = {} as Schema const partialInternalSchema = {} as Schema Object.keys(externalSchema).forEach((key) => { if (key in internalSchema) { Object.assign(partialInternalSchema, { [key]: (internalSchema as Record)[key], }) Object.assign(partialExternalSchema, { [key]: (externalSchema as Record)[key], }) } }) const isSame = this.isSameSchema(partialExternalSchema, partialInternalSchema) return await Promise.resolve(isSame) } protected async constrainInternalValue(internalValue: InternalValue): Promise { return await Promise.resolve(internalValue) } protected async constrainExternalValue(externalValue: ExternalValue): Promise { return await Promise.resolve(externalValue) } protected async internalValueToExternalValue( internalValue: InternalValue, ): Promise { return await Promise.resolve(internalValue as unknown as ExternalValue) } protected async externalValueToInternalValue( externalValue: ExternalValue, ): Promise { return await Promise.resolve(externalValue as unknown as InternalValue) } protected async resetInputStates(): Promise { clearTimeout(this.inputState.timer) this.inputState = { isInputing: false, previousInputValue: this.value, currentInputValue: this.value, timer: undefined, } return await Promise.resolve() } async applyInputValue(): Promise { await this.updateValue(this.inputState.currentInputValue) } async updateInputValue(inputValue: ExternalValue): Promise { const previousInputStates = structuredClone(this.inputState) await this.resetInputStates() this.inputState = { isInputing: true, previousInputValue: previousInputStates.currentInputValue, currentInputValue: inputValue, timer: setTimeout(() => { this.emitInputChange({ newInput: this.inputState.currentInputValue, oldInput: this.inputState.previousInputValue, oldValue: this.value, }) if (this.enableAutoChangeValueAfterInput === true) { void this.applyInputValue() } }, this.inputDebounce), } } protected async prepareUpdateContext(): Promise> { const schema = await this.getSchema() return { ...schema, internalValue: this.internalValue, } } /** * @description Will produce a new context rather than modifying the original one. */ protected async doUpdateToContext( context: UpdateContext, update: Update, ): Promise> { const newContext = update(structuredClone(context)) return await Promise.resolve(newContext) } protected async applyUpdateContext( context: UpdateContext, ): Promise { this.id = context.id this.name = context.name this.placeholder = context.placeholder this.value = context.value this.isVoid = context.isVoid this.isFocused = context.isFocused this.isTouched = context.isTouched this.isDirty = context.isDirty this.isDisabled = context.isDisabled this.isDebugMode = context.isDebugMode this.internalValue = context.internalValue await Promise.resolve() } protected async doUpdate(updater: Update): Promise { this.log("[doUpdate] doUpdate start") const prevContext = await this.prepareUpdateContext() const nextContext = await this.doUpdateToContext(prevContext, updater) await this.applyUpdateContext(nextContext) // handle emits & states updates const isSameValue = await this.isSameExternalValue(nextContext.value, prevContext.value) if (isSameValue === false) { this.log( "[doUpdate] value changed", "prevValue", prevContext.value, "nextValue", nextContext.value, ) await this.resetInputStates() this.emitValueChange({ newValue: nextContext.value, oldValue: prevContext.value, }) } const isSameSchema = await this.isSameSchema( nextContext as unknown as Schema, prevContext as unknown as Schema, ) if (isSameSchema === false) { this.log("[doUpdate] schema changed", "prevSchema", prevContext, "nextSchema", nextContext) this.emitSchemaChange({ newSchema: nextContext as unknown as Schema, oldSchema: prevContext as unknown as Schema, }) } this.log("[doUpdate] doUpdate end") } protected async updatePlaceholderToContext( context: UpdateContext, placeholder: string, ): Promise> { context.placeholder = placeholder return await Promise.resolve(context) } async updatePlaceholder(placeholder: string): Promise { await this.doUpdate(async (context) => { return await this.updatePlaceholderToContext(context, placeholder) }) } protected async updateValueToContext( context: UpdateContext, value: ExternalValue, ): Promise> { const clonedContext = structuredClone(context) const oldValue = clonedContext.value const newValue = await this.constrainExternalValue(value) const isSameValue = await this.isSameExternalValue(newValue, oldValue) if (isSameValue === true) { return clonedContext } const isVoidValue = await this.isVoidExternalValue(newValue) clonedContext.isVoid = isVoidValue clonedContext.isDirty = true clonedContext.value = newValue const newInternalValue = await this.externalValueToInternalValue(newValue) const updatedContext = await this.updateInternalValueToContext(clonedContext, newInternalValue) return updatedContext } async updateValue(value: ExternalValue): Promise { await this.doUpdate(async (context) => { return await this.updateValueToContext(context, value) }) } protected async updateIsVoidToContext( context: UpdateContext, isVoid: boolean, ): Promise> { const currentIsVoid = context.isVoid const isSameIsVoid = isVoid === currentIsVoid if (isSameIsVoid === true) { return context } if (isVoid === true) { const updatedContext = await this.updateValueToContext(context, this.voidExternalValue) return updatedContext } return context } async updateIsVoid(isVoid: true): Promise { await this.doUpdate(async (context) => { return await this.updateIsVoidToContext(context, isVoid) }) } protected async updateIsFocusedToContext( context: UpdateContext, isFocused: boolean, ): Promise> { const currentIsFocused = context.isFocused const isSameIsFocused = isFocused === currentIsFocused if (isSameIsFocused === true) { return context } context.isFocused = isFocused const updatedContext = await this.updateIsTouchedToContext(context, true) return updatedContext } async updateIsFocused(isFocused: boolean): Promise { await this.doUpdate(async (context) => { return await this.updateIsFocusedToContext(context, isFocused) }) } protected async updateIsTouchedToContext( context: UpdateContext, isTouched: boolean, ): Promise> { context.isTouched = isTouched return await Promise.resolve(context) } async updateIsTouched(isTouched: boolean): Promise { await this.doUpdate(async (context) => { return await this.updateIsTouchedToContext(context, isTouched) }) } protected async updateIsDirtyToContext( context: UpdateContext, isDirty: boolean, ): Promise> { context.isDirty = isDirty return await Promise.resolve(context) } async updateIsDirty(isDirty: boolean): Promise { await this.doUpdate(async (context) => { return await this.updateIsDirtyToContext(context, isDirty) }) } protected async updateIsDisabledToContext( context: UpdateContext, isDisabled: boolean, ): Promise> { context.isDisabled = isDisabled return await Promise.resolve(context) } async updateIsDisabled(isDisabled: boolean): Promise { await this.doUpdate(async (context) => { return await this.updateIsDisabledToContext(context, isDisabled) }) } protected async updateIsDebugModeToContext( context: UpdateContext, isDebugMode: boolean, ): Promise> { context.isDebugMode = isDebugMode return await Promise.resolve(context) } async updateIsDebugMode(isDebugMode: boolean): Promise { await this.doUpdate(async (context) => { return await this.updateIsDebugModeToContext(context, isDebugMode) }) } protected async updateInternalValueToContext( context: UpdateContext, internalValue: InternalValue, ): Promise> { const clonedContext = structuredClone(context) const oldInternalValue = clonedContext.internalValue const newInternalValue = await this.constrainInternalValue(internalValue) const isSameInternalValue = await this.isSameInternalValue(newInternalValue, oldInternalValue) if (isSameInternalValue === true) { return clonedContext } clonedContext.internalValue = newInternalValue const newValue = await this.internalValueToExternalValue(newInternalValue) const updatedContext = await this.updateValueToContext(clonedContext, newValue) return updatedContext } protected async updateInternalValue(internalValue: InternalValue): Promise { await this.doUpdate(async (context) => { return await this.updateInternalValueToContext(context, internalValue) }) } protected async updateSchemaToContext( context: UpdateContext, schema: Schema, ): Promise> { let updatedContext = structuredClone(context) updatedContext = await this.updatePlaceholderToContext(updatedContext, schema.placeholder) updatedContext = await this.updateValueToContext(updatedContext, schema.value) updatedContext = await this.updateIsVoidToContext(updatedContext, schema.isVoid) updatedContext = await this.updateIsFocusedToContext(updatedContext, schema.isFocused) updatedContext = await this.updateIsTouchedToContext(updatedContext, schema.isTouched) updatedContext = await this.updateIsDirtyToContext(updatedContext, schema.isDirty) updatedContext = await this.updateIsDisabledToContext(updatedContext, schema.isDisabled) updatedContext = await this.updateIsDebugModeToContext(updatedContext, schema.isDebugMode) return await Promise.resolve(updatedContext) } async updateSchema(schema: Schema): Promise { await this.doUpdate(async (context) => { return await this.updateSchemaToContext(context, schema) }) } destroy(): void { this.clearInputChangeSubscribers() this.clearValueChangeSubscribers() this.clearSchemaChangeSubscribers() } }