@domonda/form
Advanced tools
Comparing version 1.2.2 to 2.0.0
@@ -6,2 +6,23 @@ # Change Log | ||
# [2.0.0](https://github.com/domonda/domonda-js/compare/@domonda/form@1.2.2...@domonda/form@2.0.0) (2019-08-01) | ||
### Bug Fixes | ||
* **Form:** plumb might be disposed before submit finishes ([e8e06d9](https://github.com/domonda/domonda-js/commit/e8e06d9)) | ||
### Features | ||
* **plumb:** replace RxJS with plumb ([d37439b](https://github.com/domonda/domonda-js/commit/d37439b)) | ||
### BREAKING CHANGES | ||
* **plumb:** RxJS is not used for the form anymore. We use domonda-plumb instead. | ||
## [1.2.2](https://github.com/domonda/domonda-js/compare/@domonda/form@1.2.1...@domonda/form@1.2.2) (2019-07-31) | ||
@@ -8,0 +29,0 @@ |
@@ -6,4 +6,3 @@ /** | ||
*/ | ||
import { FormDefaultValues, FormConfig, Form, FormDestroy, FormFields } from './Form'; | ||
export declare function setChangedOnAllFormFields(fields: FormFields, changed: boolean): FormFields; | ||
export declare function createForm<DefaultValues extends FormDefaultValues>(defaultValues?: DefaultValues, initialConfig?: FormConfig<DefaultValues>): [Form<DefaultValues>, FormDestroy]; | ||
import { FormDefaultValues, FormConfig, Form, FormDispose } from './Form'; | ||
export declare function createForm<DefaultValues extends FormDefaultValues>(defaultValues?: DefaultValues, initialConfig?: FormConfig<DefaultValues>): [Form<DefaultValues>, FormDispose]; |
@@ -16,6 +16,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const equality_1 = require("./equality"); | ||
// $ | ||
const rxjs_1 = require("rxjs"); | ||
const operators_1 = require("rxjs/operators"); | ||
const plumb_1 = require("@domonda/plumb"); | ||
// form | ||
@@ -25,14 +22,6 @@ const Form_1 = require("./Form"); | ||
const DEFAULT_AUTO_SUBMIT_DELAY = 300; | ||
function setChangedOnAllFormFields(fields, changed) { | ||
return Object.keys(fields).reduce((acc, curr) => { | ||
return Object.assign({}, acc, { | ||
// if the value under a path does not exist, the field definitely changed! | ||
[curr]: Object.assign({}, fields[curr], { changed }) }); | ||
}, {}); | ||
} | ||
exports.setChangedOnAllFormFields = setChangedOnAllFormFields; | ||
function createForm(defaultValues = {}, initialConfig = {}) { | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
const configRef = new Form_1.FormConfigRef(initialConfig, handleSubmit); | ||
const $ = new rxjs_1.BehaviorSubject({ | ||
const plumb = plumb_1.createPlumb({ | ||
defaultValues, | ||
@@ -47,8 +36,29 @@ values: defaultValues, | ||
if (autoSubmit) { | ||
const submit$ = autoSubmitDelay > 0 | ||
? $.pipe(operators_1.skip(1), operators_1.debounceTime(autoSubmitDelay), operators_1.distinctUntilChanged(({ values: prevValues }, { values: currValues }) => equality_1.equal(prevValues, currValues)), operators_1.filter(({ defaultValues, values }) => !equality_1.equal(defaultValues, values))) | ||
: $.pipe(operators_1.skip(1), operators_1.distinctUntilChanged(({ values: prevValues }, { values: currValues }) => equality_1.equal(prevValues, currValues)), operators_1.filter(({ defaultValues, values }) => !equality_1.equal(defaultValues, values))); | ||
// since functions are hoisted | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
submit$.subscribe(submit); | ||
let currState = plumb.state; | ||
let currTimeout; | ||
plumb.subscribe((nextState) => { | ||
(() => { | ||
if (nextState.submitting) { | ||
return; | ||
} | ||
if (plumb_1.equal(currState.values, nextState.values) || | ||
plumb_1.equal(nextState.defaultValues, nextState.values)) { | ||
return; | ||
} | ||
if (autoSubmitDelay > 0) { | ||
if (currTimeout) { | ||
clearTimeout(currTimeout); | ||
} | ||
currTimeout = setTimeout(() => { | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
submit(); | ||
}, autoSubmitDelay); | ||
} | ||
else { | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
submit(); | ||
} | ||
})(); | ||
currState = nextState; | ||
}); | ||
} | ||
@@ -58,8 +68,8 @@ } | ||
const form = { | ||
$, | ||
plumb, | ||
get state() { | ||
return $.value; | ||
return plumb.state; | ||
}, | ||
get values() { | ||
return $.value.values; | ||
return plumb.state.values; | ||
}, | ||
@@ -69,5 +79,9 @@ configRef, | ||
submit, | ||
reset: () => $.next(Object.assign({}, $.value, { values: $.value.defaultValues, submitting: false, submitError: null, fields: setChangedOnAllFormFields($.value.fields, false) })), | ||
resetSubmitError: () => $.next(Object.assign({}, $.value, { submitting: false, submitError: null })), | ||
makeFormField: (path, config) => createFormField_1.createFormField($, path, config), | ||
reset: () => { | ||
plumb.next(Object.assign({}, plumb.state, { values: plumb.state.defaultValues, submitting: false, submitError: null })); | ||
}, | ||
resetSubmitError: () => { | ||
plumb.next(Object.assign({}, plumb.state, { submitting: false, submitError: null })); | ||
}, | ||
makeFormField: (path, config) => createFormField_1.createFormField(plumb, path, config), | ||
}; | ||
@@ -81,6 +95,5 @@ function handleSubmit(event) { | ||
} | ||
$.next(Object.assign({}, $.value, { submitting: true, submitError: null })); | ||
// TODO-db-190626 validate fields which haven't changed | ||
// wait for all validations to finish before continuing | ||
const validityMessages = yield $.pipe(operators_1.map(({ fields }) => Object.keys(fields).reduce((acc, key) => { | ||
plumb.next(Object.assign({}, plumb.state, { submitting: true, submitError: null })); | ||
const { fields } = plumb.state; | ||
const validityMessages = Object.keys(fields).reduce((acc, key) => { | ||
const field = fields[key]; | ||
@@ -91,9 +104,5 @@ if (!field) { | ||
return [...acc, field.validityMessage]; | ||
}, [])), operators_1.takeWhile( | ||
// complete the observable when all validations have finished loading | ||
(validityMessages) => validityMessages.some((validityMessage) => validityMessage === undefined), true)) | ||
// covert to promise which resolves once the stream completes | ||
.toPromise(); | ||
}, []); | ||
if (validityMessages.some((validityMessages) => validityMessages != null)) { | ||
$.next(Object.assign({}, $.value, { submitting: false })); | ||
plumb.next(Object.assign({}, plumb.state, { submitting: false })); | ||
return; | ||
@@ -103,11 +112,11 @@ } | ||
try { | ||
yield onSubmit($.value.values, form); | ||
$.next(Object.assign({}, $.value, { submitting: false, values: resetOnSuccessfulSubmit ? $.value.defaultValues : $.value.values, fields: resetOnSuccessfulSubmit | ||
? setChangedOnAllFormFields($.value.fields, false) | ||
: $.value.fields })); | ||
yield onSubmit(plumb.state.values, form); | ||
if (!plumb.disposed) { | ||
plumb.next(Object.assign({}, plumb.state, { submitting: false, values: resetOnSuccessfulSubmit ? plumb.state.defaultValues : plumb.state.values })); | ||
} | ||
} | ||
catch (error) { | ||
$.next(Object.assign({}, $.value, { submitting: false, submitError: error, values: resetOnFailedSubmit ? $.value.defaultValues : $.value.values, fields: resetOnFailedSubmit | ||
? setChangedOnAllFormFields($.value.fields, false) | ||
: $.value.fields })); | ||
if (!plumb.disposed) { | ||
plumb.next(Object.assign({}, plumb.state, { submitting: false, submitError: error, values: resetOnFailedSubmit ? plumb.state.defaultValues : plumb.state.values })); | ||
} | ||
} | ||
@@ -127,4 +136,4 @@ } | ||
} | ||
return [form, () => $.complete()]; | ||
return [form, () => plumb.dispose()]; | ||
} | ||
exports.createForm = createForm; |
@@ -6,5 +6,6 @@ /** | ||
*/ | ||
import { FormDefaultValues, Form$ } from './Form'; | ||
import { FormField, FormFieldConfig, FormFieldDestroy } from './FormField'; | ||
export declare function createFormField<DefaultValues extends FormDefaultValues, Value>(form$: Form$<DefaultValues>, path: string, // [K in keyof FormDefaultValues] | ||
config?: FormFieldConfig<Value>): [FormField<Value>, FormFieldDestroy]; | ||
import { Plumb } from '@domonda/plumb'; | ||
import { FormDefaultValues, FormState } from './Form'; | ||
import { FormField, FormFieldConfig, FormFieldDispose } from './FormField'; | ||
export declare function createFormField<DefaultValues extends FormDefaultValues, Value>(form: Plumb<FormState<DefaultValues>>, path: string, // [K in keyof FormDefaultValues] | ||
config?: FormFieldConfig<Value>): [FormField<Value>, FormFieldDispose]; |
@@ -22,3 +22,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const equality_1 = require("./equality"); | ||
const plumb_1 = require("@domonda/plumb"); | ||
const get_1 = __importDefault(require("lodash/get")); | ||
@@ -28,135 +28,82 @@ const setWith_1 = __importDefault(require("lodash/fp/setWith")); | ||
const omit_1 = __importDefault(require("lodash/fp/omit")); | ||
const pick_1 = __importDefault(require("lodash/fp/pick")); | ||
const Subject_1 = require("rxjs/internal/Subject"); | ||
// $ | ||
const operators_1 = require("rxjs/operators"); | ||
function deriveState(path, state, setState, isLocalNext) { | ||
function selector(path, state) { | ||
const { fields, defaultValues, values } = state; | ||
const defaultValue = get_1.default(defaultValues, path); | ||
const value = get_1.default(values, path); | ||
const field = fields[path]; | ||
if (!field) { | ||
return undefined; | ||
return { | ||
defaultValue, | ||
value, | ||
changed: false, | ||
validityMessage: null, | ||
}; | ||
} | ||
const defaultValue = get_1.default(defaultValues, path); | ||
const value = get_1.default(values, path); | ||
if (isLocalNext) { | ||
return Object.assign({}, field, { defaultValue, | ||
value }); | ||
} | ||
// update the `changed` flag if it is not consistent with the field itself | ||
// this case will happen when the value gets changed externally (from the form) | ||
const changed = !equality_1.equal(defaultValue, value); | ||
if (field.changed !== changed) { | ||
setState(Object.assign({}, state, { fields: Object.assign({}, state.fields, { [path]: Object.assign({}, field, { changed }) }) })); | ||
} | ||
return Object.assign({}, field, { defaultValue, | ||
value, | ||
changed }); | ||
value }); | ||
} | ||
function createFormField(form$, path, // [K in keyof FormDefaultValues] | ||
function createFormField(form, path, // [K in keyof FormDefaultValues] | ||
config = {}) { | ||
const initialDefaultValue = get_1.default(form$.value.values, path); | ||
const initialValue = get_1.default(form$.value.defaultValues, path); | ||
form$.next(Object.assign({}, form$.value, { fields: Object.assign({}, form$.value.fields, { [path]: { | ||
changed: !equality_1.equal(initialDefaultValue, initialValue), | ||
validityMessage: null, | ||
} }) })); | ||
// a flag which prevents double checking the value equality | ||
// when the field sends the next signal. if the form sends a new | ||
// value then the check will be performed while deriving the state | ||
let localNext = false; | ||
const $ = new Subject_1.AnonymousSubject({ | ||
next: (field) => { | ||
localNext = true; | ||
form$.next(Object.assign({}, form$.value, { values: setWith_1.default(clone_1.default, path, field.value, form$.value.values), fields: Object.assign({}, form$.value.fields, { [path]: pick_1.default(['changed', 'validityMessage'], field) }) })); | ||
localNext = false; | ||
const { validate, immediateValidate } = config; | ||
let defaultValue; | ||
let value; | ||
let initialTransform = true; | ||
const plumb = form.chain({ | ||
selector: (state) => selector(path, state), | ||
transformer: (selectedState) => { | ||
const changed = !plumb_1.shallowEqual(selectedState.defaultValue, selectedState.value); | ||
let validityMessage = selectedState.validityMessage; | ||
if (validate && (changed || (immediateValidate && initialTransform))) { | ||
validityMessage = validate(selectedState.value); | ||
} | ||
initialTransform = false; | ||
return Object.assign({}, selectedState, { changed, | ||
validityMessage }); | ||
}, | ||
error: () => { | ||
form$.next(Object.assign({}, form$.value, { fields: omit_1.default(path, form$.value.fields) })); | ||
updater: (state, _a) => { | ||
var { changed, validityMessage } = _a, rest = __rest(_a, ["changed", "validityMessage"]); | ||
return (Object.assign({}, state, { values: setWith_1.default(clone_1.default, path, rest.value, form.state.values), fields: Object.assign({}, form.state.fields, { [path]: { | ||
validityMessage, | ||
changed, | ||
} }) })); | ||
}, | ||
complete: () => { | ||
form$.next(Object.assign({}, form$.value, { fields: omit_1.default(path, form$.value.fields) })); | ||
filter: (selectedState) => { | ||
const changed = !plumb_1.shallowEqual(value, selectedState.value) || | ||
!plumb_1.shallowEqual(defaultValue, selectedState.defaultValue); | ||
defaultValue = selectedState.defaultValue; | ||
value = selectedState.value; | ||
return changed; | ||
}, | ||
}, form$.pipe( | ||
// we assert non-null here because the stream will complete when | ||
// this map gets an undefined value (field is removed from form) | ||
// its just a type hack so that we dont assert everywhere else... | ||
operators_1.map((state) => deriveState(path, state, (nextState) => form$.next(nextState), localNext)), | ||
// complete stream when the field gets removed | ||
operators_1.takeWhile((state) => !!state), | ||
// publish only changed state | ||
operators_1.distinctUntilChanged(equality_1.equal))); | ||
const { immediateValidate, validate, validateDebounce = 0 } = config; | ||
if (validate) { | ||
const validator = validateDebounce > 0 | ||
? $.pipe( | ||
// we skip the first iteration since we don't want to invalidate initially | ||
operators_1.skip(immediateValidate ? 0 : 1), | ||
// we don't care about the validity, just the value | ||
operators_1.distinctUntilChanged(({ value: prev }, { value: curr }) => equality_1.equal(prev, curr)), operators_1.debounceTime(validateDebounce)) | ||
: $.pipe( | ||
// we skip the first iteration since we don't want to invalidate initially | ||
operators_1.skip(immediateValidate ? 0 : 1), | ||
// we don't care about the validity, just the value | ||
operators_1.distinctUntilChanged(({ value: prev }, { value: curr }) => equality_1.equal(prev, curr))); | ||
let currValidationMessage = null; | ||
let counter = 0; | ||
validator.subscribe((state) => | ||
// we perform the validation after all subscribers have been notified about the value change | ||
setTimeout(() => { | ||
const _a = state, { value } = _a, rest = __rest(_a, ["value"]); | ||
const pendingValidityMessage = validate(value); | ||
if (!(pendingValidityMessage instanceof Promise)) { | ||
if (pendingValidityMessage !== currValidationMessage) { | ||
currValidationMessage = pendingValidityMessage; | ||
$.next(Object.assign({}, rest, { value, validityMessage: pendingValidityMessage })); | ||
} | ||
return; | ||
} | ||
// we use the counter as a simple cancel mechanism | ||
const internalCounter = counter; | ||
counter++; | ||
if (currValidationMessage !== undefined) { | ||
currValidationMessage = undefined; | ||
$.next(Object.assign({}, rest, { value, validityMessage: currValidationMessage })); | ||
} | ||
pendingValidityMessage.then((nextValidityMessage) => { | ||
// if the internalCounter does not match the outer counter that means that another, newer, validity check is pending | ||
if (internalCounter + 1 === counter) { | ||
currValidationMessage = nextValidityMessage; | ||
$.next(Object.assign({}, rest, { value, validityMessage: nextValidityMessage })); | ||
} | ||
}); | ||
}, 0)); | ||
} | ||
function getState() { | ||
const state = deriveState(path, form$.value, (nextState) => form$.next(nextState), false); | ||
if (!state) { | ||
throw new Error('domonda-form: Field state should be available here!'); | ||
} | ||
return state; | ||
} | ||
function getValue() { | ||
return getState().value; | ||
} | ||
}); | ||
plumb.subscribe({ | ||
dispose: () => { | ||
form.next(Object.assign({}, form.state, { fields: omit_1.default(path, form.state.fields) })); | ||
}, | ||
}); | ||
return [ | ||
{ | ||
$, | ||
plumb, | ||
get state() { | ||
return getState(); | ||
return plumb.state; | ||
}, | ||
get value() { | ||
return getValue(); | ||
return plumb.state.value; | ||
}, | ||
setValue: (nextValue) => { | ||
const curr = getState(); | ||
$.next(Object.assign({}, curr, { changed: !equality_1.equal(curr.defaultValue, nextValue), value: nextValue })); | ||
value = nextValue; | ||
if (!plumb_1.shallowEqual(plumb.state.value, value)) { | ||
plumb.next(Object.assign({}, plumb.state, { value: nextValue })); | ||
} | ||
}, | ||
resetValue: () => { | ||
const curr = getState(); | ||
$.next(Object.assign({}, curr, { changed: false, value: curr.defaultValue })); | ||
defaultValue = plumb.state.defaultValue; | ||
value = plumb.state.value; | ||
if (!plumb_1.shallowEqual(value, defaultValue)) { | ||
plumb.next(Object.assign({}, plumb.state, { value: plumb.state.defaultValue })); | ||
} | ||
}, | ||
}, | ||
() => $.complete(), | ||
() => plumb.dispose(), | ||
]; | ||
} | ||
exports.createFormField = createFormField; |
@@ -6,4 +6,4 @@ /** | ||
*/ | ||
import { BehaviorSubject } from 'rxjs'; | ||
import { FormField, FormFieldConfig, FormFieldDestroy } from './FormField'; | ||
import { FormField, FormFieldConfig, FormFieldValidityMessage, FormFieldDispose } from './FormField'; | ||
import { Plumb } from '@domonda/plumb'; | ||
export declare class FormConfigRef<DefaultValues extends FormDefaultValues> { | ||
@@ -44,3 +44,3 @@ private submitHandler; | ||
changed: boolean; | ||
validityMessage: string | null | undefined; | ||
validityMessage: FormFieldValidityMessage; | ||
} | ||
@@ -57,5 +57,4 @@ export interface FormFields { | ||
} | ||
export declare type Form$<T extends FormDefaultValues> = BehaviorSubject<FormState<T>>; | ||
export interface Form<T extends FormDefaultValues> { | ||
readonly $: Form$<T>; | ||
readonly plumb: Plumb<FormState<T>>; | ||
readonly state: FormState<T>; | ||
@@ -67,4 +66,4 @@ readonly values: T; | ||
resetSubmitError: () => void; | ||
makeFormField: <T>(path: string, config?: FormFieldConfig<T>) => [FormField<T>, FormFieldDestroy]; | ||
makeFormField: <T>(path: string, config?: FormFieldConfig<T>) => [FormField<T>, FormFieldDispose]; | ||
} | ||
export declare type FormDestroy = () => void; | ||
export declare type FormDispose = () => void; |
@@ -7,3 +7,3 @@ /** | ||
import { FormFieldState } from './Form'; | ||
import { AnonymousSubject } from 'rxjs/internal/Subject'; | ||
import { Plumb } from '@domonda/plumb'; | ||
export interface FormFieldStateWithValues<T> extends FormFieldState { | ||
@@ -13,11 +13,10 @@ defaultValue: Readonly<T>; | ||
} | ||
export declare type FormFieldValidate<T> = (value: Readonly<T>) => Promise<string | null> | (string | null); | ||
export declare type FormFieldValidityMessage = string | null; | ||
export declare type FormFieldValidate<T> = (value: Readonly<T>) => FormFieldValidityMessage; | ||
export interface FormFieldConfig<T> { | ||
validate?: FormFieldValidate<T>; | ||
validateDebounce?: number; | ||
immediateValidate?: boolean; | ||
} | ||
export declare type FormField$<T> = AnonymousSubject<FormFieldStateWithValues<T>>; | ||
export interface FormField<T> { | ||
readonly $: FormField$<T>; | ||
readonly plumb: Plumb<FormFieldStateWithValues<T>>; | ||
readonly state: FormFieldStateWithValues<T>; | ||
@@ -28,2 +27,2 @@ readonly value: T; | ||
} | ||
export declare type FormFieldDestroy = () => void; | ||
export declare type FormFieldDispose = () => void; |
@@ -5,2 +5,1 @@ export * from './Form'; | ||
export * from './createFormField'; | ||
export * from './equality'; |
@@ -9,2 +9,1 @@ "use strict"; | ||
__export(require("./createFormField")); | ||
__export(require("./equality")); |
{ | ||
"name": "@domonda/form", | ||
"version": "1.2.2", | ||
"description": "Powerful yet simple form library built on top of RxJS.", | ||
"version": "2.0.0", | ||
"description": "Powerful yet simple form library built using @domonda/plumb.", | ||
"keywords": [ | ||
"domonda", | ||
"form", | ||
"rxjs" | ||
"plumb" | ||
], | ||
@@ -23,7 +23,7 @@ "homepage": "https://github.com/domonda/domonda-js#readme", | ||
"dependencies": { | ||
"@domonda/plumb": "^2.0.0", | ||
"lodash.clone": "^4.5.0", | ||
"lodash.get": "^4.4.2", | ||
"lodash.omit": "^4.5.0", | ||
"lodash.setwith": "^4.3.2", | ||
"rxjs": "^6.5.2" | ||
"lodash.setwith": "^4.3.2" | ||
}, | ||
@@ -30,0 +30,0 @@ "main": "index.js", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
20084
13
395
+ Added@domonda/plumb@^2.0.0
+ Added@domonda/plumb@2.2.9(transitive)
- Removedrxjs@^6.5.2
- Removedrxjs@6.6.7(transitive)
- Removedtslib@1.14.1(transitive)