formula-one
Advanced tools
Comparing version 0.9.0-alpha.1 to 0.9.0-alpha.2
@@ -5,7 +5,23 @@ # Changelog | ||
- 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 if a `customChange` function is used. This will be addressed in a future API. _Warning_: Rendering a component which calls `onChange` during mount will result in an infinite render loop. | ||
<br> | ||
<br> | ||
- 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. | ||
**Warning**: Rendering a component which calls `onChange` during mount under a non-null returning `customChange` will result in an infinite render loop. | ||
**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. | ||
- Add `addFields` and `filterFields` 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. | ||
The API is: | ||
```jsx | ||
// A type indicating a range to be inserted at an index | ||
type Span<E> = [number, $ReadOnlyArray<E>]; | ||
// A way to atomicly add fields to an ArrayField<E> | ||
addFields: (spans: $ReadOnlyArray<Span<E>) => void; | ||
// A way to remove fields from an ArrayField<E> | ||
filterFields: (predicate: (item: E, index: number) => boolean) => void | ||
``` | ||
### v0.8.2 | ||
@@ -12,0 +28,0 @@ |
@@ -122,3 +122,3 @@ "use strict"; | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._removeChildField = function (index) { | ||
}, _this._addChildFields = function (spans) { | ||
var _this$props$link$form3 = _slicedToArray(_this.props.link.formState, 2), | ||
@@ -128,7 +128,21 @@ oldValue = _this$props$link$form3[0], | ||
var newValue = (0, _array.removeAt)(index, oldValue); | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.removeAt)(index, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
var cleanNode = { | ||
errors: _types.cleanErrors, | ||
meta: _types.cleanMeta | ||
}; | ||
var newValue = (0, _array.insertSpans)(spans, oldValue); | ||
var newNodeSpans = spans.map(function (_ref2) { | ||
var _ref3 = _slicedToArray(_ref2, 2), | ||
index = _ref3[0], | ||
content = _ref3[1]; | ||
return [index, content.map(function (v) { | ||
return (0, _shapedTree.treeFromValue)(v, cleanNode); | ||
})]; | ||
}); | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.insertSpans)(newNodeSpans, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._moveChildField = function (from, to) { | ||
}, _this._filterChildFields = function (predicate) { | ||
var _this$props$link$form4 = _slicedToArray(_this.props.link.formState, 2), | ||
@@ -138,2 +152,31 @@ oldValue = _this$props$link$form4[0], | ||
var zipped = (0, _array.zip)(oldValue, (0, _shapedTree.shapedArrayChildren)(oldTree)); | ||
var _unzip = (0, _array.unzip)(zipped.filter(function (_ref4, i) { | ||
var _ref5 = _slicedToArray(_ref4, 1), | ||
value = _ref5[0]; | ||
return predicate(value, i); | ||
})), | ||
_unzip2 = _slicedToArray(_unzip, 2), | ||
newValue = _unzip2[0], | ||
newChildren = _unzip2[1]; | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)(newChildren, oldTree); | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._removeChildField = function (index) { | ||
var _this$props$link$form5 = _slicedToArray(_this.props.link.formState, 2), | ||
oldValue = _this$props$link$form5[0], | ||
oldTree = _this$props$link$form5[1]; | ||
var newValue = (0, _array.removeAt)(index, oldValue); | ||
var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.removeAt)(index, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
_this.props.link.onChange((0, _formState3.validate)(_this.props.validation, (0, _formState3.setChanged)((0, _formState3.setTouched)([newValue, newTree])))); | ||
}, _this._moveChildField = function (from, to) { | ||
var _this$props$link$form6 = _slicedToArray(_this.props.link.formState, 2), | ||
oldValue = _this$props$link$form6[0], | ||
oldTree = _this$props$link$form6[1]; | ||
var newValue = (0, _array.moveFromTo)(from, to, oldValue); | ||
@@ -172,4 +215,4 @@ var newTree = (0, _shapedTree.dangerouslySetChildren)((0, _array.moveFromTo)(from, to, (0, _shapedTree.shapedArrayChildren)(oldTree)), oldTree); | ||
value: function forceChildRemount() { | ||
this.setState(function (_ref2) { | ||
var nonce = _ref2.nonce; | ||
this.setState(function (_ref6) { | ||
var nonce = _ref6.nonce; | ||
return { nonce: nonce + 1 }; | ||
@@ -192,3 +235,5 @@ }); | ||
removeField: this._removeChildField, | ||
moveField: this._moveChildField | ||
moveField: this._moveChildField, | ||
addFields: this._addChildFields, | ||
filterFields: this._filterChildFields | ||
}, { | ||
@@ -195,0 +240,0 @@ touched: (0, _formState3.getExtras)(formState).meta.touched, |
@@ -468,2 +468,104 @@ "use strict"; | ||
}); | ||
describe("addFields", function () { | ||
it("exposes addFields to add an entry", 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; | ||
}); | ||
_reactTestRenderer2.default.create(React.createElement( | ||
_ArrayField2.default, | ||
{ link: link }, | ||
renderFn | ||
)); | ||
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ | ||
addFields: expect.any(Function) | ||
}), expect.anything()); | ||
}); | ||
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 () { | ||
return null; | ||
}); | ||
var validation = jest.fn(function () { | ||
return ["an error"]; | ||
}); | ||
_reactTestRenderer2.default.create(React.createElement( | ||
_ArrayField2.default, | ||
{ validation: validation, link: link }, | ||
renderFn | ||
)); | ||
expect(validation).toHaveBeenCalledTimes(1); | ||
var _renderFn$mock$calls$4 = _slicedToArray(renderFn.mock.calls[0], 2), | ||
_ = _renderFn$mock$calls$4[0], | ||
addFields = _renderFn$mock$calls$4[1].addFields; | ||
addFields([[0, ["negative one", "zero"]], [3, ["four", "five"]]]); | ||
expect(validation).toHaveBeenCalledTimes(2); | ||
expect(validation).toHaveBeenLastCalledWith(["negative one", "zero", "one", "two", "three", "four", "five"]); | ||
}); | ||
}); | ||
describe("filterFields", function () { | ||
it("exposes addFields to add an entry", 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; | ||
}); | ||
_reactTestRenderer2.default.create(React.createElement( | ||
_ArrayField2.default, | ||
{ link: link }, | ||
renderFn | ||
)); | ||
expect(renderFn).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ | ||
filterFields: expect.any(Function) | ||
}), expect.anything()); | ||
}); | ||
it("validates after entry is added", 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 () { | ||
return null; | ||
}); | ||
var validation = jest.fn(function () { | ||
return ["an error"]; | ||
}); | ||
_reactTestRenderer2.default.create(React.createElement( | ||
_ArrayField2.default, | ||
{ validation: validation, link: link }, | ||
renderFn | ||
)); | ||
expect(validation).toHaveBeenCalledTimes(1); | ||
var _renderFn$mock$calls$5 = _slicedToArray(renderFn.mock.calls[0], 2), | ||
_ = _renderFn$mock$calls$5[0], | ||
filterFields = _renderFn$mock$calls$5[1].filterFields; | ||
// remove numbers without "o" and the fourth element | ||
filterFields(function (v, i) { | ||
return v.indexOf("o") !== -1 && i !== 3; | ||
}); | ||
expect(validation).toHaveBeenCalledTimes(2); | ||
expect(validation).toHaveBeenLastCalledWith(["one", "two"]); | ||
}); | ||
}); | ||
}); | ||
@@ -470,0 +572,0 @@ |
@@ -6,2 +6,5 @@ "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"); } }; }(); | ||
exports.removeAt = removeAt; | ||
@@ -11,3 +14,6 @@ exports.replaceAt = replaceAt; | ||
exports.moveFromTo = moveFromTo; | ||
exports.insertSpans = insertSpans; | ||
exports.zipWith = zipWith; | ||
exports.zip = zip; | ||
exports.unzip = unzip; | ||
@@ -35,2 +41,44 @@ 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 insertSpans(spans, arr) { | ||
// no duplicated indices are allowed, ECMAScript Array.sort is not stable by spec | ||
var indexSet = new Set(spans.map(function (_ref) { | ||
var _ref2 = _slicedToArray(_ref, 1), | ||
i = _ref2[0]; | ||
return i; | ||
})); | ||
if (indexSet.size !== spans.length) { | ||
throw new Error("You cannot insert two spans at the same index. Combine the values of the spans."); | ||
} | ||
// sort spans by insertion position | ||
var spansCopy = [].concat(_toConsumableArray(spans)); | ||
spansCopy.sort(function (_ref3, _ref4) { | ||
var _ref6 = _slicedToArray(_ref3, 1), | ||
i = _ref6[0]; | ||
var _ref5 = _slicedToArray(_ref4, 1), | ||
j = _ref5[0]; | ||
return i - j; | ||
}); | ||
// build the new array in one pass | ||
var ret = []; | ||
var lastIndexInsertedAt = 0; | ||
spansCopy.forEach(function (_ref7) { | ||
var _ref8 = _slicedToArray(_ref7, 2), | ||
index = _ref8[0], | ||
contents = _ref8[1]; | ||
// All the content before this | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt, index)); | ||
ret = ret.concat(contents); | ||
lastIndexInsertedAt = index; | ||
}); | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt)); | ||
return ret; | ||
} | ||
// Strict on length | ||
@@ -46,2 +94,21 @@ function zipWith(f, left, right) { | ||
return ret; | ||
} | ||
function zip(left, right) { | ||
return zipWith(function (l, r) { | ||
return [l, r]; | ||
}, left, right); | ||
} | ||
function unzip(zipped) { | ||
var ret = [[], []]; | ||
for (var i = 0; i < zipped.length; i += 1) { | ||
var _zipped$i = _slicedToArray(zipped[i], 2), | ||
left = _zipped$i[0], | ||
right = _zipped$i[1]; | ||
ret[0].push(left); | ||
ret[1].push(right); | ||
} | ||
return ret; | ||
} |
{ | ||
"name": "formula-one", | ||
"version": "0.9.0-alpha.1", | ||
"version": "0.9.0-alpha.2", | ||
"description": "Strongly-typed React form state management", | ||
@@ -5,0 +5,0 @@ "author": "Zach Gotsch", |
@@ -23,3 +23,10 @@ // @flow strict | ||
} from "./shapedTree"; | ||
import {removeAt, moveFromTo, insertAt} from "./utils/array"; | ||
import { | ||
removeAt, | ||
moveFromTo, | ||
insertAt, | ||
insertSpans, | ||
zip, | ||
unzip, | ||
} from "./utils/array"; | ||
import {FormContext, type FormContextPayload} from "./Form"; | ||
@@ -54,2 +61,4 @@ import { | ||
moveField: (oldIndex: number, newIndex: number) => void, | ||
addFields: (spans: $ReadOnlyArray<[number, $ReadOnlyArray<E>]>) => void, | ||
filterFields: (predicate: (E, number) => boolean) => void, | ||
}, | ||
@@ -205,2 +214,50 @@ additionalInfo: AdditionalRenderInfo<Array<E>> | ||
_addChildFields: ( | ||
spans: $ReadOnlyArray<[number, $ReadOnlyArray<E>]> | ||
) => void = spans => { | ||
const [oldValue, oldTree] = this.props.link.formState; | ||
const cleanNode = { | ||
errors: cleanErrors, | ||
meta: cleanMeta, | ||
}; | ||
const newValue = insertSpans(spans, oldValue); | ||
const newNodeSpans: Array< | ||
[number, $ReadOnlyArray<ShapedTree<E, Extras>>] | ||
> = spans.map(([index, content]) => [ | ||
index, | ||
content.map(v => treeFromValue(v, cleanNode)), | ||
]); | ||
const newTree = dangerouslySetChildren( | ||
insertSpans(newNodeSpans, shapedArrayChildren(oldTree)), | ||
oldTree | ||
); | ||
this.props.link.onChange( | ||
validate( | ||
this.props.validation, | ||
setChanged(setTouched([newValue, newTree])) | ||
) | ||
); | ||
}; | ||
_filterChildFields: ( | ||
predicate: (E, number) => boolean | ||
) => void = predicate => { | ||
const [oldValue, oldTree] = this.props.link.formState; | ||
const zipped = zip(oldValue, shapedArrayChildren(oldTree)); | ||
const [newValue, newChildren] = unzip( | ||
zipped.filter(([value], i) => predicate(value, i)) | ||
); | ||
const newTree = dangerouslySetChildren(newChildren, oldTree); | ||
this.props.link.onChange( | ||
validate( | ||
this.props.validation, | ||
setChanged(setTouched([newValue, newTree])) | ||
) | ||
); | ||
}; | ||
_removeChildField = (index: number) => { | ||
@@ -257,2 +314,4 @@ const [oldValue, oldTree] = this.props.link.formState; | ||
moveField: this._moveChildField, | ||
addFields: this._addChildFields, | ||
filterFields: this._filterChildFields, | ||
}, | ||
@@ -259,0 +318,0 @@ { |
@@ -361,2 +361,91 @@ // @flow | ||
}); | ||
describe("addFields", () => { | ||
it("exposes addFields to add an entry", () => { | ||
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>); | ||
expect(renderFn).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.objectContaining({ | ||
addFields: expect.any(Function), | ||
}), | ||
expect.anything() | ||
); | ||
}); | ||
it("validates after entry is added", () => { | ||
const formStateValue = ["one", "two", "three"]; | ||
const formState = mockFormState(formStateValue); | ||
const link = mockLink(formState); | ||
const renderFn = jest.fn(() => null); | ||
const validation = jest.fn(() => ["an error"]); | ||
TestRenderer.create( | ||
<ArrayField validation={validation} link={link}> | ||
{renderFn} | ||
</ArrayField> | ||
); | ||
expect(validation).toHaveBeenCalledTimes(1); | ||
const [_, {addFields}] = renderFn.mock.calls[0]; | ||
addFields([[0, ["negative one", "zero"]], [3, ["four", "five"]]]); | ||
expect(validation).toHaveBeenCalledTimes(2); | ||
expect(validation).toHaveBeenLastCalledWith([ | ||
"negative one", | ||
"zero", | ||
"one", | ||
"two", | ||
"three", | ||
"four", | ||
"five", | ||
]); | ||
}); | ||
}); | ||
describe("filterFields", () => { | ||
it("exposes addFields to add an entry", () => { | ||
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>); | ||
expect(renderFn).toHaveBeenCalledWith( | ||
expect.anything(), | ||
expect.objectContaining({ | ||
filterFields: expect.any(Function), | ||
}), | ||
expect.anything() | ||
); | ||
}); | ||
it("validates after entry is added", () => { | ||
const formStateValue = ["one", "two", "three", "four", "five"]; | ||
const formState = mockFormState(formStateValue); | ||
const link = mockLink(formState); | ||
const renderFn = jest.fn(() => null); | ||
const validation = jest.fn(() => ["an error"]); | ||
TestRenderer.create( | ||
<ArrayField validation={validation} link={link}> | ||
{renderFn} | ||
</ArrayField> | ||
); | ||
expect(validation).toHaveBeenCalledTimes(1); | ||
const [_, {filterFields}] = renderFn.mock.calls[0]; | ||
// remove numbers without "o" and the fourth element | ||
filterFields((v, i) => v.indexOf("o") !== -1 && i !== 3); | ||
expect(validation).toHaveBeenCalledTimes(2); | ||
expect(validation).toHaveBeenLastCalledWith(["one", "two"]); | ||
}); | ||
}); | ||
}); | ||
@@ -363,0 +452,0 @@ |
@@ -32,2 +32,32 @@ // @flow strict | ||
export function insertSpans<E>( | ||
spans: $ReadOnlyArray<[number, $ReadOnlyArray<E>]>, | ||
arr: $ReadOnlyArray<E> | ||
): Array<E> { | ||
// no duplicated indices are allowed, ECMAScript Array.sort is not stable by spec | ||
const indexSet = new Set(spans.map(([i]) => i)); | ||
if (indexSet.size !== spans.length) { | ||
throw new Error( | ||
"You cannot insert two spans at the same index. Combine the values of the spans." | ||
); | ||
} | ||
// sort spans by insertion position | ||
const spansCopy = [...spans]; | ||
spansCopy.sort(([i], [j]) => i - j); | ||
// build the new array in one pass | ||
let ret = []; | ||
let lastIndexInsertedAt = 0; | ||
spansCopy.forEach(([index, contents]) => { | ||
// All the content before this | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt, index)); | ||
ret = ret.concat(contents); | ||
lastIndexInsertedAt = index; | ||
}); | ||
ret = ret.concat(arr.slice(lastIndexInsertedAt)); | ||
return ret; | ||
} | ||
// Strict on length | ||
@@ -48,1 +78,20 @@ export function zipWith<A, B, C>( | ||
} | ||
export function zip<A, B>( | ||
left: $ReadOnlyArray<A>, | ||
right: $ReadOnlyArray<B> | ||
): Array<[A, B]> { | ||
return zipWith((l, r) => [l, r], left, right); | ||
} | ||
export function unzip<A, B>( | ||
zipped: $ReadOnlyArray<[A, B]> | ||
): [Array<A>, Array<B>] { | ||
const ret = [[], []]; | ||
for (let i = 0; i < zipped.length; i += 1) { | ||
const [left, right] = zipped[i]; | ||
ret[0].push(left); | ||
ret[1].push(right); | ||
} | ||
return ret; | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
307672
64
7750