formula-one
Advanced tools
Comparing version 0.2.0 to 0.2.1
107
dist/Form.js
@@ -8,7 +8,7 @@ "use strict"; | ||
var _slicedToArray = function () { function sliceIterator(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"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
var _slicedToArray = function () { function sliceIterator(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"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); // strict | ||
@@ -21,6 +21,8 @@ var _react = require("react"); | ||
var _formState = require("./formState"); | ||
require("./formState"); | ||
var _shapedTree = require("./shapedTree"); | ||
var _tree = require("./tree"); | ||
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)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
@@ -32,3 +34,3 @@ | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // strict | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
@@ -43,11 +45,48 @@ var FormContext = exports.FormContext = React.createContext({ | ||
function newFormState(value, serverErrors) { | ||
var cleanState = [value, (0, _shapedTree.treeFromValue)(value, { | ||
errors: _types.cleanErrors, | ||
meta: _types.cleanMeta | ||
})]; | ||
if (serverErrors != null) { | ||
return (0, _formState.replaceServerErrors)(serverErrors, cleanState); | ||
function applyServerErrorsToFormState(serverErrors, formState) { | ||
var _formState = _slicedToArray(formState, 2), | ||
value = _formState[0], | ||
oldTree = _formState[1]; | ||
var tree = void 0; | ||
if (serverErrors !== null) { | ||
// If keys do not appear, no errors | ||
tree = (0, _shapedTree.mapShapedTree)(function (_ref) { | ||
var errors = _ref.errors, | ||
meta = _ref.meta; | ||
return { | ||
errors: _extends({}, errors, { server: [] }), | ||
meta: meta | ||
}; | ||
}, oldTree); | ||
Object.keys(serverErrors).forEach(function (key) { | ||
var newErrors = serverErrors[key]; | ||
var path = (0, _shapedTree.shapePath)(value, (0, _tree.pathFromPathString)(key)); | ||
if (path != null) { | ||
// TODO(zach): make some helper functions that do this | ||
tree = (0, _shapedTree.updateAtPath)(path, function (_ref2) { | ||
var errors = _ref2.errors, | ||
meta = _ref2.meta; | ||
return { | ||
errors: _extends({}, errors, { server: newErrors }), | ||
meta: meta | ||
}; | ||
}, tree); | ||
} else { | ||
console.error("Warning: couldn't match error with path " + key + " to value " + JSON.stringify(value)); | ||
} | ||
}); | ||
} else { | ||
tree = (0, _shapedTree.mapShapedTree)(function (_ref3) { | ||
var errors = _ref3.errors, | ||
meta = _ref3.meta; | ||
return { | ||
errors: _extends({}, errors, { server: [] }), | ||
meta: meta | ||
}; | ||
}, oldTree); | ||
} | ||
return cleanState; | ||
return [value, tree]; | ||
} | ||
@@ -81,5 +120,5 @@ | ||
if (props.serverErrors !== state.oldServerErrors) { | ||
var serverErrorsTree = Form.makeServerErrorTree(state.formState[0], props.serverErrors); | ||
var newFormState = applyServerErrorsToFormState(props.serverErrors, state.formState); | ||
return { | ||
formState: (0, _formState.replaceServerErrors)(serverErrorsTree, state.formState), | ||
formState: newFormState, | ||
oldServerErrors: props.serverErrors | ||
@@ -90,20 +129,2 @@ }; | ||
} | ||
}, { | ||
key: "makeServerErrorTree", | ||
value: function makeServerErrorTree(value, errorsObj) { | ||
if (errorsObj != null) { | ||
try { | ||
var freshTree = (0, _shapedTree.treeFromValue)(value, []); | ||
// TODO(zach): Variance problems $FlowFixMe | ||
return (0, _shapedTree.setFromKeysObj)(errorsObj, freshTree); | ||
} catch (e) { | ||
console.error("Error applying server errors to value!"); | ||
console.error("\t" + e.message); | ||
console.error("The server errors will be ignored."); | ||
return (0, _shapedTree.treeFromValue)(value, "unchecked"); | ||
} | ||
} else { | ||
return (0, _shapedTree.treeFromValue)(value, "unchecked"); | ||
} | ||
} | ||
}]); | ||
@@ -134,5 +155,5 @@ | ||
var updater = function updater(newErrors) { | ||
return function (_ref) { | ||
var errors = _ref.errors, | ||
meta = _ref.meta; | ||
return function (_ref4) { | ||
var errors = _ref4.errors, | ||
meta = _ref4.meta; | ||
return { | ||
@@ -146,6 +167,6 @@ errors: _extends({}, errors, { client: newErrors }), | ||
}; | ||
_this.setState(function (_ref2) { | ||
var _ref2$formState = _slicedToArray(_ref2.formState, 2), | ||
value = _ref2$formState[0], | ||
tree = _ref2$formState[1]; | ||
_this.setState(function (_ref5) { | ||
var _ref5$formState = _slicedToArray(_ref5.formState, 2), | ||
value = _ref5$formState[0], | ||
tree = _ref5$formState[1]; | ||
@@ -158,5 +179,9 @@ return { | ||
var serverErrors = Form.makeServerErrorTree(props.initialValue, props.serverErrors); | ||
var freshTree = (0, _shapedTree.treeFromValue)(props.initialValue, { | ||
errors: _types.cleanErrors, | ||
meta: _types.cleanMeta | ||
}); | ||
var formState = applyServerErrorsToFormState(props.serverErrors, [props.initialValue, freshTree]); | ||
_this.state = { | ||
formState: newFormState(props.initialValue, serverErrors), | ||
formState: formState, | ||
pristine: true, | ||
@@ -163,0 +188,0 @@ submitted: false, |
@@ -218,5 +218,3 @@ "use strict"; | ||
function replaceServerErrors(serverErrors, formState) { | ||
return [formState[0], (0, _shapedTree.shapedZipWith)(function (es, oldExtras) { | ||
return replaceServerErrorsExtra(es, oldExtras); | ||
}, serverErrors, formState[1])]; | ||
return [formState[0], (0, _shapedTree.shapedZipWith)(replaceServerErrorsExtra, serverErrors, formState[1])]; | ||
} |
@@ -6,2 +6,3 @@ "use strict"; | ||
}); | ||
exports.rootPath = undefined; | ||
@@ -11,3 +12,3 @@ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; // strict | ||
exports.treeFromValue = treeFromValue; | ||
exports.setFromKeysObj = setFromKeysObj; | ||
exports.shapePath = shapePath; | ||
exports.updateAtPath = updateAtPath; | ||
@@ -25,2 +26,3 @@ exports.checkShape = checkShape; | ||
exports.shapedZipWith = shapedZipWith; | ||
exports.mapShapedTree = mapShapedTree; | ||
@@ -37,2 +39,4 @@ var _tree = require("./tree"); | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } | ||
@@ -42,5 +46,2 @@ | ||
// Take shape from value, data from nodeData | ||
// Shape is a phantom type used to track the shape of the Tree | ||
@@ -51,3 +52,9 @@ // eslint-disable-next-line no-unused-vars | ||
// A path on a shaped tree | ||
// TODO(zach): Make this opaque | ||
// eslint-disable-next-line no-unused-vars | ||
var rootPath = exports.rootPath = function rootPath() { | ||
return []; | ||
}; | ||
// Take shape from value, data from nodeData | ||
function treeFromValue(value, nodeData) { | ||
@@ -81,50 +88,31 @@ if (Array.isArray(value)) { | ||
function setKey(key, value, tree) { | ||
if (key[0] !== "/") { | ||
throw new Error("Error paths must start with forward-slash"); | ||
function shapePath(data, path) { | ||
if (path.length === 0) { | ||
return path; | ||
} | ||
return _setKey(key.slice(1), value, tree); | ||
} | ||
function _setKey(key, value, tree) { | ||
if (key === "") { | ||
return mapRoot(function () { | ||
return value; | ||
}, tree); | ||
} | ||
var _path = _toArray(path), | ||
firstPart = _path[0], | ||
restParts = _path.slice(1); | ||
var _key$split = key.split("/"), | ||
_key$split2 = _toArray(_key$split), | ||
firstPart = _key$split2[0], | ||
restParts = _key$split2.slice(1); | ||
if (tree.type === "leaf") { | ||
throw new Error("Theres more key, but not more Tree to match it against"); | ||
if (firstPart.type === "object" && Object.hasOwnProperty.call(data, firstPart.key)) { | ||
// $FlowFixMe: This is safe | ||
var restPath = shapePath(data[firstPart.key], restParts); | ||
if (restPath === null) { | ||
return null; | ||
} | ||
return [firstPart].concat(_toConsumableArray(restPath)); | ||
} else if (firstPart.type === "array" && Array.isArray(data) && firstPart.index < data.length) { | ||
var _restPath = shapePath(data[firstPart.index], restParts); | ||
if (_restPath === null) { | ||
return null; | ||
} | ||
return [firstPart].concat(_toConsumableArray(_restPath)); | ||
} | ||
if (tree.type === "array") { | ||
var index = Number.parseInt(firstPart); | ||
(0, _invariant2.default)(index.toString() === firstPart, "Key indexing into an array is not a number"); | ||
(0, _invariant2.default)(index >= 0, "Key indexing into array is negative"); | ||
(0, _invariant2.default)(index < tree.children.length, "Key indexing array is outside array bounds"); | ||
var newChild = _setKey(restParts.join("/"), value, tree.children[index]); | ||
// $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why | ||
return dangerouslyReplaceArrayChild(index, newChild, tree); | ||
} | ||
if (tree.type === "object") { | ||
(0, _invariant2.default)(tree.children.hasOwnProperty(firstPart), "Key indexing into object does not exist"); | ||
var _newChild = _setKey(restParts.join("/"), value, tree.children[firstPart]); | ||
// $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why | ||
return dangerouslyReplaceObjectChild(firstPart, _newChild, tree); | ||
} | ||
throw new Error("unreachable"); | ||
return null; | ||
} | ||
function setFromKeysObj(keysObj, tree) { | ||
return Object.keys(keysObj).reduce(function (memo, key) { | ||
return setKey(key, keysObj[key], memo); | ||
}, tree); | ||
} | ||
function updateAtPath(path, updater, tree) { | ||
// console.log("updateAtPath()", path, tree); | ||
if (path.length === 0) { | ||
@@ -151,5 +139,5 @@ if (tree.type === "object") { | ||
var _path = _toArray(path), | ||
firstStep = _path[0], | ||
restStep = _path.slice(1); | ||
var _path2 = _toArray(path), | ||
firstStep = _path2[0], | ||
restStep = _path2.slice(1); | ||
@@ -168,5 +156,5 @@ if (tree.type === "leaf") { | ||
(0, _invariant2.default)(firstStep.type === "object", "Trying to take a non-object path into an object"); | ||
var _newChild2 = updateAtPath(restStep, updater, tree.children[firstStep.key]); | ||
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 | ||
return dangerouslyReplaceObjectChild(firstStep.key, _newChild2, tree); | ||
return dangerouslyReplaceObjectChild(firstStep.key, _newChild, tree); | ||
} | ||
@@ -278,2 +266,7 @@ throw new Error("unreachable"); | ||
return (0, _tree.strictZipWith)(f, left, right); | ||
} | ||
// Mapping doesn't change the shape | ||
function mapShapedTree(f, tree) { | ||
return (0, _tree.mapTree)(f, tree); | ||
} |
@@ -27,2 +27,6 @@ "use strict"; | ||
var _Field = require("../Field"); | ||
var _Field2 = _interopRequireDefault(_Field); | ||
var _tools = require("./tools"); | ||
@@ -36,6 +40,2 @@ | ||
var _makeField = require("../makeField"); | ||
var _makeField2 = _interopRequireDefault(_makeField); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -75,3 +75,16 @@ | ||
var NaughtyRenderingField = (0, _makeField2.default)(NaughtyRenderingInput); | ||
function NaughtyRenderingField(props) { | ||
return React.createElement( | ||
_Field2.default, | ||
props, | ||
function (value, errors, onChange, onBlur) { | ||
return React.createElement(NaughtyRenderingInput, { | ||
value: value, | ||
errors: errors, | ||
onChange: onChange, | ||
onBlur: onBlur | ||
}); | ||
} | ||
); | ||
} | ||
@@ -168,4 +181,67 @@ describe("Form", function () { | ||
xit("??? resets the server errors when they change -- this actually belongs lower?", function () { | ||
throw "TODO"; | ||
it("updates the server errors", function () { | ||
var onSubmit = jest.fn(); | ||
var renderFn = jest.fn(function () { | ||
return null; | ||
}); | ||
var renderer = _reactTestRenderer2.default.create(React.createElement( | ||
_Form2.default, | ||
{ | ||
initialValue: { | ||
array: [] | ||
}, | ||
feedbackStrategy: "OnFirstTouch", | ||
onSubmit: onSubmit, | ||
serverErrors: { | ||
"/array": ["Cannot be empty"] | ||
} | ||
}, | ||
function (link) { | ||
return React.createElement( | ||
_ObjectField2.default, | ||
{ link: link }, | ||
renderFn | ||
); | ||
} | ||
)); | ||
expect(renderFn).toHaveBeenCalled(); | ||
var links = renderFn.mock.calls[0][0]; | ||
var newFormState = (0, _tools.mockFormState)([1]); | ||
links.array.onChange(newFormState); | ||
var anotherRenderFn = jest.fn(); | ||
renderer.update(React.createElement( | ||
_Form2.default, | ||
{ | ||
initialValue: { | ||
array: [] | ||
}, | ||
feedbackStrategy: "OnFirstTouch", | ||
onSubmit: onSubmit, | ||
serverErrors: { | ||
"/array": [], | ||
"/array/0": ["inner error"] | ||
} | ||
}, | ||
anotherRenderFn | ||
)); | ||
expect(anotherRenderFn).toHaveBeenCalled(); | ||
var link = anotherRenderFn.mock.calls[0][0]; | ||
var _link$formState3 = _slicedToArray(link.formState, 2), | ||
_ = _link$formState3[0], | ||
tree = _link$formState3[1]; | ||
// Cross your fingers | ||
var root = tree; | ||
expect(root.data.errors.server).toEqual([]); | ||
var array = root.children.array; | ||
expect(array.data.errors.server).toEqual([]); | ||
var array0 = array.children[0]; | ||
expect(array0.data.errors.server).toEqual(["inner error"]); | ||
}); | ||
@@ -172,0 +248,0 @@ |
@@ -11,3 +11,5 @@ "use strict"; | ||
exports.leaf = leaf; | ||
exports.pathFromPathString = pathFromPathString; | ||
exports.strictZipWith = strictZipWith; | ||
exports.mapTree = mapTree; | ||
@@ -31,2 +33,28 @@ var _set = require("./utils/set"); | ||
function pathFromPathString(pathString) { | ||
if (pathString[0] !== "/") { | ||
throw new Error("Error paths must start with forward-slash"); | ||
} | ||
if (pathString === "/") { | ||
return []; | ||
} | ||
return pathString.slice(1).split("/").map(function (keyPart) { | ||
// This might be dangerous, since it means you can't use numbers as object | ||
// keys. This is acceptable for now. | ||
if (!isNaN(keyPart)) { | ||
return { | ||
type: "array", | ||
index: Number.parseInt(keyPart, 10) | ||
}; | ||
} else { | ||
return { | ||
type: "object", | ||
key: keyPart | ||
}; | ||
} | ||
}); | ||
} | ||
function strictZipWith(f, left, right) { | ||
@@ -71,2 +99,28 @@ if (left.type === "object" && right.type === "object") { | ||
throw new Error("Tried to zip two nodes of different type"); | ||
} | ||
// A tree is a functor | ||
function mapTree(f, tree) { | ||
if (tree.type === "object") { | ||
return { | ||
type: "object", | ||
data: f(tree.data), | ||
children: Object.keys(tree.children).reduce(function (memo, key) { | ||
return _extends({}, memo, _defineProperty({}, key, mapTree(f, tree.children[key]))); | ||
}, {}) | ||
}; | ||
} else if (tree.type === "array") { | ||
return { | ||
type: "array", | ||
data: f(tree.data), | ||
children: tree.children.map(function (child) { | ||
return mapTree(f, child); | ||
}) | ||
}; | ||
} else { | ||
return { | ||
type: "leaf", | ||
data: f(tree.data) | ||
}; | ||
} | ||
} |
{ | ||
"name": "formula-one", | ||
"version": "0.2.0", | ||
"version": "0.2.1", | ||
"description": "Strongly-typed React form state management", | ||
@@ -5,0 +5,0 @@ "author": "Zach Gotsch", |
110
src/Form.js
@@ -11,7 +11,6 @@ // @flow strict | ||
FieldLink, | ||
ServerErrors, | ||
ClientErrors, | ||
} from "./types"; | ||
import {cleanMeta, cleanErrors} from "./types"; | ||
import {type FormState, replaceServerErrors} from "./formState"; | ||
import {type FormState} from "./formState"; | ||
import { | ||
@@ -21,5 +20,7 @@ type ShapedTree, | ||
treeFromValue, | ||
setFromKeysObj, | ||
shapePath, | ||
updateAtPath, | ||
mapShapedTree, | ||
} from "./shapedTree"; | ||
import {pathFromPathString} from "./tree"; | ||
@@ -41,17 +42,51 @@ export type FormContextPayload = { | ||
function newFormState<T>( | ||
value: T, | ||
serverErrors: null | ShapedTree<T, ServerErrors> | ||
function applyServerErrorsToFormState<T>( | ||
serverErrors: null | {[path: string]: Array<string>}, | ||
formState: FormState<T> | ||
): FormState<T> { | ||
const cleanState = [ | ||
value, | ||
treeFromValue(value, { | ||
errors: cleanErrors, | ||
meta: cleanMeta, | ||
}), | ||
]; | ||
if (serverErrors != null) { | ||
return replaceServerErrors(serverErrors, cleanState); | ||
const [value, oldTree] = formState; | ||
let tree: ShapedTree<T, Extras>; | ||
if (serverErrors !== null) { | ||
// If keys do not appear, no errors | ||
tree = mapShapedTree( | ||
({errors, meta}) => ({ | ||
errors: {...errors, server: []}, | ||
meta, | ||
}), | ||
oldTree | ||
); | ||
Object.keys(serverErrors).forEach(key => { | ||
const newErrors: Array<string> = serverErrors[key]; | ||
const path = shapePath(value, pathFromPathString(key)); | ||
if (path != null) { | ||
// TODO(zach): make some helper functions that do this | ||
tree = updateAtPath( | ||
path, | ||
({errors, meta}) => ({ | ||
errors: {...errors, server: newErrors}, | ||
meta, | ||
}), | ||
tree | ||
); | ||
} else { | ||
console.error( | ||
`Warning: couldn't match error with path ${key} to value ${JSON.stringify( | ||
value | ||
)}` | ||
); | ||
} | ||
}); | ||
} else { | ||
tree = mapShapedTree( | ||
({errors, meta}) => ({ | ||
errors: {...errors, server: []}, | ||
meta, | ||
}), | ||
oldTree | ||
); | ||
} | ||
return cleanState; | ||
return [value, tree]; | ||
} | ||
@@ -97,8 +132,8 @@ | ||
if (props.serverErrors !== state.oldServerErrors) { | ||
const serverErrorsTree = Form.makeServerErrorTree( | ||
state.formState[0], | ||
props.serverErrors | ||
const newFormState = applyServerErrorsToFormState( | ||
props.serverErrors, | ||
state.formState | ||
); | ||
return { | ||
formState: replaceServerErrors(serverErrorsTree, state.formState), | ||
formState: newFormState, | ||
oldServerErrors: props.serverErrors, | ||
@@ -110,34 +145,15 @@ }; | ||
static makeServerErrorTree<T>( | ||
value: T, | ||
errorsObj: null | {[path: string]: Array<string>} | ||
): ShapedTree<T, ServerErrors> { | ||
if (errorsObj != null) { | ||
try { | ||
const freshTree = treeFromValue(value, []); | ||
// TODO(zach): Variance problems $FlowFixMe | ||
return (setFromKeysObj(errorsObj, freshTree): ShapedTree< | ||
T, | ||
ServerErrors | ||
>); | ||
} catch (e) { | ||
console.error("Error applying server errors to value!"); | ||
console.error(`\t${e.message}`); | ||
console.error("The server errors will be ignored."); | ||
return treeFromValue(value, "unchecked"); | ||
} | ||
} else { | ||
return treeFromValue(value, "unchecked"); | ||
} | ||
} | ||
constructor(props: Props<T>) { | ||
super(props); | ||
const serverErrors = Form.makeServerErrorTree( | ||
const freshTree = treeFromValue(props.initialValue, { | ||
errors: cleanErrors, | ||
meta: cleanMeta, | ||
}); | ||
const formState = applyServerErrorsToFormState(props.serverErrors, [ | ||
props.initialValue, | ||
props.serverErrors | ||
); | ||
freshTree, | ||
]); | ||
this.state = { | ||
formState: newFormState(props.initialValue, serverErrors), | ||
formState, | ||
pristine: true, | ||
@@ -144,0 +160,0 @@ submitted: false, |
@@ -234,8 +234,4 @@ // @flow strict | ||
formState[0], | ||
shapedZipWith( | ||
(es, oldExtras) => replaceServerErrorsExtra(es, oldExtras), | ||
serverErrors, | ||
formState[1] | ||
), | ||
shapedZipWith(replaceServerErrorsExtra, serverErrors, formState[1]), | ||
]; | ||
} |
// @flow strict | ||
import {type Tree, type Path, leaf, strictZipWith} from "./tree"; | ||
import {type Tree, type Path, leaf, strictZipWith, mapTree} from "./tree"; | ||
import invariant from "./utils/invariant"; | ||
@@ -12,4 +12,6 @@ import {replaceAt} from "./utils/array"; | ||
// A path on a shaped tree | ||
// TODO(zach): Make this opaque | ||
// eslint-disable-next-line no-unused-vars | ||
export type ShapedPath<Shape> = Path; | ||
export /* opaque */ type ShapedPath<Shape> = Path; | ||
export const rootPath: <T>() => ShapedPath<T> = () => []; | ||
@@ -50,68 +52,32 @@ // Take shape from value, data from nodeData | ||
function setKey<T, Node>( | ||
key: string, | ||
value: Node, | ||
tree: ShapedTree<T, Node> | ||
): ShapedTree<T, Node> { | ||
if (key[0] !== "/") { | ||
throw new Error("Error paths must start with forward-slash"); | ||
export function shapePath<T>(data: T, path: Path): null | ShapedPath<T> { | ||
if (path.length === 0) { | ||
return path; | ||
} | ||
return _setKey(key.slice(1), value, tree); | ||
} | ||
function _setKey<T, Node>( | ||
key: string, | ||
value: Node, | ||
tree: ShapedTree<T, Node> | ||
): ShapedTree<T, Node> { | ||
if (key === "") { | ||
return mapRoot(() => value, tree); | ||
const [firstPart, ...restParts] = path; | ||
if ( | ||
firstPart.type === "object" && | ||
Object.hasOwnProperty.call(data, firstPart.key) | ||
) { | ||
// $FlowFixMe: This is safe | ||
const restPath = shapePath(data[firstPart.key], restParts); | ||
if (restPath === null) { | ||
return null; | ||
} | ||
return [firstPart, ...restPath]; | ||
} else if ( | ||
firstPart.type === "array" && | ||
Array.isArray(data) && | ||
firstPart.index < data.length | ||
) { | ||
const restPath = shapePath(data[firstPart.index], restParts); | ||
if (restPath === null) { | ||
return null; | ||
} | ||
return [firstPart, ...restPath]; | ||
} | ||
const [firstPart, ...restParts] = key.split("/"); | ||
if (tree.type === "leaf") { | ||
throw new Error("Theres more key, but not more Tree to match it against"); | ||
} | ||
if (tree.type === "array") { | ||
const index = Number.parseInt(firstPart); | ||
invariant( | ||
index.toString() === firstPart, | ||
"Key indexing into an array is not a number" | ||
); | ||
invariant(index >= 0, "Key indexing into array is negative"); | ||
invariant( | ||
index < tree.children.length, | ||
"Key indexing array is outside array bounds" | ||
); | ||
const newChild = _setKey(restParts.join("/"), value, tree.children[index]); | ||
// $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why | ||
return dangerouslyReplaceArrayChild(index, newChild, tree); | ||
} | ||
if (tree.type === "object") { | ||
invariant( | ||
tree.children.hasOwnProperty(firstPart), | ||
"Key indexing into object does not exist" | ||
); | ||
const newChild = _setKey( | ||
restParts.join("/"), | ||
value, | ||
tree.children[firstPart] | ||
); | ||
// $FlowFixMe(zach): I think this is safe, might need GADTs for the type checker to understand why | ||
return dangerouslyReplaceObjectChild(firstPart, newChild, tree); | ||
} | ||
throw new Error("unreachable"); | ||
return null; | ||
} | ||
export function setFromKeysObj<T, Node>( | ||
keysObj: {[path: string]: Node}, | ||
tree: ShapedTree<T, Node> | ||
): ShapedTree<T, Node> { | ||
return Object.keys(keysObj).reduce( | ||
(memo: ShapedTree<T, Node>, key: string) => setKey(key, keysObj[key], memo), | ||
tree | ||
); | ||
} | ||
@@ -123,2 +89,3 @@ export function updateAtPath<T, Node>( | ||
): ShapedTree<T, Node> { | ||
// console.log("updateAtPath()", path, tree); | ||
if (path.length === 0) { | ||
@@ -330,1 +297,9 @@ if (tree.type === "object") { | ||
} | ||
// Mapping doesn't change the shape | ||
export function mapShapedTree<T, A, B>( | ||
f: A => B, | ||
tree: ShapedTree<T, A> | ||
): ShapedTree<T, B> { | ||
return mapTree(f, tree); | ||
} |
@@ -8,2 +8,3 @@ // @flow | ||
import ArrayField from "../ArrayField"; | ||
import Field from "../Field"; | ||
@@ -13,3 +14,2 @@ import {expectLink, mockFormState} from "./tools"; | ||
import {forgetShape} from "../shapedTree"; | ||
import makeField from "../makeField"; | ||
@@ -29,3 +29,16 @@ class NaughtyRenderingInput extends React.Component<{| | ||
} | ||
const NaughtyRenderingField = makeField(NaughtyRenderingInput); | ||
function NaughtyRenderingField(props) { | ||
return ( | ||
<Field {...props}> | ||
{(value, errors, onChange, onBlur) => ( | ||
<NaughtyRenderingInput | ||
value={value} | ||
errors={errors} | ||
onChange={onChange} | ||
onBlur={onBlur} | ||
/> | ||
)} | ||
</Field> | ||
); | ||
} | ||
@@ -111,4 +124,55 @@ describe("Form", () => { | ||
xit("??? resets the server errors when they change -- this actually belongs lower?", () => { | ||
throw "TODO"; | ||
it("updates the server errors", () => { | ||
const onSubmit = jest.fn(); | ||
const renderFn = jest.fn(() => null); | ||
const renderer = TestRenderer.create( | ||
<Form | ||
initialValue={{ | ||
array: [], | ||
}} | ||
feedbackStrategy="OnFirstTouch" | ||
onSubmit={onSubmit} | ||
serverErrors={{ | ||
"/array": ["Cannot be empty"], | ||
}} | ||
> | ||
{link => <ObjectField link={link}>{renderFn}</ObjectField>} | ||
</Form> | ||
); | ||
expect(renderFn).toHaveBeenCalled(); | ||
const links = renderFn.mock.calls[0][0]; | ||
const newFormState = mockFormState([1]); | ||
links.array.onChange(newFormState); | ||
const anotherRenderFn = jest.fn(); | ||
renderer.update( | ||
<Form | ||
initialValue={{ | ||
array: [], | ||
}} | ||
feedbackStrategy="OnFirstTouch" | ||
onSubmit={onSubmit} | ||
serverErrors={{ | ||
"/array": [], | ||
"/array/0": ["inner error"], | ||
}} | ||
> | ||
{anotherRenderFn} | ||
</Form> | ||
); | ||
expect(anotherRenderFn).toHaveBeenCalled(); | ||
const link = anotherRenderFn.mock.calls[0][0]; | ||
const [_, tree] = link.formState; | ||
// Cross your fingers | ||
const root: any = tree; | ||
expect(root.data.errors.server).toEqual([]); | ||
const array = root.children.array; | ||
expect(array.data.errors.server).toEqual([]); | ||
const array0 = array.children[0]; | ||
expect(array0.data.errors.server).toEqual(["inner error"]); | ||
}); | ||
@@ -115,0 +179,0 @@ |
@@ -38,2 +38,31 @@ // @flow strict | ||
export function pathFromPathString(pathString: string): Path { | ||
if (pathString[0] !== "/") { | ||
throw new Error("Error paths must start with forward-slash"); | ||
} | ||
if (pathString === "/") { | ||
return []; | ||
} | ||
return pathString | ||
.slice(1) | ||
.split("/") | ||
.map(keyPart => { | ||
// This might be dangerous, since it means you can't use numbers as object | ||
// keys. This is acceptable for now. | ||
if (!isNaN(keyPart)) { | ||
return { | ||
type: "array", | ||
index: Number.parseInt(keyPart, 10), | ||
}; | ||
} else { | ||
return { | ||
type: "object", | ||
key: keyPart, | ||
}; | ||
} | ||
}); | ||
} | ||
export function strictZipWith<A, B, C>( | ||
@@ -89,1 +118,26 @@ f: (A, B) => C, | ||
} | ||
// A tree is a functor | ||
export function mapTree<A, B>(f: A => B, tree: Tree<A>): Tree<B> { | ||
if (tree.type === "object") { | ||
return { | ||
type: "object", | ||
data: f(tree.data), | ||
children: Object.keys(tree.children).reduce( | ||
(memo, key) => ({...memo, [key]: mapTree(f, tree.children[key])}), | ||
{} | ||
), | ||
}; | ||
} else if (tree.type === "array") { | ||
return { | ||
type: "array", | ||
data: f(tree.data), | ||
children: tree.children.map(child => mapTree(f, child)), | ||
}; | ||
} else { | ||
return { | ||
type: "leaf", | ||
data: f(tree.data), | ||
}; | ||
} | ||
} |
207596
57
5251