Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

formula-one

Package Overview
Dependencies
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

formula-one - npm Package Compare versions

Comparing version 0.9.0-alpha.9 to 0.9.0-rc.1

src/EncodedPath.js

27

CHANGELOG.md
# Changelog
### v0.9.0-alpha
### v0.9.0
- Add `customChange` prop to `ObjectField` and `ArrayField`. This allows changes in one part of the object to affect other parts of the form. Currently, no metadata is preserved (all fields are marked **changed** and **touched**) if a `customChange` function is used. This will be addressed in a future API.
#### Breaking changes
**Warning**: Rendering a component which calls `onChange` during mount under a non-null returning `customChange` will result in an infinite render loop.
- Rename `serverErrors` to `externalErrors`. Validation errors from outside Formula One aren't necessarily from a server. This prop is also no longer a required parameter to `Form`, so only set it if you need it.
- Bump internal flow version to 0.95.1.
**Warning**: returning non-null from `customChange` forces a remount of all children. This can cause unintended consequences such as loss of focus on inputs. This will be fixed in a future 0.9 release.
#### New features
- Add `customChange` prop to `ObjectField` and `ArrayField`. This allows changes in one part of the object to affect other parts of the form. Currently, no metadata is preserved (all fields are marked **changed**, **touched**, and **succeeded** loses history) if a `customChange` function is used. This will be addressed in a future API.
The API is:
```js
// Override nextValue by returning a non-null result
customChange: <T>(prevValue: T, nextValue: T) => null | T;
```
- Add `addFields`, `filterFields`, and `modifyFields` array manipulators. These are currently necessary due to the non-atomic nature of the current `addField` and `removeField` manipulators. They will be made atomic in a future version.

@@ -32,5 +42,8 @@

- Fix flow types for ErrorHelper.
- Bump internal flow version to 0.95.1
- `Form`'s `feedbackStrategy` prop now defaults to `Always`, which is convenient while building a form, though you'll likely want to pick another option for better UX in production.
#### Minor changes
- Fix flow types for `ErrorHelper`.
### v0.8.2

@@ -93,3 +106,3 @@

- Reworked how server errors are updated. Fixes a bug where an exception would be thrown if the tree shapes didn't match. This could happen if you have a field which creates an object or array, which are not translated to leaf nodes internally.
- Reworked how external errors are updated. Fixes a bug where an exception would be thrown if the tree shapes didn't match. This could happen if you have a field which creates an object or array, which are not translated to leaf nodes internally.
- Added CHANGELOG

@@ -18,3 +18,3 @@ "use strict";

var _formState3 = require("./formState");
var _formState2 = require("./formState");

@@ -25,10 +25,2 @@ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

@@ -52,2 +44,10 @@

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }

@@ -61,3 +61,3 @@

function makeLinks(formState, onChildChange, onChildBlur, onChildValidation) {
function makeLinks(path, formState, onChildChange, onChildBlur) {
var _formState = _slicedToArray(formState, 1),

@@ -68,3 +68,3 @@ oldValue = _formState[0];

return {
formState: (0, _formState3.arrayChild)(i, formState),
formState: (0, _formState2.arrayChild)(i, formState),
onChange: function onChange(childFormState) {

@@ -76,5 +76,6 @@ onChildChange(i, childFormState);

},
onValidation: function onValidation(childPath, clientErrors) {
onChildValidation(i, childPath, clientErrors);
}
path: [].concat(_toConsumableArray(path), [{
type: "array",
index: i
}])
};

@@ -102,8 +103,6 @@ });

_defineProperty(_assertThisInitialized(_this), "state", {
nonce: 0
});
_defineProperty(_assertThisInitialized(_this), "validationFnOps", (0, _Form.validationFnNoOps)());
_defineProperty(_assertThisInitialized(_this), "_handleChildChange", function (index, newChild) {
var newFormState = (0, _formState3.replaceArrayChild)(index, newChild, _this.props.link.formState);
var newFormState = (0, _formState2.replaceArrayChild)(index, newChild, _this.props.link.formState);
var oldValue = _this.props.link.formState[0];

@@ -114,18 +113,13 @@ var newValue = newFormState[0];

var nextFormState;
var validatedFormState;
if (customValue) {
// Create a fresh form state for the new value.
// TODO(zach): It's kind of gross that this is happening outside of Form.
nextFormState = (0, _formState3.changedFormState)(customValue);
// A custom change occurred, which means the whole array needs to be
// revalidated.
validatedFormState = _this.context.updateTreeAtPath(_this.props.link.path, [customValue, newFormState[1]]);
} else {
nextFormState = newFormState;
validatedFormState = _this.context.updateNodeAtPath(_this.props.link.path, newFormState);
}
_this.props.link.onChange((0, _formState3.setChanged)((0, _formState3.validate)(_this.props.validation, nextFormState))); // Need to remount children so they will run validations
if (customValue) {
_this.forceChildRemount();
}
_this.props.link.onChange(validatedFormState);
});

@@ -138,12 +132,9 @@

_this.props.link.onBlur((0, _shapedTree.mapRoot)(_formState3.setExtrasTouched, (0, _shapedTree.dangerouslyReplaceArrayChild)(index, childTree, tree)));
_this.props.link.onBlur((0, _shapedTree.mapRoot)(_formState2.setExtrasTouched, (0, _shapedTree.dangerouslyReplaceArrayChild)(index, childTree, tree)));
});
_defineProperty(_assertThisInitialized(_this), "_handleChildValidation", function (index, childPath, errors) {
var extendedPath = [{
type: "array",
index: index
}].concat(_toConsumableArray(childPath));
_defineProperty(_assertThisInitialized(_this), "_validateThenApplyChange", function (formState) {
var validatedFormState = _this.context.updateNodeAtPath(_this.props.link.path, formState);
_this.props.link.onValidation(extendedPath, errors);
_this.props.link.onChange(validatedFormState);
});

@@ -163,3 +154,3 @@

_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree]))));
_this._validateThenApplyChange([newValue, newTree]);
});

@@ -188,3 +179,3 @@

_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree]))));
_this._validateThenApplyChange([newValue, newTree]);
});

@@ -216,3 +207,3 @@

_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree]))));
_this._validateThenApplyChange([newValue, newTree]);
});

@@ -270,3 +261,3 @@

_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree]))));
_this._validateThenApplyChange([newValue, newTree]);
});

@@ -282,3 +273,3 @@

_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree]))));
_this._validateThenApplyChange([newValue, newTree]);
});

@@ -294,3 +285,3 @@

_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree]))));
_this._validateThenApplyChange([newValue, newTree]);
});

@@ -302,44 +293,28 @@

