@dereekb/dbx-form 13.10.8 → 13.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { isPast, addSeconds, startOfDay, addMinutes, addDays, isBefore } from 'd
6
6
  import { map, of, shareReplay, switchMap, first, exhaustMap, catchError, delay, filter, combineLatest, distinctUntilChanged, BehaviorSubject, Subject, startWith, throttleTime, scan, timer, merge, EMPTY, skip, debounceTime, combineLatestWith, interval, tap, withLatestFrom, mergeMap } from 'rxjs';
7
7
  import * as i1$2 from '@dereekb/dbx-core';
8
8
  import { DbxActionContextStoreSourceInstance, cleanLockSet, cleanSubscription, completeOnDestroy, cleanWithLockSet, GetValuePipe, DateDistancePipe, TimeDistancePipe, DbxInjectionComponent, mergeDbxInjectionComponentConfigs } from '@dereekb/dbx-core';
9
- import { makeIsModifiedFunctionObservable, asObservable, LockSet, filterMaybe, switchMapFilterMaybe, scanCount, errorOnEmissionsInPeriod, asObservableFromGetter, maybeValueFromObservableOrValue, valueFromFinishedLoadingState, switchMapMaybeDefault, skipAllInitialMaybe, successResult, startWithBeginLoading, skipUntilTimeElapsedAfterLastEmission, mapLoadingStateResults, isLoadingStateWithDefinedValue, isLoadingStateLoading, beginLoading, mapLoadingStateValueWithOperator, loadingStateContext, distinctUntilHasDifferentValues, SimpleLoadingContext, listLoadingStateContext, mapIsListLoadingStateWithEmptyValue, isLoadingStateInLoadingState, asyncPusherCache } from '@dereekb/rxjs';
9
+ import { makeIsModifiedFunctionObservable, asObservable, LockSet, filterMaybe, switchMapFilterMaybe, scanCount, errorOnEmissionsInPeriod, asObservableFromGetter, tapLog, maybeValueFromObservableOrValue, valueFromFinishedLoadingState, switchMapMaybeDefault, skipAllInitialMaybe, successResult, startWithBeginLoading, skipUntilTimeElapsedAfterLastEmission, mapLoadingStateResults, isLoadingStateWithDefinedValue, isLoadingStateLoading, beginLoading, mapLoadingStateValueWithOperator, loadingStateContext, distinctUntilHasDifferentValues, SimpleLoadingContext, listLoadingStateContext, mapIsListLoadingStateWithEmptyValue, isLoadingStateInLoadingState, asyncPusherCache } from '@dereekb/rxjs';
10
10
  import { toObservable, toSignal, rxResource } from '@angular/core/rxjs-interop';
11
11
  import { BooleanStringKeyArrayUtility, iterablesAreSetEquivalent, filterUndefinedValues, filterMaybeArrayValues, filterUniqueValues, areEqualPOJOValuesUsingPojoFilter, NOOP_MODIFIER, asArray, mergeArrays, filterNullAndUndefinedValues, objectHasNoKeys, mapMaybeFunction, isWebsiteUrlWithPrefix, websiteUrlDetails, transformStringFunction, US_STATE_CODE_STRING_REGEX, ZIP_CODE_STRING_REGEX, LAT_LNG_PATTERN, transformNumberFunction, DOLLAR_AMOUNT_PRECISION, stripObject, getValueFromGetter, asGetter, dateFromMinuteOfDay, dateToMinuteOfDay, isISO8601DayStringStart, mapIdentityFunction, isDate, MS_IN_MINUTE, isMonthDaySlashDate, filterFromPOJO, TIME_UNIT_SHORT_LABEL_MAP, timeUnitToMilliseconds, ALL_TIME_UNITS, minutesToHoursAndMinutes, millisecondsToTimeUnit, hoursAndMinutesToTimeUnit, isE164PhoneNumber as isE164PhoneNumber$1, isValidPhoneExtensionNumber, e164PhoneNumberExtensionPair, e164PhoneNumberFromE164PhoneNumberExtensionPair, mergeArraysIntoArray, convertMaybeToArray, lastValue, filterEmptyArrayValues, setContainsAllValues, addToSetCopy, setsAreEquivalent, makeValuesGroupMap, sortByStringFunction, separateValues, isSelectedDecisionFunctionFactory, readKeysFrom, hasDifferentValues, capitalizeFirstLetter, objectIsEmpty, mergeObjectsFunction, filterFromPOJOFunction, mergeObjects, addPlusPrefixToNumber, searchStringFilterFunction, caseInsensitiveFilterByIndexOfDecisionFactory, arrayToMap, firstValue, cachedGetter, makeGetter, asDecisionFunction, TIME_UNIT_LABEL_MAP, HAS_WEBSITE_DOMAIN_NAME_REGEX, KeyValueTypleValueFilter, valuesFromPOJO, allObjectsAreEqual, isNumberDivisibleBy, nearestDivisibleValues, concatArrays } from '@dereekb/util';
