diff --git a/packages/dbx-form/src/lib/form/action/form.action.directive.spec.ts b/packages/dbx-form/src/lib/form/action/form.action.directive.spec.ts index 30646451e..92066464e 100644 --- a/packages/dbx-form/src/lib/form/action/form.action.directive.spec.ts +++ b/packages/dbx-form/src/lib/form/action/form.action.directive.spec.ts @@ -87,6 +87,8 @@ describe('FormActionDirective', () => { }); + // todo: test dbxActionFormDisabledWhileWorking + }); @Component({ diff --git a/packages/dbx-form/src/lib/form/action/form.action.directive.ts b/packages/dbx-form/src/lib/form/action/form.action.directive.ts index b9615bf9c..29944be0e 100644 --- a/packages/dbx-form/src/lib/form/action/form.action.directive.ts +++ b/packages/dbx-form/src/lib/form/action/form.action.directive.ts @@ -1,7 +1,6 @@ import { Directive, Host, OnInit, OnDestroy, Input } from '@angular/core'; import { addSeconds, isPast } from 'date-fns'; -import { Observable, of, combineLatest, exhaustMap } from 'rxjs'; -import { catchError, filter, first, map, switchMap } from 'rxjs/operators'; +import { Observable, of, combineLatest, exhaustMap, catchError, delay, filter, first, map, switchMap, BehaviorSubject, distinctUntilChanged } from 'rxjs'; import { DbxActionContextStoreSourceInstance } from '@dereekb/dbx-core'; import { ReadableError } from '@dereekb/util'; import { SubscriptionObject, LockSet } from '@dereekb/rxjs'; @@ -15,7 +14,7 @@ export interface DbxActionFormTriggerResult { export type DbxActionFormValidateFn = (value: T) => Observable; export type DbxActionFormModifiedFn = (value: T) => Observable; -export const APP_ACTION_FORM_DISABLED_KEY = 'actionForm'; +export const APP_ACTION_FORM_DISABLED_KEY = 'dbx_action_form'; /** * Used with an action to bind a form to an action as it's value source. @@ -36,16 +35,19 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { * ready to send before the context store is marked enabled. */ @Input() - appActionFormValidator?: DbxActionFormValidateFn; + dbxActionFormValidator?: DbxActionFormValidateFn; /** * Optional function that checks whether or not the value has been modified. */ @Input() - appActionFormModified?: DbxActionFormModifiedFn; + dbxActionFormModified?: DbxActionFormModifiedFn; + + private _formDisabledWhileWorking = new BehaviorSubject(true); private _triggeredSub = new SubscriptionObject(); private _isCompleteSub = new SubscriptionObject(); + private _isWorkingSub = new SubscriptionObject(); constructor(@Host() public readonly form: DbxMutableForm, public readonly source: DbxActionContextStoreSourceInstance) { if (form.lockSet) { @@ -55,6 +57,15 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { this.lockSet.addChildLockSet(source.lockSet, 'source'); } + @Input() + get dbxActionFormDisabledWhileWorking() { + return this._formDisabledWhileWorking.value; + } + + set dbxActionFormDisabledWhileWorking(dbxActionFormDisabledWhileWorking: boolean) { + this._formDisabledWhileWorking.next(Boolean(dbxActionFormDisabledWhileWorking ?? true)); + } + ngOnInit(): void { // Pass data from the form to the source when triggered. @@ -98,6 +109,7 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { // Update the enabled/disabled state this._isCompleteSub.subscription = this.form.stream$.pipe( + delay(0), filter((x) => x.state !== DbxFormState.INITIALIZING), switchMap((event) => { return this.form.getValue().pipe( @@ -114,7 +126,7 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { const initialIsValidCheck = event.isComplete; if (initialIsValidCheck) { - validatorObs = (this.appActionFormValidator) ? this.appActionFormValidator(value) : of(true); + validatorObs = (this.dbxActionFormValidator) ? this.dbxActionFormValidator(value) : of(true); } else { validatorObs = of(false); } @@ -123,7 +135,7 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { const isConsideredModified = (event.pristine === false && isProbablyTouched); if (isConsideredModified) { - modifiedObs = (this.appActionFormModified) ? this.appActionFormModified(value) : of(true); + modifiedObs = (this.dbxActionFormModified) ? this.dbxActionFormModified(value) : of(true); } else { modifiedObs = of(false); } @@ -148,8 +160,13 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { this.source.enable(APP_ACTION_FORM_DISABLED_KEY, valid); }); - // TODO: Watch the working state and stop allowing input on working..? - // TODO: Watch the disabled state for when another disabled key disables this form. + // Watch the working state and disable form while working + this._isWorkingSub.subscription = combineLatest([this.source.isWorking$, this._formDisabledWhileWorking]).pipe( + map(([isWorking, lockOnWorking]: [boolean, boolean]) => lockOnWorking && isWorking), + distinctUntilChanged() + ).subscribe((disable) => { + this.form.setDisabled(APP_ACTION_FORM_DISABLED_KEY, disable); + }); } ngOnDestroy(): void { @@ -157,12 +174,15 @@ export class DbxActionFormDirective implements OnInit, OnDestroy { this.lockSet.destroyOnNextUnlock(() => { this._triggeredSub.destroy(); this._isCompleteSub.destroy(); + this._isWorkingSub.destroy(); + this._formDisabledWhileWorking.complete(); + this.form.setDisabled(APP_ACTION_FORM_DISABLED_KEY, false); }); } protected preCheckReadyValue(value: T): Observable { - let validatorObs: Observable = (this.appActionFormValidator) ? this.appActionFormValidator(value) : of(true); - let modifiedObs: Observable = (this.appActionFormModified) ? this.appActionFormModified(value) : of(true); + let validatorObs: Observable = (this.dbxActionFormValidator) ? this.dbxActionFormValidator(value) : of(true); + let modifiedObs: Observable = (this.dbxActionFormModified) ? this.dbxActionFormModified(value) : of(true); return combineLatest([ validatorObs, diff --git a/packages/dbx-form/src/lib/form/form.ts b/packages/dbx-form/src/lib/form/form.ts index 2a60cb319..e0679943e 100644 --- a/packages/dbx-form/src/lib/form/form.ts +++ b/packages/dbx-form/src/lib/form/form.ts @@ -1,7 +1,7 @@ import { forwardRef, Provider, Type } from '@angular/core'; import { Observable } from 'rxjs'; import { LockSet } from '@dereekb/rxjs'; -import { Maybe } from '@dereekb/util'; +import { BooleanStringKeyArray, Maybe } from '@dereekb/util'; /** * Current state of a DbxForm @@ -12,6 +12,14 @@ export enum DbxFormState { USED = 1 } + +/** + * Unique key for disabling/enabling. + */ +export type DbxFormDisabledKey = string; + +export const DEFAULT_FORM_DISABLED_KEY = 'dbx_form_disabled'; + /** * DbxForm stream event */ @@ -22,7 +30,14 @@ export interface DbxFormEvent { readonly untouched?: boolean; readonly lastResetAt?: Date; readonly changesCount?: number; + /** + * Whether or not the form is disabled. + */ readonly isDisabled?: boolean; + /** + * Current disabled state keys. + */ + readonly disabled?: BooleanStringKeyArray; } /** @@ -35,6 +50,11 @@ export abstract class DbxForm { * Returns an observable that returns the current state of the form. */ abstract getValue(): Observable; + + /** + * Returns an observable that returns the current disabled keys. + */ + abstract getDisabled(): Observable; } export abstract class DbxMutableForm extends DbxForm { @@ -42,6 +62,7 @@ export abstract class DbxMutableForm extends DbxForm { * LockSet for the form. */ abstract readonly lockSet?: LockSet; + /** * Sets the initial value of the form, and resets the form. * @@ -55,11 +76,11 @@ export abstract class DbxMutableForm extends DbxForm { abstract resetForm(): void; /** - * Sets the form's disabled state. + * Disables the form * * @param disabled */ - abstract setDisabled(disabled?: boolean): void; + abstract setDisabled(key?: DbxFormDisabledKey, disabled?: boolean): void; /** * Force the form to update itself as if it was changed. diff --git a/packages/dbx-form/src/lib/formly/formly.context.ts b/packages/dbx-form/src/lib/formly/formly.context.ts index f10069e63..e75e47928 100644 --- a/packages/dbx-form/src/lib/formly/formly.context.ts +++ b/packages/dbx-form/src/lib/formly/formly.context.ts @@ -1,14 +1,14 @@ -import { Provider, Type } from '@angular/core'; +import { Provider } from '@angular/core'; import { BehaviorSubject, Observable, of, switchMap, shareReplay, distinctUntilChanged } from 'rxjs'; -import { DbxForm, DbxFormEvent, DbxFormState, DbxMutableForm, ProvideDbxMutableForm } from '../form/form'; +import { DbxForm, DbxFormDisabledKey, DbxFormEvent, DbxFormState, DbxMutableForm, DEFAULT_FORM_DISABLED_KEY, ProvideDbxMutableForm } from '../form/form'; import { FormlyFieldConfig } from '@ngx-formly/core'; import { LockSet, filterMaybe } from '@dereekb/rxjs'; -import { Maybe } from '@dereekb/util'; +import { BooleanStringKeyArray, BooleanStringKeyArrayUtilityInstance, Maybe } from '@dereekb/util'; export interface DbxFormlyInitialize { fields: Observable; + initialDisabled: BooleanStringKeyArray; initialValue: Maybe>; - initialDisabled: boolean; } /** @@ -16,7 +16,7 @@ export interface DbxFormlyInitialize { * * This is usually the component or element that contains the form itself. */ -export interface DbxFormlyContextDelegate extends Omit, 'lockSet' | 'setDisabled'> { +export interface DbxFormlyContextDelegate extends Omit, 'lockSet'> { readonly stream$: Observable; init(initialize: DbxFormlyInitialize): void; } @@ -43,10 +43,11 @@ export class DbxFormlyContext implements DbxForm { private _fields = new BehaviorSubject>(undefined); private _initialValue = new BehaviorSubject>>(undefined); - private _disabled = new BehaviorSubject(false); + private _disabled = new BehaviorSubject(undefined); private _delegate = new BehaviorSubject>>(undefined); readonly fields$ = this._fields.pipe(filterMaybe()); + readonly disabled$ = this._disabled.pipe(filterMaybe()); readonly stream$: Observable = this._delegate.pipe(distinctUntilChanged(), switchMap(x => (x) ? x.stream$ : of(DbxFormlyContext.INITIAL_STATE)), shareReplay(1)); constructor() { } @@ -66,8 +67,8 @@ export class DbxFormlyContext implements DbxForm { if (delegate != null) { delegate.init({ fields: this.fields$, - initialValue: this._initialValue.value, - initialDisabled: this._disabled.value + initialDisabled: this._disabled.value, + initialValue: this._initialValue.value }); } @@ -103,11 +104,23 @@ export class DbxFormlyContext implements DbxForm { } isDisabled(): boolean { + return BooleanStringKeyArrayUtilityInstance.isTrue(this.disabled); + } + + get disabled(): BooleanStringKeyArray { return this._disabled.value; } - setDisabled(disabled = true): void { - this._disabled.next(disabled); + getDisabled(): Observable { + return this._disabled.asObservable(); + } + + setDisabled(key?: DbxFormDisabledKey, disabled = true): void { + this._disabled.next(BooleanStringKeyArrayUtilityInstance.set(this.disabled, key ?? DEFAULT_FORM_DISABLED_KEY, disabled)); + + if (this._delegate.value) { + this._delegate.value.setDisabled(key, disabled); + } } resetForm(): void { diff --git a/packages/dbx-form/src/lib/formly/formly.directive.ts b/packages/dbx-form/src/lib/formly/formly.directive.ts index bb87f3a35..9a96b36d7 100644 --- a/packages/dbx-form/src/lib/formly/formly.directive.ts +++ b/packages/dbx-form/src/lib/formly/formly.directive.ts @@ -4,6 +4,7 @@ import { FormlyFieldConfig } from '@ngx-formly/core/lib/core'; import { OnInit, OnDestroy, Directive, Input } from '@angular/core'; import { DbxFormlyContext } from './formly.context'; import { Maybe } from '@dereekb/util'; +import { DbxFormDisabledKey } from '../form/form'; /** * Abstract component for wrapping a form. @@ -17,7 +18,7 @@ export abstract class AbstractFormlyFormDirective implements OnDestroy { } set disabled(disabled: boolean) { - this.context.setDisabled(disabled); + this.context.setDisabled(undefined, disabled); } constructor(public readonly context: DbxFormlyContext) { } @@ -43,6 +44,10 @@ export abstract class AbstractFormlyFormDirective implements OnDestroy { this.setValue({}); } + setDisabled(key?: DbxFormDisabledKey, disabled?: boolean): void { + this.context.setDisabled(key, disabled); + } + } diff --git a/packages/dbx-form/src/lib/formly/formly.form.component.spec.ts b/packages/dbx-form/src/lib/formly/formly.form.component.spec.ts index 6cefd5d45..ef852c29e 100644 --- a/packages/dbx-form/src/lib/formly/formly.form.component.spec.ts +++ b/packages/dbx-form/src/lib/formly/formly.form.component.spec.ts @@ -18,7 +18,7 @@ describe('DbxInputFormControlComponent', () => { let fixture: ComponentFixture; beforeEach(async () => { - fixture = TestBed.createComponent(DbxTestDbxFormComponent); + fixture = TestBed.createComponent(DbxTestDbxFormComponent) as ComponentFixture; testComponent = fixture.componentInstance; fixture.detectChanges(); }); @@ -27,4 +27,6 @@ describe('DbxInputFormControlComponent', () => { expect(testComponent).toBeDefined(); }); + // TODO: Test disabled + }); diff --git a/packages/dbx-form/src/lib/formly/formly.form.component.ts b/packages/dbx-form/src/lib/formly/formly.form.component.ts index a99252f74..79e17594f 100644 --- a/packages/dbx-form/src/lib/formly/formly.form.component.ts +++ b/packages/dbx-form/src/lib/formly/formly.form.component.ts @@ -3,11 +3,13 @@ import { FormGroup } from '@angular/forms'; import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; import { distinctUntilChanged, map, throttleTime, startWith, BehaviorSubject, Observable, Subject, switchMap, shareReplay, of } from 'rxjs'; import { AbstractSubscriptionDirective } from '@dereekb/dbx-core'; -import { DbxForm, DbxFormEvent, DbxFormState, ProvideDbxMutableForm } from '../form/form'; +import { DbxForm, DbxFormDisabledKey, DbxFormEvent, DbxFormState, DEFAULT_FORM_DISABLED_KEY, ProvideDbxMutableForm } from '../form/form'; import { DbxFormlyContext, DbxFormlyContextDelegate, DbxFormlyInitialize } from './formly.context'; import { cloneDeep } from 'lodash'; -import { scanCount, switchMapMaybeObs } from '@dereekb/rxjs'; -import { Maybe } from '@dereekb/util'; +import { scanCount, switchMapMaybeObs, SubscriptionObject } from '@dereekb/rxjs'; +import { BooleanStringKeyArray, BooleanStringKeyArrayUtilityInstance, Maybe } from '@dereekb/util'; + + /** * Used for rending a form from a DbxFormlyContext. @@ -29,10 +31,13 @@ export class DbxFormlyFormComponent extends AbstractSubscripti private _fields = new BehaviorSubject>>(undefined); private _events = new BehaviorSubject({ isComplete: false, state: DbxFormState.INITIALIZING }); + private _disabled = new BehaviorSubject(undefined); private _reset = new BehaviorSubject(new Date()); private _forceUpdate = new Subject(); + private _disabledSub = new SubscriptionObject(); + form = new FormGroup({}); model: any = {}; options: FormlyFormOptions = {}; @@ -56,7 +61,8 @@ export class DbxFormlyFormComponent extends AbstractSubscripti pristine: this.form.pristine, changesCount: changesSinceLastResetCount, lastResetAt, - isDisabled: this.disabled + disabled: this.disabled, + isDisabled: this.isDisabled }; return nextState; @@ -71,6 +77,18 @@ export class DbxFormlyFormComponent extends AbstractSubscripti ngOnInit(): void { this.context.setDelegate(this); + + this._disabledSub.subscription = this._disabled.pipe(distinctUntilChanged()).subscribe((disabled) => { + const isDisabled = BooleanStringKeyArrayUtilityInstance.isTrue(disabled); + + if (this.form.disabled !== isDisabled) { + if (isDisabled) { + this.form.disable({ emitEvent: true }); + } else { + this.form.enable({ emitEvent: true }); + } + } + }); } override ngOnDestroy(): void { @@ -81,12 +99,15 @@ export class DbxFormlyFormComponent extends AbstractSubscripti this._fields.complete(); this._reset.complete(); this._forceUpdate.complete(); + this._disabled.complete(); + this._disabledSub.destroy(); }); } // MARK: Delegate init(initialize: DbxFormlyInitialize): void { this._fields.next(initialize.fields); + this._disabled.next(initialize.initialDisabled); } getValue(): Observable { @@ -121,18 +142,20 @@ export class DbxFormlyFormComponent extends AbstractSubscripti } } - get disabled(): boolean { - return this.form.disabled; + get isDisabled(): boolean { + return BooleanStringKeyArrayUtilityInstance.isTrue(this.disabled); } - setDisabled(disabled = true): void { - if (disabled !== this.disabled) { - if (disabled) { - this.form.disable({ emitEvent: true }); - } else { - this.form.enable({ emitEvent: true }); - } - } + get disabled(): BooleanStringKeyArray { + return this._disabled.value; + } + + getDisabled(): Observable { + return this._disabled.asObservable(); + } + + setDisabled(key?: DbxFormDisabledKey, disabled = true): void { + this._disabled.next(BooleanStringKeyArrayUtilityInstance.set(this.disabled, key ?? DEFAULT_FORM_DISABLED_KEY, disabled)) } // MARK: Update diff --git a/packages/dbx-form/src/test/test.formly.component.ts b/packages/dbx-form/src/test/test.formly.component.ts index a5e54e7d6..e83a2c143 100644 --- a/packages/dbx-form/src/test/test.formly.component.ts +++ b/packages/dbx-form/src/test/test.formly.component.ts @@ -1,7 +1,9 @@ +import { BehaviorSubject, Observable } from 'rxjs'; +import { OnDestroy } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { Component } from '@angular/core'; import { FormlyFieldConfig } from '@ngx-formly/core'; -import { AbstractSyncFormlyFormDirective, formlyField, ProvideFormlyContext } from '../lib'; +import { AbstractAsyncFormlyFormDirective, formlyField, ProvideFormlyContext } from '../lib'; import { AbstractControl } from '@angular/forms'; export interface TestFormValue { @@ -31,13 +33,24 @@ export function testTextField(): FormlyFieldConfig { selector: 'dbx-test-dbx-form', providers: [ProvideFormlyContext()] }) -export class DbxTestDbxFormComponent extends AbstractSyncFormlyFormDirective { +export class DbxTestDbxFormComponent extends AbstractAsyncFormlyFormDirective implements OnDestroy { - fields: FormlyFieldConfig[] = [ + private _fields = new BehaviorSubject([ testTextField() - ]; + ]); + + readonly fields$: Observable = this._fields.asObservable(); + + override ngOnDestroy() { + super.ngOnDestroy(); + this._fields.complete(); + } // MARK: Testing + setFields(fields: FormlyFieldConfig[]) { + this._fields.next(fields); + } + setValidTextForTest(fixture: ComponentFixture): string { const text = 'valid'; this.setTextForTest(text, fixture); @@ -49,7 +62,7 @@ export class DbxTestDbxFormComponent extends AbstractSyncFormlyFormDirective): void { - this.setValue({ text }); + this.setValue({ text } as any); this.detectFormChanges(fixture); } diff --git a/packages/util/src/lib/array/array.boolean.ts b/packages/util/src/lib/array/array.boolean.ts index 11015fec5..479dadae8 100644 --- a/packages/util/src/lib/array/array.boolean.ts +++ b/packages/util/src/lib/array/array.boolean.ts @@ -1,4 +1,5 @@ import { removeModelsWithKey, removeModelsWithSameKey, ReadModelKeyFunction } from "../model"; +import { Maybe } from "../value"; export type BooleanStringKey = string; @@ -12,7 +13,7 @@ export type BooleanStringKeyArray = BooleanKeyArray; * * Having any values in the array is considered "true". */ -export type BooleanKeyArray = T[] | undefined; +export type BooleanKeyArray = Maybe; export function readBooleanKeySafetyWrap(readKey: ReadModelKeyFunction): ReadModelKeyFunction { return (value: T) => { @@ -58,6 +59,18 @@ export class BooleanKeyArrayUtilityInstance { return isTrueBooleanKeyArray(value); } + set(array: BooleanKeyArray, value: T, enable = true): BooleanKeyArray { + let result: BooleanKeyArray; + + if (enable) { + result = this.insert(array, value); + } else { + result = this.remove(array, value); + } + + return result; + } + insert(array: BooleanKeyArray, value: T): BooleanKeyArray { return insertIntoBooleanKeyArray(array, value, this.readKey); }