_createClass(ArrayField, [{
key: "initialValidate",
value: function initialValidate() {
var _this$props = this.props,
_this$props$link = _this$props.link,
formState = _this$props$link.formState,
onValidation = _this$props$link.onValidation,
validation = _this$props.validation;
var _formState2 = _slicedToArray(formState, 1),
value = _formState2[0];
var _getExtras = (0, _formState3.getExtras)(formState),
errors = _getExtras.errors;
if (errors.client === "pending") {
onValidation([], validation(value));
}
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
this.initialValidate();
this.validationFnOps = this.context.registerValidation(this.props.link.path, this.props.validation);
}
}, {
key: "forceChildRemount",
value: function forceChildRemount() {
this.setState(function (_ref14) {
var nonce = _ref14.nonce;
return {
nonce: nonce + 1
};
});
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
if (prevProps.validation !== this.props.validation) {
this.validationFnOps.replace(this.props.validation);
}
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
this.validationFnOps.unregister();
this.validationFnOps = (0, _Form.validationFnNoOps)();
}
}, {
key: "render",
value: function render() {
var formState = this.props.link.formState;
var _this$props$link = this.props.link,
formState = _this$props$link.formState,
path = _this$props$link.path;
var shouldShowError = this.context.shouldShowError;
var links = makeLinks(formState, this._handleChildChange, this._handleChildBlur, this._handleChildValidation);
return React.createElement(React.Fragment, {
key: this.state.nonce
}, this.props.children(links, {
var links = makeLinks(path, formState, this._handleChildChange, this._handleChildBlur);
return React.createElement(React.Fragment, null, this.props.children(links, {
addField: this._addChildField,

@@ -352,9 +327,9 @@ removeField: this._removeChildField,

}, {
touched: (0, _formState3.getExtras)(formState).meta.touched,
changed: (0, _formState3.getExtras)(formState).meta.changed,
shouldShowErrors: shouldShowError((0, _formState3.getExtras)(formState).meta),
unfilteredErrors: (0, _formState3.flatRootErrors)(formState),
touched: (0, _formState2.getExtras)(formState).meta.touched,
changed: (0, _formState2.getExtras)(formState).meta.changed,
shouldShowErrors: shouldShowError((0, _formState2.getExtras)(formState).meta),
unfilteredErrors: (0, _formState2.flatRootErrors)(formState),
asyncValidationInFlight: false,
// no validations on Form
valid: (0, _formState3.isValid)(formState),
valid: (0, _formState2.isValid)(formState),
value: formState[0]

@@ -361,0 +336,0 @@ }));

@@ -23,4 +23,4 @@ "use strict";

if (errors.server !== "unchecked") {
flatErrors = flatErrors.concat(errors.server);
if (errors.external !== "unchecked") {
flatErrors = flatErrors.concat(errors.external);
}

@@ -42,5 +42,5 @@

client: errors.client,
server: errors.server,
external: errors.external,
flattened: flattened
});
}

@@ -14,3 +14,3 @@ "use strict";

var _formState3 = require("./formState");
var _formState2 = require("./formState");

@@ -54,4 +54,4 @@ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }

if (errors.server !== "unchecked") {
flatErrors = flatErrors.concat(errors.server);
if (errors.external !== "unchecked") {
flatErrors = flatErrors.concat(errors.external);
}

@@ -80,8 +80,15 @@

_defineProperty(_assertThisInitialized(_this), "validationFnOps", (0, _Form.validationFnNoOps)());
_defineProperty(_assertThisInitialized(_this), "onChange", function (newValue) {
var _this$props$link$form = _slicedToArray(_this.props.link.formState, 2),
var _this$props$link = _this.props.link,
path = _this$props$link.path,
_this$props$link$form = _slicedToArray(_this$props$link.formState, 2),
_ = _this$props$link$form[0],
oldTree = _this$props$link$form[1];
oldTree = _this$props$link$form[1],
onChange = _this$props$link.onChange;
_this.props.link.onChange((0, _formState3.setChanged)((0, _formState3.validate)(_this.props.validation, [newValue, oldTree])));
var newFormState = _this.context.updateNodeAtPath(path, [newValue, oldTree]);
onChange(newFormState);
});

@@ -94,4 +101,4 @@

_this.props.link.onBlur( // TODO(zach): Not sure if we should blow away server errors here
(0, _shapedTree.mapRoot)(_formState3.setExtrasTouched, tree));
_this.props.link.onBlur( // TODO(zach): Not sure if we should blow away external errors here
(0, _shapedTree.mapRoot)(_formState2.setExtrasTouched, tree));
});

@@ -103,24 +110,18 @@

_createClass(Field, [{
key: "initialValidate",
value: function initialValidate() {
var _this$props = this.props,
_this$props$link = _this$props.link,
formState = _this$props$link.formState,
onValidation = _this$props$link.onValidation,
validation = _this$props.validation;
var _formState = _slicedToArray(formState, 1),
value = _formState[0];
var _getExtras = (0, _formState3.getExtras)(formState),
errors = _getExtras.errors;
if (errors.client === "pending") {
onValidation([], validation(value));
key: "componentDidMount",
value: function componentDidMount() {
this.validationFnOps = this.context.registerValidation(this.props.link.path, this.props.validation);
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
if (prevProps.validation !== this.props.validation) {
this.validationFnOps.replace(this.props.validation);
}
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
this.initialValidate();
key: "componentWillUnmount",
value: function componentWillUnmount() {
this.validationFnOps.unregister();
this.validationFnOps = (0, _Form.validationFnNoOps)();
}

@@ -132,8 +133,8 @@ }, {

var _formState2 = _slicedToArray(formState, 1),
value = _formState2[0];
var _formState = _slicedToArray(formState, 1),
value = _formState[0];
var _getExtras2 = (0, _formState3.getExtras)(formState),
meta = _getExtras2.meta,
errors = _getExtras2.errors;
var _getExtras = (0, _formState2.getExtras)(formState),
meta = _getExtras.meta,
errors = _getExtras.errors;

@@ -149,3 +150,3 @@ var shouldShowError = this.context.shouldShowError;

// no validations on Form
valid: (0, _formState3.isValid)(formState),
valid: (0, _formState2.isValid)(formState),
value: value

@@ -152,0 +153,0 @@ });

@@ -6,2 +6,3 @@ "use strict";

});
exports.validationFnNoOps = validationFnNoOps;
exports.default = exports.FormContext = void 0;

@@ -11,2 +12,6 @@

var _invariant = _interopRequireDefault(require("./utils/invariant"));
var _array = require("./utils/array");
var _formState2 = require("./formState");

@@ -18,2 +23,4 @@

var _EncodedPath = require("./EncodedPath");
var _feedbackStrategies = _interopRequireDefault(require("./feedbackStrategies"));

@@ -25,4 +32,2 @@

function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

@@ -44,2 +49,14 @@

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest(); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }

@@ -57,2 +74,12 @@

var noOps = {
unregister: function unregister() {},
replace: function replace() {}
}; // noOps can't be used directly because Flow doesn't typecheck a constant as
// being parametric in T.
function validationFnNoOps() {
return noOps;
}
var FormContext = React.createContext({

@@ -63,7 +90,19 @@ shouldShowError: function shouldShowError() {

pristine: false,
submitted: true
submitted: true,
registerValidation: function registerValidation() {
return {
replace: function replace() {},
unregister: function unregister() {}
};
},
updateTreeAtPath: function updateTreeAtPath(path, formState) {
return formState;
},
updateNodeAtPath: function updateNodeAtPath(path, formState) {
return formState;
}
});
exports.FormContext = FormContext;
function applyServerErrorsToFormState(serverErrors, formState) {
function applyExternalErrorsToFormState(externalErrors, formState) {
var _formState = _slicedToArray(formState, 2),

@@ -75,3 +114,3 @@ value = _formState[0],

if (serverErrors !== null) {
if (externalErrors !== null) {
// If keys do not appear, no errors

@@ -83,3 +122,3 @@ tree = (0, _shapedTree.mapShapedTree)(function (_ref) {

errors: _objectSpread({}, errors, {
server: []
external: []
}),

@@ -89,4 +128,4 @@ meta: meta

}, oldTree);
Object.keys(serverErrors).forEach(function (key) {
var newErrors = serverErrors[key];
Object.keys(externalErrors).forEach(function (key) {
var newErrors = externalErrors[key];
var path = (0, _shapedTree.shapePath)(value, (0, _tree.pathFromPathString)(key));

@@ -101,3 +140,3 @@

errors: _objectSpread({}, errors, {
server: newErrors
external: newErrors
}),

@@ -123,3 +162,3 @@ meta: meta

errors: _objectSpread({}, errors, {
server: []
external: []
}),

@@ -134,2 +173,158 @@ meta: meta

function getValueAtPath(path, value) {
if (path.length === 0) {
return value;
}
var _path = _toArray(path),
p = _path[0],
rest = _path.slice(1);
if (p.type === "array") {
(0, _invariant.default)(Array.isArray(value), "Path/value shape mismatch: expected array");
return getValueAtPath(rest, value[p.index]);
} else if (p.type === "object") {
(0, _invariant.default)(_typeof(value) === "object" && value !== null && !Array.isArray(value), "Path/value shape mismatch: expected object");
return getValueAtPath(rest, value[p.key]);
}
throw new Error("Path is too long");
}
function pathSegmentEqual(a, b) {
return a.type === "array" && b.type === "array" && a.index === b.index || a.type === "object" && b.type === "object" && a.key === b.key;
}
function getRelativePath(path, prefix) {
for (var i = 0; i < prefix.length; i++) {
(0, _invariant.default)(pathSegmentEqual(path[i], prefix[i]), "Expect prefix to be a prefix of path");
}
return path.slice(prefix.length);
}
/**
* Deeply updates the FormState tree to reflect a new value. Similar to
* applyValidationToTree, but in response to a deep update, so updates all child
* paths too. Used in response to a custom change.
*/
function _updateTreeAtPath(prefix, _ref4, validations) {
var _ref5 = _slicedToArray(_ref4, 2),
initialValue = _ref5[0],
_initialTree = _ref5[1];
// Create a fresh form state for the new value. Note that this overwrites any
// existing `succeeded` values with false. `succeeded` will only be set back
// to true if and only if the current validation passes. We lose history. For
// most use cases this is likely desirible behaviour, but there could be uses
// cases for which it isn't desired. We'll fix it if we encounter one of those
var _changedFormState = (0, _formState2.changedFormState)(initialValue),
_changedFormState2 = _slicedToArray(_changedFormState, 2),
value = _changedFormState2[0],
changedTree = _changedFormState2[1];
var validatedTree = _toConsumableArray(validations.entries()).filter(function (_ref6) {
var _ref7 = _slicedToArray(_ref6, 1),
path = _ref7[0];
return (0, _EncodedPath.startsWith)(path, prefix);
}).map(function (_ref8) {
var _ref9 = _slicedToArray(_ref8, 2),
path = _ref9[0],
validationsMap = _ref9[1];
// Note that value is not the root value, it's the value at this path.
// So convert absolute validation paths to relative before attempting to
// pull out the value on which to run them.
var relativePath = getRelativePath((0, _EncodedPath.decodePath)(path), prefix);
var valueAtPath = getValueAtPath(relativePath, value); // Run all validation functions on x
var errors = _toConsumableArray(validationsMap.values()).reduce(function (errors, validationFn) {
return errors.concat(validationFn(valueAtPath));
}, []);
return [relativePath, errors];
}).reduce(function (tree, _ref10) {
var _ref11 = _slicedToArray(_ref10, 2),
path = _ref11[0],
newErrors = _ref11[1];
return (// Here we don't reset `errors: {external}` or set `meta: {touched: true,
// changed: true}`. This is because we already called changedFormState
// above.
(0, _shapedTree.updateAtPath)(path, function (_ref12) {
var errors = _ref12.errors,
meta = _ref12.meta;
return {
errors: _objectSpread({}, errors, {
client: newErrors
}),
meta: _objectSpread({}, meta, {
succeeded: meta.succeeded || newErrors.length === 0
})
};
}, tree)
);
}, changedTree);
return [value, validatedTree];
} // Unique id for each field so that errors can be tracked by the fields that
// produced them. This is necessary because it's possible for multiple fields
// to reference the same link "aliasing".
var _nextFieldId = 0;
function nextFieldId() {
return _nextFieldId++;
} // TODO(dmnd): This function is confusing to use because pathToValue and
// validations are conceptually "absolute" (i.e. they are defined with respect
// to the root), but valueAtPath is *not* absolute: it's the value deeper in the
// tree, defined respective to pathToValue.
function validateAtPath(pathToValue, valueAtPath, // TODO(dmnd): Better typechecking with ShapedPath?
validations) {
var map = validations.get((0, _EncodedPath.encodePath)(pathToValue));
if (!map) {
return [];
}
return _toConsumableArray(map.values()).reduce(function (errors, validationFn) {
return errors.concat(validationFn(valueAtPath));
}, []);
}
/**
* Updates the FormState tree to reflect a new value:
* - run validations at path (but not child paths)
* - remove existing, now obsolete errors
* - calculate & write new client side errors
* - ensure that meta reflects that the value has changed
*/
function _updateNodeAtPath(path, _ref13, validations) {
var _ref14 = _slicedToArray(_ref13, 2),
value = _ref14[0],
tree = _ref14[1];
var errors = validateAtPath(path, value, validations);
return [value, (0, _shapedTree.mapRoot)(function (_ref15) {
var meta = _ref15.meta;
return {
errors: {
client: errors,
external: "unchecked"
},
meta: _objectSpread({}, meta, {
succeeded: meta.succeeded || errors.length === 0,
touched: true,
changed: true
})
};
}, tree)];
}
var Form =

@@ -143,7 +338,7 @@ /*#__PURE__*/

value: function getDerivedStateFromProps(props, state) {
if (props.serverErrors !== state.oldServerErrors) {
var newFormState = applyServerErrorsToFormState(props.serverErrors, state.formState);
if (props.externalErrors !== state.oldExternalErrors) {
var newTree = applyExternalErrorsToFormState(props.externalErrors, state.formState);
return {
formState: newFormState,
oldServerErrors: props.serverErrors
formState: newTree,
oldExternalErrors: props.externalErrors
};

@@ -163,2 +358,6 @@ }

_defineProperty(_assertThisInitialized(_this), "validations", void 0);
_defineProperty(_assertThisInitialized(_this), "initialValidationComplete", false);
_defineProperty(_assertThisInitialized(_this), "_handleSubmit", function (extraData) {

@@ -194,33 +393,98 @@ _this.setState({

_defineProperty(_assertThisInitialized(_this), "_handleValidation", function (path, errors) {
// TODO(zach): Move this into formState.js, it is gross
var updater = function updater(newErrors) {
return function (_ref4) {
var errors = _ref4.errors,
meta = _ref4.meta;
return {
errors: _objectSpread({}, errors, {
client: newErrors
}),
meta: _objectSpread({}, meta, {
succeeded: newErrors.length === 0 ? true : meta.succeeded
_defineProperty(_assertThisInitialized(_this), "recomputeErrorsAtPathAndRender", function (path) {
_this.setState(function (_ref16) {
var _ref16$formState = _slicedToArray(_ref16.formState, 2),
rootValue = _ref16$formState[0],
tree = _ref16$formState[1];
var value = getValueAtPath(path, rootValue);
var errors = validateAtPath(path, value, _this.validations);
var updatedTree = (0, _shapedTree.updateAtPath)(path, function (extras) {
return _objectSpread({}, extras, {
errors: _objectSpread({}, extras.errors, {
client: errors
})
};
});
}, tree);
return {
formState: [rootValue, updatedTree]
};
});
});
_defineProperty(_assertThisInitialized(_this), "handleRegisterValidation", function (path, fn) {
var encodedPath = (0, _EncodedPath.encodePath)(path);
var fieldId = nextFieldId();
var map = _this.validations.get(encodedPath) || new Map();
map.set(fieldId, fn);
_this.validations.set(encodedPath, map);
if (_this.initialValidationComplete) {
// Form validates all Fields at once during mount. When fields are added
// after the Form has already mounted, their initial values need to be
// validated.
_this.recomputeErrorsAtPathAndRender(path);
}
return {
replace: function replace(fn) {
return _this.replaceValidation(path, fieldId, fn);
},
unregister: function unregister() {
return _this.unregisterValidation(path, fieldId);
}
};
});
_this.setState(function (_ref5) {
var _ref5$formState = _slicedToArray(_ref5.formState, 2),
value = _ref5$formState[0],
tree = _ref5$formState[1];
_defineProperty(_assertThisInitialized(_this), "replaceValidation", function (path, fieldId, fn) {
var encodedPath = (0, _EncodedPath.encodePath)(path);
return {
formState: [value, (0, _shapedTree.updateAtPath)(path, updater(errors), tree)]
};
}, function () {
_this.props.onValidation((0, _formState2.isValid)(_this.state.formState));
});
var map = _this.validations.get(encodedPath);
(0, _invariant.default)(map != null, "Expected to find handler map");
var oldFn = map.get(fieldId);
(0, _invariant.default)(oldFn != null, "Expected to find previous validation function");
map.set(fieldId, fn); // Now that the old validation is gone, make sure there are no left over
// errors from it.
var value = getValueAtPath(path, _this.state.formState[0]);
if ((0, _array.equals)(oldFn(value), fn(value))) {
// The errors haven't changed, so don't bother calling setState.
// You might think this is a silly performance optimization but actually
// we need this for annoying React reasons:
// If the validation function is an inline function, its identity changes
// every render. This means replaceValidation gets called every time
// componentDidUpdate runs (i.e. each render). Then when setState is
// called from recomputeErrorsAtPathAndRender, it'll cause another render,
// which causes another componentDidUpdate, and so on. So, take care to
// avoid an infinite loop by returning early here.
return;
} // The new validation function returns different errors, so re-render.
_this.recomputeErrorsAtPathAndRender(path);
});
var formState = applyServerErrorsToFormState(props.serverErrors, (0, _formState2.freshFormState)(props.initialValue));
_defineProperty(_assertThisInitialized(_this), "unregisterValidation", function (path, fieldId) {
var encodedPath = (0, _EncodedPath.encodePath)(path);
var map = _this.validations.get(encodedPath);
(0, _invariant.default)(map != null, "Couldn't find handler map during unregister");
map.delete(fieldId); // If the entire path was deleted from the tree, any left over errors are
// already gone. For example, this happens when an array child is removed.
if (!(0, _shapedTree.pathExistsInTree)(path, _this.state.formState[1])) {
return;
} // now that the validation is gone, make sure there are no left over
// errors from it
_this.recomputeErrorsAtPathAndRender(path);
});
_this.validations = new Map();
var formState = applyExternalErrorsToFormState(props.externalErrors, (0, _formState2.freshFormState)(props.initialValue));
_this.state = {

@@ -230,9 +494,32 @@ formState: formState,

submitted: false,
oldServerErrors: props.serverErrors
oldExternalErrors: props.externalErrors
};
return _this;
} // Public API: submit from the outside
}
_createClass(Form, [{
key: "componentDidMount",
value: function componentDidMount() {
var _this2 = this;
_createClass(Form, [{
// After the the Form mounts, all validations get ran as a batch. Note that
// this is different from how initial validations get run on all
// subsequently mounted Fields. When a Field is mounted after the Form, its
// validation gets run individually.
// TODO(dmnd): It'd be nice to consolidate validation to a single code path.
// Take care to use an updater to avoid clobbering changes from fields that
// call onChange during cDM.
this.setState(function (_ref17) {
var formState = _ref17.formState;
return {
formState: _updateTreeAtPath([], formState, _this2.validations)
};
}, function () {
_this2.initialValidationComplete = true;
_this2.props.onValidation((0, _formState2.isValid)(_this2.state.formState));
});
} // Public API: submit from the outside
}, {
key: "submit",

@@ -246,2 +533,4 @@ value: function submit(extraData) {

value: function render() {
var _this3 = this;
var formState = this.state.formState;

@@ -254,3 +543,10 @@ var metaForm = {

value: _objectSpread({
shouldShowError: this.props.feedbackStrategy.bind(null, metaForm)
shouldShowError: this.props.feedbackStrategy.bind(null, metaForm),
registerValidation: this.handleRegisterValidation,
updateTreeAtPath: function updateTreeAtPath(path, formState) {
return _updateTreeAtPath(path, formState, _this3.validations);
},
updateNodeAtPath: function updateNodeAtPath(path, formState) {
return _updateNodeAtPath(path, formState, _this3.validations);
}
}, metaForm)

@@ -261,3 +557,3 @@ }, this.props.children({

onBlur: this._handleBlur,
onValidation: this._handleValidation
path: []
}, this._handleSubmit, {

@@ -286,3 +582,3 @@ touched: (0, _formState2.getExtras)(formState).meta.touched,

feedbackStrategy: _feedbackStrategies.default.Always,
serverErrors: null
externalErrors: null
});

@@ -12,12 +12,5 @@ "use strict";

exports.arrayChild = arrayChild;
exports.validate = validate;
exports.setChanged = setChanged;
exports.setTouched = setTouched;
exports.setClientErrors = setClientErrors;
exports.setExtrasTouched = setExtrasTouched;
exports.replaceObjectChild = replaceObjectChild;
exports.replaceArrayChild = replaceArrayChild;
exports.replaceArrayChildren = replaceArrayChildren;
exports.monoidallyCombineFormStatesForValidation = monoidallyCombineFormStatesForValidation;
exports.replaceServerErrors = replaceServerErrors;
exports.isValid = isValid;

@@ -31,6 +24,2 @@

var _invariant = _interopRequireDefault(require("./utils/invariant"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }

@@ -79,4 +68,4 @@

if (errors.server !== "unchecked") {
flatErrors = flatErrors.concat(errors.server);
if (errors.external !== "unchecked") {
flatErrors = flatErrors.concat(errors.external);
}

@@ -103,65 +92,5 @@

function validate(validation, formState) {
var _formState3 = _slicedToArray(formState, 2),
value = _formState3[0],
tree = _formState3[1];
var newErrors = validation(value);
return [value, (0, _shapedTree.mapRoot)(function (_ref) {
var meta = _ref.meta;
return {
errors: {
client: newErrors,
server: "unchecked"
},
meta: _objectSpread({}, meta, {
succeeded: meta.succeeded || newErrors.length === 0
})
};
}, tree)];
}
function setChanged(formState) {
return [formState[0], (0, _shapedTree.mapRoot)(function (_ref2) {
var errors = _ref2.errors,
meta = _ref2.meta;
return {
errors: errors,
meta: _objectSpread({}, meta, {
touched: true,
changed: true
})
};
}, formState[1])];
}
function setTouched(formState) {
return [formState[0], (0, _shapedTree.mapRoot)(function (_ref3) {
var errors = _ref3.errors,
meta = _ref3.meta;
return {
errors: errors,
meta: _objectSpread({}, meta, {
touched: true
})
};
}, formState[1])];
}
function setClientErrors(newErrors, formState) {
return [formState[0], (0, _shapedTree.mapRoot)(function (_ref4) {
var errors = _ref4.errors,
meta = _ref4.meta;
return {
errors: _objectSpread({}, errors, {
client: newErrors
}),
meta: meta
};
}, formState[1])];
}
function setExtrasTouched(_ref5) {
var errors = _ref5.errors,
meta = _ref5.meta;
function setExtrasTouched(_ref) {
var errors = _ref.errors,
meta = _ref.meta;
return {

@@ -176,5 +105,5 @@ errors: errors,

function replaceObjectChild(key, child, formState) {
var _formState4 = _slicedToArray(formState, 2),
value = _formState4[0],
tree = _formState4[1];
var _formState3 = _slicedToArray(formState, 2),
value = _formState3[0],
tree = _formState3[1];

@@ -189,5 +118,5 @@ var _child = _slicedToArray(child, 2),

function replaceArrayChild(index, child, formState) {
var _formState5 = _slicedToArray(formState, 2),
value = _formState5[0],
tree = _formState5[1];
var _formState4 = _slicedToArray(formState, 2),
value = _formState4[0],
tree = _formState4[1];

@@ -199,80 +128,2 @@ var _child2 = _slicedToArray(child, 2),

return [(0, _array.replaceAt)(index, childValue, value), (0, _shapedTree.dangerouslyReplaceArrayChild)(index, childTree, tree)];
}
function replaceArrayChildren(children, formState) {
var _formState6 = _slicedToArray(formState, 2),
_ = _formState6[0],
tree = _formState6[1];
var _children$reduce = children.reduce(function (memo, child) {
var _child3 = _slicedToArray(child, 2),
childValue = _child3[0],
childTree = _child3[1];
return [memo[0].concat([childValue]), memo[1].concat([childTree])];
}, [[], []]),
_children$reduce2 = _slicedToArray(_children$reduce, 2),
childValues = _children$reduce2[0],
childTrees = _children$reduce2[1];
return [childValues, (0, _shapedTree.dangerouslySetChildren)(childTrees, tree)];
}
function combineExtrasForValidation(oldExtras, newExtras) {
var oldMeta = oldExtras.meta,
oldErrors = oldExtras.errors;
var newMeta = newExtras.meta,
newErrors = newExtras.errors; // Only asyncValidationInFlight + succeeded may change
(0, _invariant.default)(oldMeta.touched === newMeta.touched, "Recieved a new meta.touched when monoidally combining errors");
(0, _invariant.default)(oldMeta.changed === newMeta.changed, "Recieved a new meta.changed when monoidally combining errors"); // No combination is possible if the old client errors are not pending
if (oldErrors.client !== "pending") {
return oldExtras;
} // No combination is possible if the new client errors are pending
if (newErrors.client === "pending") {
return oldExtras;
}
return {
meta: {
touched: oldMeta.touched,
changed: oldMeta.changed,
succeeded: newMeta.succeeded,
asyncValidationInFlight: oldMeta.asyncValidationInFlight || newMeta.asyncValidationInFlight
},
errors: {
client: newErrors.client,
server: newErrors.server
}
};
}
function monoidallyCombineTreesForValidation(oldTree, newTree) {
return (0, _shapedTree.shapedZipWith)(combineExtrasForValidation, oldTree, newTree);
} // Also sets asyncValidationInFlight
function monoidallyCombineFormStatesForValidation(oldState, newState) {
// Value should never change when combining errors
(0, _invariant.default)(oldState[0] === newState[0], "Received a new value when monoidally combining errors");
return [oldState[0], monoidallyCombineTreesForValidation(oldState[1], newState[1])];
}
function replaceServerErrorsExtra(newErrors, oldExtras) {
var meta = oldExtras.meta,
errors = oldExtras.errors;
return {
meta: meta,
errors: {
client: errors.client,
server: newErrors
}
};
}
function replaceServerErrors(serverErrors, formState) {
return [formState[0], (0, _shapedTree.shapedZipWith)(replaceServerErrorsExtra, serverErrors, formState[1])];
} // Is whole tree client valid?

@@ -286,4 +137,4 @@ // TODO(zach): This will have to change with asynchronous validations. We will

function isValid(formState) {
return (0, _shapedTree.foldMapShapedTree)(function (_ref6) {
var client = _ref6.errors.client;
return (0, _shapedTree.foldMapShapedTree)(function (_ref2) {
var client = _ref2.errors.client;
return client === "pending" || client.length === 0;

@@ -290,0 +141,0 @@ }, true, function (l, r) {

@@ -12,3 +12,3 @@ "use strict";

var _formState3 = require("./formState");
var _formState2 = require("./formState");

@@ -21,10 +21,2 @@ var _shapedTree = require("./shapedTree");

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

@@ -50,2 +42,10 @@

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }

@@ -59,3 +59,3 @@

function makeLinks(formState, onChildChange, onChildBlur, onChildValidation) {
function makeLinks(path, formState, onChildChange, onChildBlur) {
var _formState = _slicedToArray(formState, 1),

@@ -66,3 +66,3 @@ value = _formState[0];

var l = {
formState: (0, _formState3.objectChild)(k, formState),
formState: (0, _formState2.objectChild)(k, formState),
onChange: function onChange(childFormState) {

@@ -74,5 +74,6 @@ onChildChange(k, childFormState);

},
onValidation: function onValidation(path, errors) {
onChildValidation(k, path, errors);
}
path: [].concat(_toConsumableArray(path), [{
type: "object",
key: k
}])
};

@@ -102,8 +103,6 @@ memo[k] = l;

_defineProperty(_assertThisInitialized(_this), "state", {
nonce: 0
});
_defineProperty(_assertThisInitialized(_this), "validationFnOps", (0, _Form.validationFnNoOps)());
_defineProperty(_assertThisInitialized(_this), "_handleChildChange", function (key, newChild) {
var newFormState = (0, _formState3.replaceObjectChild)(key, newChild, _this.props.link.formState);
var newFormState = (0, _formState2.replaceObjectChild)(key, newChild, _this.props.link.formState);
var oldValue = _this.props.link.formState[0];

@@ -114,18 +113,13 @@ var newValue = newFormState[0];

var nextFormState;
var validatedFormState;
if (customValue) {
// Create a fresh form state for the new value.
// TODO(zach): It's kind of gross that this is happening outside of Form.
nextFormState = (0, _formState3.changedFormState)(customValue);
// A custom change occurred, which means the whole object needs to be
// revalidated.
validatedFormState = _this.context.updateTreeAtPath(_this.props.link.path, [customValue, newFormState[1]]);
} else {
nextFormState = newFormState;
validatedFormState = _this.context.updateNodeAtPath(_this.props.link.path, newFormState);
}
_this.props.link.onChange((0, _formState3.setChanged)((0, _formState3.validate)(_this.props.validation, nextFormState))); // Need to remount children so they will run validations
if (customValue) {
_this.forceChildRemount();
}
_this.props.link.onChange(validatedFormState);
});

@@ -138,14 +132,5 @@

_this.props.link.onBlur((0, _shapedTree.mapRoot)(_formState3.setExtrasTouched, (0, _shapedTree.dangerouslyReplaceObjectChild)(key, childTree, tree)));
_this.props.link.onBlur((0, _shapedTree.mapRoot)(_formState2.setExtrasTouched, (0, _shapedTree.dangerouslyReplaceObjectChild)(key, childTree, tree)));
});
_defineProperty(_assertThisInitialized(_this), "_handleChildValidation", function (key, childPath, errors) {
var extendedPath = [{
type: "object",
key: key
}].concat(_toConsumableArray(childPath));
_this.props.link.onValidation(extendedPath, errors);
});
return _this;

@@ -155,36 +140,20 @@ }

_createClass(ObjectField, [{
key: "_initialValidate",
value: function _initialValidate() {
var _this$props = this.props,
_this$props$link = _this$props.link,
formState = _this$props$link.formState,
onValidation = _this$props$link.onValidation,
validation = _this$props.validation;
var _formState2 = _slicedToArray(formState, 1),
value = _formState2[0];
var _getExtras = (0, _formState3.getExtras)(formState),
errors = _getExtras.errors;
if (errors.client === "pending") {
onValidation([], validation(value));
}
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
this._initialValidate();
this.validationFnOps = this.context.registerValidation(this.props.link.path, this.props.validation);
}
}, {
key: "forceChildRemount",
value: function forceChildRemount() {
this.setState(function (_ref) {
var nonce = _ref.nonce;
return {
nonce: nonce + 1
};
});
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
if (prevProps.validation !== this.props.validation) {
this.validationFnOps.replace(this.props.validation);
}
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
this.validationFnOps.unregister();
this.validationFnOps = (0, _Form.validationFnNoOps)();
}
}, {
key: "render",

@@ -194,13 +163,11 @@ value: function render() {

var shouldShowError = this.context.shouldShowError;
var links = makeLinks(this.props.link.formState, this._handleChildChange, this._handleChildBlur, this._handleChildValidation);
return React.createElement(React.Fragment, {
key: this.state.nonce
}, this.props.children(links, {
touched: (0, _formState3.getExtras)(formState).meta.touched,
changed: (0, _formState3.getExtras)(formState).meta.changed,
shouldShowErrors: shouldShowError((0, _formState3.getExtras)(formState).meta),
unfilteredErrors: (0, _formState3.flatRootErrors)(formState),
var links = makeLinks(this.props.link.path, this.props.link.formState, this._handleChildChange, this._handleChildBlur);
return React.createElement(React.Fragment, null, this.props.children(links, {
touched: (0, _formState2.getExtras)(formState).meta.touched,
changed: (0, _formState2.getExtras)(formState).meta.changed,
shouldShowErrors: shouldShowError((0, _formState2.getExtras)(formState).meta),
unfilteredErrors: (0, _formState2.flatRootErrors)(formState),
asyncValidationInFlight: false,
// no validations on Form
valid: (0, _formState3.isValid)(formState),
valid: (0, _formState2.isValid)(formState),
value: formState[0]

@@ -207,0 +174,0 @@ }));

@@ -8,4 +8,4 @@ "use strict";

exports.shapePath = shapePath;
exports.pathExistsInTree = pathExistsInTree;
exports.updateAtPath = updateAtPath;
exports.checkShape = checkShape;
exports.shapedArrayChild = shapedArrayChild;

@@ -19,4 +19,2 @@ exports.shapedArrayChildren = shapedArrayChildren;

exports.dangerouslySetChildren = dangerouslySetChildren;
exports.shapedLeaf = shapedLeaf;
exports.shapedZipWith = shapedZipWith;
exports.mapShapedTree = mapShapedTree;

@@ -35,6 +33,2 @@ exports.foldMapShapedTree = foldMapShapedTree;

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }

@@ -124,4 +118,51 @@

function pathExistsInTree(path, tree) {
// This function is equivalent to:
// try {
// updateAtPath(path, x => x, tree);
// } catch {
// return false;
// }
// return true;
// }
if (path.length === 0 && tree.type != null) {
return true;
}
var _path2 = _toArray(path),
first = _path2[0],
rest = _path2.slice(1);
if (tree.type === "leaf") {
return false;
}
if (tree.type === "array") {
if (first.type !== "array") {
return false;
}
if (!(0 <= first.index && first.index < tree.children.length)) {
return false;
}
return pathExistsInTree(rest, tree.children[first.index]);
}
if (tree.type === "object") {
if (first.type !== "object") {
return false;
}
if (!(first.key in tree.children)) {
return false;
}
return pathExistsInTree(rest, tree.children[first.key]);
}
throw new Error("unreachable");
}
function updateAtPath(path, updater, tree) {
// console.log("updateAtPath()", path, tree);
if (path.length === 0) {

@@ -150,5 +191,5 @@ if (tree.type === "object") {

var _path2 = _toArray(path),
firstStep = _path2[0],
restStep = _path2.slice(1);
var _path3 = _toArray(path),
firstStep = _path3[0],
restStep = _path3.slice(1);

@@ -161,2 +202,3 @@ if (tree.type === "leaf") {

(0, _invariant.default)(firstStep.type === "array", "Trying to take a non-array path into an array");
(0, _invariant.default)(0 <= firstStep.index && firstStep.index < tree.children.length, "Tried to take path index ".concat(firstStep.index, " but array from tree has length ").concat(tree.children.length));
var newChild = updateAtPath(restStep, updater, tree.children[firstStep.index]); // $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why

@@ -169,4 +211,6 @@

(0, _invariant.default)(firstStep.type === "object", "Trying to take a non-object path into an object");
var nextTree = tree.children[firstStep.key];
(0, _invariant.default)(nextTree !== undefined, "Tried to take path key ".concat(firstStep.key, " but it isn't present on object in tree"));
var _newChild = updateAtPath(restStep, updater, tree.children[firstStep.key]); // $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why
var _newChild = updateAtPath(restStep, updater, nextTree); // $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why

@@ -180,30 +224,2 @@

function checkShape(value, tree) {
if (tree.type === "array") {
(0, _invariant.default)(Array.isArray(value), "value isn't an array");
(0, _invariant.default)(value.length === tree.children.length, "value and tree children have different lengths");
tree.children.forEach(function (child, i) {
checkShape(value[i], child);
});
}
if (tree.type === "object") {
(0, _invariant.default)(value instanceof Object, "value isn't an object in checkTree");
var valueEntries = Object.entries(value);
var childrenKeys = new Set(Object.keys(tree.children));
(0, _invariant.default)(valueEntries.length === childrenKeys.size, "value doesn't have the right number of keys");
valueEntries.forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
key = _ref2[0],
value = _ref2[1];
(0, _invariant.default)(childrenKeys.has(key));
checkShape(value, tree.children[key]);
});
} // leaves are allowed to stand in for complex types in T
return tree;
}
function shapedArrayChild(index, tree) {

@@ -285,12 +301,2 @@ (0, _invariant.default)(tree.type === "array", "Tried to get an array child of a non-array node");

};
} // A leaf matches any shape
function shapedLeaf(node) {
return (0, _tree.leaf)(node);
}
function shapedZipWith(f, left, right) {
// Don't actually need the checks here if our invariant holds
return (0, _tree.strictZipWith)(f, left, right);
} // Mapping doesn't change the shape

@@ -297,0 +303,0 @@

@@ -7,2 +7,4 @@ "use strict";

var _Form = _interopRequireDefault(require("../Form"));
var _ArrayField = _interopRequireDefault(require("../ArrayField"));

@@ -12,2 +14,4 @@

var _TestForm = _interopRequireDefault(require("./TestForm"));
var _tools = require("./tools");

@@ -28,121 +32,104 @@

describe("ArrayField", function () {
describe("ArrayField is a field", function () {
describe("validates on mount", function () {
it("ensures that the link inner type matches the type of the validation", function () {
var formState = (0, _tools.mockFormState)(["one", "two", "three"]);
var link = (0, _tools.mockLink)(formState); // $ExpectError
describe("is a field", function () {
it("ensures that the link inner type matches the type of the validation", function () {
var formState = (0, _tools.mockFormState)(["one", "two", "three"]);
var link = (0, _tools.mockLink)(formState); // $ExpectError
React.createElement(_ArrayField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
React.createElement(_ArrayField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
React.createElement(_ArrayField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
it("Sets errors.client and meta.succeeded when there are no errors", function () {
var validation = jest.fn(function () {
React.createElement(_ArrayField.default, {
link: link,
validation: function validation(_e) {
return [];
});
var formState = (0, _tools.mockFormState)([]);
var link = (0, _tools.mockLink)(formState);
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link,
validation: validation
}, jest.fn(function () {
return null;
})));
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
var _link$onValidation$mo = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo[0],
errors = _link$onValidation$mo[1];
expect(path).toEqual([]);
expect(errors).toEqual([]);
}
}, function () {
return null;
});
it("Sets errors.client and meta.succeeded when there are errors", function () {
var validation = jest.fn(function () {
return ["This is an error", "another error"];
});
var formState = (0, _tools.mockFormState)([]);
var link = (0, _tools.mockLink)(formState);
});
it("Registers and unregisters for validation", function () {
var formState = (0, _tools.mockFormState)([]);
var link = (0, _tools.mockLink)(formState);
var unregister = jest.fn();
var registerValidation = jest.fn(function () {
return {
replace: jest.fn(),
unregister: unregister
};
});
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link,
validation: validation
}, jest.fn(function () {
return null;
})));
var renderer = _reactTestRenderer.default.create(React.createElement(_TestForm.default, {
registerValidation: registerValidation
}, React.createElement(_ArrayField.default, {
link: link,
validation: function validation() {
return [];
}
}, function () {
return null;
})));
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
var _link$onValidation$mo2 = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo2[0],
errors = _link$onValidation$mo2[1];
expect(path).toEqual([]);
expect(errors).toEqual(["This is an error", "another error"]);
expect(registerValidation).toBeCalledTimes(1);
renderer.unmount();
expect(unregister).toBeCalledTimes(1);
});
it("calls replace when changing the validation function", function () {
var replace = jest.fn();
var registerValidation = jest.fn(function () {
return {
replace: replace,
unregister: jest.fn()
};
});
it("Treats no validation as always passing", function () {
var formState = (0, _tools.mockFormState)([]);
var link = (0, _tools.mockLink)(formState);
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link
}, jest.fn(function () {
function Component() {
return React.createElement(_TestForm.default, {
registerValidation: registerValidation
}, React.createElement(_ArrayField.default, {
link: (0, _tools.mockLink)((0, _tools.mockFormState)(["hello", "world"])),
validation: function validation() {
return [];
}
}, function () {
return null;
})));
}));
}
expect(link.onValidation).toHaveBeenCalledTimes(1);
var renderer = _reactTestRenderer.default.create(React.createElement(Component, null));
var _link$onValidation$mo3 = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo3[0],
errors = _link$onValidation$mo3[1];
expect(registerValidation).toBeCalledTimes(1);
renderer.update(React.createElement(Component, null));
expect(replace).toBeCalledTimes(1);
});
it("Passes additional information to its render function", function () {
var formState = (0, _tools.mockFormState)(["value"]); // $FlowFixMe
expect(path).toEqual([]);
expect(errors).toEqual([]);
formState[1].data.errors = {
external: ["An external error"],
client: ["A client error"]
};
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
});
it("Passes additional information to its render function", function () {
var formState = (0, _tools.mockFormState)(["value"]); // $FlowFixMe
formState[1].data.errors = {
server: ["A server error"],
client: ["A client error"]
};
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
});
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link
}, renderFn));
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining(["A server error", "A client error"]),
valid: false,
asyncValidationInFlight: false,
value: ["value"]
}));
});
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining(["An external error", "A client error"]),
valid: false,
asyncValidationInFlight: false,
value: ["value"]
}));
});

@@ -184,5 +171,4 @@ });

});
it("calls onChange when a child changes", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
it("validates new values from children and passes result to onChange", function () {
var formState = (0, _tools.mockFormState)(["one", "two", "three"]);
var link = (0, _tools.mockLink)(formState);

@@ -192,18 +178,22 @@ var renderFn = jest.fn(function () {

});
var updateNodeAtPath = jest.fn(function (path, formState) {
return formState;
});
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
_reactTestRenderer.default.create(React.createElement(_TestForm.default, {
updateNodeAtPath: updateNodeAtPath
}, React.createElement(_ArrayField.default, {
link: link
}, renderFn));
}, renderFn)));
expect(updateNodeAtPath).toHaveBeenCalledTimes(0);
expect(link.onChange).toHaveBeenCalledTimes(0); // call a child's onChange
var arrayLinks = renderFn.mock.calls[0][0];
var newElementFormState = (0, _tools.mockFormState)("newTwo");
arrayLinks[1].onChange(newElementFormState);
expect(link.onChange).toHaveBeenCalled();
var newArrayFormState = link.onChange.mock.calls[0][0];
expect(newArrayFormState[0]).toEqual(["one", "newTwo", "three"]);
expect(newArrayFormState[1].data.meta).toMatchObject({
touched: true,
changed: true
});
expect(newArrayFormState[1].children[1]).toBe(newElementFormState[1]);
expect(updateNodeAtPath).toHaveBeenCalledTimes(1);
expect(updateNodeAtPath).toHaveBeenCalledWith([], [["one", "newTwo", "three"], expect.anything()]);
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([["one", "newTwo", "three"], formState[1]]);
});

@@ -233,32 +223,3 @@ it("calls onBlur when a child is blurred", function () {

});
it("calls onValidation when a child initially validates", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
});
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link
}, renderFn));
var arrayLinks = renderFn.mock.calls[0][0];
arrayLinks[2].onValidation([], ["These are", "some errors"]);
expect(link.onValidation).toHaveBeenCalledTimes(2); // Important: the first call to onValidation is for the initial render validation
var _link$onValidation$mo4 = _slicedToArray(link.onValidation.mock.calls[1], 2),
path = _link$onValidation$mo4[0],
errors = _link$onValidation$mo4[1];
expect(path).toEqual([{
type: "array",
index: 2
}]);
expect(errors).toEqual(["These are", "some errors"]);
});
it("calls its validation when a child changes", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -271,6 +232,10 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link,
validation: validation
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
link: link,
validation: validation
}, renderFn);
}));

@@ -304,5 +269,2 @@ expect(validation).toHaveBeenCalledTimes(1);

it("validates after entry is added", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -315,6 +277,10 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn);
}));

@@ -350,8 +316,14 @@ expect(validation).toHaveBeenCalledTimes(1);

it("validates after entry is removed", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
var elementValidation = jest.fn(function () {
return ["an element error"];
});
var renderFn = jest.fn(function (links) {
return links.map(function (link, i) {
return React.createElement(_TestField.default, {
key: i,
link: link,
validation: elementValidation
});
});
});
var validation = jest.fn(function () {

@@ -361,6 +333,10 @@ return ["an error"];

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn);
}));

@@ -396,5 +372,2 @@ expect(validation).toHaveBeenCalledTimes(1);

it("validates after the entry is moved", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -407,6 +380,10 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn);
}));

@@ -442,5 +419,2 @@ expect(validation).toHaveBeenCalledTimes(1);

it("validates after fields are added", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -453,6 +427,10 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn);
}));

@@ -488,5 +466,2 @@ expect(validation).toHaveBeenCalledTimes(1);

it("validates after fields are filtered", function () {
var formStateValue = ["one", "two", "three", "four", "five"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -499,6 +474,10 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn);
}));

@@ -537,5 +516,2 @@ expect(validation).toHaveBeenCalledTimes(1);

it("validates after fields are modified", function () {
var formStateValue = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateValue);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -548,6 +524,10 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link
}, renderFn);
}));

@@ -572,3 +552,3 @@ expect(validation).toHaveBeenCalledTimes(1);

describe("customChange", function () {
it("allows the default change behavior to be overwritten with customChange", function () {
it("allows sibling fields to be overwritten", function () {
var formStateInner = ["one", "two", "three"];

@@ -587,7 +567,7 @@ var formState = (0, _tools.mockFormState)(formStateInner);

_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
_reactTestRenderer.default.create(React.createElement(_TestForm.default, null, React.createElement(_ArrayField.default, {
link: link,
validation: validation,
customChange: customChange
}, renderFn));
}, renderFn)));

@@ -603,11 +583,8 @@ var arrayLinks = renderFn.mock.calls[0][0]; // call the child onChange

expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([["uno", "dos", "tres"], expect.anything()]); // Validated the result of customChange
expect(validation).toHaveBeenCalledTimes(2);
expect(validation.mock.calls[1][0]).toEqual(["uno", "dos", "tres"]);
expect(link.onChange).toHaveBeenCalledWith([["uno", "dos", "tres"], expect.anything()]);
});
it("can return null to signal there was no custom change", function () {
var formStateInner = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState);
var customChange = jest.fn(function (_oldValue, _newValue) {
return null;
});
var renderFn = jest.fn(function () {

@@ -617,13 +594,14 @@ return null;

var validation = jest.fn(function () {
return ["This is an error"];
return ["an error"];
});
var customChange = jest.fn(function (_oldValue, _newValue) {
return null;
});
_reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link,
validation: validation,
customChange: customChange
}, renderFn));
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["one", "two", "three"]
}, function (link) {
return React.createElement(_ArrayField.default, {
validation: validation,
link: link,
customChange: customChange
}, renderFn);
}));

@@ -636,4 +614,4 @@ var arrayLinks = renderFn.mock.calls[0][0]; // call the child onChange

expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([["one", "zwei", "three"], expect.anything()]); // Validated the result of customChange
var link = renderer.root.findByType(_ArrayField.default).instance.props.link;
expect(link.formState).toEqual([["one", "zwei", "three"], expect.anything()]); // Validated the result of customChange

@@ -644,5 +622,2 @@ expect(validation).toHaveBeenCalledTimes(2);

it("doesn't break validations for child fields", function () {
var formStateInner = ["one", "two", "three"];
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState);
var customChange = jest.fn(function (_oldValue, _newValue) {

@@ -654,93 +629,66 @@ return ["1", "2"];

});
var parentValidation = jest.fn(function () {
return ["This is an error from the parent"];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: ["1", "2"]
}, function (link) {
return React.createElement(_ArrayField.default, {
link: link,
customChange: customChange,
validation: parentValidation
}, function (links) {
return React.createElement(React.Fragment, null, links.map(function (link, i) {
return React.createElement(_TestField.default, {
key: i,
link: link,
validation: childValidation
});
}));
});
})); // after mount, validate everything
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(2); // Now change one of the values
parentValidation.mockClear();
childValidation.mockClear();
var inner = renderer.root.findAllByType(_TestField.TestInput)[0];
inner.instance.change("zach"); // Validate the whole subtree due to the customChange child validates
// once. Note that child validation will be called 3 times. Once after the
// change, then twice more after the customChange triggers a validation fo
// the entire subtree.
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(1 + 2);
var link = renderer.root.findByType(_ArrayField.default).instance.props.link;
expect(link.formState).toEqual([["1", "2"], expect.anything()]);
});
it("doesn't create a new instance (i.e. remount)", function () {
var customChange = jest.fn(function (_oldValue, _newValue) {
return ["uno", "dos"];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_ArrayField.default, {
link: link,
link: (0, _tools.mockLink)((0, _tools.mockFormState)(["1", "2"])),
customChange: customChange
}, function (links) {
return React.createElement(React.Fragment, null, links.map(function (link, i) {
return React.createElement(_TestField.default, {
key: i,
link: link,
validation: childValidation
});
}));
})); // 6 validations:
// 1) Child initial validation x3
// 2) Parent initial validation
// 3) Child validation on remount x3
// (No parent onValidation call, because it will use onChange)
// 1) and 2)
return React.createElement(_TestField.default, {
link: links[0]
});
}));
var testInstance = renderer.root.findAllByType(_TestField.TestInput)[0].instance; // now trigger a customChange, which used to cause a remount
expect(link.onValidation).toHaveBeenCalledTimes(4);
link.onValidation.mockClear();
var inner = renderer.root.findAllByType(_TestField.TestInput)[0];
inner.instance.change("zach"); // 3)
testInstance.change("hi");
expect(customChange).toHaveBeenCalledTimes(1); // but we no longer cause a remount, so the instances should be the same
expect(link.onValidation).toHaveBeenCalledTimes(3);
expect(link.onValidation).toHaveBeenCalledWith([{
type: "array",
index: 0
}], ["This is an error"]);
expect(link.onValidation).toHaveBeenCalledWith([{
type: "array",
index: 1
}], ["This is an error"]); // NOTE(zach): This may be surprising since there are only two values in
// the new value, but there is no guarantee that the next commit will
// have occurred yet.
var nextTestInstance = renderer.root.findAllByType(_TestField.TestInput)[0].instance; // Using Object.is here because toBe hangs as the objects are
// self-referential and thus not printable
expect(link.onValidation).toHaveBeenCalledWith([{
type: "array",
index: 2
}], ["This is an error"]); // onChange should be called with the result of customChange
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([["1", "2"], {
type: "array",
data: {
errors: {
client: [],
server: "unchecked"
},
meta: {
touched: true,
changed: true,
succeeded: true,
asyncValidationInFlight: false
}
},
children: [{
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked"
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false
}
}
}, {
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked"
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false
}
}
}]
}]);
expect(Object.is(testInstance, nextTestInstance)).toBe(true);
});
});
});