12
12
  import * as i1 from '@angular/forms';
@@ -101,6 +101,8 @@ class DbxForm {
101
101
  * value to feed user-supplied isValid/isModified functions even while the form is invalid.
102
102
  * Defaults to {@link getValue}; implementations that gate {@link getValue} on validity
103
103
  * should override this to bypass that gate.
104
+ *
105
+ * @returns An observable of the form's current value, even when validity gates would suppress {@link getValue}.
104
106
  */
105
107
  currentValue() {
106
108
  return this.getValue();
@@ -1321,7 +1323,7 @@ class DbxForgeFormContext {
1321
1323
  _isValid = new BehaviorSubject(false);
1322
1324
  _setValue = new BehaviorSubject(undefined);
1323
1325
  _reset = new BehaviorSubject(new Date());
1324
- _internalConfig$ = this._config.pipe(scan((acc, config) => {
1326
+ _internalConfig$ = this._config.pipe(tapLog('internal config'), scan((acc, config) => {
1325
1327
  let result;
1326
1328
  if (config) {
1327
1329
  if (acc?.input !== config) {
@@ -1335,8 +1337,8 @@ class DbxForgeFormContext {
1335
1337
  result = undefined;
1336
1338
  }
1337
1339
  return result;
1338
- }, undefined), shareReplay(1));
1339
- config$ = this._internalConfig$.pipe(filterMaybe(), map(({ config }) => config), shareReplay(1));
1340
+ }, undefined), tapLog('internal config result'), shareReplay(1));
1341
+ config$ = this._internalConfig$.pipe(filterMaybe(), map(({ config }) => config), tapLog('config'), shareReplay(1));
1340
1342
  /**
1341
1343
  * Form event stream that restarts on each reset, mirroring the formly form's
1342
1344
  * switchMap-on-reset pattern. This ensures that each resetForm() produces a fresh
@@ -1378,6 +1380,8 @@ class DbxForgeFormContext {
1378
1380
  * Emits the current form value regardless of {@link requireValid}. Used by infrastructure
1379
1381
  * that needs the underlying value while the form is invalid (e.g. {@link DbxActionFormDirective}
1380
1382
  * feeding the value into user-supplied isModified functions to drive the action's disabled state).
1383
+ *
1384
+ * @returns An observable of the latest non-null form value, regardless of validity.
1381
1385
  */
1382
1386
  currentValue() {
1383
1387
  return this._value.pipe(filterMaybe());
@@ -1573,7 +1577,7 @@ class DbxForgeFormComponent {
1573
1577
  _disabledSub = cleanSubscription();
1574
1578
  dynamicForm = viewChild(DynamicForm, ...(ngDevMode ? [{ debugName: "dynamicForm" }] : /* istanbul ignore next */ []));
1575
1579
  formValue = signal({}, { ...(ngDevMode ? { debugName: "formValue" } : /* istanbul ignore next */ {}), equal: (a, b) => _forgeFormValueEqual(a, b, this._context) });
1576
- configSignal = toSignal(this._context.config$, { initialValue: undefined });
1580
+ configSignal = toSignal(this._context.config$.pipe(tapLog('forge.config')), { initialValue: undefined });
1577
1581
  _changesCount = signal(0, ...(ngDevMode ? [{ debugName: "_changesCount" }] : /* istanbul ignore next */ []));
1578
1582
  _lastResetAt = signal(new Date(), ...(ngDevMode ? [{ debugName: "_lastResetAt" }] : /* istanbul ignore next */ []));
1579
1583
  _isReset = signal(true, ...(ngDevMode ? [{ debugName: "_isReset" }] : /* istanbul ignore next */ []));
@@ -3373,6 +3377,7 @@ function isFlexFieldConfig(input) {
3373
3377
  *
3374
3378
  * @param input - {@link DbxForgeFlexLayoutConfig} with a `fields` property and layout defaults.
3375
3379
  * For backwards compatibility, may also be passed as a deprecated array of fields paired with an optional config.
3380
+ * @param legacyConfig - Only consulted when `input` is the deprecated array form; supplies the layout defaults that would otherwise live on the config object.
3376
3381
  * @returns A {@link ContainerField} with the flex wrapper applied and sized children
3377
3382
  *
3378
3383
  * @dbxFormField
@@ -4058,8 +4063,8 @@ function configureDbxForgeFormFieldWrapper(instance) {
4058
4063
  * config entirely when no values remain — keeps the wrapper bare in the common case
4059
4064
  * (e.g. a checkbox/toggle with no label override).
4060
4065
  *
4061
- * @param props - wrapper props applied to the inserted wrapper config
4062
- * @returns a configurator that mutates the builder instance to add the wrapper
4066
+ * @param inputProps - Wrapper props applied to the inserted wrapper config; undefined entries are stripped and the `props` block is omitted entirely when no values remain.
4067
+ * @returns A configurator that mutates the builder instance to add the form-field wrapper with the resolved props.
4063
4068
  */
4064
4069
  function configureDbxForgeFormFieldWrapperWith(inputProps) {
4065
4070
  const props = stripObject(inputProps);
@@ -6051,7 +6056,12 @@ class DbxForgeDateTimeFieldComponent {
6051
6056
  return;
6052
6057
  const currentDateCtrl = this.dateCtrl.value;
6053
6058
  if (!currentDateCtrl || !isSameDateDay(currentDateCtrl, date)) {
6054
- this.dateCtrl.setValue(date, { emitEvent: false });
6059
+ // Emit valueChanges (default) so currentDate$ and therefore date$/dateValue$/_rawDateTimeDate$
6060
+ // — see the inbound date. Without this emission, rawDateTime$.combineLatest is starved on the
6061
+ // date side and a user-typed time edit can never propagate back to the form value.
6062
+ // The outbound pipeline's `filter(!dbxDateTimeIsSameDateTimeFieldValue(x, currentValue))`
6063
+ // prevents the resulting _updateTime tick from causing a feedback write.
6064
+ this.dateCtrl.setValue(date);
6055
6065
  }
6056
6066
  // Do not overwrite time control while user is actively editing it
6057
6067
  if (!isTimeFocused) {
@@ -6061,10 +6071,14 @@ class DbxForgeDateTimeFieldComponent {
6061
6071
  }
6062
6072
  }
6063
6073
  });
6064
- // Main output subscription: timeOutput$ → field value
6065
- this._sub.subscription = this.valueInSystemTimezone$
6066
- .pipe(combineLatestWith$1(this.timezoneInstance$.pipe(map$1((tz) => dbxDateTimeOutputValueFactory(this.valueMode(), tz)))), throttleTime$1(TIME_OUTPUT_THROTTLE_TIME$2, undefined, { leading: false, trailing: true }), switchMap$1(([currentValue, valueFactory]) => {
6067
- return this.timeOutput$.pipe(throttleTime$1(TIME_OUTPUT_THROTTLE_TIME$2 * 2, undefined, { leading: false, trailing: true }), skipAllInitialMaybe(), distinctUntilChanged$1(isSameDateHoursAndMinutes), map$1((x) => valueFactory(x)), filter$1((x) => !dbxDateTimeIsSameDateTimeFieldValue(x, currentValue)));
6074
+ // Main output subscription: timeOutput$ → field value.
6075
+ // The filter must compare in storage form against the raw fieldValue. Comparing against
6076
+ // valueInSystemTimezone$ (display-shifted) is wrong: with a non-zero system↔target tz shift,
6077
+ // a freshly picked time can have the same timestamp as the previous valueInSystemTimezone$
6078
+ // and be incorrectly dropped (e.g. CDT system + UTC field tz: pick 12pm → pick 5pm collides).
6079
+ this._sub.subscription = this.fieldValue$
6080
+ .pipe(combineLatestWith$1(this.timezoneInstance$.pipe(map$1((tz) => dbxDateTimeOutputValueFactory(this.valueMode(), tz)))), throttleTime$1(TIME_OUTPUT_THROTTLE_TIME$2, undefined, { leading: false, trailing: true }), switchMap$1(([currentRawValue, valueFactory]) => {
6081
+ return this.timeOutput$.pipe(throttleTime$1(TIME_OUTPUT_THROTTLE_TIME$2 * 2, undefined, { leading: false, trailing: true }), skipAllInitialMaybe(), distinctUntilChanged$1(isSameDateHoursAndMinutes), map$1((x) => valueFactory(x)), filter$1((x) => !dbxDateTimeIsSameDateTimeFieldValue(x, currentRawValue)));
6068
6082
  }))
6069
6083
  .subscribe((value) => {
6070
6084
  this._setFieldValue(value);
@@ -8092,8 +8106,8 @@ class DbxForgeValueSelectionFieldComponent {
8092
8106
  addClearOptionSignal = computed(() => this.props()?.addClearOption, ...(ngDevMode ? [{ debugName: "addClearOptionSignal" }] : /* istanbul ignore next */ []));
8093
8107
  addClearOption$ = toObservable(this.addClearOptionSignal).pipe(distinctUntilChanged());
8094
8108
  inputOptionsSignal = computed(() => this.props()?.options, ...(ngDevMode ? [{ debugName: "inputOptionsSignal" }] : /* istanbul ignore next */ []));
8095
- inputOptions$ = toObservable(this.inputOptionsSignal).pipe(maybeValueFromObservableOrValue());
8096
- resolvedOptions$ = combineLatest([this.inputOptions$, this.addClearOption$]).pipe(map(([options, addClearOption]) => resolveForgeSelectionOptions(options ?? [], addClearOption ?? false)));
8109
+ inputOptions$ = toObservable(this.inputOptionsSignal).pipe(tapLog('yyy'), maybeValueFromObservableOrValue());
8110
+ resolvedOptions$ = combineLatest([this.inputOptions$, this.addClearOption$]).pipe(tapLog('xxxx'), map(([options, addClearOption]) => resolveForgeSelectionOptions(options ?? [], addClearOption ?? false)));
8097
8111
  resolvedOptionsSignal = toSignal(this.resolvedOptions$);
8098
8112
  multipleSignal = computed(() => this.props()?.multiple ?? false, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : /* istanbul ignore next */ []));
8099
8113
  effectiveAppearance = computed(() => this.props()?.appearance ?? this.materialConfig?.appearance ?? 'outline', ...(ngDevMode ? [{ debugName: "effectiveAppearance" }] : /* istanbul ignore next */ []));