@shopify/react-form-state
Advanced tools
Comparing version 0.7.4 to 0.8.0
@@ -30,2 +30,3 @@ import * as React from 'react'; | ||
initialValues: Fields; | ||
children(form: FormDetails<Fields>): React.ReactNode; | ||
validators?: Partial<ValidatorDictionary<Fields>>; | ||
@@ -35,3 +36,3 @@ onSubmit?: SubmitHandler<Fields>; | ||
onInitialValuesChange?: 'reset-all' | 'reset-where-changed' | 'ignore'; | ||
children(form: FormDetails<Fields>): React.ReactNode; | ||
externalErrors?: RemoteError[]; | ||
} | ||
@@ -43,2 +44,3 @@ interface State<Fields> { | ||
errors: RemoteError[]; | ||
externalErrors: RemoteError[]; | ||
} | ||
@@ -48,3 +50,6 @@ export default class FormState<Fields extends Object> extends React.PureComponent<Props<Fields>, State<Fields>> { | ||
static Nested: typeof Nested; | ||
static getDerivedStateFromProps<T>(newProps: Props<T>, oldState: State<T>): State<T> | null; | ||
static getDerivedStateFromProps<T>(newProps: Props<T>, oldState: State<T>): { | ||
externalErrors: RemoteError[]; | ||
fields: FieldStates<T>; | ||
} | null; | ||
state: State<Fields>; | ||
@@ -56,5 +61,5 @@ private mounted; | ||
render(): React.ReactNode; | ||
private readonly formData; | ||
validateForm(): Promise<{}>; | ||
reset: () => Promise<{}>; | ||
private readonly formData; | ||
private readonly dirty; | ||
@@ -61,0 +66,0 @@ private readonly valid; |
@@ -12,3 +12,3 @@ "use strict"; | ||
var _this = _super !== null && _super.apply(this, arguments) || this; | ||
_this.state = createFormState(_this.props.initialValues); | ||
_this.state = createFormState(_this.props.initialValues, _this.props.externalErrors); | ||
_this.mounted = false; | ||
@@ -18,3 +18,5 @@ _this.fieldsWithHandlers = new WeakMap(); | ||
return new Promise(function (resolve) { | ||
_this.setState(function (_state, props) { return createFormState(props.initialValues); }, function () { return resolve(); }); | ||
_this.setState(function (_state, props) { | ||
return createFormState(props.initialValues, props.externalErrors); | ||
}, function () { return resolve(); }); | ||
}); | ||
@@ -77,8 +79,16 @@ }; | ||
FormState.getDerivedStateFromProps = function (newProps, oldState) { | ||
var initialValues = newProps.initialValues, onInitialValuesChange = newProps.onInitialValuesChange; | ||
var initialValues = newProps.initialValues, onInitialValuesChange = newProps.onInitialValuesChange, _a = newProps.externalErrors, externalErrors = _a === void 0 ? [] : _a; | ||
var externalErrorsChanged = externalErrors !== oldState.externalErrors || | ||
externalErrors.length !== oldState.externalErrors.length; | ||
var updatedExternalErrors = externalErrorsChanged | ||
? { | ||
externalErrors: externalErrors, | ||
fields: fieldsWithErrors(oldState.fields, tslib_1.__spread(externalErrors, oldState.errors)), | ||
} | ||
: null; | ||
switch (onInitialValuesChange) { | ||
case 'ignore': | ||
return null; | ||
return updatedExternalErrors; | ||
case 'reset-where-changed': | ||
return reconcileFormState(initialValues, oldState); | ||
return reconcileFormState(initialValues, oldState, externalErrors); | ||
case 'reset-all': | ||
@@ -89,5 +99,5 @@ default: | ||
if (valuesMatch) { | ||
return null; | ||
return updatedExternalErrors; | ||
} | ||
return createFormState(initialValues); | ||
return createFormState(initialValues, externalErrors); | ||
} | ||
@@ -109,10 +119,17 @@ }; | ||
}; | ||
FormState.prototype.validateForm = function () { | ||
var _this = this; | ||
return new Promise(function (resolve) { | ||
_this.setState(runAllValidators, function () { return resolve(); }); | ||
}); | ||
}; | ||
Object.defineProperty(FormState.prototype, "formData", { | ||
get: function () { | ||
var errors = this.state.errors; | ||
var _a = this, fields = _a.fields, dirty = _a.dirty, valid = _a.valid; | ||
var _a = this.props.externalErrors, externalErrors = _a === void 0 ? [] : _a; | ||
var _b = this, fields = _b.fields, dirty = _b.dirty, valid = _b.valid; | ||
return { | ||
dirty: dirty, | ||
valid: valid, | ||
errors: errors, | ||
errors: tslib_1.__spread(errors, externalErrors), | ||
fields: fields, | ||
@@ -124,8 +141,2 @@ }; | ||
}); | ||
FormState.prototype.validateForm = function () { | ||
var _this = this; | ||
return new Promise(function (resolve) { | ||
_this.setState(runAllValidators, function () { return resolve(); }); | ||
}); | ||
}; | ||
Object.defineProperty(FormState.prototype, "dirty", { | ||
@@ -140,4 +151,6 @@ get: function () { | ||
get: function () { | ||
var errors = this.state.errors; | ||
return !this.hasClientErrors && errors.length === 0; | ||
var _a = this.state, errors = _a.errors, externalErrors = _a.externalErrors; | ||
return (!this.hasClientErrors && | ||
errors.length === 0 && | ||
externalErrors.length === 0); | ||
}, | ||
@@ -250,16 +263,7 @@ enumerable: true, | ||
this.setState(function (_a) { | ||
var fields = _a.fields; | ||
var errorDictionary = errors.reduce(function (accumulator, _a) { | ||
var field = _a.field, message = _a.message; | ||
if (field == null) { | ||
return accumulator; | ||
} | ||
return utilities_1.set(accumulator, field, message); | ||
}, {}); | ||
return { | ||
var fields = _a.fields, externalErrors = _a.externalErrors; | ||
return ({ | ||
errors: errors, | ||
fields: utilities_1.mapObject(fields, function (field, path) { | ||
return tslib_1.__assign({}, field, { error: errorDictionary[path] }); | ||
}), | ||
}; | ||
fields: fieldsWithErrors(fields, tslib_1.__spread(errors, externalErrors)), | ||
}); | ||
}); | ||
@@ -272,3 +276,16 @@ }; | ||
exports.default = FormState; | ||
function reconcileFormState(values, oldState) { | ||
function fieldsWithErrors(fields, errors) { | ||
var errorDictionary = errors.reduce(function (accumulator, _a) { | ||
var field = _a.field, message = _a.message; | ||
if (field == null) { | ||
return accumulator; | ||
} | ||
return utilities_1.set(accumulator, field, message); | ||
}, {}); | ||
return utilities_1.mapObject(fields, function (field, path) { | ||
return tslib_1.__assign({}, field, { error: errorDictionary[path] }); | ||
}); | ||
} | ||
function reconcileFormState(values, oldState, externalErrors) { | ||
if (externalErrors === void 0) { externalErrors = []; } | ||
var oldFields = oldState.fields; | ||
@@ -288,5 +305,6 @@ var dirtyFields = new Set(oldState.dirtyFields); | ||
}); | ||
return tslib_1.__assign({}, oldState, { dirtyFields: Array.from(dirtyFields), fields: fields }); | ||
return tslib_1.__assign({}, oldState, { dirtyFields: Array.from(dirtyFields), fields: fieldsWithErrors(fields, externalErrors) }); | ||
} | ||
function createFormState(values) { | ||
function createFormState(values, externalErrors) { | ||
if (externalErrors === void 0) { externalErrors = []; } | ||
var fields = utilities_1.mapObject(values, function (value) { | ||
@@ -303,3 +321,4 @@ return { | ||
submitting: false, | ||
fields: fields, | ||
externalErrors: externalErrors, | ||
fields: fieldsWithErrors(fields, externalErrors), | ||
}; | ||
@@ -306,0 +325,0 @@ } |
@@ -401,2 +401,43 @@ # Building forms with FormState | ||
## External errors | ||
You can use the `externalErrors` prop to supply `<FormState />` with external errors. This is useful for displaying errors that occur outside of the normal form submit flow. These errors will be available alongside regular errors via the `errors` array. | ||
```typescript | ||
class MyComponent extends React.Component { | ||
state = { | ||
externalErrors: [{message: 'Example error', field: 'foo'}] | ||
} | ||
render() { | ||
const {externalErrors} = this.state; | ||
return ( | ||
<FormState | ||
initialValues={{foo: ''}} | ||
externalErrors={externalErrors} | ||
onSubmit={async (formDetails) => { | ||
const response = await submitFormMutation(formDetails); | ||
if (response.errors) { | ||
// we still need to clear the errors ourselves because formstate | ||
// doesn't know what they represent or when to clear them | ||
this.setState({externalErrors: null}); | ||
return response.errors; | ||
} | ||
}} | ||
> | ||
{({fields, errors}) => { | ||
return ( | ||
<Banner> | ||
{/* the externalErrors get seamlessly added to the errors array */} | ||
{errors.map(error => <li>{error.message}</li>)} | ||
</Banner> | ||
{/* and passed down to matching fields */} | ||
<TextField {...fields.foo} /> | ||
) | ||
}} | ||
</FormState> | ||
) | ||
} | ||
} | ||
``` | ||
## Compound fields | ||
@@ -403,0 +444,0 @@ |
{ | ||
"name": "@shopify/react-form-state", | ||
"version": "0.7.4", | ||
"version": "0.8.0", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "description": "Manage react forms tersely and type-safe with no magic.", |
@@ -49,2 +49,3 @@ /* eslint-disable no-case-declarations */ | ||
initialValues: Fields; | ||
children(form: FormDetails<Fields>): React.ReactNode; | ||
validators?: Partial<ValidatorDictionary<Fields>>; | ||
@@ -54,3 +55,3 @@ onSubmit?: SubmitHandler<Fields>; | ||
onInitialValuesChange?: 'reset-all' | 'reset-where-changed' | 'ignore'; | ||
children(form: FormDetails<Fields>): React.ReactNode; | ||
externalErrors?: RemoteError[]; | ||
} | ||
@@ -63,2 +64,3 @@ | ||
errors: RemoteError[]; | ||
externalErrors: RemoteError[]; | ||
} | ||
@@ -73,9 +75,27 @@ | ||
static getDerivedStateFromProps<T>(newProps: Props<T>, oldState: State<T>) { | ||
const {initialValues, onInitialValuesChange} = newProps; | ||
const { | ||
initialValues, | ||
onInitialValuesChange, | ||
externalErrors = [], | ||
} = newProps; | ||
const externalErrorsChanged = | ||
externalErrors !== oldState.externalErrors || | ||
externalErrors.length !== oldState.externalErrors.length; | ||
const updatedExternalErrors = externalErrorsChanged | ||
? { | ||
externalErrors, | ||
fields: fieldsWithErrors(oldState.fields, [ | ||
...externalErrors, | ||
...oldState.errors, | ||
]), | ||
} | ||
: null; | ||
switch (onInitialValuesChange) { | ||
case 'ignore': | ||
return null; | ||
return updatedExternalErrors; | ||
case 'reset-where-changed': | ||
return reconcileFormState(initialValues, oldState); | ||
return reconcileFormState(initialValues, oldState, externalErrors); | ||
case 'reset-all': | ||
@@ -87,10 +107,10 @@ default: | ||
if (valuesMatch) { | ||
return null; | ||
return updatedExternalErrors; | ||
} | ||
return createFormState(initialValues); | ||
return createFormState(initialValues, externalErrors); | ||
} | ||
} | ||
state = createFormState(this.props.initialValues); | ||
state = createFormState(this.props.initialValues, this.props.externalErrors); | ||
private mounted = false; | ||
@@ -120,14 +140,2 @@ private fieldsWithHandlers = new WeakMap(); | ||
private get formData() { | ||
const {errors} = this.state; | ||
const {fields, dirty, valid} = this; | ||
return { | ||
dirty, | ||
valid, | ||
errors, | ||
fields, | ||
}; | ||
} | ||
public validateForm() { | ||
@@ -142,3 +150,4 @@ return new Promise(resolve => { | ||
this.setState( | ||
(_state, props) => createFormState(props.initialValues), | ||
(_state, props) => | ||
createFormState(props.initialValues, props.externalErrors), | ||
() => resolve(), | ||
@@ -149,2 +158,15 @@ ); | ||
private get formData() { | ||
const {errors} = this.state; | ||
const {externalErrors = []} = this.props; | ||
const {fields, dirty, valid} = this; | ||
return { | ||
dirty, | ||
valid, | ||
errors: [...errors, ...externalErrors], | ||
fields, | ||
}; | ||
} | ||
private get dirty() { | ||
@@ -155,5 +177,9 @@ return this.state.dirtyFields.length > 0; | ||
private get valid() { | ||
const {errors} = this.state; | ||
const {errors, externalErrors} = this.state; | ||
return !this.hasClientErrors && errors.length === 0; | ||
return ( | ||
!this.hasClientErrors && | ||
errors.length === 0 && | ||
externalErrors.length === 0 | ||
); | ||
} | ||
@@ -372,25 +398,30 @@ | ||
private updateRemoteErrors(errors: RemoteError[]) { | ||
this.setState(({fields}: State<Fields>) => { | ||
const errorDictionary = errors.reduce( | ||
(accumulator: any, {field, message}) => { | ||
if (field == null) { | ||
return accumulator; | ||
} | ||
this.setState(({fields, externalErrors}) => ({ | ||
errors, | ||
fields: fieldsWithErrors(fields, [...errors, ...externalErrors]), | ||
})); | ||
} | ||
} | ||
return set(accumulator, field, message); | ||
}, | ||
{}, | ||
); | ||
function fieldsWithErrors<Fields>( | ||
fields: Fields, | ||
errors: RemoteError[], | ||
): Fields { | ||
const errorDictionary = errors.reduce( | ||
(accumulator: any, {field, message}) => { | ||
if (field == null) { | ||
return accumulator; | ||
} | ||
return { | ||
errors, | ||
fields: mapObject(fields, (field, path) => { | ||
return { | ||
...field, | ||
error: errorDictionary[path], | ||
}; | ||
}), | ||
}; | ||
}); | ||
} | ||
return set(accumulator, field, message); | ||
}, | ||
{}, | ||
); | ||
return mapObject(fields, (field, path) => { | ||
return { | ||
...field, | ||
error: errorDictionary[path], | ||
}; | ||
}); | ||
} | ||
@@ -401,2 +432,3 @@ | ||
oldState: State<Fields>, | ||
externalErrors: RemoteError[] = [], | ||
): State<Fields> { | ||
@@ -425,7 +457,10 @@ const {fields: oldFields} = oldState; | ||
dirtyFields: Array.from(dirtyFields), | ||
fields, | ||
fields: fieldsWithErrors(fields, externalErrors), | ||
}; | ||
} | ||
function createFormState<Fields>(values: Fields): State<Fields> { | ||
function createFormState<Fields>( | ||
values: Fields, | ||
externalErrors: RemoteError[] = [], | ||
): State<Fields> { | ||
const fields: FieldStates<Fields> = mapObject(values, value => { | ||
@@ -443,3 +478,4 @@ return { | ||
submitting: false, | ||
fields, | ||
externalErrors, | ||
fields: fieldsWithErrors(fields, externalErrors), | ||
}; | ||
@@ -446,0 +482,0 @@ } |
Sorry, the diff of this file is not supported yet
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
179866
3905