@@ -13,2 +13,4 @@ "use strict";

var _TestForm = _interopRequireDefault(require("./TestForm"));
var _shapedTree = require("../shapedTree");

@@ -24,120 +26,99 @@

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
describe("Field", function () {
describe("validates on mount", function () {
it("ensures that the link inner type matches the type of the validation", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState); // $ExpectError
it("ensures that the link inner type matches the type of the validation", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState); // $ExpectError
React.createElement(_Field.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
React.createElement(_Field.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
React.createElement(_Field.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
it("Sets errors.client and meta.succeeded when there are no errors", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState);
var validation = jest.fn(function () {
React.createElement(_Field.default, {
link: link,
validation: function validation(_e) {
return [];
});
_reactTestRenderer.default.create(React.createElement(_TestField.default, {
link: link,
validation: validation
}));
expect(validation).toHaveBeenCalledTimes(1);
expect(link.onValidation).toHaveBeenCalledTimes(1);
var _link$onValidation$mo = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo[0],
errors = _link$onValidation$mo[1];
expect(path).toEqual([]);
expect(errors).toEqual([]);
}
}, function () {
return null;
});
it("Sets errors.client and meta.succeeded when there are errors", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState);
var validation = jest.fn(function () {
return ["This is an error"];
});
});
it("registers and unregisters for validation", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState);
var unregister = jest.fn();
var registerValidation = jest.fn(function () {
return {
replace: jest.fn(),
unregister: unregister
};
});
_reactTestRenderer.default.create(React.createElement(_TestField.default, {
link: link,
validation: validation
}));
var renderer = _reactTestRenderer.default.create(React.createElement(_TestForm.default, {
registerValidation: registerValidation
}, React.createElement(_Field.default, {
link: link,
validation: jest.fn(function () {
return [];
})
}, jest.fn(function () {
return null;
}))));
expect(validation).toHaveBeenCalledTimes(1);
expect(link.onValidation).toHaveBeenCalledTimes(1);
var _link$onValidation$mo2 = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo2[0],
errors = _link$onValidation$mo2[1];
expect(path).toEqual([]);
expect(errors).toEqual(["This is an error"]);
expect(registerValidation).toBeCalledTimes(1);
renderer.unmount();
expect(unregister).toBeCalledTimes(1);
});
it("calls replace when changing the validation function", function () {
var replace = jest.fn();
var registerValidation = jest.fn(function () {
return {
replace: replace,
unregister: jest.fn()
};
});
it("Counts as successfully validated if there is no validation", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState);
_reactTestRenderer.default.create(React.createElement(_TestField.default, {
link: link
function Component() {
return React.createElement(_TestForm.default, {
registerValidation: registerValidation
}, React.createElement(_Field.default, {
link: (0, _tools.mockLink)((0, _tools.mockFormState)("Hello world.")),
validation: function validation() {
return [];
}
}, function () {
return null;
}));
}
expect(link.onValidation).toHaveBeenCalledTimes(1);
var renderer = _reactTestRenderer.default.create(React.createElement(Component, null));
var _link$onValidation$mo3 = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo3[0],
errors = _link$onValidation$mo3[1];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
expect(registerValidation).toBeCalledTimes(1);
renderer.update(React.createElement(Component, null));
expect(replace).toBeCalledTimes(1);
});
it("calls the link onChange with new values and correct meta", function () {
it("validates new values and passes result to onChange", function () {
var formState = (0, _tools.mockFormState)("Hello world.");
var link = (0, _tools.mockLink)(formState);
var updateNodeAtPath = jest.fn(function (path, formState) {
return formState;
});
var renderer = _reactTestRenderer.default.create(React.createElement(_TestField.default, {
var renderer = _reactTestRenderer.default.create(React.createElement(_TestForm.default, {
updateNodeAtPath: updateNodeAtPath
}, React.createElement(_TestField.default, {
link: link
}));
})));
var inner = renderer.root.findByType(_TestField.TestInput);
expect(updateNodeAtPath).toHaveBeenCalledTimes(0);
expect(link.onChange).toHaveBeenCalledTimes(0);
inner.instance.change("You've got mail");
expect(updateNodeAtPath).toHaveBeenCalledTimes(1);
expect(updateNodeAtPath).toHaveBeenCalledWith([], ["You've got mail", expect.anything()]);
expect(link.onChange).toHaveBeenCalledTimes(1);
var _link$onChange$mock$c = _slicedToArray(link.onChange.mock.calls[0][0], 2),
value = _link$onChange$mock$c[0],
tree = _link$onChange$mock$c[1];
expect(value).toBe("You've got mail");
expect(tree.data).toMatchObject({
meta: {
touched: true,
changed: true,
succeeded: true
}
});
expect(link.onChange).toHaveBeenCalledWith(["You've got mail", formState[1]]);
});

@@ -171,3 +152,3 @@ it("calls the link onBlur with correct meta", function () {

client: ["Some", "client", "errors"],
server: ["Server errors", "go here"]
external: ["External errors", "go here"]
}

@@ -183,3 +164,3 @@ });

var inner = renderer.root.findByType(_TestField.TestInput);
expect(inner.props.errors).toEqual(["Some", "client", "errors", "Server errors", "go here"]);
expect(inner.props.errors).toEqual(["Some", "client", "errors", "External errors", "go here"]);
});

@@ -224,3 +205,3 @@ it("Passes value of the right type to its render function", function () {

formState[1].data.errors = {
server: ["A server error"],
external: ["An external error"],
client: ["A client error"]

@@ -242,3 +223,3 @@ };

shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining(["A server error", "A client error"]),
unfilteredErrors: expect.arrayContaining(["An external error", "A client error"]),
valid: false,

@@ -245,0 +226,0 @@ asyncValidationInFlight: false,

@@ -21,2 +21,4 @@ "use strict";

var _LinkTap = _interopRequireDefault(require("../testutils/LinkTap"));
var _shapedTree = require("../shapedTree");

@@ -28,2 +30,10 @@

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }

@@ -93,4 +103,268 @@

describe("Form", function () {
describe("validations", function () {
it("runs validations", function () {
var objectValidation = jest.fn(function () {
return [];
});
var arrayValidation = jest.fn(function () {
return [];
});
var arrayElValidation = jest.fn(function () {
return [];
});
var fieldValidation = jest.fn(function () {
return [];
});
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
a: ["1", "2"],
s: "string"
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link,
validation: objectValidation
}, function (links) {
return React.createElement(React.Fragment, null, React.createElement(_ArrayField.default, {
link: links.a,
validation: arrayValidation
}, function (links) {
return links.map(function (link, i) {
return React.createElement(_TestField.default, {
key: i,
link: link,
validation: arrayElValidation
});
});
}), React.createElement(_TestField.default, {
link: links.s,
validation: fieldValidation
}));
});
}));
expect(objectValidation).toHaveBeenCalledTimes(1);
expect(objectValidation).toHaveBeenCalledWith({
a: ["1", "2"],
s: "string"
});
expect(arrayValidation).toHaveBeenCalledTimes(1);
expect(arrayValidation).toHaveBeenCalledWith(["1", "2"]);
expect(arrayElValidation).toHaveBeenCalledTimes(2);
expect(arrayElValidation).toHaveBeenCalledWith("1");
expect(arrayElValidation).toHaveBeenCalledWith("2");
expect(fieldValidation).toHaveBeenCalledTimes(1);
expect(fieldValidation).toHaveBeenCalledWith("string");
});
it("sets validation information on formState", function () {
var objectValidation = jest.fn(function () {
return ["object error"];
});
var arrayValidation = jest.fn(function () {
return ["array", "error"];
});
var arrayElValidation = jest.fn(function (s) {
return ["error ".concat(s)];
});
var fieldValidation = jest.fn(function () {
return [];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
a: ["1", "2"],
s: "string"
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link,
validation: objectValidation
}, function (links) {
return React.createElement(React.Fragment, null, React.createElement(_ArrayField.default, {
link: links.a,
validation: arrayValidation
}, function (links) {
return links.map(function (link, i) {
return React.createElement(_TestField.default, {
key: i,
link: link,
validation: arrayElValidation
});
});
}), React.createElement(_TestField.default, {
link: links.s,
validation: fieldValidation
}));
});
}));
var formState = renderer.root.findByType(_ObjectField.default).instance.props.link.formState;
var node = formState[1];
expect(node.data.errors.client).toEqual(["object error"]);
expect(node.data.meta.succeeded).toBe(false);
node = node.children.a;
expect(node.data.errors.client).toEqual(["array", "error"]);
expect(node.data.meta.succeeded).toBe(false);
var child0 = node.children[0];
expect(child0.data.errors.client).toEqual(["error 1"]);
expect(child0.data.meta.succeeded).toBe(false);
var child1 = node.children[1];
expect(child1.data.errors.client).toEqual(["error 2"]);
expect(child1.data.meta.succeeded).toBe(false);
node = formState[1].children.s;
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
});
it("treats no validation as always passing", function () {
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
a: ["1", "2"],
s: "string"
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link
}, function (links) {
return React.createElement(React.Fragment, null, React.createElement(_ArrayField.default, {
link: links.a
}, function (links) {
return links.map(function (link, i) {
return React.createElement(_TestField.default, {
key: i,
link: link
});
});
}), React.createElement(_TestField.default, {
link: links.s
}));
});
}));
var formState = renderer.root.findByType(_ObjectField.default).instance.props.link.formState;
var node = formState[1];
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
node = node.children.a;
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
var child0 = node.children[0];
expect(child0.data.errors.client).toEqual([]);
expect(child0.data.meta.succeeded).toBe(true);
var child1 = node.children[1];
expect(child1.data.errors.client).toEqual([]);
expect(child1.data.meta.succeeded).toBe(true);
node = formState[1].children.s;
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
});
it("validates newly mounted Fields", function () {
function expectClientErrors(link) {
var tree = link.formState[1];
return expect((0, _shapedTree.forgetShape)(tree).data.errors.client);
}
var renderFn = jest.fn(function () {
return null;
});
var validationA = jest.fn(function () {
return ["error a"];
});
var validationB = jest.fn(function () {
return ["error b"];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
key: "hello"
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(_LinkTap.default, {
link: link.key
}, renderFn), React.createElement(_TestField.default, {
link: link.key,
validation: validationA
}));
});
}));
expect(renderFn).toHaveBeenCalledTimes(2); // on the initial render, we expose "pending" as part of our API
// TODO(dmnd): It'd be nice if we could avoid this.
expectClientErrors(renderFn.mock.calls[0][0]).toEqual("pending"); // After the second render the error arrives.
expectClientErrors(renderFn.mock.calls[1][0]).toEqual(["error a"]);
expect(validationA).toHaveBeenCalledTimes(1);
expect(validationA).toHaveBeenCalledWith("hello"); // When a new Field is mounted, we expect the errors to show up.
renderFn.mockClear();
validationA.mockClear();
renderer.update(React.createElement(_Form.default, {
initialValue: {
key: "hello"
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(_LinkTap.default, {
link: link.key
}, renderFn), React.createElement(_TestField.default, {
link: link.key,
validation: validationA
}), React.createElement(_TestField.default, {
link: link.key,
validation: validationB
}));
});
}));
expect(renderFn).toHaveBeenCalledTimes(2); // There's an initial render where the field hasn't yet been validated
// TODO(dmnd): It'd be nice if we could avoid this.
expectClientErrors(renderFn.mock.calls[0][0]).toEqual(["error a"]); // After the update, the new error should be present.
expectClientErrors(renderFn.mock.calls[1][0]).toEqual(["error a", "error b"]); // Validation functions should receive the correct parameters. These
// assertions protect against bugs that confuse relative and absolute
// paths/values.
expect(validationA).toHaveBeenCalledTimes(1);
expect(validationA).toHaveBeenCalledWith("hello");
expect(validationB).toHaveBeenCalledTimes(1);
expect(validationB).toHaveBeenCalledWith("hello");
});
it("updates errors when a new validation function is provided via props", function () {
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: "hello"
}, function (link) {
return React.createElement(_TestField.default, {
link: link,
validation: function validation() {
return ["error 1"];
}
});
}));
var link = renderer.root.findAllByType(_TestField.default)[0].instance.props.link;
var errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 1"]);
renderer.update(React.createElement(_Form.default, {
initialValue: "hello"
}, function (link) {
return React.createElement(_TestField.default, {
link: link,
validation: function validation() {
return ["error 2"];
}
});
}));
link = renderer.root.findAllByType(_TestField.default)[0].instance.props.link;
errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 2"]);
});
});
describe("Form manages form state", function () {
it("creates the initial formState from initialValue and serverErrors", function () {
it("creates the initial formState from initialValue and externalErrors", function () {
var onSubmit = jest.fn();

@@ -104,4 +378,4 @@ var renderFn = jest.fn(function () {

onSubmit: onSubmit,
serverErrors: {
"/": ["Server error", "Another server error"]
externalErrors: {
"/": ["External error", "Another external error"]
}

@@ -129,7 +403,7 @@ }, renderFn));

client: "pending",
server: ["Server error", "Another server error"]
external: ["External error", "Another external error"]
}
});
});
it("parses and sets complex server errors", function () {
it("parses and sets complex external errors", function () {
var onSubmit = jest.fn();

@@ -150,3 +424,3 @@ var renderFn = jest.fn(function () {

onSubmit: onSubmit,
serverErrors: {
externalErrors: {
"/": ["Root error"],

@@ -169,13 +443,13 @@ "/simple": ["One", "level", "down"],

var root = tree;
expect(root.data.errors.server).toEqual(["Root error"]);
expect(root.data.errors.external).toEqual(["Root error"]);
var simple = root.children.simple;
expect(simple.data.errors.server).toEqual(["One", "level", "down"]);
expect(simple.data.errors.external).toEqual(["One", "level", "down"]);
var complex = root.children.complex;
expect(complex.data.errors.server).toEqual([]);
expect(complex.data.errors.external).toEqual([]);
var complex0 = complex.children[0];
expect(complex0.data.errors.server).toEqual(["in an", "array"]);
expect(complex0.data.errors.external).toEqual(["in an", "array"]);
var complex1 = complex.children[1];
expect(complex1.data.errors.server).toEqual([]);
expect(complex1.data.errors.external).toEqual([]);
});
it("updates the server errors", function () {
it("updates the external errors", function () {
var onSubmit = jest.fn();

@@ -191,3 +465,3 @@ var renderFn = jest.fn(function () {

onSubmit: onSubmit,
serverErrors: {
externalErrors: {
"/array": ["Cannot be empty"]

@@ -211,3 +485,3 @@ }

onSubmit: onSubmit,
serverErrors: {
externalErrors: {
"/array": [],

@@ -226,8 +500,22 @@ "/array/0": ["inner error"]

var root = tree;
expect(root.data.errors.server).toEqual([]);
expect(root.data.errors.external).toEqual([]);
var array = root.children.array;
expect(array.data.errors.server).toEqual([]);
expect(array.data.errors.external).toEqual([]);
var array0 = array.children[0];
expect(array0.data.errors.server).toEqual(["inner error"]);
expect(array0.data.errors.external).toEqual(["inner error"]);
});
it("doesn't cause an infinite loop when using inline validation function", function () {
expect(function () {
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: "hello"
}, function (link) {
return React.createElement(_TestField.default, {
link: link,
validation: function validation() {
return [];
}
});
}));
}).not.toThrow(/Maximum update depth exceeded/);
});
it("collects the initial validations", function () {

@@ -456,4 +744,4 @@ // This test is not very unit-y, but that's okay! It's more useful to

onSubmit: jest.fn(),
serverErrors: {
"/": ["Server error", "Another server error"]
externalErrors: {
"/": ["External error", "Another external error"]
}

@@ -466,3 +754,3 @@ }, renderFn));

shouldShowErrors: false,
unfilteredErrors: expect.arrayContaining(["Server error", "Another server error"]),
unfilteredErrors: expect.arrayContaining(["External error", "Another external error"]),
// Currently, only care about client errors

@@ -474,2 +762,157 @@ valid: true,

});
it("removes errors when a child unmounts", function () {
var validation1 = jest.fn(function () {
return ["error 1"];
});
var validation2 = jest.fn(function () {
return ["error 2"];
});
var TestForm =
/*#__PURE__*/
function (_React$Component2) {
_inherits(TestForm, _React$Component2);
function TestForm() {
_classCallCheck(this, TestForm);
return _possibleConstructorReturn(this, _getPrototypeOf(TestForm).apply(this, arguments));
}
_createClass(TestForm, [{
key: "render",
value: function render() {
var _this = this;
return React.createElement(_ObjectField.default, {
link: this.props.link
}, function (links) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
key: "1",
link: links.string1,
validation: validation1
}), _this.props.hideSecondField ? null : React.createElement(_TestField.default, {
key: "2",
link: links.string2,
validation: validation2
}));
});
}
}]);
return TestForm;
}(React.Component);
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
string1: "hello",
string2: "world"
}
}, function (link) {
return React.createElement(TestForm, {
link: link,
hideSecondField: false
});
}));
expect(validation1).toHaveBeenCalledTimes(1);
expect(validation2).toHaveBeenCalledTimes(1);
var rootFormState = renderer.root.findByType(TestForm).instance.props.link.formState[1];
var string1Errors = rootFormState.children.string1.data.errors.client;
expect(string1Errors).toEqual(["error 1"]);
var string2Errors = rootFormState.children.string2.data.errors.client;
expect(string2Errors).toEqual(["error 2"]); // now hide the second field, causing it to unmount and unregister the
// validation handler
renderer.update(React.createElement(_Form.default, {
initialValue: {
string1: "hello",
string2: "world"
}
}, function (link) {
return React.createElement(TestForm, {
link: link,
hideSecondField: true
});
})); // no addition validation calls
expect(validation1).toHaveBeenCalledTimes(1);
expect(validation2).toHaveBeenCalledTimes(1);
rootFormState = renderer.root.findByType(TestForm).instance.props.link.formState[1]; // error for string1 remains
string1Errors = rootFormState.children.string1.data.errors.client;
expect(string1Errors).toEqual(["error 1"]); // string2's error is gone
string2Errors = rootFormState.children.string2.data.errors.client;
expect(string2Errors).toEqual([]);
});
it("runs all validations when a link has multiple fields", function () {
var validation1 = jest.fn(function () {
return ["error 1"];
});
var validation2 = jest.fn(function () {
return ["error 2"];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: "hello"
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
key: "1",
link: link,
validation: validation1
}), React.createElement(_TestField.default, {
key: "2",
link: link,
validation: validation2
}));
}));
expect(validation1).toHaveBeenCalledTimes(1);
expect(validation2).toHaveBeenCalledTimes(1);
renderer.root.findAllByType(_TestField.TestInput)[0].instance.change("dmnd");
expect(validation1).toHaveBeenCalledTimes(2);
expect(validation2).toHaveBeenCalledTimes(2);
renderer.root.findAllByType(_TestField.TestInput)[1].instance.change("zach");
expect(validation1).toHaveBeenCalledTimes(3);
expect(validation2).toHaveBeenCalledTimes(3);
});
it("only removes errors from validation that was unmounted", function () {
var validation1 = jest.fn(function () {
return ["error 1"];
});
var validation2 = jest.fn(function () {
return ["error 2"];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: "hello"
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
key: "1",
link: link,
validation: validation1
}), React.createElement(_TestField.default, {
key: "2",
link: link,
validation: validation2
}));
}));
var link = renderer.root.findAllByType(_TestField.default)[0].instance.props.link;
var errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 1", "error 2"]);
renderer.update(React.createElement(_Form.default, {
initialValue: "hello"
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
key: "1",
link: link,
validation: validation1
}));
}));
link = renderer.root.findAllByType(_TestField.default)[0].instance.props.link;
errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 1"]);
});
});

@@ -571,2 +1014,84 @@ it("Calls onSubmit with the value when submitted", function () {

});
describe("performance", function () {
it("batches setState calls when unmounting components", function () {
// Record the number of render and commit phases
var renders = 0;
var commits = 0;
var RenderCalls =
/*#__PURE__*/
function (_React$Component3) {
_inherits(RenderCalls, _React$Component3);
function RenderCalls() {
_classCallCheck(this, RenderCalls);
return _possibleConstructorReturn(this, _getPrototypeOf(RenderCalls).apply(this, arguments));
}
_createClass(RenderCalls, [{
key: "componentDidUpdate",
value: function componentDidUpdate() {
commits += 1;
}
}, {
key: "render",
value: function render() {
renders += 1;
return React.createElement("div", null, "Rendered ", renders, " times, committed ", commits, " times.");
}
}]);
return RenderCalls;
}(React.Component);
var validation = jest.fn(); // N in the O(N) sense for this perf test.
var N = 10;
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: "A string"
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(RenderCalls, null), React.createElement(React.Fragment, null, _toConsumableArray(Array(N).keys()).map(function (i) {
return React.createElement(_Field.default, {
key: i,
link: link,
validation: validation
}, function () {
return React.createElement("div", null, "input");
});
})));
})); // One render for initial mount, and another after Form validates
// everything from componentDidMount.
// TODO(dmnd): Can we adjust implementation to make this only render once?
expect(renders).toBe(1 + 1);
expect(commits).toBe(1);
expect(validation).toBeCalledTimes(N);
renders = 0;
commits = 0;
validation.mockClear(); // now unmount all the fields
renderer.update(React.createElement(_Form.default, {
initialValue: "A string"
}, function () {
return React.createElement(React.Fragment, null, React.createElement(RenderCalls, null));
})); // We expect only two renders. The first to build the VDOM without Fields.
// Then during reconciliation React realizes the Fields have to unmount,
// so it calls componentWillUnmount on each Field, which then causes a
// setState for each Field. But then we expect that React batches all
// these setStates into a single render, not one render per each Field.
expect(renders).toBe(1 + 1);
expect(renders).not.toBe(1 + N); // Similarly, we expect only 2 commits, not one for each Field. You might
// expect only a single commit, but componentWillUnmount happens during
// the commit phase, so when setState is called React enqueues another
// render phase which commits separately. Oh well. At least the number of
// commits is constant!
expect(commits).toBe(1 + 1);
expect(commits).not.toBe(1 + N);
});
});
});

@@ -7,3 +7,3 @@ "use strict";

var _Form = require("../Form");
var _Form = _interopRequireWildcard(require("../Form"));

@@ -16,2 +16,6 @@ var _ObjectField = _interopRequireDefault(require("../ObjectField"));

var _TestForm = _interopRequireDefault(require("./TestForm"));
var _LinkTap = _interopRequireDefault(require("../testutils/LinkTap"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

@@ -21,10 +25,2 @@

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
describe("ObjectField", function () {

@@ -36,144 +32,125 @@ describe("Sneaky hacks", function () {

});
describe("ObjectField is a field", function () {
describe("validates on mount", function () {
it("ensures that the link inner type matches the type of the validation", function () {
var formStateInner = {
string: "hello",
number: 42
};
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState); // $ExpectError
describe("is a field", function () {
it("ensures that the link inner type matches the type of the validation", function () {
var formStateInner = {
string: "hello",
number: 42
};
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState); // $ExpectError
React.createElement(_ObjectField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
}); // $ExpectError
React.createElement(_ObjectField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
}); // $ExpectError
React.createElement(_ObjectField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
React.createElement(_ObjectField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
React.createElement(_ObjectField.default, {
link: link,
validation: function validation(_e) {
return [];
}
}, function () {
return null;
});
it("Sets errors.client and meta.succeeded when there are no errors", function () {
var validation = jest.fn(function () {
React.createElement(_ObjectField.default, {
link: link,
validation: function validation(_e) {
return [];
});
var formState = (0, _tools.mockFormState)({
inner: "value"
});
var link = (0, _tools.mockLink)(formState);
}
}, function () {
return null;
});
});
it("Registers and unregisters for validation", function () {
var formState = (0, _tools.mockFormState)({
inner: "value"
});
var link = (0, _tools.mockLink)(formState);
var unregister = jest.fn();
var registerValidation = jest.fn(function () {
return {
replace: jest.fn(),
unregister: unregister
};
});
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link,
validation: validation
}, jest.fn(function () {
return null;
})));
var renderer = _reactTestRenderer.default.create(React.createElement(_TestForm.default, {
registerValidation: registerValidation
}, React.createElement(_ObjectField.default, {
link: link,
validation: jest.fn(function () {
return [];
})
}, jest.fn(function () {
return null;
}))));
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
var _link$onValidation$mo = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo[0],
errors = _link$onValidation$mo[1];
expect(path).toEqual([]);
expect(errors).toEqual([]);
expect(registerValidation).toBeCalledTimes(1);
renderer.unmount();
expect(unregister).toBeCalledTimes(1);
});
it("calls replace when changing the validation function", function () {
var replace = jest.fn();
var registerValidation = jest.fn(function () {
return {
replace: replace,
unregister: jest.fn()
};
});
it("Sets errors.client and meta.succeeded when there are errors", function () {
var validation = jest.fn(function () {
return ["This is an error"];
});
var formState = (0, _tools.mockFormState)({
inner: "value"
});
var link = (0, _tools.mockLink)(formState);
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link,
validation: validation
}, jest.fn(function () {
function Component() {
return React.createElement(_TestForm.default, {
registerValidation: registerValidation
}, React.createElement(_ObjectField.default, {
link: (0, _tools.mockLink)((0, _tools.mockFormState)({
hello: "world"
})),
validation: function validation() {
return [];
}
}, function () {
return null;
})));
}));
}
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
var renderer = _reactTestRenderer.default.create(React.createElement(Component, null));
var _link$onValidation$mo2 = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo2[0],
errors = _link$onValidation$mo2[1];
expect(registerValidation).toBeCalledTimes(1);
renderer.update(React.createElement(Component, null));
expect(replace).toBeCalledTimes(1);
});
it("Passes additional information to its render function", function () {
var formState = (0, _tools.mockFormState)({
inner: "value"
}); // $FlowFixMe
expect(path).toEqual([]);
expect(errors).toEqual(["This is an error"]);
formState[1].data.errors = {
external: ["An external error"],
client: ["A client error"]
};
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
});
it("Treats no validation as always passing", function () {
var formState = (0, _tools.mockFormState)({
inner: "value"
});
var link = (0, _tools.mockLink)(formState);
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link
}, jest.fn(function () {
return null;
})));
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link
}, renderFn));
expect(link.onValidation).toHaveBeenCalledTimes(1);
var _link$onValidation$mo3 = _slicedToArray(link.onValidation.mock.calls[0], 2),
path = _link$onValidation$mo3[0],
errors = _link$onValidation$mo3[1];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
it("Passes additional information to its render function", function () {
var formState = (0, _tools.mockFormState)({
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining(["An external error", "A client error"]),
valid: false,
asyncValidationInFlight: false,
value: {
inner: "value"
}); // $FlowFixMe
formState[1].data.errors = {
server: ["A server error"],
client: ["A client error"]
};
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
});
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link
}, renderFn));
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining(["A server error", "A client error"]),
valid: false,
asyncValidationInFlight: false,
value: {
inner: "value"
}
}));
});
}
}));
});

@@ -232,8 +209,7 @@ });

});
it("calls onChange when a child changes", function () {
var formStateInner = {
it("validates new values from children and passes result to onChange", function () {
var formState = (0, _tools.mockFormState)({
string: "hello",
number: 42
};
var formState = (0, _tools.mockFormState)(formStateInner);
});
var link = (0, _tools.mockLink)(formState);

@@ -243,19 +219,28 @@ var renderFn = jest.fn(function () {

});
var updateNodeAtPath = jest.fn(function (path, formState) {
return formState;
});
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
_reactTestRenderer.default.create(React.createElement(_TestForm.default, {
updateNodeAtPath: updateNodeAtPath
}, React.createElement(_ObjectField.default, {
link: link
}, renderFn));
}, renderFn)));
var objectLinks = renderFn.mock.calls[0][0]; // call the child onChange
expect(updateNodeAtPath).toHaveBeenCalledTimes(0);
expect(link.onChange).toHaveBeenCalledTimes(0); // call the child onChange
var objectLinks = renderFn.mock.calls[0][0];
var newChildMeta = (0, _tools.mockFormState)("newString");
objectLinks.string.onChange(newChildMeta);
expect(link.onChange).toHaveBeenCalled();
var newObjectFormState = link.onChange.mock.calls[0][0];
expect(newObjectFormState[0]).toHaveProperty("string", "newString");
expect(newObjectFormState[1].data.meta).toMatchObject({
touched: true,
changed: true
});
expect(newObjectFormState[1].children.string).toBe(newChildMeta[1]);
expect(updateNodeAtPath).toHaveBeenCalledTimes(1);
expect(updateNodeAtPath).toHaveBeenCalledWith([], [{
string: "newString",
number: 42
}, expect.anything()]);
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([{
string: "newString",
number: 42
}, formState[1]]);
});

@@ -289,39 +274,3 @@ it("calls onBlur when a child is blurred", function () {

});
it("calls onValidation when a child runs validations", function () {
var formStateInner = {
string: "hello",
number: 42
};
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {
return null;
});
_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link
}, renderFn));
var objectLinks = renderFn.mock.calls[0][0]; // call the child onValidation
objectLinks.string.onValidation([], ["Some", "errors"]);
expect(link.onValidation).toHaveBeenCalledTimes(2); // Important: the first call to onValidation is for the initial render validation
var _link$onValidation$mo4 = _slicedToArray(link.onValidation.mock.calls[1], 2),
path = _link$onValidation$mo4[0],
errors = _link$onValidation$mo4[1];
expect(path).toEqual([{
type: "object",
key: "string"
}]);
expect(errors).toEqual(["Some", "errors"]);
});
it("calls its own validation when a child changes", function () {
var formStateInner = {
string: "hello",
number: 42
};
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -334,6 +283,13 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link,
validation: validation
}, renderFn));
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
string: "hello",
number: 42
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link,
validation: validation
}, renderFn);
}));

@@ -353,3 +309,3 @@ expect(validation).toHaveBeenCalledTimes(1);

describe("customChange", function () {
it("allows the default change behavior to be overwritten with customChange", function () {
it("allows sibling fields to be overwritten", function () {
var formStateInner = {

@@ -364,5 +320,2 @@ string: "hello",

});
var validation = jest.fn(function () {
return ["This is an error"];
});
var customChange = jest.fn(function (_oldValue, _newValue) {

@@ -375,7 +328,9 @@ return {

_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
_reactTestRenderer.default.create(React.createElement(_TestForm.default, null, React.createElement(_ObjectField.default, {
link: link,
validation: validation,
validation: jest.fn(function () {
return ["This is an error"];
}),
customChange: customChange
}, renderFn));
}, renderFn)));

@@ -400,17 +355,5 @@ var objectLinks = renderFn.mock.calls[0][0]; // call the child onChange

number: 0
}, expect.anything()]); // Validated the result of customChange
expect(validation).toHaveBeenCalledTimes(2);
expect(validation.mock.calls[1][0]).toEqual({
string: "A whole new value",
number: 0
});
}, expect.anything()]);
});
it("can return null to signal there was no custom change", function () {
var formStateInner = {
string: "hello",
number: 42
};
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState);
var renderFn = jest.fn(function () {

@@ -426,7 +369,14 @@ return null;

_reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link,
validation: validation,
customChange: customChange
}, renderFn));
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
string: "hello",
number: 42
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link,
validation: validation,
customChange: customChange
}, renderFn);
}));

@@ -437,6 +387,6 @@ var objectLinks = renderFn.mock.calls[0][0]; // call the child onChange

objectLinks.string.onChange(newChildMeta);
expect(customChange).toHaveBeenCalledTimes(1); // onChange should be called with the result of customChange
expect(customChange).toHaveBeenCalledTimes(1);
var link = renderer.root.findByType(_ObjectField.default).instance.props.link; // the value we get out is as if customChange didn't exist
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([{
expect(link.formState).toEqual([{
string: "newString",

@@ -453,8 +403,2 @@ number: 42

it("doesn't break validations for child fields", function () {
var formStateInner = {
string: "hello",
string2: "goodbye"
};
var formState = (0, _tools.mockFormState)(formStateInner);
var link = (0, _tools.mockLink)(formState);
var customChange = jest.fn(function (_oldValue, _newValue) {

@@ -469,92 +413,129 @@ return {

});
var parentValidation = jest.fn(function () {
return ["This is an error from the parent"];
});
var renderer = _reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
string: "hello",
string2: "goodbye"
}
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link,
customChange: customChange,
validation: parentValidation
}, function (links) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
link: links.string,
validation: childValidation
}), React.createElement(_TestField.default, {
link: links.string2,
validation: childValidation
}));
});
})); // after mount, validate everything
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(2); // Now change one of the values
parentValidation.mockClear();
childValidation.mockClear();
var inner = renderer.root.findAllByType(_TestField.TestInput)[0];
inner.instance.change("zach"); // Validate the whole subtree due to the customChange child validates
// once. Note that child validation will be called 3 times. Once after the
// change, then twice more after the customChange triggers a validation fo
// the entire subtree.
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(1 + 2);
var link = renderer.root.findByType(_ObjectField.default).instance.props.link;
expect(link.formState).toEqual([{
string: "a whole new value",
string2: "modified sibling value"
}, expect.anything()]);
});
it("doesn't create a new instance (i.e. remount)", function () {
var customChange = jest.fn(function (_oldValue, _newValue) {
return {
string: "A whole new value",
number: 0
};
});
var renderer = _reactTestRenderer.default.create(React.createElement(_ObjectField.default, {
link: link,
link: (0, _tools.mockLink)((0, _tools.mockFormState)({
string: "hello",
number: 42
})),
customChange: customChange
}, function (links) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
link: links.string,
validation: childValidation
}), React.createElement(_TestField.default, {
link: links.string2,
validation: childValidation
}));
})); // 5 validations:
// 1) Child initial validation x2
// 2) Parent initial validation
// 3) Child validation on remount x2
// (No parent onValidation call, because it will use onChange)
// 1) and 2)
return React.createElement(_TestField.default, {
link: links.string
});
}));
var testInstance = renderer.root.findAllByType(_TestField.TestInput)[0].instance; // now trigger a customChange, which used to cause a remount
expect(link.onValidation).toHaveBeenCalledTimes(3);
link.onValidation.mockClear();
var inner = renderer.root.findAllByType(_TestField.TestInput)[0];
inner.instance.change("zach"); // 3)
testInstance.change("hi");
expect(customChange).toHaveBeenCalledTimes(1); // but we no longer cause a remount, so the instances should be the same
expect(link.onValidation).toHaveBeenCalledTimes(2);
expect(link.onValidation).toHaveBeenCalledWith([{
type: "object",
key: "string"
}], ["This is an error"]);
expect(link.onValidation).toHaveBeenCalledWith([{
type: "object",
key: "string2"
}], ["This is an error"]); // onChange should be called with the result of customChange
var nextTestInstance = renderer.root.findAllByType(_TestField.TestInput)[0].instance; // Using Object.is here because toBe hangs as the objects are
// self-referential and thus not printable
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([{
string: "a whole new value",
string2: "modified sibling value"
}, {
type: "object",
data: {
errors: {
client: [],
server: "unchecked"
},
meta: {
touched: true,
changed: true,
succeeded: true,
asyncValidationInFlight: false
expect(Object.is(testInstance, nextTestInstance)).toBe(true);
});
it("works fine even if not at the root of the form", function () {
var customChange = jest.fn(function (_oldValue, _newValue) {
return {
string: "A whole new value",
number: 0
};
});
var objectRenderFn = jest.fn(function () {
return null;
});
var linkTapFn = jest.fn(function () {
return null;
});
_reactTestRenderer.default.create(React.createElement(_Form.default, {
initialValue: {
uncle: "Bob",
nested: {
string: "hello",
number: 42
}
},
children: {
string: {
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked"
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false
}
}
},
string2: {
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked"
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false
}
}
}
}
}]);
}, function (link) {
return React.createElement(_ObjectField.default, {
link: link
}, function (link) {
return React.createElement(React.Fragment, null, React.createElement(_TestField.default, {
link: link.uncle
}), React.createElement(_LinkTap.default, {
link: link.nested
}, linkTapFn), React.createElement(_ObjectField.default, {
link: link.nested,
validation: jest.fn(function () {
return ["This is an error"];
}),
customChange: customChange
}, objectRenderFn));
});
}));
linkTapFn.mockClear(); // call the child onChange to trigger a customChange
var objectLinks = objectRenderFn.mock.calls[0][0];
objectLinks.string.onChange((0, _tools.mockFormState)("newString")); // onChange should be called with the result of customChange
expect(linkTapFn).toHaveBeenCalledTimes(1);
expect(linkTapFn.mock.calls[0][0].formState).toEqual([{
string: "A whole new value",
number: 0
}, expect.anything()]);
});
});
});

@@ -32,10 +32,10 @@ "use strict";

} : _ref$registerValidati,
_ref$applyValidationT = _ref.applyValidationToTreeAtPath,
applyValidationToTreeAtPath = _ref$applyValidationT === void 0 ? function (path, formState) {
_ref$updateTreeAtPath = _ref.updateTreeAtPath,
updateTreeAtPath = _ref$updateTreeAtPath === void 0 ? function (path, formState) {
return formState;
} : _ref$applyValidationT,
_ref$applyValidationA = _ref.applyValidationAtPath,
applyValidationAtPath = _ref$applyValidationA === void 0 ? function (path, formState) {
} : _ref$updateTreeAtPath,
_ref$updateNodeAtPath = _ref.updateNodeAtPath,
updateNodeAtPath = _ref$updateNodeAtPath === void 0 ? function (path, formState) {
return formState;
} : _ref$applyValidationA;
} : _ref$updateNodeAtPath;

@@ -48,6 +48,6 @@ return React.createElement(_Form.FormContext.Provider, {

registerValidation: registerValidation,
applyValidationToTreeAtPath: applyValidationToTreeAtPath,
applyValidationAtPath: applyValidationAtPath
updateTreeAtPath: updateTreeAtPath,
updateNodeAtPath: updateNodeAtPath
}
}, children);
}

@@ -23,6 +23,12 @@ "use strict";

expect(link).toEqual(expect.objectContaining({
// TODO(dmnd): Would be nice if we could do something like
// path: expect.arrayContaining(
// expect.objectContaining({
// type: expect.stringMatching(/(object)|(array)/),
// })
// ),
path: expect.anything(),
formState: expect.anything(),
onChange: expect.any(Function),
onBlur: expect.any(Function),
onValidation: expect.any(Function)
onBlur: expect.any(Function)
}));

@@ -34,7 +40,7 @@ expect(Object.keys(link).length).toBe(4);

return {
path: [],
formState: formState,
onChange: jest.fn(),
onBlur: jest.fn(),
onValidation: jest.fn()
onBlur: jest.fn()
};
}

@@ -66,10 +66,2 @@ "use strict";

_defineProperty(_assertThisInitialized(_this), "handleValidation", function (path, errors) {
if (_this.props.onValidation) {
_this.props.onValidation(path, errors);
}
_this.props.link.onValidation(path, errors);
});
return _this;

@@ -83,6 +75,6 @@ }

var tappedLink = {
path: link.path,
formState: link.formState,
onChange: this.handleChange,
onBlur: this.handleBlur,
onValidation: this.handleValidation
onBlur: this.handleBlur
};

@@ -89,0 +81,0 @@ return this.props.children(tappedLink);

@@ -16,4 +16,4 @@ "use strict";

client: "pending",
server: "unchecked"
external: "unchecked"
};
exports.cleanErrors = cleanErrors;

@@ -15,2 +15,3 @@ "use strict";

exports.unzip = unzip;
exports.equals = equals;

@@ -155,2 +156,16 @@ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }

return ret;
}
function equals(a, b) {
if (a.length !== b.length) {
return false;
}
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
{
"name": "formula-one",
"version": "0.9.0-alpha.9",
"version": "0.9.0-rc.1",
"description": "Strongly-typed React form state management",

@@ -5,0 +5,0 @@ "author": "Zach Gotsch",

@@ -18,3 +18,3 @@ # formula-one

age: string,
side: "Empire" | "Rebels",
faction: "Empire" | "Rebels",
};

@@ -25,3 +25,3 @@

age: "",
side: "Empire",
faction: "Empire",
};

@@ -35,5 +35,2 @@

onSubmit={person => console.log("Submitted", person)}
// TODO(dmnd): Remove following props after new version is published
serverErrors={null}
feedbackStrategy={FeedbackStrategies.Always}
>

@@ -68,6 +65,6 @@ {(link, onSubmit) => (

</Field>
<Field link={links.side}>
<Field link={links.faction}>
{(value, errors, onChange) => (
<label>
<div>Side</div>
<div>Faction</div>
<select

@@ -158,3 +155,3 @@ onChange={e => onChange(e.target.value)}

| ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| `FeedbackStrategies.Always` | Always show errors |
| `FeedbackStrategies.Always` | Always show errors (default) |
| `FeedbackStrategies.Touched` | Show errors for fields which have been touched (changed or blurred) |

@@ -192,4 +189,10 @@ | `FeedbackStrategies.Changed` | Show errors for fields which have been changed |

## Arrays in forms
### Complex inputs
Even inputs which are complex (e.g. a datepicker) can be wrapped in a `<Field>` wrapper, but validations are tracked at the field level, so you won't be able to use **formula-one** to track changes and validations below the field level. For example, you can't represent a validation error for _just the day part_ of a date if you only have a single `<Field>` wrapping a datepicker. Instead, the error will be associated with the entire date.
## Common use cases
### Arrays in forms
Often, you may want to edit a list of items in a form. **formula-one** exposes an aggregator called `<ArrayField>`, which allows you to manipulate a list of *Field*s.

@@ -220,5 +223,2 @@

onSubmit={p => console.log("Submitted", p)}
// TODO(dmnd): Remove following props after new version is published
serverErrors={null}
feedbackStrategy={FeedbackStrategies.Always}
>

@@ -288,14 +288,13 @@ {(link, onSubmit) => (

- `addField(index: number, value: T)`: Add a field at a position in the array
- `addField(index: number, value: E)`: Add a field at a position in the array
- `addFields(spans: $ReadOnlyArray<Span<E>>)`: Add multiple fields to an array
- `removeField(index: number)`: Remove a field at a position in array
- `filterFields(predicate: (item: E, index: number) => boolean)`: Remove multiple fields from an array
- `modifyFields({insertSpans: $ReadOnlyArray<Span<E>>, filterPredicate: (item: E, index: number) => boolean})`: Simultaneously add and remove fields from an array
- `moveField(fromIndex: number, toIndex: number)`: Move a field in an array (preserves metadata for the field)
## Complex inputs
where `Span<E>` is a range to be inserted at an index: `[number, $ReadOnlyArray<E>]`.
Even inputs which are complex can be wrapped in a `<Field>` wrapper, but validations are tracked at the field level, so you won't be able to use **formula-one** to track changes and validations below the field level.
<!-- ### Form state vs actual model -->
## Common use cases
### Form in a modal

@@ -317,5 +316,84 @@

### Custom changes
Sometimes a change in one field has to be reflected in another field. `<ObjectField>` and `<ArrayField>` have a prop `customChange` to allow this. It will be called when a child Field changes, and by returning a non-null result you can override the whole Currently, no metadata is preserved (all fields are marked **changed**, **touched**, and not **succeeded**) if a `customChange` function is used.
The API is:
```jsx
// Override nextValue by returning a non-null result
customChange: <T>(prevValue: T, nextValue: T) => null | T;
```
[Edit the working example on CodeSandbox](https://codesandbox.io/s/x7nw8w3p34?module=%2Fsrc%2FCustomChange.js)
```jsx
const SHIPS = {
"X-Wing": {faction: "Rebels", name: "X-Wing"},
"Y-Wing": {faction: "Rebels", name: "Y-Wing"},
"TIE Fighter": {faction: "Empire", name: "TIE Fighter"},
"TIE Defender": {faction: "Empire", name: "TIE Defender"},
};
type Person = {
name: string,
age: string,
faction: "Empire" | "Rebels",
ship: string,
};
function ensureFactionShipConsistency(
prevPerson: Person,
person: Person
): null | Person {
const ship = SHIPS[person.ship];
if (person.faction !== ship.faction) {
// person's ship is inconsistent with their faction: need an update
if (prevPerson.ship !== person.ship) {
// ship changed; update faction to match
return {...person, faction: ship.faction};
} else if (prevPerson.faction !== person.faction) {
// faction changed; give them a ship from their new faction
const newShip = Object.keys(SHIPS).find(
x => SHIPS[x].faction === person.faction
);
return {...person, ship: newShip};
} else {
throw new Error("unreachable");
}
} else {
return null;
}
}
export default function CustomChange() {
return (
<div className="App">
<Form
initialValue={EMPTY_PERSON}
onSubmit={person => console.log("Submitted", person)}
>
{(link, onSubmit) => (
<ObjectField link={link} customChange={ensureFactionShipConsistency}>
{links => (
<>
<Field link={links.faction}>{/*...*/}</Field>
<Field link={links.ship}>{/*...*/}</Field>
<div>
<button onClick={onSubmit}>Submit</button>
</div>
</>
)}
</ObjectField>
)}
</Form>
</div>
);
}
```
### External validation
Oftentimes, you will want to show errors from an external source (such as the server) in your form alongside any client-side validation errors. These can be passed into your `<Form>` component using the `serverErrors` (TODO(zach): change to `externalErrors`?) prop.
Oftentimes, you will want to show errors from an external source (such as the server) in your form alongside any client-side validation errors. These can be passed into your `<Form>` component using the `externalErrors` prop.

@@ -325,3 +403,3 @@ These errors must be in an object with keys representing the path to the field they should be associated with. For example, the errors:

```js
const serverErrors = {
const externalErrors = {
"/": "User failed to save!",

@@ -335,15 +413,15 @@ "/email": "A user with this email already exists!",

```jsx
<Form serverErrors={serverErrors}>
<Form externalErrors={externalErrors}>
{(link, handleSubmit) => (
<>
<ObjectField link={link}>
{links => (
<>
<StringField link={links.name} />
<StringField link={links.email} />
</>
)}
</ObjectField>
<button onClick={handleSubmit}>Submit</button>
</>
<>
<ObjectField link={link}>
{links => (
<>
<StringField link={links.name} />
<StringField link={links.email} />
</>
)}
</ObjectField>
<button onClick={handleSubmit}>Submit</button>
</>
)}

@@ -374,21 +452,21 @@ </Form>

{(link, handleSubmit, {valid}) => (
<>
<Field link={link}>
{(value, errors, onChange, onBlur, {changed}) => (
<label>
Name
<input
type="text"
value={value}
onChange={onChange}
onBlur={onBlur}
/>
{changed ? "(Modified)" : null}
</label>
)}
</Field>
<button disabled={!valid} onClick={() => handleSubmit()}>
Submit
</button>
</>
<>
<Field link={link}>
{(value, errors, onChange, onBlur, {changed}) => (
<label>
Name
<input
type="text"
value={value}
onChange={onChange}
onBlur={onBlur}
/>
{changed ? "(Modified)" : null}
</label>
)}
</Field>
<button disabled={!valid} onClick={() => handleSubmit()}>
Submit
</button>
</>
)}

@@ -415,9 +493,9 @@ </Form>

{(link, handleSubmit) => (
<>
<UserField link={link} />
<div>
<button onClick={() => handleSubmit("save")}>Save</button>
<button onClick={() => handleSubmit("submit")}>Submit</button>
</div>
</>
<>
<UserField link={link} />
<div>
<button onClick={() => handleSubmit("save")}>Save</button>
<button onClick={() => handleSubmit("submit")}>Submit</button>
</div>
</>
)}

@@ -432,3 +510,5 @@ </Form>;

```jsx
function handleSubmit(value) { /* ... */ }
function handleSubmit(value) {
/* ... */
}

@@ -459,3 +539,3 @@ class MyExternalButtonExample extends React.Component<Props> {

>
{link => (<UserField link={link} />)}
{link => <UserField link={link} />}
</Form>

@@ -462,0 +542,0 @@ <button onClick={this.handleSubmitClick}>Submit</button>

@@ -9,3 +9,2 @@ // @flow strict

Extras,
ClientErrors,
AdditionalRenderInfo,

@@ -17,3 +16,2 @@ CustomChange,

type ShapedTree,
type ShapedPath,
treeFromValue,

@@ -34,16 +32,13 @@ dangerouslyReplaceArrayChild,

} from "./utils/array";
import {FormContext} from "./Form";
import {FormContext, type ValidationOps, validationFnNoOps} from "./Form";
import {
type FormState,
replaceArrayChild,
setTouched,
setChanged,
setExtrasTouched,
arrayChild,
validate,
getExtras,
flatRootErrors,
isValid,
changedFormState,
} from "./formState";
import type {Path} from "./tree";

@@ -77,6 +72,6 @@ type ToFieldLink = <T>(T) => FieldLink<T>;

function makeLinks<E>(
path: Path,
formState: FormState<Array<E>>,
onChildChange: (number, FormState<E>) => void,
onChildBlur: (number, ShapedTree<E, Extras>) => void,
onChildValidation: (number, ShapedPath<E>, ClientErrors) => void
onChildBlur: (number, ShapedTree<E, Extras>) => void
): Links<E> {

@@ -93,5 +88,3 @@ const [oldValue] = formState;

},
onValidation: (childPath, clientErrors) => {
onChildValidation(i, childPath, clientErrors);
},
path: [...path, {type: "array", index: i}],
};

@@ -101,7 +94,3 @@ });

type State = {|
nonce: number,
|};
export default class ArrayField<E> extends React.Component<Props<E>, State> {
export default class ArrayField<E> extends React.Component<Props<E>, void> {
static defaultProps = {

@@ -112,27 +101,22 @@ validation: () => [],

state = {
nonce: 0,
};
validationFnOps: ValidationOps<Array<E>> = validationFnNoOps();
initialValidate() {
const {
link: {formState, onValidation},
validation,
} = this.props;
const [value] = formState;
const {errors} = getExtras(formState);
componentDidMount() {
this.validationFnOps = this.context.registerValidation(
this.props.link.path,
this.props.validation
);
}
if (errors.client === "pending") {
onValidation([], validation(value));
componentDidUpdate(prevProps: Props<E>) {
if (prevProps.validation !== this.props.validation) {
this.validationFnOps.replace(this.props.validation);
}
}
componentDidMount() {
this.initialValidate();
componentWillUnmount() {
this.validationFnOps.unregister();
this.validationFnOps = validationFnNoOps();
}
forceChildRemount() {
this.setState(({nonce}) => ({nonce: nonce + 1}));
}
_handleChildChange: (number, FormState<E>) => void = (

@@ -150,23 +134,20 @@ index: number,

const newValue = newFormState[0];
const customValue =
this.props.customChange && this.props.customChange(oldValue, newValue);
let nextFormState: FormState<Array<E>>;
let validatedFormState: FormState<Array<E>>;
if (customValue) {
// Create a fresh form state for the new value.
// TODO(zach): It's kind of gross that this is happening outside of Form.
nextFormState = changedFormState(customValue);
// A custom change occurred, which means the whole array needs to be
// revalidated.
validatedFormState = this.context.updateTreeAtPath(this.props.link.path, [
customValue,
newFormState[1],
]);
} else {
nextFormState = newFormState;
validatedFormState = this.context.updateNodeAtPath(
this.props.link.path,
newFormState
);
}
this.props.link.onChange(
setChanged(validate(this.props.validation, nextFormState))
);
// Need to remount children so they will run validations
if (customValue) {
this.forceChildRemount();
}
this.props.link.onChange(validatedFormState);
};

@@ -187,15 +168,8 @@

_handleChildValidation: (number, ShapedPath<E>, ClientErrors) => void = (
index,
childPath,
errors
) => {
const extendedPath = [
{
type: "array",
index,
},
...childPath,
];
this.props.link.onValidation(extendedPath, errors);
_validateThenApplyChange = <E>(formState: FormState<Array<E>>) => {
const validatedFormState = this.context.updateNodeAtPath(
this.props.link.path,
formState
);
this.props.link.onChange(validatedFormState);
};

@@ -219,9 +193,3 @@

);
this.props.link.onChange(
validate(
this.props.validation,
setChanged(setTouched([newValue, newTree]))
)
);
this._validateThenApplyChange([newValue, newTree]);
};

@@ -250,8 +218,3 @@

this.props.link.onChange(
validate(
this.props.validation,
setChanged(setTouched([newValue, newTree]))
)
);
this._validateThenApplyChange([newValue, newTree]);
};

@@ -272,8 +235,3 @@

this.props.link.onChange(
validate(
this.props.validation,
setChanged(setTouched([newValue, newTree]))
)
);
this._validateThenApplyChange([newValue, newTree]);
};

@@ -319,8 +277,3 @@

this.props.link.onChange(
validate(
this.props.validation,
setChanged(setTouched([newValue, newTree]))
)
);
this._validateThenApplyChange([newValue, newTree]);
};

@@ -337,8 +290,3 @@

this.props.link.onChange(
validate(
this.props.validation,
setChanged(setTouched([newValue, newTree]))
)
);
this._validateThenApplyChange([newValue, newTree]);
};

@@ -354,22 +302,18 @@

);
this.props.link.onChange(
validate(
this.props.validation,
setChanged(setTouched([newValue, newTree]))
)
);
this._validateThenApplyChange([newValue, newTree]);
};
render() {
const {formState} = this.props.link;
const {formState, path} = this.props.link;
const {shouldShowError} = this.context;
const links = makeLinks(
path,
formState,
this._handleChildChange,
this._handleChildBlur,
this._handleChildValidation
this._handleChildBlur
);
return (
<React.Fragment key={this.state.nonce}>
<React.Fragment>
{this.props.children(

@@ -376,0 +320,0 @@ links,

// @flow strict
import * as React from "react";
import type {FieldLink, ClientErrors, ServerErrors, Err} from "./types";
import type {FieldLink, ClientErrors, ExternalErrors, Err} from "./types";
import {FormContext} from "./Form";

@@ -13,4 +13,4 @@ import {getExtras} from "./formState";

}
if (errors.server !== "unchecked") {
flatErrors = flatErrors.concat(errors.server);
if (errors.external !== "unchecked") {
flatErrors = flatErrors.concat(errors.external);
}

@@ -25,3 +25,3 @@ return flatErrors;

client: ClientErrors,
server: ServerErrors,
external: ExternalErrors,
flattened: Array<string>,

@@ -38,5 +38,5 @@ }) => React.Node,

client: errors.client,
server: errors.server,
external: errors.external,
flattened,
});
}

@@ -6,10 +6,4 @@ // @flow strict

import {mapRoot} from "./shapedTree";
import {FormContext} from "./Form";
import {
setExtrasTouched,
getExtras,
setChanged,
validate,
isValid,
} from "./formState";
import {FormContext, type ValidationOps, validationFnNoOps} from "./Form";
import {setExtrasTouched, getExtras, isValid} from "./formState";

@@ -33,4 +27,4 @@ type Props<T> = {|

}
if (errors.server !== "unchecked") {
flatErrors = flatErrors.concat(errors.server);
if (errors.external !== "unchecked") {
flatErrors = flatErrors.concat(errors.external);
}

@@ -46,24 +40,33 @@ return flatErrors;

initialValidate() {
const {
link: {formState, onValidation},
validation,
} = this.props;
const [value] = formState;
const {errors} = getExtras(formState);
validationFnOps: ValidationOps<T> = validationFnNoOps();
if (errors.client === "pending") {
onValidation([], validation(value));
componentDidMount() {
this.validationFnOps = this.context.registerValidation(
this.props.link.path,
this.props.validation
);
}
componentDidUpdate(prevProps: Props<T>) {
if (prevProps.validation !== this.props.validation) {
this.validationFnOps.replace(this.props.validation);
}
}
componentDidMount() {
this.initialValidate();
componentWillUnmount() {
this.validationFnOps.unregister();
this.validationFnOps = validationFnNoOps();
}
onChange: T => void = (newValue: T) => {
const [_, oldTree] = this.props.link.formState;
this.props.link.onChange(
setChanged(validate(this.props.validation, [newValue, oldTree]))
);
const {
path,
formState: [_, oldTree],
onChange,
} = this.props.link;
const newFormState = this.context.updateNodeAtPath(path, [
newValue,
oldTree,
]);
onChange(newFormState);
};

@@ -75,3 +78,3 @@

this.props.link.onBlur(
// TODO(zach): Not sure if we should blow away server errors here
// TODO(zach): Not sure if we should blow away external errors here
mapRoot(setExtrasTouched, tree)

@@ -78,0 +81,0 @@ );

// @flow strict
import * as React from "react";
import invariant from "./utils/invariant";
import {equals as arrayEquals} from "./utils/array";
import {changedFormState} from "./formState";

@@ -8,8 +11,7 @@ import type {

OnBlur,
OnValidation,
Extras,
FieldLink,
ClientErrors,
AdditionalRenderInfo,
} from "./types";
import {type Direction} from "./tree";
import {

@@ -24,11 +26,34 @@ type FormState,

type ShapedTree,
type ShapedPath,
shapePath,
updateAtPath,
mapShapedTree,
mapRoot,
pathExistsInTree,
} from "./shapedTree";
import {pathFromPathString} from "./tree";
import {pathFromPathString, type Path} from "./tree";
import {
startsWith,
encodePath,
decodePath,
type EncodedPath,
} from "./EncodedPath";
import FeedbackStrategies, {type FeedbackStrategy} from "./feedbackStrategies";
export type FormContextPayload = {
export type ValidationOps<T> = {
unregister: () => void,
replace: (fn: (T) => Array<string>) => void,
};
const noOps = {
unregister() {},
replace() {},
};
// noOps can't be used directly because Flow doesn't typecheck a constant as
// being parametric in T.
export function validationFnNoOps<T>(): ValidationOps<T> {
return noOps;
}
export type FormContextPayload = {|
shouldShowError: (metaField: MetaField) => boolean,

@@ -39,3 +64,11 @@ // These values are taken into account in shouldShowError, but are also

submitted: boolean,
};
registerValidation: (
path: Path,
fn: (mixed) => Array<string>
) => ValidationOps<mixed>,
// TODO(dmnd): Try to get rid of * here by writing a type-level function of
// Path and FormState<T>
updateTreeAtPath: (Path, FormState<*>) => FormState<*>,
updateNodeAtPath: (Path, FormState<*>) => FormState<*>,
|};
export const FormContext: React.Context<FormContextPayload> = React.createContext(

@@ -46,7 +79,10 @@ {

submitted: true,
registerValidation: () => ({replace: () => {}, unregister: () => {}}),
updateTreeAtPath: (path, formState) => formState,
updateNodeAtPath: (path, formState) => formState,
}
);
function applyServerErrorsToFormState<T>(
serverErrors: null | {[path: string]: Array<string>},
function applyExternalErrorsToFormState<T>(
externalErrors: null | {[path: string]: Array<string>},
formState: FormState<T>

@@ -57,7 +93,7 @@ ): FormState<T> {

let tree: ShapedTree<T, Extras>;
if (serverErrors !== null) {
if (externalErrors !== null) {
// If keys do not appear, no errors
tree = mapShapedTree(
({errors, meta}) => ({
errors: {...errors, server: []},
errors: {...errors, external: []},
meta,

@@ -67,4 +103,4 @@ }),

);
Object.keys(serverErrors).forEach(key => {
const newErrors: Array<string> = serverErrors[key];
Object.keys(externalErrors).forEach(key => {
const newErrors: Array<string> = externalErrors[key];
const path = shapePath(value, pathFromPathString(key));

@@ -77,3 +113,3 @@

({errors, meta}) => ({
errors: {...errors, server: newErrors},
errors: {...errors, external: newErrors},
meta,

@@ -96,3 +132,3 @@ }),

({errors, meta}) => ({
errors: {...errors, server: []},
errors: {...errors, external: []},
meta,

@@ -107,2 +143,166 @@ }),

type Value =
| mixed
| number
| string
| null
| void
| Array<Value>
| {[string]: Value};
function getValueAtPath(path: Path, value: Value) {
if (path.length === 0) {
return value;
}
const [p, ...rest] = path;
if (p.type === "array") {
invariant(
Array.isArray(value),
"Path/value shape mismatch: expected array"
);
return getValueAtPath(rest, value[p.index]);
} else if (p.type === "object") {
invariant(
typeof value === "object" && value !== null && !Array.isArray(value),
"Path/value shape mismatch: expected object"
);
return getValueAtPath(rest, value[p.key]);
}
throw new Error("Path is too long");
}
function pathSegmentEqual(a: Direction, b: Direction) {
return (
(a.type === "array" && b.type === "array" && a.index === b.index) ||
(a.type === "object" && b.type === "object" && a.key === b.key)
);
}
function getRelativePath(path: Path, prefix: Path) {
for (let i = 0; i < prefix.length; i++) {
invariant(
pathSegmentEqual(path[i], prefix[i]),
"Expect prefix to be a prefix of path"
);
}
return path.slice(prefix.length);
}
/**
* Deeply updates the FormState tree to reflect a new value. Similar to
* applyValidationToTree, but in response to a deep update, so updates all child
* paths too. Used in response to a custom change.
*/
function updateTreeAtPath<T>(
prefix: Path,
[initialValue, _initialTree]: FormState<T>,
validations: Map<EncodedPath, Map<number, (mixed) => Array<string>>>
): FormState<T> {
// Create a fresh form state for the new value. Note that this overwrites any
// existing `succeeded` values with false. `succeeded` will only be set back
// to true if and only if the current validation passes. We lose history. For
// most use cases this is likely desirible behaviour, but there could be uses
// cases for which it isn't desired. We'll fix it if we encounter one of those
// use cases.
const [value, changedTree] = changedFormState(initialValue);
const validatedTree = [...validations.entries()]
.filter(([path]) => startsWith(path, prefix))
.map(([path, validationsMap]) => {
// Note that value is not the root value, it's the value at this path.
// So convert absolute validation paths to relative before attempting to
// pull out the value on which to run them.
const relativePath = getRelativePath(decodePath(path), prefix);
const valueAtPath = getValueAtPath(relativePath, value);
// Run all validation functions on x
const errors = [...validationsMap.values()].reduce(
(errors, validationFn) => errors.concat(validationFn(valueAtPath)),
[]
);
return [relativePath, errors];
})
.reduce(
(tree, [path, newErrors]) =>
// Here we don't reset `errors: {external}` or set `meta: {touched: true,
// changed: true}`. This is because we already called changedFormState
// above.
updateAtPath(
path,
({errors, meta}) => ({
errors: {...errors, client: newErrors},
meta: {
...meta,
succeeded: meta.succeeded || newErrors.length === 0,
},
}),
tree
),
changedTree
);
return [value, validatedTree];
}
// Unique id for each field so that errors can be tracked by the fields that
// produced them. This is necessary because it's possible for multiple fields
// to reference the same link "aliasing".
let _nextFieldId = 0;
function nextFieldId() {
return _nextFieldId++;
}
// TODO(dmnd): This function is confusing to use because pathToValue and
// validations are conceptually "absolute" (i.e. they are defined with respect
// to the root), but valueAtPath is *not* absolute: it's the value deeper in the
// tree, defined respective to pathToValue.
function validateAtPath(
pathToValue: Path,
valueAtPath: mixed, // TODO(dmnd): Better typechecking with ShapedPath?
validations: Map<EncodedPath, Map<number, (mixed) => Array<string>>>
): Array<string> {
const map = validations.get(encodePath(pathToValue));
if (!map) {
return [];
}
return [...map.values()].reduce(
(errors, validationFn) => errors.concat(validationFn(valueAtPath)),
[]
);
}
/**
* Updates the FormState tree to reflect a new value:
* - run validations at path (but not child paths)
* - remove existing, now obsolete errors
* - calculate & write new client side errors
* - ensure that meta reflects that the value has changed
*/
function updateNodeAtPath<T>(
path: Path,
[value, tree]: FormState<T>,
validations: Map<EncodedPath, Map<number, (mixed) => Array<string>>>
): FormState<T> {
const errors = validateAtPath(path, value, validations);
return [
value,
mapRoot(
({meta}) => ({
errors: {
client: errors,
external: "unchecked",
},
meta: {
...meta,
succeeded: meta.succeeded || errors.length === 0,
touched: true,
changed: true,
},
}),
tree
),
];
}
type Props<T, ExtraSubmitData> = {|

@@ -115,3 +315,3 @@ // This is *only* used to intialize the form. Further changes will be ignored

+onValidation: boolean => void,
+serverErrors: null | {[path: string]: Array<string>},
+externalErrors: null | {[path: string]: Array<string>},
+children: (

@@ -127,3 +327,3 @@ link: FieldLink<T>,

submitted: boolean,
oldServerErrors: null | {[path: string]: Array<string>},
oldExternalErrors: null | {[path: string]: Array<string>},
};

@@ -139,3 +339,3 @@ export default class Form<T, ExtraSubmitData> extends React.Component<

feedbackStrategy: FeedbackStrategies.Always,
serverErrors: null,
externalErrors: null,
};

@@ -147,10 +347,10 @@

) {
if (props.serverErrors !== state.oldServerErrors) {
const newFormState = applyServerErrorsToFormState<T>(
props.serverErrors,
if (props.externalErrors !== state.oldExternalErrors) {
const newTree = applyExternalErrorsToFormState<T>(
props.externalErrors,
state.formState
);
return {
formState: newFormState,
oldServerErrors: props.serverErrors,
formState: newTree,
oldExternalErrors: props.externalErrors,
};

@@ -161,7 +361,12 @@ }

validations: Map<EncodedPath, Map<number, (mixed) => Array<string>>>;
initialValidationComplete = false;
constructor(props: Props<T, ExtraSubmitData>) {
super(props);
const formState = applyServerErrorsToFormState(
props.serverErrors,
this.validations = new Map();
const formState = applyExternalErrorsToFormState(
props.externalErrors,
freshFormState(props.initialValue)

@@ -173,6 +378,26 @@ );

submitted: false,
oldServerErrors: props.serverErrors,
oldExternalErrors: props.externalErrors,
};
}
componentDidMount() {
// After the the Form mounts, all validations get ran as a batch. Note that
// this is different from how initial validations get run on all
// subsequently mounted Fields. When a Field is mounted after the Form, its
// validation gets run individually.
// TODO(dmnd): It'd be nice to consolidate validation to a single code path.
// Take care to use an updater to avoid clobbering changes from fields that
// call onChange during cDM.
this.setState(
({formState}) => ({
formState: updateTreeAtPath([], formState, this.validations),
}),
() => {
this.initialValidationComplete = true;
this.props.onValidation(isValid(this.state.formState));
}
);
}
// Public API: submit from the outside

@@ -210,24 +435,91 @@ submit(extraData: ExtraSubmitData) {

_handleValidation: OnValidation<T> = (
path: ShapedPath<T>,
errors: ClientErrors
) => {
// TODO(zach): Move this into formState.js, it is gross
const updater = newErrors => ({errors, meta}) => ({
errors: {...errors, client: newErrors},
meta: {
...meta,
succeeded: newErrors.length === 0 ? true : meta.succeeded,
},
/**
* Keeps validation errors from becoming stale when validation functions of
* children change.
*/
recomputeErrorsAtPathAndRender = (path: Path) => {
this.setState(({formState: [rootValue, tree]}) => {
const value = getValueAtPath(path, rootValue);
const errors = validateAtPath(path, value, this.validations);
const updatedTree = updateAtPath(
path,
extras => ({...extras, errors: {...extras.errors, client: errors}}),
tree
);
return {formState: [rootValue, updatedTree]};
});
this.setState(
({formState: [value, tree]}) => ({
formState: [value, updateAtPath(path, updater(errors), tree)],
}),
() => {
this.props.onValidation(isValid(this.state.formState));
}
);
};
handleRegisterValidation = (path: Path, fn: mixed => Array<string>) => {
const encodedPath = encodePath(path);
let fieldId = nextFieldId();
const map = this.validations.get(encodedPath) || new Map();
map.set(fieldId, fn);
this.validations.set(encodedPath, map);
if (this.initialValidationComplete) {
// Form validates all Fields at once during mount. When fields are added
// after the Form has already mounted, their initial values need to be
// validated.
this.recomputeErrorsAtPathAndRender(path);
}
return {
replace: fn => this.replaceValidation(path, fieldId, fn),
unregister: () => this.unregisterValidation(path, fieldId),
};
};
replaceValidation = (
path: Path,
fieldId: number,
fn: mixed => Array<string>
) => {
const encodedPath = encodePath(path);
const map = this.validations.get(encodedPath);
invariant(map != null, "Expected to find handler map");
const oldFn = map.get(fieldId);
invariant(oldFn != null, "Expected to find previous validation function");
map.set(fieldId, fn);
// Now that the old validation is gone, make sure there are no left over
// errors from it.
const value = getValueAtPath(path, this.state.formState[0]);
if (arrayEquals(oldFn(value), fn(value))) {
// The errors haven't changed, so don't bother calling setState.
// You might think this is a silly performance optimization but actually
// we need this for annoying React reasons:
// If the validation function is an inline function, its identity changes
// every render. This means replaceValidation gets called every time
// componentDidUpdate runs (i.e. each render). Then when setState is
// called from recomputeErrorsAtPathAndRender, it'll cause another render,
// which causes another componentDidUpdate, and so on. So, take care to
// avoid an infinite loop by returning early here.
return;
}
// The new validation function returns different errors, so re-render.
this.recomputeErrorsAtPathAndRender(path);
};
unregisterValidation = (path: Path, fieldId: number) => {
const encodedPath = encodePath(path);
const map = this.validations.get(encodedPath);
invariant(map != null, "Couldn't find handler map during unregister");
map.delete(fieldId);
// If the entire path was deleted from the tree, any left over errors are
// already gone. For example, this happens when an array child is removed.
if (!pathExistsInTree(path, this.state.formState[1])) {
return;
}
// now that the validation is gone, make sure there are no left over
// errors from it
this.recomputeErrorsAtPathAndRender(path);
};
render() {

@@ -244,2 +536,7 @@ const {formState} = this.state;

shouldShowError: this.props.feedbackStrategy.bind(null, metaForm),
registerValidation: this.handleRegisterValidation,
updateTreeAtPath: (path, formState) =>
updateTreeAtPath(path, formState, this.validations),
updateNodeAtPath: (path, formState) =>
updateNodeAtPath(path, formState, this.validations),
...metaForm,

@@ -253,3 +550,3 @@ }}

onBlur: this._handleBlur,
onValidation: this._handleValidation,
path: [],
},

@@ -256,0 +553,0 @@ this._handleSubmit,

@@ -5,10 +5,7 @@ // @flow strict

type ShapedTree,
mapRoot,
dangerouslyReplaceObjectChild,
dangerouslyReplaceArrayChild,
forgetShape,
dangerouslySetChildren,
shapedObjectChild,
shapedArrayChild,
shapedZipWith,
foldMapShapedTree,

@@ -19,5 +16,4 @@ getRootData,

import {cleanMeta, cleanErrors} from "./types";
import type {Extras, ClientErrors, Validation, ServerErrors} from "./types";
import type {Extras} from "./types";
import {replaceAt} from "./utils/array";
import invariant from "./utils/invariant";

@@ -63,4 +59,4 @@ // invariant, Tree is shaped like T

}
if (errors.server !== "unchecked") {
flatErrors = flatErrors.concat(errors.server);
if (errors.external !== "unchecked") {
flatErrors = flatErrors.concat(errors.external);
}

@@ -86,65 +82,2 @@ return flatErrors;

export function validate<T>(
validation: Validation<T>,
formState: FormState<T>
): FormState<T> {
const [value, tree] = formState;
const newErrors = validation(value);
return [
value,
mapRoot(
({meta}) => ({
errors: {
client: newErrors,
server: "unchecked",
},
meta: {
...meta,
succeeded: meta.succeeded || newErrors.length === 0,
},
}),
tree
),
];
}
export function setChanged<T>(formState: FormState<T>): FormState<T> {
return [
formState[0],
mapRoot(
({errors, meta}) => ({
errors,
meta: {...meta, touched: true, changed: true},
}),
formState[1]
),
];
}
export function setTouched<T>(formState: FormState<T>): FormState<T> {
return [
formState[0],
mapRoot(
({errors, meta}) => ({errors, meta: {...meta, touched: true}}),
formState[1]
),
];
}
export function setClientErrors<T>(
newErrors: ClientErrors,
formState: FormState<T>
): FormState<T> {
return [
formState[0],
mapRoot(
({errors, meta}) => ({
errors: {...errors, client: newErrors},
meta,
}),
formState[1]
),
];
}
export function setExtrasTouched({errors, meta}: Extras): Extras {

@@ -180,106 +113,2 @@ return {errors, meta: {...meta, touched: true}};

export function replaceArrayChildren<E>(
children: Array<FormState<E>>,
formState: FormState<Array<E>>
): FormState<Array<E>> {
const [_, tree] = formState;
const [childValues, childTrees]: [
Array<E>,
Array<ShapedTree<E, Extras>>,
] = children.reduce(
(memo, child) => {
const [childValue, childTree] = child;
return [memo[0].concat([childValue]), memo[1].concat([childTree])];
},
[[], []]
);
return [childValues, dangerouslySetChildren(childTrees, tree)];
}
function combineExtrasForValidation(oldExtras: Extras, newExtras: Extras) {
const {meta: oldMeta, errors: oldErrors} = oldExtras;
const {meta: newMeta, errors: newErrors} = newExtras;
// Only asyncValidationInFlight + succeeded may change
invariant(
oldMeta.touched === newMeta.touched,
"Recieved a new meta.touched when monoidally combining errors"
);
invariant(
oldMeta.changed === newMeta.changed,
"Recieved a new meta.changed when monoidally combining errors"
);
// No combination is possible if the old client errors are not pending
if (oldErrors.client !== "pending") {
return oldExtras;
}
// No combination is possible if the new client errors are pending
if (newErrors.client === "pending") {
return oldExtras;
}
return {
meta: {
touched: oldMeta.touched,
changed: oldMeta.changed,
succeeded: newMeta.succeeded,
asyncValidationInFlight:
oldMeta.asyncValidationInFlight || newMeta.asyncValidationInFlight,
},
errors: {
client: newErrors.client,
server: newErrors.server,
},
};
}
function monoidallyCombineTreesForValidation<T>(
oldTree: ShapedTree<T, Extras>,
newTree: ShapedTree<T, Extras>
): ShapedTree<T, Extras> {
return shapedZipWith(combineExtrasForValidation, oldTree, newTree);
}
// Also sets asyncValidationInFlight
export function monoidallyCombineFormStatesForValidation<T>(
oldState: FormState<T>,
newState: FormState<T>
): FormState<T> {
// Value should never change when combining errors
invariant(
oldState[0] === newState[0],
"Received a new value when monoidally combining errors"
);
return [
oldState[0],
monoidallyCombineTreesForValidation(oldState[1], newState[1]),
];
}
function replaceServerErrorsExtra(
newErrors: ServerErrors,
oldExtras: Extras
): Extras {
const {meta, errors} = oldExtras;
return {
meta,
errors: {
client: errors.client,
server: newErrors,
},
};
}
export function replaceServerErrors<T>(
serverErrors: ShapedTree<T, ServerErrors>,
formState: FormState<T>
): FormState<T> {
return [
formState[0],
shapedZipWith(replaceServerErrorsExtra, serverErrors, formState[1]),
];
}
// Is whole tree client valid?

@@ -286,0 +115,0 @@ // TODO(zach): This will have to change with asynchronous validations. We will

@@ -9,25 +9,21 @@ // @flow strict

Extras,
ClientErrors,
AdditionalRenderInfo,
CustomChange,
} from "./types";
import {FormContext} from "./Form";
import {FormContext, type ValidationOps, validationFnNoOps} from "./Form";
import {
type FormState,
setChanged,
replaceObjectChild,
setExtrasTouched,
objectChild,
validate,
getExtras,
flatRootErrors,
isValid,
changedFormState,
} from "./formState";
import {
type ShapedTree,
type ShapedPath,
mapRoot,
dangerouslyReplaceObjectChild,
} from "./shapedTree";
import type {Path} from "./tree";

@@ -48,6 +44,6 @@ type ToFieldLink = <T>(T) => FieldLink<T>;

function makeLinks<T: {}, V>(
path: Path,
formState: FormState<T>,
onChildChange: (string, FormState<V>) => void,
onChildBlur: (string, ShapedTree<V, Extras>) => void,
onChildValidation: (string, ShapedPath<V>, ClientErrors) => void
onChildBlur: (string, ShapedTree<V, Extras>) => void
): Links<T> {

@@ -64,5 +60,3 @@ const [value] = formState;

},
onValidation: (path, errors) => {
onChildValidation(k, path, errors);
},
path: [...path, {type: "object", key: k}],
};

@@ -77,9 +71,5 @@ memo[k] = l;

type State = {|
nonce: number,
|};
export default class ObjectField<T: {}> extends React.Component<
Props<T>,
State
void
> {

@@ -92,27 +82,22 @@ static contextType = FormContext;

state = {
nonce: 0,
};
validationFnOps: ValidationOps<T> = validationFnNoOps();
_initialValidate() {
const {
link: {formState, onValidation},
validation,
} = this.props;
const [value] = formState;
const {errors} = getExtras(formState);
componentDidMount() {
this.validationFnOps = this.context.registerValidation(
this.props.link.path,
this.props.validation
);
}
if (errors.client === "pending") {
onValidation([], validation(value));
componentDidUpdate(prevProps: Props<T>) {
if (prevProps.validation !== this.props.validation) {
this.validationFnOps.replace(this.props.validation);
}
}
componentDidMount() {
this._initialValidate();
componentWillUnmount() {
this.validationFnOps.unregister();
this.validationFnOps = validationFnNoOps();
}
forceChildRemount() {
this.setState(({nonce}) => ({nonce: nonce + 1}));
}
_handleChildChange: <V>(string, FormState<V>) => void = <V>(

@@ -130,23 +115,20 @@ key: string,

const newValue = newFormState[0];
const customValue =
this.props.customChange && this.props.customChange(oldValue, newValue);
let nextFormState: FormState<T>;
let validatedFormState: FormState<T>;
if (customValue) {
// Create a fresh form state for the new value.
// TODO(zach): It's kind of gross that this is happening outside of Form.
nextFormState = changedFormState(customValue);
// A custom change occurred, which means the whole object needs to be
// revalidated.
validatedFormState = this.context.updateTreeAtPath(this.props.link.path, [
customValue,
newFormState[1],
]);
} else {
nextFormState = newFormState;
validatedFormState = this.context.updateNodeAtPath(
this.props.link.path,
newFormState
);
}
this.props.link.onChange(
setChanged(validate(this.props.validation, nextFormState))
);
// Need to remount children so they will run validations
if (customValue) {
this.forceChildRemount();
}
this.props.link.onChange(validatedFormState);
};

@@ -167,19 +149,2 @@

_handleChildValidation: <V>(string, ShapedPath<V>, ClientErrors) => void = <
V
>(
key: string,
childPath: ShapedPath<V>,
errors: ClientErrors
) => {
const extendedPath = [
{
type: "object",
key,
},
...childPath,
];
this.props.link.onValidation(extendedPath, errors);
};
render() {

@@ -190,9 +155,9 @@ const {formState} = this.props.link;

const links = makeLinks(
this.props.link.path,
this.props.link.formState,
this._handleChildChange,
this._handleChildBlur,
this._handleChildValidation
this._handleChildBlur
);
return (
<React.Fragment key={this.state.nonce}>
<React.Fragment>
{this.props.children(links, {

@@ -199,0 +164,0 @@ touched: getExtras(formState).meta.touched,

// @flow strict
import {
type Tree,
type Path,
leaf,
strictZipWith,
mapTree,
foldMapTree,
} from "./tree";
import {type Tree, type Path, mapTree, foldMapTree} from "./tree";
import invariant from "./utils/invariant";

@@ -89,2 +82,50 @@ import {replaceAt} from "./utils/array";

export function pathExistsInTree<T, Node>(
path: Path,
tree: ShapedTree<T, Node>
): boolean {
// This function is equivalent to:
// try {
// updateAtPath(path, x => x, tree);
// } catch {
// return false;
// }
// return true;
// }
if (path.length === 0 && tree.type != null) {
return true;
}
const [first, ...rest] = path;
if (tree.type === "leaf") {
return false;
}
if (tree.type === "array") {
if (first.type !== "array") {
return false;
}
if (!(0 <= first.index && first.index < tree.children.length)) {
return false;
}
return pathExistsInTree(rest, tree.children[first.index]);
}
if (tree.type === "object") {
if (first.type !== "object") {
return false;
}
if (!(first.key in tree.children)) {
return false;
}
return pathExistsInTree(rest, tree.children[first.key]);
}
throw new Error("unreachable");
}
export function updateAtPath<T, Node>(

@@ -95,3 +136,2 @@ path: ShapedPath<T>,

): ShapedTree<T, Node> {
// console.log("updateAtPath()", path, tree);
if (path.length === 0) {

@@ -128,2 +168,8 @@ if (tree.type === "object") {

);
invariant(
0 <= firstStep.index && firstStep.index < tree.children.length,
`Tried to take path index ${
firstStep.index
} but array from tree has length ${tree.children.length}`
);
const newChild = updateAtPath(

@@ -142,7 +188,10 @@ restStep,

);
const newChild = updateAtPath(
restStep,
updater,
tree.children[firstStep.key]
const nextTree = tree.children[firstStep.key];
invariant(
nextTree !== undefined,
`Tried to take path key ${
firstStep.key
} but it isn't present on object in tree`
);
const newChild = updateAtPath(restStep, updater, nextTree);
// $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why

@@ -154,33 +203,2 @@ return dangerouslyReplaceObjectChild(firstStep.key, newChild, tree);

export function checkShape<T, Node>(
value: T,
tree: Tree<Node>
): ShapedTree<T, Node> {
if (tree.type === "array") {
invariant(Array.isArray(value), "value isn't an array");
invariant(
value.length === tree.children.length,
"value and tree children have different lengths"
);
tree.children.forEach((child, i) => {
checkShape(value[i], child);
});
}
if (tree.type === "object") {
invariant(value instanceof Object, "value isn't an object in checkTree");
const valueEntries = Object.entries(value);
const childrenKeys = new Set(Object.keys(tree.children));
invariant(
valueEntries.length === childrenKeys.size,
"value doesn't have the right number of keys"
);
valueEntries.forEach(([key, value]) => {
invariant(childrenKeys.has(key));
checkShape(value, tree.children[key]);
});
}
// leaves are allowed to stand in for complex types in T
return tree;
}
export function shapedArrayChild<E, Node>(

@@ -300,16 +318,2 @@ index: number,

// A leaf matches any shape
export function shapedLeaf<T, Node>(node: Node): ShapedTree<T, Node> {
return leaf(node);
}
export function shapedZipWith<T, A, B, C>(
f: (A, B) => C,
left: ShapedTree<T, A>,
right: ShapedTree<T, B>
): ShapedTree<T, C> {
// Don't actually need the checks here if our invariant holds
return strictZipWith(f, left, right);
}
// Mapping doesn't change the shape

@@ -316,0 +320,0 @@ export function mapShapedTree<T, A, B>(

@@ -5,5 +5,7 @@ // @flow

import TestRenderer from "react-test-renderer";
import Form from "../Form";
import ArrayField from "../ArrayField";
import {type FieldLink} from "../types";
import TestField, {TestInput} from "./TestField";
import TestForm from "./TestForm";

@@ -13,103 +15,95 @@ import {expectLink, mockLink, mockFormState} from "./tools";

describe("ArrayField", () => {
describe("ArrayField is a field", () => {
describe("validates on mount", () => {
it("ensures that the link inner type matches the type of the validation", () => {
const formState = mockFormState(["one", "two", "three"]);
const link = mockLink(formState);
describe("is a field", () => {
it("ensures that the link inner type matches the type of the validation", () => {
const formState = mockFormState(["one", "two", "three"]);
const link = mockLink(formState);
// $ExpectError
<ArrayField link={link} validation={(_e: empty) => []}>
{() => null}
</ArrayField>;
// $ExpectError
<ArrayField link={link} validation={(_e: empty) => []}>
{() => null}
</ArrayField>;
<ArrayField link={link} validation={(_e: Array<string>) => []}>
{() => null}
</ArrayField>;
});
<ArrayField link={link} validation={(_e: Array<string>) => []}>
{() => null}
</ArrayField>;
});
it("Sets errors.client and meta.succeeded when there are no errors", () => {
const validation = jest.fn(() => []);
const formState = mockFormState([]);
const link = mockLink(formState);
it("Registers and unregisters for validation", () => {
const formState = mockFormState([]);
const link = mockLink(formState);
const unregister = jest.fn();
const registerValidation = jest.fn(() => ({
replace: jest.fn(),
unregister,
}));
TestRenderer.create(
<ArrayField link={link} validation={validation}>
{jest.fn(() => null)}
const renderer = TestRenderer.create(
<TestForm registerValidation={registerValidation}>
<ArrayField link={link} validation={() => []}>
{() => null}
</ArrayField>
);
</TestForm>
);
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
expect(registerValidation).toBeCalledTimes(1);
renderer.unmount();
expect(unregister).toBeCalledTimes(1);
});
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
it("calls replace when changing the validation function", () => {
const replace = jest.fn();
const registerValidation = jest.fn(() => ({
replace,
unregister: jest.fn(),
}));
it("Sets errors.client and meta.succeeded when there are errors", () => {
const validation = jest.fn(() => ["This is an error", "another error"]);
const formState = mockFormState([]);
const link = mockLink(formState);
TestRenderer.create(
<ArrayField link={link} validation={validation}>
{jest.fn(() => null)}
</ArrayField>
function Component() {
return (
<TestForm registerValidation={registerValidation}>
<ArrayField
link={mockLink(mockFormState(["hello", "world"]))}
validation={() => []}
>
{() => null}
</ArrayField>
</TestForm>
);
}
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
const renderer = TestRenderer.create(<Component />);
expect(registerValidation).toBeCalledTimes(1);
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual(["This is an error", "another error"]);
});
renderer.update(<Component />);
expect(replace).toBeCalledTimes(1);
});
it("Treats no validation as always passing", () => {
const formState = mockFormState([]);
const link = mockLink(formState);
it("Passes additional information to its render function", () => {
const formState = mockFormState(["value"]);
// $FlowFixMe
formState[1].data.errors = {
external: ["An external error"],
client: ["A client error"],
};
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(
<ArrayField link={link}>{jest.fn(() => null)}</ArrayField>
);
TestRenderer.create(<ArrayField link={link}>{renderFn}</ArrayField>);
expect(link.onValidation).toHaveBeenCalledTimes(1);
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
it("Passes additional information to its render function", () => {
const formState = mockFormState(["value"]);
// $FlowFixMe
formState[1].data.errors = {
server: ["A server error"],
client: ["A client error"],
};
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(<ArrayField link={link}>{renderFn}</ArrayField>);
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining([
"A server error",
"A client error",
]),
valid: false,
asyncValidationInFlight: false,
value: ["value"],
})
);
});
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining([
"An external error",
"A client error",
]),
valid: false,
asyncValidationInFlight: false,
value: ["value"],
})
);
});

@@ -153,10 +147,19 @@ });

it("calls onChange when a child changes", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
it("validates new values from children and passes result to onChange", () => {
const formState = mockFormState(["one", "two", "three"]);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(<ArrayField link={link}>{renderFn}</ArrayField>);
const updateNodeAtPath = jest.fn((path, formState) => formState);
TestRenderer.create(
<TestForm updateNodeAtPath={updateNodeAtPath}>
<ArrayField link={link}>{renderFn}</ArrayField>
</TestForm>
);
expect(updateNodeAtPath).toHaveBeenCalledTimes(0);
expect(link.onChange).toHaveBeenCalledTimes(0);
// call a child's onChange
const arrayLinks = renderFn.mock.calls[0][0];

@@ -166,10 +169,13 @@ const newElementFormState = mockFormState("newTwo");

expect(link.onChange).toHaveBeenCalled();
const newArrayFormState = link.onChange.mock.calls[0][0];
expect(newArrayFormState[0]).toEqual(["one", "newTwo", "three"]);
expect(newArrayFormState[1].data.meta).toMatchObject({
touched: true,
changed: true,
});
expect(newArrayFormState[1].children[1]).toBe(newElementFormState[1]);
expect(updateNodeAtPath).toHaveBeenCalledTimes(1);
expect(updateNodeAtPath).toHaveBeenCalledWith(
[],
[["one", "newTwo", "three"], expect.anything()]
);
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([
["one", "newTwo", "three"],
formState[1],
]);
});

@@ -198,24 +204,3 @@

it("calls onValidation when a child initially validates", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(<ArrayField link={link}>{renderFn}</ArrayField>);
const arrayLinks = renderFn.mock.calls[0][0];
arrayLinks[2].onValidation([], ["These are", "some errors"]);
expect(link.onValidation).toHaveBeenCalledTimes(2);
// Important: the first call to onValidation is for the initial render validation
const [path, errors] = link.onValidation.mock.calls[1];
expect(path).toEqual([{type: "array", index: 2}]);
expect(errors).toEqual(["These are", "some errors"]);
});
it("calls its validation when a child changes", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -225,5 +210,9 @@ const validation = jest.fn(() => ["This is an error"]);

TestRenderer.create(
<ArrayField link={link} validation={validation}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField link={link} validation={validation}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -261,5 +250,2 @@

it("validates after entry is added", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -269,5 +255,9 @@ const validation = jest.fn(() => ["an error"]);

TestRenderer.create(
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -308,12 +298,17 @@

it("validates after entry is removed", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
const elementValidation = jest.fn(() => ["an element error"]);
const renderFn = jest.fn(links =>
links.map((link, i) => (
<TestField key={i} link={link} validation={elementValidation} />
))
);
const validation = jest.fn(() => ["an error"]);
TestRenderer.create(
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -349,5 +344,2 @@

it("validates after the entry is moved", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -357,5 +349,9 @@ const validation = jest.fn(() => ["an error"]);

TestRenderer.create(
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -391,5 +387,2 @@

it("validates after fields are added", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -399,5 +392,9 @@ const validation = jest.fn(() => ["an error"]);

TestRenderer.create(
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -441,5 +438,2 @@

it("validates after fields are filtered", () => {
const formStateValue = ["one", "two", "three", "four", "five"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -449,5 +443,9 @@ const validation = jest.fn(() => ["an error"]);

TestRenderer.create(
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -484,5 +482,2 @@

it("validates after fields are modified", () => {
const formStateValue = ["one", "two", "three"];
const formState = mockFormState(formStateValue);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -492,5 +487,9 @@ const validation = jest.fn(() => ["an error"]);

TestRenderer.create(
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField validation={validation} link={link}>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -519,3 +518,3 @@

describe("customChange", () => {
it("allows the default change behavior to be overwritten with customChange", () => {
it("allows sibling fields to be overwritten", () => {
const formStateInner = ["one", "two", "three"];

@@ -534,9 +533,11 @@ const formState = mockFormState(formStateInner);

TestRenderer.create(
<ArrayField
link={link}
validation={validation}
customChange={customChange}
>
{renderFn}
</ArrayField>
<TestForm>
<ArrayField
link={link}
validation={validation}
customChange={customChange}
>
{renderFn}
</ArrayField>
</TestForm>
);

@@ -562,25 +563,22 @@

]);
// Validated the result of customChange
expect(validation).toHaveBeenCalledTimes(2);
expect(validation.mock.calls[1][0]).toEqual(["uno", "dos", "tres"]);
});
it("can return null to signal there was no custom change", () => {
const formStateInner = ["one", "two", "three"];
const formState = mockFormState(formStateInner);
const link = mockLink(formState);
const customChange = jest.fn((_oldValue, _newValue) => null);
const renderFn = jest.fn(() => null);
const validation = jest.fn(() => ["This is an error"]);
const validation = jest.fn(() => ["an error"]);
const customChange = jest.fn((_oldValue, _newValue) => null);
TestRenderer.create(
<ArrayField
link={link}
validation={validation}
customChange={customChange}
>
{renderFn}
</ArrayField>
const renderer = TestRenderer.create(
<Form initialValue={["one", "two", "three"]}>
{link => (
<ArrayField
validation={validation}
link={link}
customChange={customChange}
>
{renderFn}
</ArrayField>
)}
</Form>
);

@@ -596,4 +594,4 @@

// onChange should be called with the result of customChange
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([
const link = renderer.root.findByType(ArrayField).instance.props.link;
expect(link.formState).toEqual([
["one", "zwei", "three"],

@@ -609,109 +607,81 @@ expect.anything(),

it("doesn't break validations for child fields", () => {
const formStateInner = ["one", "two", "three"];
const formState = mockFormState(formStateInner);
const link = mockLink(formState);
const customChange = jest.fn((_oldValue, _newValue) => ["1", "2"]);
const childValidation = jest.fn(() => ["This is an error"]);
const parentValidation = jest.fn(() => [
"This is an error from the parent",
]);
const renderer = TestRenderer.create(
<ArrayField link={link} customChange={customChange}>
{links => (
<React.Fragment>
{links.map((link, i) => (
<TestField key={i} link={link} validation={childValidation} />
))}
</React.Fragment>
<Form initialValue={["1", "2"]}>
{link => (
<ArrayField
link={link}
customChange={customChange}
validation={parentValidation}
>
{links => (
<React.Fragment>
{links.map((link, i) => (
<TestField
key={i}
link={link}
validation={childValidation}
/>
))}
</React.Fragment>
)}
</ArrayField>
)}
</ArrayField>
</Form>
);
// 6 validations:
// 1) Child initial validation x3
// 2) Parent initial validation
// 3) Child validation on remount x3
// (No parent onValidation call, because it will use onChange)
// after mount, validate everything
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(2);
// 1) and 2)
expect(link.onValidation).toHaveBeenCalledTimes(4);
link.onValidation.mockClear();
// Now change one of the values
parentValidation.mockClear();
childValidation.mockClear();
const inner = renderer.root.findAllByType(TestInput)[0];
inner.instance.change("zach");
// 3)
expect(link.onValidation).toHaveBeenCalledTimes(3);
expect(link.onValidation).toHaveBeenCalledWith(
[{type: "array", index: 0}],
["This is an error"]
// Validate the whole subtree due to the customChange child validates
// once. Note that child validation will be called 3 times. Once after the
// change, then twice more after the customChange triggers a validation fo
// the entire subtree.
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(1 + 2);
const link = renderer.root.findByType(ArrayField).instance.props.link;
expect(link.formState).toEqual([["1", "2"], expect.anything()]);
});
it("doesn't create a new instance (i.e. remount)", () => {
const customChange = jest.fn((_oldValue, _newValue) => ["uno", "dos"]);
const renderer = TestRenderer.create(
<ArrayField
link={mockLink(mockFormState(["1", "2"]))}
customChange={customChange}
>
{links => <TestField link={links[0]} />}
</ArrayField>
);
expect(link.onValidation).toHaveBeenCalledWith(
[{type: "array", index: 1}],
["This is an error"]
);
// NOTE(zach): This may be surprising since there are only two values in
// the new value, but there is no guarantee that the next commit will
// have occurred yet.
expect(link.onValidation).toHaveBeenCalledWith(
[{type: "array", index: 2}],
["This is an error"]
);
// onChange should be called with the result of customChange
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([
["1", "2"],
{
type: "array",
data: {
errors: {
client: [],
server: "unchecked",
},
meta: {
touched: true,
changed: true,
succeeded: true,
asyncValidationInFlight: false,
},
},
children: [
{
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked",
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false,
},
},
},
{
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked",
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false,
},
},
},
],
},
]);
const testInstance = renderer.root.findAllByType(TestInput)[0].instance;
// now trigger a customChange, which used to cause a remount
testInstance.change("hi");
expect(customChange).toHaveBeenCalledTimes(1);
// but we no longer cause a remount, so the instances should be the same
const nextTestInstance = renderer.root.findAllByType(TestInput)[0]
.instance;
// Using Object.is here because toBe hangs as the objects are
// self-referential and thus not printable
expect(Object.is(testInstance, nextTestInstance)).toBe(true);
});
});
});

@@ -10,84 +10,98 @@ // @flow

import TestField, {TestInput} from "./TestField";
import TestForm from "./TestForm";
import {mapRoot} from "../shapedTree";
describe("Field", () => {
describe("validates on mount", () => {
it("ensures that the link inner type matches the type of the validation", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
it("ensures that the link inner type matches the type of the validation", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
// $ExpectError
<Field link={link} validation={(_e: empty) => []}>
{() => null}
</Field>;
// $ExpectError
<Field link={link} validation={(_e: empty) => []}>
{() => null}
</Field>;
<Field link={link} validation={(_e: string) => []}>
{() => null}
</Field>;
});
<Field link={link} validation={(_e: string) => []}>
{() => null}
</Field>;
});
it("Sets errors.client and meta.succeeded when there are no errors", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
const validation = jest.fn(() => []);
it("registers and unregisters for validation", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
const unregister = jest.fn();
const registerValidation = jest.fn(() => ({
replace: jest.fn(),
unregister,
}));
TestRenderer.create(<TestField link={link} validation={validation} />);
const renderer = TestRenderer.create(
<TestForm registerValidation={registerValidation}>
<Field link={link} validation={jest.fn(() => [])}>
{jest.fn(() => null)}
</Field>
</TestForm>
);
expect(validation).toHaveBeenCalledTimes(1);
expect(link.onValidation).toHaveBeenCalledTimes(1);
expect(registerValidation).toBeCalledTimes(1);
renderer.unmount();
expect(unregister).toBeCalledTimes(1);
});
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
it("calls replace when changing the validation function", () => {
const replace = jest.fn();
const registerValidation = jest.fn(() => ({
replace,
unregister: jest.fn(),
}));
it("Sets errors.client and meta.succeeded when there are errors", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
const validation = jest.fn(() => ["This is an error"]);
function Component() {
return (
<TestForm registerValidation={registerValidation}>
<Field
link={mockLink(mockFormState("Hello world."))}
validation={() => []}
>
{() => null}
</Field>
</TestForm>
);
}
TestRenderer.create(<TestField link={link} validation={validation} />);
const renderer = TestRenderer.create(<Component />);
expect(registerValidation).toBeCalledTimes(1);
expect(validation).toHaveBeenCalledTimes(1);
expect(link.onValidation).toHaveBeenCalledTimes(1);
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual(["This is an error"]);
});
it("Counts as successfully validated if there is no validation", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
TestRenderer.create(<TestField link={link} />);
expect(link.onValidation).toHaveBeenCalledTimes(1);
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
renderer.update(<Component />);
expect(replace).toBeCalledTimes(1);
});
it("calls the link onChange with new values and correct meta", () => {
it("validates new values and passes result to onChange", () => {
const formState = mockFormState("Hello world.");
const link = mockLink(formState);
const renderer = TestRenderer.create(<TestField link={link} />);
const updateNodeAtPath = jest.fn((path, formState) => formState);
const renderer = TestRenderer.create(
<TestForm updateNodeAtPath={updateNodeAtPath}>
<TestField link={link} />
</TestForm>
);
const inner = renderer.root.findByType(TestInput);
expect(updateNodeAtPath).toHaveBeenCalledTimes(0);
expect(link.onChange).toHaveBeenCalledTimes(0);
inner.instance.change("You've got mail");
expect(updateNodeAtPath).toHaveBeenCalledTimes(1);
expect(updateNodeAtPath).toHaveBeenCalledWith(
[],
["You've got mail", expect.anything()]
);
expect(link.onChange).toHaveBeenCalledTimes(1);
const [value, tree] = link.onChange.mock.calls[0][0];
expect(value).toBe("You've got mail");
expect(tree.data).toMatchObject({
meta: {
touched: true,
changed: true,
succeeded: true,
},
});
expect(link.onChange).toHaveBeenCalledWith([
"You've got mail",
formState[1],
]);
});

@@ -123,3 +137,3 @@

client: ["Some", "client", "errors"],
server: ["Server errors", "go here"],
external: ["External errors", "go here"],
},

@@ -138,3 +152,3 @@ }),

"errors",
"Server errors",
"External errors",
"go here",

@@ -179,3 +193,3 @@ ]);

formState[1].data.errors = {
server: ["A server error"],
external: ["An external error"],
client: ["A client error"],

@@ -199,3 +213,3 @@ };

unfilteredErrors: expect.arrayContaining([
"A server error",
"An external error",
"A client error",

@@ -202,0 +216,0 @@ ]),

@@ -10,5 +10,7 @@ // @flow

import Field from "../Field";
import type {FieldLink} from "../types";
import {expectLink, mockFormState} from "./tools";
import TestField, {TestInput} from "./TestField";
import LinkTap from "../testutils/LinkTap";
import {forgetShape} from "../shapedTree";

@@ -45,4 +47,250 @@

describe("Form", () => {
describe("validations", () => {
it("runs validations", () => {
const objectValidation = jest.fn(() => []);
const arrayValidation = jest.fn(() => []);
const arrayElValidation = jest.fn(() => []);
const fieldValidation = jest.fn(() => []);
TestRenderer.create(
<Form initialValue={{a: ["1", "2"], s: "string"}}>
{link => (
<ObjectField link={link} validation={objectValidation}>
{links => (
<>
<ArrayField link={links.a} validation={arrayValidation}>
{links =>
links.map((link, i) => (
<TestField
key={i}
link={link}
validation={arrayElValidation}
/>
))
}
</ArrayField>
<TestField link={links.s} validation={fieldValidation} />
</>
)}
</ObjectField>
)}
</Form>
);
expect(objectValidation).toHaveBeenCalledTimes(1);
expect(objectValidation).toHaveBeenCalledWith({
a: ["1", "2"],
s: "string",
});
expect(arrayValidation).toHaveBeenCalledTimes(1);
expect(arrayValidation).toHaveBeenCalledWith(["1", "2"]);
expect(arrayElValidation).toHaveBeenCalledTimes(2);
expect(arrayElValidation).toHaveBeenCalledWith("1");
expect(arrayElValidation).toHaveBeenCalledWith("2");
expect(fieldValidation).toHaveBeenCalledTimes(1);
expect(fieldValidation).toHaveBeenCalledWith("string");
});
it("sets validation information on formState", () => {
const objectValidation = jest.fn(() => ["object error"]);
const arrayValidation = jest.fn(() => ["array", "error"]);
const arrayElValidation = jest.fn(s => [`error ${s}`]);
const fieldValidation = jest.fn(() => []);
const renderer = TestRenderer.create(
<Form initialValue={{a: ["1", "2"], s: "string"}}>
{link => (
<ObjectField link={link} validation={objectValidation}>
{links => (
<>
<ArrayField link={links.a} validation={arrayValidation}>
{links =>
links.map((link, i) => (
<TestField
key={i}
link={link}
validation={arrayElValidation}
/>
))
}
</ArrayField>
<TestField link={links.s} validation={fieldValidation} />
</>
)}
</ObjectField>
)}
</Form>
);
const formState = renderer.root.findByType(ObjectField).instance.props
.link.formState;
let node = formState[1];
expect(node.data.errors.client).toEqual(["object error"]);
expect(node.data.meta.succeeded).toBe(false);
node = node.children.a;
expect(node.data.errors.client).toEqual(["array", "error"]);
expect(node.data.meta.succeeded).toBe(false);
const child0 = node.children[0];
expect(child0.data.errors.client).toEqual(["error 1"]);
expect(child0.data.meta.succeeded).toBe(false);
const child1 = node.children[1];
expect(child1.data.errors.client).toEqual(["error 2"]);
expect(child1.data.meta.succeeded).toBe(false);
node = formState[1].children.s;
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
});
it("treats no validation as always passing", () => {
const renderer = TestRenderer.create(
<Form initialValue={{a: ["1", "2"], s: "string"}}>
{link => (
<ObjectField link={link}>
{links => (
<>
<ArrayField link={links.a}>
{links =>
links.map((link, i) => <TestField key={i} link={link} />)
}
</ArrayField>
<TestField link={links.s} />
</>
)}
</ObjectField>
)}
</Form>
);
const formState = renderer.root.findByType(ObjectField).instance.props
.link.formState;
let node = formState[1];
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
node = node.children.a;
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
const child0 = node.children[0];
expect(child0.data.errors.client).toEqual([]);
expect(child0.data.meta.succeeded).toBe(true);
const child1 = node.children[1];
expect(child1.data.errors.client).toEqual([]);
expect(child1.data.meta.succeeded).toBe(true);
node = formState[1].children.s;
expect(node.data.errors.client).toEqual([]);
expect(node.data.meta.succeeded).toBe(true);
});
it("validates newly mounted Fields", () => {
function expectClientErrors(link) {
const tree = link.formState[1];
return expect(forgetShape(tree).data.errors.client);
}
const renderFn = jest.fn(() => null);
const validationA = jest.fn(() => ["error a"]);
const validationB = jest.fn(() => ["error b"]);
const renderer = TestRenderer.create(
<Form initialValue={{key: "hello"}}>
{link => (
<ObjectField link={link}>
{link => (
<>
<LinkTap link={link.key}>{renderFn}</LinkTap>
<TestField link={link.key} validation={validationA} />
{/*<TestField link={link.key} validation={validationB} />*/}
</>
)}
</ObjectField>
)}
</Form>
);
expect(renderFn).toHaveBeenCalledTimes(2);
// on the initial render, we expose "pending" as part of our API
// TODO(dmnd): It'd be nice if we could avoid this.
expectClientErrors(renderFn.mock.calls[0][0]).toEqual("pending");
// After the second render the error arrives.
expectClientErrors(renderFn.mock.calls[1][0]).toEqual(["error a"]);
expect(validationA).toHaveBeenCalledTimes(1);
expect(validationA).toHaveBeenCalledWith("hello");
// When a new Field is mounted, we expect the errors to show up.
renderFn.mockClear();
validationA.mockClear();
renderer.update(
<Form initialValue={{key: "hello"}}>
{link => (
<ObjectField link={link}>
{link => (
<>
<LinkTap link={link.key}>{renderFn}</LinkTap>
<TestField link={link.key} validation={validationA} />
<TestField link={link.key} validation={validationB} />
</>
)}
</ObjectField>
)}
</Form>
);
expect(renderFn).toHaveBeenCalledTimes(2);
// There's an initial render where the field hasn't yet been validated
// TODO(dmnd): It'd be nice if we could avoid this.
expectClientErrors(renderFn.mock.calls[0][0]).toEqual(["error a"]);
// After the update, the new error should be present.
expectClientErrors(renderFn.mock.calls[1][0]).toEqual([
"error a",
"error b",
]);
// Validation functions should receive the correct parameters. These
// assertions protect against bugs that confuse relative and absolute
// paths/values.
expect(validationA).toHaveBeenCalledTimes(1);
expect(validationA).toHaveBeenCalledWith("hello");
expect(validationB).toHaveBeenCalledTimes(1);
expect(validationB).toHaveBeenCalledWith("hello");
});
it("updates errors when a new validation function is provided via props", () => {
const renderer = TestRenderer.create(
<Form initialValue="hello">
{link => <TestField link={link} validation={() => ["error 1"]} />}
</Form>
);
let link = renderer.root.findAllByType(TestField)[0].instance.props.link;
let errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 1"]);
renderer.update(
<Form initialValue="hello">
{link => <TestField link={link} validation={() => ["error 2"]} />}
</Form>
);
link = renderer.root.findAllByType(TestField)[0].instance.props.link;
errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 2"]);
});
});
describe("Form manages form state", () => {
it("creates the initial formState from initialValue and serverErrors", () => {
it("creates the initial formState from initialValue and externalErrors", () => {
const onSubmit = jest.fn();

@@ -54,3 +302,3 @@ const renderFn = jest.fn(() => null);

onSubmit={onSubmit}
serverErrors={{"/": ["Server error", "Another server error"]}}
externalErrors={{"/": ["External error", "Another external error"]}}
>

@@ -78,3 +326,3 @@ {renderFn}

client: "pending",
server: ["Server error", "Another server error"],
external: ["External error", "Another external error"],
},

@@ -84,3 +332,3 @@ });

it("parses and sets complex server errors", () => {
it("parses and sets complex external errors", () => {
const onSubmit = jest.fn();

@@ -95,3 +343,3 @@ const renderFn = jest.fn(() => null);

onSubmit={onSubmit}
serverErrors={{
externalErrors={{
"/": ["Root error"],

@@ -115,14 +363,14 @@ "/simple": ["One", "level", "down"],

const root: any = tree;
expect(root.data.errors.server).toEqual(["Root error"]);
expect(root.data.errors.external).toEqual(["Root error"]);
const simple = root.children.simple;
expect(simple.data.errors.server).toEqual(["One", "level", "down"]);
expect(simple.data.errors.external).toEqual(["One", "level", "down"]);
const complex = root.children.complex;
expect(complex.data.errors.server).toEqual([]);
expect(complex.data.errors.external).toEqual([]);
const complex0 = complex.children[0];
expect(complex0.data.errors.server).toEqual(["in an", "array"]);
expect(complex0.data.errors.external).toEqual(["in an", "array"]);
const complex1 = complex.children[1];
expect(complex1.data.errors.server).toEqual([]);
expect(complex1.data.errors.external).toEqual([]);
});
it("updates the server errors", () => {
it("updates the external errors", () => {
const onSubmit = jest.fn();

@@ -136,3 +384,3 @@ const renderFn = jest.fn(() => null);

onSubmit={onSubmit}
serverErrors={{
externalErrors={{
"/array": ["Cannot be empty"],

@@ -158,3 +406,3 @@ }}

onSubmit={onSubmit}
serverErrors={{
externalErrors={{
"/array": [],

@@ -175,9 +423,19 @@ "/array/0": ["inner error"],

const root: any = tree;
expect(root.data.errors.server).toEqual([]);
expect(root.data.errors.external).toEqual([]);
const array = root.children.array;
expect(array.data.errors.server).toEqual([]);
expect(array.data.errors.external).toEqual([]);
const array0 = array.children[0];
expect(array0.data.errors.server).toEqual(["inner error"]);
expect(array0.data.errors.external).toEqual(["inner error"]);
});
it("doesn't cause an infinite loop when using inline validation function", () => {
expect(() => {
TestRenderer.create(
<Form initialValue="hello">
{link => <TestField link={link} validation={() => []} />}
</Form>
);
}).not.toThrow(/Maximum update depth exceeded/);
});
it("collects the initial validations", () => {

@@ -415,3 +673,3 @@ // This test is not very unit-y, but that's okay! It's more useful to

onSubmit={jest.fn()}
serverErrors={{"/": ["Server error", "Another server error"]}}
externalErrors={{"/": ["External error", "Another external error"]}}
>

@@ -430,4 +688,4 @@ {renderFn}

unfilteredErrors: expect.arrayContaining([
"Server error",
"Another server error",
"External error",
"Another external error",
]),

@@ -441,2 +699,150 @@ // Currently, only care about client errors

});
it("removes errors when a child unmounts", () => {
const validation1 = jest.fn(() => ["error 1"]);
const validation2 = jest.fn(() => ["error 2"]);
class TestForm extends React.Component<{
hideSecondField: boolean,
link: FieldLink<{string1: string, string2: string}>,
}> {
render() {
return (
<ObjectField link={this.props.link}>
{links => (
<>
<TestField
key={"1"}
link={links.string1}
validation={validation1}
/>
{this.props.hideSecondField ? null : (
<TestField
key={"2"}
link={links.string2}
validation={validation2}
/>
)}
</>
)}
</ObjectField>
);
}
}
const renderer = TestRenderer.create(
<Form
initialValue={{
string1: "hello",
string2: "world",
}}
>
{link => <TestForm link={link} hideSecondField={false} />}
</Form>
);
expect(validation1).toHaveBeenCalledTimes(1);
expect(validation2).toHaveBeenCalledTimes(1);
let rootFormState = renderer.root.findByType(TestForm).instance.props.link
.formState[1];
let string1Errors = rootFormState.children.string1.data.errors.client;
expect(string1Errors).toEqual(["error 1"]);
let string2Errors = rootFormState.children.string2.data.errors.client;
expect(string2Errors).toEqual(["error 2"]);
// now hide the second field, causing it to unmount and unregister the
// validation handler
renderer.update(
<Form
initialValue={{
string1: "hello",
string2: "world",
}}
>
{link => <TestForm link={link} hideSecondField={true} />}
</Form>
);
// no addition validation calls
expect(validation1).toHaveBeenCalledTimes(1);
expect(validation2).toHaveBeenCalledTimes(1);
rootFormState = renderer.root.findByType(TestForm).instance.props.link
.formState[1];
// error for string1 remains
string1Errors = rootFormState.children.string1.data.errors.client;
expect(string1Errors).toEqual(["error 1"]);
// string2's error is gone
string2Errors = rootFormState.children.string2.data.errors.client;
expect(string2Errors).toEqual([]);
});
it("runs all validations when a link has multiple fields", () => {
const validation1 = jest.fn(() => ["error 1"]);
const validation2 = jest.fn(() => ["error 2"]);
const renderer = TestRenderer.create(
<Form initialValue="hello">
{link => (
<>
{/* note both fields point to the same link!! */}
<TestField key={"1"} link={link} validation={validation1} />
<TestField key={"2"} link={link} validation={validation2} />
</>
)}
</Form>
);
expect(validation1).toHaveBeenCalledTimes(1);
expect(validation2).toHaveBeenCalledTimes(1);
renderer.root.findAllByType(TestInput)[0].instance.change("dmnd");
expect(validation1).toHaveBeenCalledTimes(2);
expect(validation2).toHaveBeenCalledTimes(2);
renderer.root.findAllByType(TestInput)[1].instance.change("zach");
expect(validation1).toHaveBeenCalledTimes(3);
expect(validation2).toHaveBeenCalledTimes(3);
});
it("only removes errors from validation that was unmounted", () => {
const validation1 = jest.fn(() => ["error 1"]);
const validation2 = jest.fn(() => ["error 2"]);
const renderer = TestRenderer.create(
<Form initialValue="hello">
{link => (
<>
{/* note both fields point to the same link!! */}
<TestField key={"1"} link={link} validation={validation1} />
<TestField key={"2"} link={link} validation={validation2} />
</>
)}
</Form>
);
let link = renderer.root.findAllByType(TestField)[0].instance.props.link;
let errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 1", "error 2"]);
renderer.update(
<Form initialValue="hello">
{link => (
<>
<TestField key={"1"} link={link} validation={validation1} />
</>
)}
</Form>
);
link = renderer.root.findAllByType(TestField)[0].instance.props.link;
errors = link.formState[1].data.errors.client;
expect(errors).toEqual(["error 1"]);
});
});

@@ -546,2 +952,84 @@

});
describe("performance", () => {
it("batches setState calls when unmounting components", () => {
// Record the number of render and commit phases
let renders = 0;
let commits = 0;
class RenderCalls extends React.Component<{||}> {
componentDidUpdate() {
commits += 1;
}
render() {
renders += 1;
return (
<div>
Rendered {renders} times, committed {commits} times.
</div>
);
}
}
const validation = jest.fn();
// N in the O(N) sense for this perf test.
const N = 10;
const renderer = TestRenderer.create(
<Form initialValue={"A string"}>
{link => (
<>
<RenderCalls />
<>
{[...Array(N).keys()].map(i => (
<Field key={i} link={link} validation={validation}>
{() => <div>input</div>}
</Field>
))}
</>
</>
)}
</Form>
);
// One render for initial mount, and another after Form validates
// everything from componentDidMount.
// TODO(dmnd): Can we adjust implementation to make this only render once?
expect(renders).toBe(1 + 1);
expect(commits).toBe(1);
expect(validation).toBeCalledTimes(N);
renders = 0;
commits = 0;
validation.mockClear();
// now unmount all the fields
renderer.update(
<Form initialValue={"A string"}>
{() => (
<>
<RenderCalls />
</>
)}
</Form>
);
// We expect only two renders. The first to build the VDOM without Fields.
// Then during reconciliation React realizes the Fields have to unmount,
// so it calls componentWillUnmount on each Field, which then causes a
// setState for each Field. But then we expect that React batches all
// these setStates into a single render, not one render per each Field.
expect(renders).toBe(1 + 1);
expect(renders).not.toBe(1 + N);
// Similarly, we expect only 2 commits, not one for each Field. You might
// expect only a single commit, but componentWillUnmount happens during
// the commit phase, so when setState is called React enqueues another
// render phase which commits separately. Oh well. At least the number of
// commits is constant!
expect(commits).toBe(1 + 1);
expect(commits).not.toBe(1 + N);
});
});
});

@@ -7,2 +7,3 @@ // @flow

import ObjectField from "../ObjectField";
import Form from "../Form";
import {type FieldLink} from "../types";

@@ -12,2 +13,4 @@

import TestField, {TestInput} from "./TestField";
import TestForm from "./TestForm";
import LinkTap from "../testutils/LinkTap";

@@ -21,115 +24,107 @@ describe("ObjectField", () => {

describe("ObjectField is a field", () => {
describe("validates on mount", () => {
it("ensures that the link inner type matches the type of the validation", () => {
type TestObject = {|
string: string,
number: number,
|};
const formStateInner: TestObject = {
string: "hello",
number: 42,
};
const formState = mockFormState(formStateInner);
const link: FieldLink<TestObject> = mockLink(formState);
describe("is a field", () => {
it("ensures that the link inner type matches the type of the validation", () => {
type TestObject = {|
string: string,
number: number,
|};
const formStateInner: TestObject = {
string: "hello",
number: 42,
};
const formState = mockFormState(formStateInner);
const link: FieldLink<TestObject> = mockLink(formState);
// $ExpectError
<ObjectField link={link} validation={(_e: empty) => []}>
{() => null}
</ObjectField>;
// $ExpectError
<ObjectField link={link} validation={(_e: empty) => []}>
{() => null}
</ObjectField>;
// $ExpectError
<ObjectField link={link} validation={(_e: {|string: string|}) => []}>
{() => null}
</ObjectField>;
// $ExpectError
<ObjectField link={link} validation={(_e: {|string: string|}) => []}>
{() => null}
</ObjectField>;
<ObjectField link={link} validation={(_e: TestObject) => []}>
{() => null}
</ObjectField>;
});
<ObjectField link={link} validation={(_e: TestObject) => []}>
{() => null}
</ObjectField>;
});
it("Sets errors.client and meta.succeeded when there are no errors", () => {
const validation = jest.fn(() => []);
const formState = mockFormState({inner: "value"});
const link = mockLink(formState);
it("Registers and unregisters for validation", () => {
const formState = mockFormState({inner: "value"});
const link = mockLink(formState);
const unregister = jest.fn();
const registerValidation = jest.fn(() => ({
replace: jest.fn(),
unregister,
}));
TestRenderer.create(
<ObjectField link={link} validation={validation}>
const renderer = TestRenderer.create(
<TestForm registerValidation={registerValidation}>
<ObjectField link={link} validation={jest.fn(() => [])}>
{jest.fn(() => null)}
</ObjectField>
);
</TestForm>
);
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
expect(registerValidation).toBeCalledTimes(1);
renderer.unmount();
expect(unregister).toBeCalledTimes(1);
});
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
it("calls replace when changing the validation function", () => {
const replace = jest.fn();
const registerValidation = jest.fn(() => ({
replace,
unregister: jest.fn(),
}));
it("Sets errors.client and meta.succeeded when there are errors", () => {
const validation = jest.fn(() => ["This is an error"]);
const formState = mockFormState({inner: "value"});
const link = mockLink(formState);
TestRenderer.create(
<ObjectField link={link} validation={validation}>
{jest.fn(() => null)}
</ObjectField>
function Component() {
return (
<TestForm registerValidation={registerValidation}>
<ObjectField
link={mockLink(mockFormState({hello: "world"}))}
validation={() => []}
>
{() => null}
</ObjectField>
</TestForm>
);
}
expect(validation).toHaveBeenCalledTimes(1);
expect(validation).toHaveBeenCalledWith(formState[0]);
expect(link.onValidation).toHaveBeenCalledTimes(1);
const renderer = TestRenderer.create(<Component />);
expect(registerValidation).toBeCalledTimes(1);
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual(["This is an error"]);
});
renderer.update(<Component />);
expect(replace).toBeCalledTimes(1);
});
it("Treats no validation as always passing", () => {
const formState = mockFormState({inner: "value"});
const link = mockLink(formState);
it("Passes additional information to its render function", () => {
const formState = mockFormState({inner: "value"});
// $FlowFixMe
formState[1].data.errors = {
external: ["An external error"],
client: ["A client error"],
};
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(
<ObjectField link={link}>{jest.fn(() => null)}</ObjectField>
);
TestRenderer.create(<ObjectField link={link}>{renderFn}</ObjectField>);
expect(link.onValidation).toHaveBeenCalledTimes(1);
const [path, errors] = link.onValidation.mock.calls[0];
expect(path).toEqual([]);
expect(errors).toEqual([]);
});
it("Passes additional information to its render function", () => {
const formState = mockFormState({inner: "value"});
// $FlowFixMe
formState[1].data.errors = {
server: ["A server error"],
client: ["A client error"],
};
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(<ObjectField link={link}>{renderFn}</ObjectField>);
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining([
"A server error",
"A client error",
]),
valid: false,
asyncValidationInFlight: false,
value: {inner: "value"},
})
);
});
expect(renderFn).toHaveBeenCalled();
expect(renderFn).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
touched: false,
changed: false,
shouldShowErrors: expect.anything(),
unfilteredErrors: expect.arrayContaining([
"An external error",
"A client error",
]),
valid: false,
asyncValidationInFlight: false,
value: {inner: "value"},
})
);
});

@@ -196,26 +191,37 @@ });

it("calls onChange when a child changes", () => {
const formStateInner = {
it("validates new values from children and passes result to onChange", () => {
const formState = mockFormState({
string: "hello",
number: 42,
};
const formState = mockFormState(formStateInner);
});
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(<ObjectField link={link}>{renderFn}</ObjectField>);
const updateNodeAtPath = jest.fn((path, formState) => formState);
TestRenderer.create(
<TestForm updateNodeAtPath={updateNodeAtPath}>
<ObjectField link={link}>{renderFn}</ObjectField>
</TestForm>
);
expect(updateNodeAtPath).toHaveBeenCalledTimes(0);
expect(link.onChange).toHaveBeenCalledTimes(0);
// call the child onChange
const objectLinks = renderFn.mock.calls[0][0];
// call the child onChange
const newChildMeta = mockFormState("newString");
objectLinks.string.onChange(newChildMeta);
expect(link.onChange).toHaveBeenCalled();
const newObjectFormState = link.onChange.mock.calls[0][0];
expect(newObjectFormState[0]).toHaveProperty("string", "newString");
expect(newObjectFormState[1].data.meta).toMatchObject({
touched: true,
changed: true,
});
expect(newObjectFormState[1].children.string).toBe(newChildMeta[1]);
expect(updateNodeAtPath).toHaveBeenCalledTimes(1);
expect(updateNodeAtPath).toHaveBeenCalledWith(
[],
[{string: "newString", number: 42}, expect.anything()]
);
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([
{string: "newString", number: 42},
formState[1],
]);
});

@@ -248,31 +254,3 @@

it("calls onValidation when a child runs validations", () => {
const formStateInner = {
string: "hello",
number: 42,
};
const formState = mockFormState(formStateInner);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);
TestRenderer.create(<ObjectField link={link}>{renderFn}</ObjectField>);
const objectLinks = renderFn.mock.calls[0][0];
// call the child onValidation
objectLinks.string.onValidation([], ["Some", "errors"]);
expect(link.onValidation).toHaveBeenCalledTimes(2);
// Important: the first call to onValidation is for the initial render validation
const [path, errors] = link.onValidation.mock.calls[1];
expect(path).toEqual([{type: "object", key: "string"}]);
expect(errors).toEqual(["Some", "errors"]);
});
it("calls its own validation when a child changes", () => {
const formStateInner = {
string: "hello",
number: 42,
};
const formState = mockFormState(formStateInner);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -282,5 +260,14 @@ const validation = jest.fn(() => ["This is an error"]);

TestRenderer.create(
<ObjectField link={link} validation={validation}>
{renderFn}
</ObjectField>
<Form
initialValue={{
string: "hello",
number: 42,
}}
>
{link => (
<ObjectField link={link} validation={validation}>
{renderFn}
</ObjectField>
)}
</Form>
);

@@ -304,3 +291,3 @@

describe("customChange", () => {
it("allows the default change behavior to be overwritten with customChange", () => {
it("allows sibling fields to be overwritten", () => {
const formStateInner = {

@@ -313,3 +300,2 @@ string: "hello",

const renderFn = jest.fn(() => null);
const validation = jest.fn(() => ["This is an error"]);

@@ -322,9 +308,11 @@ const customChange = jest.fn((_oldValue, _newValue) => ({

TestRenderer.create(
<ObjectField
link={link}
validation={validation}
customChange={customChange}
>
{renderFn}
</ObjectField>
<TestForm>
<ObjectField
link={link}
validation={jest.fn(() => ["This is an error"])}
customChange={customChange}
>
{renderFn}
</ObjectField>
</TestForm>
);

@@ -359,18 +347,5 @@

]);
// Validated the result of customChange
expect(validation).toHaveBeenCalledTimes(2);
expect(validation.mock.calls[1][0]).toEqual({
string: "A whole new value",
number: 0,
});
});
it("can return null to signal there was no custom change", () => {
const formStateInner = {
string: "hello",
number: 42,
};
const formState = mockFormState(formStateInner);
const link = mockLink(formState);
const renderFn = jest.fn(() => null);

@@ -381,10 +356,19 @@ const validation = jest.fn(() => ["This is an error"]);

TestRenderer.create(
<ObjectField
link={link}
validation={validation}
customChange={customChange}
const renderer = TestRenderer.create(
<Form
initialValue={{
string: "hello",
number: 42,
}}
>
{renderFn}
</ObjectField>
{link => (
<ObjectField
link={link}
validation={validation}
customChange={customChange}
>
{renderFn}
</ObjectField>
)}
</Form>
);

@@ -399,5 +383,5 @@

// onChange should be called with the result of customChange
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([
const link = renderer.root.findByType(ObjectField).instance.props.link;
// the value we get out is as if customChange didn't exist
expect(link.formState).toEqual([
{

@@ -419,9 +403,2 @@ string: "newString",

it("doesn't break validations for child fields", () => {
const formStateInner = {
string: "hello",
string2: "goodbye",
};
const formState = mockFormState(formStateInner);
const link = mockLink(formState);
const customChange = jest.fn((_oldValue, _newValue) => ({

@@ -433,41 +410,52 @@ string: "a whole new value",

const childValidation = jest.fn(() => ["This is an error"]);
const parentValidation = jest.fn(() => [
"This is an error from the parent",
]);
const renderer = TestRenderer.create(
<ObjectField link={link} customChange={customChange}>
{links => (
<React.Fragment>
<TestField link={links.string} validation={childValidation} />
<TestField link={links.string2} validation={childValidation} />
</React.Fragment>
<Form
initialValue={{
string: "hello",
string2: "goodbye",
}}
>
{link => (
<ObjectField
link={link}
customChange={customChange}
validation={parentValidation}
>
{links => (
<React.Fragment>
<TestField link={links.string} validation={childValidation} />
<TestField
link={links.string2}
validation={childValidation}
/>
</React.Fragment>
)}
</ObjectField>
)}
</ObjectField>
</Form>
);
// 5 validations:
// 1) Child initial validation x2
// 2) Parent initial validation
// 3) Child validation on remount x2
// (No parent onValidation call, because it will use onChange)
// after mount, validate everything
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(2);
// 1) and 2)
expect(link.onValidation).toHaveBeenCalledTimes(3);
link.onValidation.mockClear();
// Now change one of the values
parentValidation.mockClear();
childValidation.mockClear();
const inner = renderer.root.findAllByType(TestInput)[0];
inner.instance.change("zach");
// 3)
expect(link.onValidation).toHaveBeenCalledTimes(2);
expect(link.onValidation).toHaveBeenCalledWith(
[{type: "object", key: "string"}],
["This is an error"]
);
expect(link.onValidation).toHaveBeenCalledWith(
[{type: "object", key: "string2"}],
["This is an error"]
);
// Validate the whole subtree due to the customChange child validates
// once. Note that child validation will be called 3 times. Once after the
// change, then twice more after the customChange triggers a validation fo
// the entire subtree.
expect(parentValidation).toHaveBeenCalledTimes(1);
expect(childValidation).toHaveBeenCalledTimes(1 + 2);
// onChange should be called with the result of customChange
expect(link.onChange).toHaveBeenCalledTimes(1);
expect(link.onChange).toHaveBeenCalledWith([
const link = renderer.root.findByType(ObjectField).instance.props.link;
expect(link.formState).toEqual([
{

@@ -477,51 +465,93 @@ string: "a whole new value",

},
expect.anything(),
]);
});
it("doesn't create a new instance (i.e. remount)", () => {
const customChange = jest.fn((_oldValue, _newValue) => ({
string: "A whole new value",
number: 0,
}));
const renderer = TestRenderer.create(
<ObjectField
link={mockLink(
mockFormState({
string: "hello",
number: 42,
})
)}
customChange={customChange}
>
{links => <TestField link={links.string} />}
</ObjectField>
);
const testInstance = renderer.root.findAllByType(TestInput)[0].instance;
// now trigger a customChange, which used to cause a remount
testInstance.change("hi");
expect(customChange).toHaveBeenCalledTimes(1);
// but we no longer cause a remount, so the instances should be the same
const nextTestInstance = renderer.root.findAllByType(TestInput)[0]
.instance;
// Using Object.is here because toBe hangs as the objects are
// self-referential and thus not printable
expect(Object.is(testInstance, nextTestInstance)).toBe(true);
});
it("works fine even if not at the root of the form", () => {
const customChange = jest.fn((_oldValue, _newValue) => ({
string: "A whole new value",
number: 0,
}));
const objectRenderFn = jest.fn(() => null);
const linkTapFn = jest.fn(() => null);
TestRenderer.create(
<Form
initialValue={{
uncle: "Bob",
nested: {
string: "hello",
number: 42,
},
}}
>
{link => (
<ObjectField link={link}>
{link => (
<>
<TestField link={link.uncle} />
<LinkTap link={link.nested}>{linkTapFn}</LinkTap>
<ObjectField
link={link.nested}
validation={jest.fn(() => ["This is an error"])}
customChange={customChange}
>
{objectRenderFn}
</ObjectField>
</>
)}
</ObjectField>
)}
</Form>
);
linkTapFn.mockClear();
// call the child onChange to trigger a customChange
const objectLinks = objectRenderFn.mock.calls[0][0];
objectLinks.string.onChange(mockFormState("newString"));
// onChange should be called with the result of customChange
expect(linkTapFn).toHaveBeenCalledTimes(1);
expect(linkTapFn.mock.calls[0][0].formState).toEqual([
{
type: "object",
data: {
errors: {
client: [],
server: "unchecked",
},
meta: {
touched: true,
changed: true,
succeeded: true,
asyncValidationInFlight: false,
},
},
children: {
string: {
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked",
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false,
},
},
},
string2: {
type: "leaf",
data: {
errors: {
// Validations happen after the initial onchange
client: "pending",
server: "unchecked",
},
meta: {
touched: true,
changed: true,
succeeded: false,
asyncValidationInFlight: false,
},
},
},
},
string: "A whole new value",
number: 0,
},
expect.anything(),
]);

@@ -528,0 +558,0 @@ });

@@ -21,6 +21,12 @@ // @flow

expect.objectContaining({
// TODO(dmnd): Would be nice if we could do something like
// path: expect.arrayContaining(
// expect.objectContaining({
// type: expect.stringMatching(/(object)|(array)/),
// })
// ),
path: expect.anything(),
formState: expect.anything(),
onChange: expect.any(Function),
onBlur: expect.any(Function),
onValidation: expect.any(Function),
})

@@ -33,7 +39,7 @@ );

return {
path: [],
formState,
onChange: jest.fn(),
onBlur: jest.fn(),
onValidation: jest.fn(),
};
}

@@ -5,5 +5,5 @@ // @flow strict

import type {FieldLink, Extras, ClientErrors} from "../types";
import type {FieldLink, Extras} from "../types";
import type {FormState} from "../formState";
import type {ShapedTree, ShapedPath} from "../shapedTree";
import type {ShapedTree} from "../shapedTree";

@@ -14,3 +14,2 @@ type Props<T> = {|

+onBlur?: () => void,
+onValidation?: (path: ShapedPath<T>, errors: ClientErrors) => void,
+children: (link: FieldLink<T>) => React.Node,

@@ -36,19 +35,9 @@ |};

handleValidation: (ShapedPath<T>, ClientErrors) => void = (
path: ShapedPath<T>,
errors: ClientErrors
) => {
if (this.props.onValidation) {
this.props.onValidation(path, errors);
}
this.props.link.onValidation(path, errors);
};
render() {
const {link} = this.props;
const tappedLink: FieldLink<T> = {
path: link.path,
formState: link.formState,
onChange: this.handleChange,
onBlur: this.handleBlur,
onValidation: this.handleValidation,
};

@@ -55,0 +44,0 @@

// @flow strict
import type {ShapedTree, ShapedPath} from "./shapedTree";
import type {Path} from "./tree";
import {type FormState} from "./formState";
export type ClientErrors = Array<string> | "pending";
export type ServerErrors = Array<string> | "unchecked";
export type ExternalErrors = Array<string> | "unchecked";
export type Err = {
client: ClientErrors,
server: ServerErrors,
external: ExternalErrors,
};

@@ -34,3 +35,3 @@

client: "pending",
server: "unchecked",
external: "unchecked",
};

@@ -62,3 +63,3 @@

+onBlur: OnBlur<T>,
+onValidation: OnValidation<T>,
+path: Path,
|};

@@ -68,2 +69,2 @@

export type CustomChange<T> = (oldValue: T, newValue: T) => null | T;
export type CustomChange<T> = (prevValue: T, nextValue: T) => null | T;

@@ -127,1 +127,13 @@ // @flow strict

}
export function equals<E>(a: $ReadOnlyArray<E>, b: $ReadOnlyArray<E>): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc