diff --git a/.babelrc b/.babelrc index cf2fb46..b1e238d 100644 --- a/.babelrc +++ b/.babelrc @@ -9,10 +9,9 @@ } } ], - "@babel/preset-flow" + "@babel/preset-typescript" ], "plugins": [ - "@babel/plugin-transform-flow-strip-types", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-import-meta", "@babel/plugin-proposal-class-properties", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..db54c7c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: [push] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 22 + uses: actions/setup-node@v2 + with: + node-version: "22" + - name: Prepare env + run: yarn install --ignore-scripts --frozen-lockfile + - name: Run linter + run: yarn start lint + + prettier: + name: Prettier Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 22 + uses: actions/setup-node@v2 + with: + node-version: "22" + - name: Prepare env + run: yarn install --ignore-scripts --frozen-lockfile + - name: Run prettier + run: yarn start prettier + + test: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 22 + uses: actions/setup-node@v2 + with: + node-version: "22" + - name: Prepare env + run: yarn install --ignore-scripts --frozen-lockfile + - name: Run unit tests + run: yarn start test + - name: Run code coverage + uses: codecov/codecov-action@v2.1.0 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 0000000..4e08172 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,24 @@ +name: "Lock Threads" + +on: + schedule: + - cron: "0 * * * *" + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v3 + with: + issue-inactive-days: "365" + issue-lock-reason: "resolved" + pr-inactive-days: "365" + pr-lock-reason: "resolved" diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..6745f1e --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,44 @@ +import js from '@eslint/js' +import tsPlugin from '@typescript-eslint/eslint-plugin' +import tsParser from '@typescript-eslint/parser' +import globals from 'globals' + +export default [ + js.configs.recommended, + { + files: ['**/*.{js,jsx,ts,tsx,mjs}'], + languageOptions: { + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + ...globals.jest + } + }, + plugins: { + '@typescript-eslint': tsPlugin + }, + rules: { + ...tsPlugin.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' } + ], + 'no-unused-vars': 'off' + } + }, + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'coverage/**', + '.nyc_output/**', + '*.config.js', + 'package-scripts.js', + 'src/index.d.test.ts' + ] + } +] diff --git a/package-scripts.js b/package-scripts.js index 0dd4285..241ce9b 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -34,7 +34,7 @@ module.exports = { ) ), es: { - description: 'run the build with rollup (uses rollup.config.js)', + description: 'run the build with rollup (uses rollup.config.mjs)', script: 'rollup --config --environment FORMAT:es' }, cjs: { @@ -57,37 +57,23 @@ module.exports = { description: 'Generates table of contents in README', script: 'doctoc README.md' }, - copyTypes: series( - npsUtils.copy('src/*.js.flow src/*.d.ts dist'), - npsUtils.copy( - 'dist/index.js.flow dist --rename="final-form-arrays.cjs.js.flow"' - ), - npsUtils.copy( - 'dist/index.js.flow dist --rename="final-form-arrays.es.js.flow"' - ) - ), + prettier: { + description: 'Runs prettier on everything', + script: 'prettier --write "**/*.([jt]s*)"' + }, + copyTypes: series('tsc --declaration --emitDeclarationOnly --outDir dist'), lint: { description: 'lint the entire project', script: 'eslint .' }, - flow: { - description: 'flow check the entire project', - script: 'flow check' - }, typescript: { description: 'typescript check the entire project', - script: 'tsc' + script: 'tsc --noEmit' }, validate: { description: 'This runs several scripts to make sure things look good before committing or on clean install', - default: concurrent.nps( - 'lint', - 'flow', - 'typescript', - 'build.andTest', - 'test' - ) + default: concurrent.nps('lint', 'typescript', 'build.andTest', 'test') } }, options: { diff --git a/package.json b/package.json index 3fe5ff7..54738cf 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "scripts": { "start": "nps", "test": "nps test", - "precommit": "lint-staged && npm start validate" + "precommit": "lint-staged && npm start validate", + "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist", + "prebuild": "yarn build:types" }, "author": "Erik Rasmussen (http://github.com/erikras)", "license": "MIT", @@ -25,52 +27,55 @@ }, "homepage": "https://github.com/final-form/final-form-arrays#readme", "devDependencies": { - "@babel/core": "^7.5.4", - "@babel/plugin-external-helpers": "^7.0.0", - "@babel/plugin-proposal-class-properties": "^7.5.0", - "@babel/plugin-proposal-decorators": "^7.4.4", - "@babel/plugin-proposal-export-namespace-from": "^7.5.2", - "@babel/plugin-proposal-function-sent": "^7.5.0", - "@babel/plugin-proposal-json-strings": "^7.0.0", - "@babel/plugin-proposal-numeric-separator": "^7.0.0", - "@babel/plugin-proposal-throw-expressions": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-import-meta": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.4.4", - "@babel/plugin-transform-runtime": "^7.5.0", - "@babel/preset-env": "^7.5.4", - "@babel/preset-flow": "^7.0.0", + "@types/jest": "^29.5.14", + "@babel/core": "^7.27.1", + "@babel/plugin-external-helpers": "^7.27.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-decorators": "^7.27.1", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-function-sent": "^7.27.1", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-throw-expressions": "^7.27.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-flow-strip-types": "^7.27.1", + "@babel/plugin-transform-runtime": "^7.27.1", + "@babel/preset-env": "^7.27.2", + "@babel/preset-typescript": "^7.27.1", "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^10.0.2", - "babel-jest": "^24.8.0", - "bundlesize": "^0.18.0", - "doctoc": "^1.3.0", - "eslint": "^6.0.1", - "eslint-config-react-app": "^3.0.6", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-flowtype": "^3.2.1", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-react": "^7.13.0", - "final-form": "^4.20.8", - "flow-bin": "^0.102.0", + "babel-eslint": "^10.1.0", + "babel-jest": "^29.7.0", + "bundlesize": "^0.18.2", + "doctoc": "^2.2.1", + "@eslint/js": "^9.27.0", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "eslint": "^9.27.0", + "globals": "^16.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "final-form": "^5.0.0-3", "glow": "^1.2.2", - "husky": "^3.0.0", - "jest": "^24.8.0", - "lint-staged": "^9.2.0", - "nps": "^5.9.5", - "nps-utils": "^1.5.0", - "prettier": "^1.18.2", - "prettier-eslint-cli": "^5.0.0", - "react": "^16.8.6", - "rollup": "^1.16.7", - "rollup-plugin-babel": "^4.3.3", - "rollup-plugin-commonjs": "^10.0.1", + "husky": "^9.1.7", + "jest": "^29.7.0", + "lint-staged": "^16.0.0", + "nps": "^5.10.0", + "nps-utils": "^1.7.0", + "prettier": "^3.5.3", + "prettier-eslint-cli": "^8.0.1", + "react": "^19.1.0", + "rollup": "^4.41.1", + "rollup-plugin-babel": "^4.4.0", + "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-flow": "^1.1.1", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", - "rollup-plugin-uglify": "^6.0.2", - "typescript": "^3.5.3" + "rollup-plugin-uglify": "^6.0.4", + "rollup-plugin-typescript2": "^0.36.0", + "typescript": "^5.8.3", + "ts-jest": "^29.2.5" }, "peerDependencies": { "final-form": "^4.20.8" @@ -82,10 +87,21 @@ ] }, "jest": { + "preset": "ts-jest", "testEnvironment": "node", "testPathIgnorePatterns": [ - ".*\\.ts" - ] + "/node_modules/", + "src/index.d.test.ts" + ], + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx" + ], + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + } }, "bundlesize": [ { diff --git a/rollup.config.js b/rollup.config.mjs similarity index 83% rename from rollup.config.js rename to rollup.config.mjs index 41ccabc..d4502eb 100644 --- a/rollup.config.js +++ b/rollup.config.mjs @@ -1,9 +1,10 @@ import resolve from 'rollup-plugin-node-resolve' import babel from 'rollup-plugin-babel' -import flow from 'rollup-plugin-flow' +import typescript from 'rollup-plugin-typescript2' import commonjs from 'rollup-plugin-commonjs' import { uglify } from 'rollup-plugin-uglify' import replace from 'rollup-plugin-replace' +import ts from 'typescript' const minify = process.env.MINIFY const format = process.env.FORMAT @@ -33,7 +34,7 @@ if (es) { } export default { - input: 'src/index.js', + input: 'src/index.ts', output: Object.assign( { name: 'final-form-arrays', @@ -43,8 +44,21 @@ export default { ), external: [], plugins: [ - resolve({ jsnext: true, main: true }), - flow(), + resolve({ + mainFields: ['module', 'jsnext:main', 'main'], + browser: true, + preferBuiltins: false + }), + typescript({ + typescript: ts, + clean: true, + tsconfigOverride: { + compilerOptions: { + declaration: false, + declarationMap: false + } + } + }), commonjs({ include: 'node_modules/**' }), babel({ exclude: 'node_modules/**', @@ -57,12 +71,10 @@ export default { modules: false, loose: true } - ], - '@babel/preset-flow' + ] ], plugins: [ ['@babel/plugin-transform-runtime', { useESModules: !cjs }], - '@babel/plugin-transform-flow-strip-types', '@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-import-meta', '@babel/plugin-proposal-class-properties', diff --git a/src/concat.test.js b/src/concat.test.ts similarity index 63% rename from src/concat.test.js rename to src/concat.test.ts index 1705d06..bd093c1 100644 --- a/src/concat.test.js +++ b/src/concat.test.ts @@ -1,16 +1,31 @@ import concat from './concat' +import { MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('concat', () => { - const getOp = value => { + const getOp = (value: any) => { const changeValue = jest.fn() - concat(['foo', value], {}, { changeValue }) + const mockState: MutableState = { + fieldSubscribers: {}, + fields: {}, + formState: { + values: {} + } + } as any + concat(['foo', value], mockState, createMockTools({ changeValue })) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} - const result = concat(['foo', ['bar', 'baz']], state, { changeValue }) + const state: MutableState = { + fieldSubscribers: {}, + fields: {}, + formState: { + values: {} + } + } as any + const result = concat(['foo', ['bar', 'baz']], state, createMockTools({ changeValue })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -32,4 +47,4 @@ describe('concat', () => { expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'b', 'c', 'd', 'e']) }) -}) +}) \ No newline at end of file diff --git a/src/concat.js b/src/concat.ts similarity index 50% rename from src/concat.js rename to src/concat.ts index f303feb..e9a0c42 100644 --- a/src/concat.js +++ b/src/concat.ts @@ -1,14 +1,13 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' const concat: Mutator = ( [name, value]: any[], state: MutableState, { changeValue }: Tools -) => { - changeValue(state, name, (array: ?(any[])): any[] => +): void => { + changeValue(state, name, (array?: any[]): any[] => array ? [...array, ...value] : value ) } -export default concat +export default concat \ No newline at end of file diff --git a/src/copyField.js b/src/copyField.ts similarity index 79% rename from src/copyField.js rename to src/copyField.ts index 33d3bb1..35abbf5 100644 --- a/src/copyField.js +++ b/src/copyField.ts @@ -1,12 +1,11 @@ -// @flow -import type { InternalFieldState } from 'final-form/dist/types' +import { InternalFieldState } from 'final-form' function copyField( - oldFields: { [string]: InternalFieldState }, + oldFields: { [key: string]: InternalFieldState }, oldKey: string, - newFields: { [string]: InternalFieldState }, + newFields: { [key: string]: Partial> }, newKey: string -) { +): void { newFields[newKey] = { ...oldFields[oldKey], name: newKey, @@ -32,4 +31,4 @@ function copyField( } } -export default copyField +export default copyField \ No newline at end of file diff --git a/src/index.d.test.ts b/src/index.d.test.ts index ea48168..8b2c810 100644 --- a/src/index.d.test.ts +++ b/src/index.d.test.ts @@ -1,10 +1,10 @@ // tslint:disable no-console -import { Config, createForm, AnyObject } from 'final-form' +import { Config, createForm } from 'final-form' import arrayMutators from './index' import { Mutators } from './index' -const onSubmit: Config['onSubmit'] = (values, callback) => {} +const onSubmit: Config['onSubmit'] = (_values, _callback) => { } const form = createForm({ mutators: { ...arrayMutators }, diff --git a/src/index.js b/src/index.js deleted file mode 100644 index bb419a5..0000000 --- a/src/index.js +++ /dev/null @@ -1,28 +0,0 @@ -// @flow -import type { Mutator } from 'final-form' -import insert from './insert' -import concat from './concat' -import move from './move' -import pop from './pop' -import push from './push' -import remove from './remove' -import removeBatch from './removeBatch' -import shift from './shift' -import swap from './swap' -import unshift from './unshift' -import update from './update' - -const mutators: { [string]: Mutator } = { - insert, - concat, - move, - pop, - push, - remove, - removeBatch, - shift, - swap, - unshift, - update -} -export default mutators diff --git a/src/index.js.flow b/src/index.js.flow deleted file mode 100644 index 285bcf6..0000000 --- a/src/index.js.flow +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import type { Mutator } from 'final-form' - -type DefaultType = { [string]: Mutator } - -declare export default DefaultType - -/** The shape of the mutators once final-form has bound them to state */ -export type Mutators = { - insert: (name: string, index: number, value: any) => void, - concat: (name: string, value: Array) => void, - move: (name: string, from: number, to: number) => void, - pop: (name: string) => any, - push: (name: string, value: any) => void, - remove: (name: string, index: number) => any, - removeBatch: (name: string, indexes: Array) => any, - shift: (name: string) => any, - swap: (name: string, indexA: number, indexB: number) => void, - update: (name: string, index: number, value: any) => void, - unshift: (name: string, value: any) => void -} diff --git a/src/index.d.ts b/src/index.ts similarity index 62% rename from src/index.d.ts rename to src/index.ts index 2b8fd37..de99e4b 100644 --- a/src/index.d.ts +++ b/src/index.ts @@ -1,16 +1,15 @@ import { Mutator } from 'final-form' - -export const insert: Mutator -export const concat: Mutator -export const move: Mutator -export const pop: Mutator -export const push: Mutator -export const removeBatch: Mutator -export const remove: Mutator -export const shift: Mutator -export const swap: Mutator -export const update: Mutator -export const unshift: Mutator +import insert from './insert' +import concat from './concat' +import move from './move' +import pop from './pop' +import push from './push' +import remove from './remove' +import removeBatch from './removeBatch' +import shift from './shift' +import swap from './swap' +import unshift from './unshift' +import update from './update' export interface DefaultType { insert: Mutator @@ -26,8 +25,36 @@ export interface DefaultType { unshift: Mutator } -declare const d: DefaultType -export default d +const mutators: DefaultType = { + insert, + concat, + move, + pop, + push, + remove, + removeBatch, + shift, + swap, + unshift, + update +} + +export default mutators + +// Export individual mutators +export { + insert, + concat, + move, + pop, + push, + remove, + removeBatch, + shift, + swap, + unshift, + update +} /** The shape of the mutators once final-form has bound them to state */ export interface Mutators { @@ -42,4 +69,4 @@ export interface Mutators { swap: (name: string, indexA: number, indexB: number) => void update: (name: string, index: number, value: any) => void unshift: (name: string, value: any) => void -} +} \ No newline at end of file diff --git a/src/insert.test.js b/src/insert.test.ts similarity index 81% rename from src/insert.test.js rename to src/insert.test.ts index 41663fd..024a283 100644 --- a/src/insert.test.js +++ b/src/insert.test.ts @@ -1,49 +1,50 @@ import insert from './insert' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('insert', () => { - const getOp = (index, value) => { + const getOp = (index, value: any) => { const changeValue = jest.fn() const resetFieldState = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } } - insert(['foo', index, value], state, { changeValue, resetFieldState }) + insert(['foo', index, value], state, createMockTools({ changeValue, resetFieldState })) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() const resetFieldState = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'], anotherField: 42 - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false @@ -54,10 +55,7 @@ describe('insert', () => { } } } - const result = insert(['foo', 0, 'bar'], state, { - changeValue, - resetFieldState - }) + const result = insert(['foo', 0, 'bar'], state, createMockTools({ changeValue, resetFieldState })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -86,47 +84,44 @@ describe('insert', () => { it('should increment other field data from the specified index', () => { const array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - const resetFieldState = name => { + const resetFieldState = (name: string) => { state.fields[name].touched = false } - const state = { + const state: MutableState = { formState: { values: { foo: array - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: true, error: 'B Error' - }, + } as any, 'foo[9]': { name: 'foo[9]', touched: true, error: 'J Error' - }, + } as any, 'foo[10]': { name: 'foo[10]', touched: false, error: 'K Error' - } + } as any } } - const returnValue = insert(['foo', 1, 'NEWVALUE'], state, { - changeValue, - resetFieldState - }) + const returnValue = insert(['foo', 1, 'NEWVALUE'], state, createMockTools({ changeValue, resetFieldState })) expect(returnValue).toBeUndefined() expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -146,14 +141,14 @@ describe('insert', () => { 'j', 'k' ] - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'A Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', touched: true, @@ -179,61 +174,58 @@ describe('insert', () => { it('should increment other field data from the specified index (nested arrays)', () => { const array = ['a', 'b', 'c', 'd'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - const resetFieldState = name => { + const resetFieldState = (name: string) => { state.fields[name].touched = false } - const state = { + const state: MutableState = { formState: { values: { foo: [array] - } + } as any }, fields: { 'foo[0][0]': { name: 'foo[0][0]', touched: true, error: 'A Error' - }, + } as any, 'foo[0][1]': { name: 'foo[0][1]', touched: true, error: 'B Error' - }, + } as any, 'foo[0][2]': { name: 'foo[0][2]', touched: true, error: 'C Error' - }, + } as any, 'foo[0][3]': { name: 'foo[0][3]', touched: false, error: 'D Error' - } + } as any } } - const returnValue = insert(['foo[0]', 1, 'NEWVALUE'], state, { - changeValue, - resetFieldState - }) + const returnValue = insert(['foo[0]', 1, 'NEWVALUE'], state, createMockTools({ changeValue, resetFieldState })) expect(returnValue).toBeUndefined() expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ formState: { values: { foo: [['a', 'NEWVALUE', 'b', 'c', 'd']] - } + } as any }, fields: { 'foo[0][0]': { name: 'foo[0][0]', touched: true, error: 'A Error' - }, + } as any, 'foo[0][2]': { name: 'foo[0][2]', touched: true, diff --git a/src/insert.js b/src/insert.ts similarity index 80% rename from src/insert.js rename to src/insert.ts index 89a0443..1d1b823 100644 --- a/src/insert.js +++ b/src/insert.ts @@ -1,5 +1,4 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' import copyField from './copyField' import { escapeRegexTokens } from './utils' @@ -7,16 +6,16 @@ const insert: Mutator = ( [name, index, value]: any[], state: MutableState, { changeValue }: Tools -) => { - changeValue(state, name, (array: ?(any[])): any[] => { +): void => { + changeValue(state, name, (array?: any[]): any[] => { const copy = [...(array || [])] copy.splice(index, 0, value) return copy }) - // now we have increment any higher indexes + // now increment any higher indices const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const newFields = {} + const newFields: { [key: string]: any } = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { @@ -37,4 +36,4 @@ const insert: Mutator = ( state.fields = newFields } -export default insert +export default insert \ No newline at end of file diff --git a/src/move.test.js b/src/move.test.ts similarity index 83% rename from src/move.test.js rename to src/move.test.ts index 91044c2..353f560 100644 --- a/src/move.test.js +++ b/src/move.test.ts @@ -1,24 +1,28 @@ import move from './move' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('move', () => { - const getOp = (from, to) => { + const getOp = (from: any, to: any) => { const changeValue = jest.fn() - move(['foo', from, to], { fields: {} }, { changeValue }) + const mockTools = createMockTools({ changeValue }) + move(['foo', from, to], { fields: {} } as any, mockTools) return changeValue.mock.calls[0][2] } it('should do nothing if from and to are equal', () => { const changeValue = jest.fn() - const result = move(['foo', 1, 1], { fields: {} }, { changeValue }) + const mockTools = createMockTools({ changeValue }) + const result = move(['foo', 1, 1], { fields: {} } as any, mockTools) expect(result).toBeUndefined() expect(changeValue).not.toHaveBeenCalled() }) it('should call changeValue once', () => { const changeValue = jest.fn() - const state = { fields: {} } - const result = move(['foo', 0, 2], state, { changeValue }) + const state: MutableState = { fields: {} } as any + const mockTools = createMockTools({ changeValue }) + const result = move(['foo', 0, 2], state, mockTools) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -47,16 +51,16 @@ describe('move', () => { it('should move field state from low index to high index', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: ['apple', 'banana', 'carrot', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -85,12 +89,12 @@ describe('move', () => { } } } - move(['foo', 0, 2], state, { changeValue }) + move(['foo', 0, 2], state, createMockTools({ changeValue })) expect(state).toEqual({ formState: { values: { foo: ['banana', 'carrot', 'apple', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -123,16 +127,16 @@ describe('move', () => { it('should move field state from high index to low index', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: ['apple', 'banana', 'carrot', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -161,12 +165,12 @@ describe('move', () => { } } } - move(['foo', 2, 0], state, { changeValue }) + move(['foo', 2, 0], state, createMockTools({ changeValue })) expect(state).toEqual({ formState: { values: { foo: ['carrot', 'apple', 'banana', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -199,12 +203,12 @@ describe('move', () => { it('should move deep field state from low index to high index', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: [ @@ -220,45 +224,45 @@ describe('move', () => { name: 'foo[0].dog', touched: true, error: 'Error A Dog' - }, + } as any, 'foo[0].cat': { name: 'foo[0].cat', touched: false, error: 'Error A Cat' - }, + } as any, 'foo[1].dog': { name: 'foo[1].dog', touched: true, error: 'Error B Dog' - }, + } as any, 'foo[1].cat': { name: 'foo[1].cat', touched: true, error: 'Error B Cat' - }, + } as any, 'foo[2].dog': { name: 'foo[2].dog', touched: true, error: 'Error C Dog' - }, + } as any, 'foo[2].cat': { name: 'foo[2].cat', touched: false, error: 'Error C Cat' - }, + } as any, 'foo[3].dog': { name: 'foo[3].dog', touched: false, error: 'Error D Dog' - }, + } as any, 'foo[3].cat': { name: 'foo[3].cat', touched: true, error: 'Error D Cat' - } + } as any } } - move(['foo', 0, 2], state, { changeValue }) + move(['foo', 0, 2], state, createMockTools({ changeValue })) expect(state).toMatchObject({ formState: { values: { @@ -281,17 +285,17 @@ describe('move', () => { name: 'foo[0].cat', touched: true, error: 'Error B Cat' - }, + } as any, 'foo[1].dog': { name: 'foo[1].dog', touched: true, error: 'Error C Dog' - }, + } as any, 'foo[1].cat': { name: 'foo[1].cat', touched: false, error: 'Error C Cat' - }, + } as any, 'foo[2].dog': { name: 'foo[2].dog', touched: true, @@ -308,24 +312,24 @@ describe('move', () => { name: 'foo[3].dog', touched: false, error: 'Error D Dog' - }, + } as any, 'foo[3].cat': { name: 'foo[3].cat', touched: true, error: 'Error D Cat' - } + } as any } }) }) it('should move deep field state from high index to low index', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: [ @@ -341,45 +345,45 @@ describe('move', () => { name: 'foo[0].dog', touched: true, error: 'Error A Dog' - }, + } as any, 'foo[0].cat': { name: 'foo[0].cat', touched: false, error: 'Error A Cat' - }, + } as any, 'foo[1].dog': { name: 'foo[1].dog', touched: true, error: 'Error B Dog' - }, + } as any, 'foo[1].cat': { name: 'foo[1].cat', touched: true, error: 'Error B Cat' - }, + } as any, 'foo[2].dog': { name: 'foo[2].dog', touched: true, error: 'Error C Dog' - }, + } as any, 'foo[2].cat': { name: 'foo[2].cat', touched: false, error: 'Error C Cat' - }, + } as any, 'foo[3].dog': { name: 'foo[3].dog', touched: false, error: 'Error D Dog' - }, + } as any, 'foo[3].cat': { name: 'foo[3].cat', touched: true, error: 'Error D Cat' - } + } as any } } - move(['foo', 2, 0], state, { changeValue }) + move(['foo', 2, 0], state, createMockTools({ changeValue })) expect(state).toMatchObject({ formState: { values: { @@ -408,12 +412,12 @@ describe('move', () => { name: 'foo[1].dog', touched: true, error: 'Error A Dog' - }, + } as any, 'foo[1].cat': { name: 'foo[1].cat', touched: false, error: 'Error A Cat' - }, + } as any, 'foo[2].dog': { name: 'foo[2].dog', touched: true, @@ -430,53 +434,53 @@ describe('move', () => { name: 'foo[3].dog', touched: false, error: 'Error D Dog' - }, + } as any, 'foo[3].cat': { name: 'foo[3].cat', touched: true, error: 'Error D Cat' - } + } as any } }) }) it('should move fields with different shapes', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: [{ dog: 'apple dog', cat: 'apple cat' }, { dog: 'banana dog' }] - } + } as any }, fields: { 'foo[0].dog': { name: 'foo[0].dog', touched: true, error: 'Error A Dog' - }, + } as any, 'foo[0].cat': { name: 'foo[0].cat', touched: false, error: 'Error A Cat' - }, + } as any, 'foo[1].dog': { name: 'foo[1].dog', touched: true, error: 'Error B Dog' - } + } as any } } - move(['foo', 0, 1], state, { changeValue }) + move(['foo', 0, 1], state, createMockTools({ changeValue })) expect(state).toMatchObject({ formState: { values: { foo: [{ dog: 'banana dog' }, { dog: 'apple dog', cat: 'apple cat' }] - } + } as any }, fields: { 'foo[0].dog': { @@ -502,16 +506,16 @@ describe('move', () => { }) it('should move fields with different complex not matching shapes', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { - foo: [{ dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red'}, { name: 'blue'}], deep: { inside: { rock: 'black'}} }, - { dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' }} }] + foo: [{ dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red' }, { name: 'blue' }], deep: { inside: { rock: 'black' } } }, + { dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' } } }] } }, fields: { @@ -519,50 +523,50 @@ describe('move', () => { name: 'foo[0].dog', touched: true, error: 'Error A Dog' - }, + } as any, 'foo[0].cat': { name: 'foo[0].cat', touched: false, error: 'Error A Cat' - }, + } as any, 'foo[0].colors[0].name': { name: 'foo[0].colors[0].name', touched: true, error: 'Error A Colors Red' - }, + } as any, 'foo[0].colors[1].name': { name: 'foo[0].colors[1].name', touched: true, error: 'Error A Colors Blue' - }, + } as any, 'foo[0].deep.inside.rock': { name: 'foo[0].deep.inside.rock', touched: true, error: 'Error A Deep Inside Rock Black' - }, + } as any, 'foo[1].dog': { name: 'foo[1].dog', touched: true, error: 'Error B Dog' - }, + } as any, 'foo[1].mouse': { name: 'foo[1].mouse', touched: true, error: 'Error B Mickey' - }, + } as any, 'foo[1].deep.inside.axe': { name: 'foo[1].deep.inside.axe', touched: true, error: 'Error B Deep Inside Axe Golden' - }, + } as any, } } - move(['foo', 0, 1], state, { changeValue }) + move(['foo', 0, 1], state, createMockTools({ changeValue })) expect(state).toMatchObject({ formState: { values: { - foo: [{ dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' }} }, - { dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red'}, { name: 'blue'}], deep: { inside: { rock: 'black'}} }] + foo: [{ dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' } } }, + { dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red' }, { name: 'blue' }], deep: { inside: { rock: 'black' } } }] } }, fields: { @@ -582,7 +586,7 @@ describe('move', () => { name: 'foo[0].deep.inside.axe', touched: true, error: 'Error B Deep Inside Axe Golden' - }, + } as any, 'foo[1].dog': { name: 'foo[1].dog', touched: true, @@ -611,23 +615,23 @@ describe('move', () => { name: 'foo[1].deep.inside.rock', touched: true, error: 'Error A Deep Inside Rock Black' - }, + } as any, } }) }) it('should preserve functions in field state', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: ['apple', 'banana', 'carrot', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -660,7 +664,7 @@ describe('move', () => { } } } - move(['foo', 0, 2], state, { changeValue }) + move(['foo', 0, 2], state, createMockTools({ changeValue })) expect(state.fields['foo[0]'].change()).toBe('foo[0]') expect(state.fields['foo[1]'].change()).toBe('foo[1]') expect(state.fields['foo[2]'].change()).toBe('foo[2]') diff --git a/src/move.js b/src/move.ts similarity index 83% rename from src/move.js rename to src/move.ts index 1529569..70578f6 100644 --- a/src/move.js +++ b/src/move.ts @@ -1,5 +1,4 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' import copyField from './copyField' import { escapeRegexTokens } from './utils' @@ -7,11 +6,11 @@ const move: Mutator = ( [name, from, to]: any[], state: MutableState, { changeValue }: Tools -) => { +): void => { if (from === to) { return } - changeValue(state, name, (array: ?(any[])): any[] => { + changeValue(state, name, (array?: any[]): any[] => { const copy = [...(array || [])] const value = copy[from] copy.splice(from, 1) @@ -19,11 +18,11 @@ const move: Mutator = ( return copy }) - const newFields = {} + const newFields: { [key: string]: any } = {} const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - let lowest - let highest - let increment + let lowest: number + let highest: number + let increment: number if (from > to) { lowest = to highest = from @@ -42,7 +41,7 @@ const move: Mutator = ( copyField(state.fields, key, newFields, newKey) return } - + if (lowest <= fieldIndex && fieldIndex <= highest) { // Shift all indices const newKey = `${name}[${fieldIndex + increment}]${tokens[2]}` @@ -59,4 +58,4 @@ const move: Mutator = ( state.fields = newFields } -export default move +export default move \ No newline at end of file diff --git a/src/pop.test.js b/src/pop.test.ts similarity index 75% rename from src/pop.test.js rename to src/pop.test.ts index adfd1da..b1a320f 100644 --- a/src/pop.test.js +++ b/src/pop.test.ts @@ -1,29 +1,30 @@ import pop from './pop' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('pop', () => { it('should call changeValue once', () => { const changeValue = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } } - const result = pop(['foo'], state, { changeValue, getIn, setIn }) + const result = pop(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -34,20 +35,20 @@ describe('pop', () => { it('should return undefined if array is undefined', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: undefined - } + } as any }, fields: {} - } - const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) + } as any + const returnValue = pop(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBeUndefined() const result = state.formState.foo expect(result).toBeUndefined() @@ -55,20 +56,20 @@ describe('pop', () => { it('should return empty array if array is empty', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: [] - } + } as any }, fields: {} - } - const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) + } as any + const returnValue = pop(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBeUndefined() const result = state.formState.values.foo expect(Array.isArray(result)).toBe(true) @@ -77,31 +78,31 @@ describe('pop', () => { it('should pop value off the end of array and return it', () => { // implementation of changeValue taken directly from Final Form - const changeValue = jest.fn((state, name, mutate) => { + const changeValue = jest.fn((state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any }) - const state = { + const state: MutableState = { formState: { values: { foo: ['a', 'b', 'c'] - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } } - const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) + const returnValue = pop(['foo'], state, createMockTools({ changeValue, getIn, setIn })) const result = state.formState.values.foo expect(returnValue).toBe('c') expect(Array.isArray(result)).toBe(true) @@ -111,46 +112,46 @@ describe('pop', () => { it('should pop value off the end of array and return it', () => { const array = ['a', 'b', 'c', 'd'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: array, anotherField: 42 - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'B Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', touched: true, error: 'C Error' - }, + } as any, 'foo[3]': { name: 'foo[3]', touched: false, error: 'D Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) + const returnValue = pop(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBe('d') expect(Array.isArray(state.formState.values.foo)).toBe(true) expect(state.formState.values.foo).not.toBe(array) // copied @@ -159,24 +160,24 @@ describe('pop', () => { values: { foo: ['a', 'b', 'c'], anotherField: 42 - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'B Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', touched: true, error: 'C Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false @@ -188,46 +189,46 @@ describe('pop', () => { it('should pop value off the end of array and return it (nested arrays)', () => { const array = ['a', 'b', 'c', 'd'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: [array], anotherField: 42 - } + } as any }, fields: { 'foo[0][0]': { name: 'foo[0][0]', touched: true, error: 'A Error' - }, + } as any, 'foo[0][1]': { name: 'foo[0][1]', touched: false, error: 'B Error' - }, + } as any, 'foo[0][2]': { name: 'foo[0][2]', touched: true, error: 'C Error' - }, + } as any, 'foo[0][3]': { name: 'foo[0][3]', touched: false, error: 'D Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = pop(['foo[0]'], state, { changeValue, getIn, setIn }) + const returnValue = pop(['foo[0]'], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBe('d') expect(Array.isArray(state.formState.values.foo)).toBe(true) expect(state.formState.values.foo).not.toBe(array) // copied @@ -236,24 +237,24 @@ describe('pop', () => { values: { foo: [['a', 'b', 'c']], anotherField: 42 - } + } as any }, fields: { 'foo[0][0]': { name: 'foo[0][0]', touched: true, error: 'A Error' - }, + } as any, 'foo[0][1]': { name: 'foo[0][1]', touched: false, error: 'B Error' - }, + } as any, 'foo[0][2]': { name: 'foo[0][2]', touched: true, error: 'C Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false diff --git a/src/pop.js b/src/pop.ts similarity index 75% rename from src/pop.js rename to src/pop.ts index cfdb486..0767ada 100644 --- a/src/pop.js +++ b/src/pop.ts @@ -1,12 +1,11 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' import remove from './remove' const pop: Mutator = ( [name]: any[], state: MutableState, tools: Tools -) => { +): any => { const { getIn } = tools; const array = getIn(state.formState.values, name) return array && array.length > 0 @@ -14,4 +13,4 @@ const pop: Mutator = ( : undefined } -export default pop +export default pop \ No newline at end of file diff --git a/src/push.test.js b/src/push.test.ts similarity index 71% rename from src/push.test.js rename to src/push.test.ts index 9e261f9..218a83d 100644 --- a/src/push.test.js +++ b/src/push.test.ts @@ -1,16 +1,20 @@ import push from './push' +import { createMockState, createMockTools } from './testUtils' describe('push', () => { - const getOp = value => { + const getOp = (value: any) => { const changeValue = jest.fn() - push(['foo', value], {}, { changeValue }) + const mockState = createMockState() + const mockTools = createMockTools({ changeValue }) + push(['foo', value], mockState, mockTools) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} - const result = push(['foo', 'bar'], state, { changeValue }) + const state = createMockState() + const tools = createMockTools({ changeValue }) + const result = push(['foo', 'bar'], state, tools) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -33,4 +37,4 @@ describe('push', () => { expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'b', 'c', 'd']) }) -}) +}) \ No newline at end of file diff --git a/src/push.js b/src/push.ts similarity index 50% rename from src/push.js rename to src/push.ts index 45eb8ab..c6544e9 100644 --- a/src/push.js +++ b/src/push.ts @@ -1,14 +1,13 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' const push: Mutator = ( [name, value]: any[], state: MutableState, { changeValue }: Tools -) => { - changeValue(state, name, (array: ?(any[])): any[] => +): void => { + changeValue(state, name, (array?: any[]): any[] => array ? [...array, value] : [value] ) } -export default push +export default push \ No newline at end of file diff --git a/src/remove.test.js b/src/remove.test.ts similarity index 72% rename from src/remove.test.js rename to src/remove.test.ts index 245f4ff..8f5a213 100644 --- a/src/remove.test.js +++ b/src/remove.test.ts @@ -1,29 +1,30 @@ import remove from './remove' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('remove', () => { it('should call changeValue once', () => { const changeValue = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } } - const result = remove(['foo', 0], state, { changeValue, getIn, setIn }) + const result = remove(['foo', 0], state, createMockTools({ changeValue, getIn, setIn })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -34,47 +35,63 @@ describe('remove', () => { it('should treat undefined like an empty array', () => { const changeValue = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: undefined - } + } as any }, fields: {} - } - const returnValue = remove(['foo', 1], state, { changeValue, getIn, setIn }) + } as any + const returnValue = remove(['foo', 1], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) expect(result).toBeUndefined() }) + it('should return undefined when removing last element from array', () => { + const changeValue = jest.fn() + const state: MutableState = { + formState: { + values: { + foo: ['only'] + } as any + }, + fields: {} + } as any + remove(['foo', 0], state, createMockTools({ changeValue, getIn, setIn })) + const op = changeValue.mock.calls[0][2] + const result = op(['only']) + expect(result).toBeUndefined() + }) + it('should remove value from the specified index, and return it', () => { const array = ['a', 'b', 'c', 'd'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - function blur3() {} - function change3() {} - function focus3() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + function blur1() { } + function change1() { } + function focus1() { } + function blur2() { } + function change2() { } + function focus2() { } + function blur3() { } + function change3() { } + function focus3() { } + const state: MutableState = { formState: { values: { foo: array, anotherField: 42 - } + } as any }, fields: { 'foo[0]': { @@ -84,7 +101,7 @@ describe('remove', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -92,7 +109,7 @@ describe('remove', () => { focus: focus1, touched: false, error: 'B Error' - }, + } as any, 'foo[3]': { name: 'foo[3]', blur: blur3, @@ -100,7 +117,7 @@ describe('remove', () => { focus: focus3, touched: false, error: 'D Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -108,14 +125,14 @@ describe('remove', () => { focus: focus2, touched: true, error: 'C Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = remove(['foo', 1], state, { changeValue, getIn, setIn }) + const returnValue = remove(['foo', 1], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBe('b') expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -123,7 +140,7 @@ describe('remove', () => { values: { foo: ['a', 'c', 'd'], anotherField: 42 - } + } as any }, fields: { 'foo[0]': { @@ -133,7 +150,7 @@ describe('remove', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -163,29 +180,29 @@ describe('remove', () => { it('should remove value from the specified index, and return it (nested arrays)', () => { const array = ['a', 'b', 'c', 'd'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - function blur3() {} - function change3() {} - function focus3() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + function blur1() { } + function change1() { } + function focus1() { } + function blur2() { } + function change2() { } + function focus2() { } + function blur3() { } + function change3() { } + function focus3() { } + const state: MutableState = { formState: { values: { foo: [array], anotherField: 42 - } + } as any }, fields: { 'foo[0][3]': { @@ -195,7 +212,7 @@ describe('remove', () => { focus: focus3, touched: false, error: 'D Error' - }, + } as any, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -203,7 +220,7 @@ describe('remove', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[0][1]': { name: 'foo[0][1]', blur: blur1, @@ -211,7 +228,7 @@ describe('remove', () => { focus: focus1, touched: false, error: 'B Error' - }, + } as any, 'foo[0][2]': { name: 'foo[0][2]', blur: blur2, @@ -219,18 +236,18 @@ describe('remove', () => { focus: focus2, touched: true, error: 'C Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = remove(['foo[0]', 1], state, { + const returnValue = remove(['foo[0]', 1], state, createMockTools({ changeValue, getIn, setIn - }) + })) expect(returnValue).toBe('b') expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -238,7 +255,7 @@ describe('remove', () => { values: { foo: [['a', 'c', 'd']], anotherField: 42 - } + } as any }, fields: { 'foo[0][2]': { @@ -257,7 +274,7 @@ describe('remove', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[0][1]': { name: 'foo[0][1]', blur: blur1, @@ -279,16 +296,16 @@ describe('remove', () => { const array = ['a', { key: 'val' }] const changeValue = jest.fn() const renameField = jest.fn() - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + function blur1() { } + function change1() { } + function focus1() { } + function blur2() { } + function change2() { } + function focus2() { } + const state: MutableState = { formState: { values: { foo: array, @@ -301,7 +318,7 @@ describe('remove', () => { } ] } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', @@ -310,7 +327,7 @@ describe('remove', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[0].key': { name: 'foo[0].key', blur: blur2, @@ -318,7 +335,7 @@ describe('remove', () => { focus: focus2, touched: false, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -326,7 +343,7 @@ describe('remove', () => { focus: focus1, touched: false, error: 'B Error' - }, + } as any, 'foo[1].key': { name: 'foo[1].key', blur: blur2, @@ -334,20 +351,15 @@ describe('remove', () => { focus: focus2, touched: false, error: 'B Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } - } + } as any - const returnValue = remove(['foo', 0], state, { - renameField, - changeValue, - getIn, - setIn - }) + const returnValue = remove(['foo', 0], state, createMockTools({ renameField, changeValue, getIn, setIn })) expect(returnValue).toBeUndefined() expect(getIn(state, 'formState.submitErrors')).toEqual({ foo: [] }) }) @@ -356,16 +368,16 @@ describe('remove', () => { const array = ['a', { key: 'val' }] const changeValue = jest.fn() const renameField = jest.fn() - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + function blur1() { } + function change1() { } + function focus1() { } + function blur2() { } + function change2() { } + function focus2() { } + const state: MutableState = { formState: { values: { foo: array, @@ -381,7 +393,7 @@ describe('remove', () => { } ] } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', @@ -390,7 +402,7 @@ describe('remove', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[0].key': { name: 'foo[0].key', blur: blur2, @@ -398,7 +410,7 @@ describe('remove', () => { focus: focus2, touched: false, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -406,7 +418,7 @@ describe('remove', () => { focus: focus1, touched: false, error: 'B Error' - }, + } as any, 'foo[1].key': { name: 'foo[1].key', blur: blur2, @@ -414,20 +426,15 @@ describe('remove', () => { focus: focus2, touched: false, error: 'B Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } - } + } as any - const returnValue = remove(['foo', 0], state, { - renameField, - changeValue, - getIn, - setIn - }) + const returnValue = remove(['foo', 0], state, createMockTools({ renameField, changeValue, getIn, setIn })) expect(returnValue).toBeUndefined() expect(getIn(state, 'formState.submitErrors')).toEqual({ foo: [{ key: 'B Submit Error' }] diff --git a/src/remove.js b/src/remove.ts similarity index 85% rename from src/remove.js rename to src/remove.ts index dce9a24..86bf025 100644 --- a/src/remove.js +++ b/src/remove.ts @@ -1,5 +1,4 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' import copyField from './copyField' import { escapeRegexTokens } from './utils' @@ -7,9 +6,9 @@ const remove: Mutator = ( [name, index]: any[], state: MutableState, { changeValue, getIn, setIn }: Tools -) => { - let returnValue - changeValue(state, name, (array: ?(any[])): ?(any[]) => { +): any => { + let returnValue: any + changeValue(state, name, (array?: any[]): any[] | undefined => { if (!array) { return array } @@ -25,7 +24,7 @@ const remove: Mutator = ( // now we have to remove any subfields for our index, // and decrement all higher indexes. const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const newFields = {} + const newFields: { [key: string]: any } = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { @@ -39,7 +38,7 @@ const remove: Mutator = ( // if has submitErrors for array if (Array.isArray(submitErrors)) { submitErrors.splice(index, 1) - state = setIn(state, path, submitErrors) + setIn(state, path, submitErrors) } } @@ -63,4 +62,4 @@ const remove: Mutator = ( return returnValue } -export default remove +export default remove \ No newline at end of file diff --git a/src/removeBatch.test.js b/src/removeBatch.test.ts similarity index 75% rename from src/removeBatch.test.js rename to src/removeBatch.test.ts index aaec2e5..f444964 100644 --- a/src/removeBatch.test.js +++ b/src/removeBatch.test.ts @@ -1,53 +1,54 @@ import removeBatch from './removeBatch' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('removeBatch', () => { const getOp = value => { const changeValue = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] - } + } as any }, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } } - removeBatch(['foo', value], state, { changeValue }) + removeBatch(['foo', value], state, createMockTools({ changeValue })) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { // implementation of changeValue taken directly from Final Form - const changeValue = jest.fn((state, name, mutate) => { + const changeValue = jest.fn((state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any }) - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + function blur1() { } + function change1() { } + function focus1() { } + function blur2() { } + function change2() { } + function focus2() { } + const state: MutableState = { formState: { values: { foo: ['one', 'two', 'three'] - } + } as any }, fields: { 'foo[0]': { @@ -57,7 +58,7 @@ describe('removeBatch', () => { focus: focus0, touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -65,7 +66,7 @@ describe('removeBatch', () => { focus: focus1, touched: false, error: 'Second Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -73,10 +74,10 @@ describe('removeBatch', () => { focus: focus2, touched: true, error: 'Third Error' - } + } as any } } - const result = removeBatch(['foo', [1, 2]], state, { changeValue }) + const result = removeBatch(['foo', [1, 2]], state, createMockTools({ changeValue })) expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['two', 'three']) expect(changeValue).toHaveBeenCalled() @@ -88,7 +89,7 @@ describe('removeBatch', () => { formState: { values: { foo: ['one'] - } + } as any }, fields: { 'foo[0]': { @@ -106,25 +107,25 @@ describe('removeBatch', () => { it('should not matter if indexes are out of order', () => { // implementation of changeValue taken directly from Final Form - const changeValue = jest.fn((state, name, mutate) => { + const changeValue = jest.fn((state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any }) - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + function blur1() { } + function change1() { } + function focus1() { } + function blur2() { } + function change2() { } + function focus2() { } + const state: MutableState = { formState: { values: { foo: ['one', 'two', 'three'] - } + } as any }, fields: { 'foo[0]': { @@ -134,7 +135,7 @@ describe('removeBatch', () => { focus: focus0, touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -142,7 +143,7 @@ describe('removeBatch', () => { focus: focus1, touched: false, error: 'Second Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -150,10 +151,10 @@ describe('removeBatch', () => { focus: focus2, touched: true, error: 'Third Error' - } + } as any } } - const result = removeBatch(['foo', [2, 0]], state, { changeValue }) + const result = removeBatch(['foo', [2, 0]], state, createMockTools({ changeValue })) expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['three', 'one']) expect(changeValue).toHaveBeenCalled() @@ -165,7 +166,7 @@ describe('removeBatch', () => { formState: { values: { foo: ['two'] - } + } as any }, fields: { 'foo[0]': { @@ -189,14 +190,14 @@ describe('removeBatch', () => { it('should keep the original state if no indexes are specified to be removed', () => { const array = ['a', 'b', 'c', 'd', 'e'] - function blur0() {} - function change0() {} - function focus0() {} - const state = { + function blur0() { } + function change0() { } + function focus0() { } + const state: MutableState = { formState: { values: { foo: array - } + } as any }, fields: { 'foo[0]': { @@ -206,20 +207,18 @@ describe('removeBatch', () => { focus: focus0, touched: true, error: 'A Error' - } + } as any } } const changeValue = jest.fn() - const returnValue = removeBatch(['foo[0]', []], state, { - changeValue - }) + const returnValue = removeBatch(['foo[0]', []], state, createMockTools({ changeValue })) expect(returnValue).toEqual([]) expect(state.formState.values.foo).toBe(array) // no change expect(state).toEqual({ formState: { values: { foo: array - } + } as any }, fields: { 'foo[0]': { @@ -229,7 +228,7 @@ describe('removeBatch', () => { focus: focus0, touched: true, error: 'A Error' - } + } as any } }) }) @@ -251,32 +250,32 @@ describe('removeBatch', () => { it('should adjust higher indexes when removing', () => { const array = ['a', 'b', 'c', 'd', 'e'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - function blur0() {} - function blur1() {} - function blur2() {} - function blur3() {} - function blur4() {} - function change0() {} - function change1() {} - function change2() {} - function change3() {} - function change4() {} - function focus0() {} - function focus1() {} - function focus2() {} - function focus3() {} - function focus4() {} - const state = { + function blur0() { } + function blur1() { } + function blur2() { } + function blur3() { } + function blur4() { } + function change0() { } + function change1() { } + function change2() { } + function change3() { } + function change4() { } + function focus0() { } + function focus1() { } + function focus2() { } + function focus3() { } + function focus4() { } + const state: MutableState = { formState: { values: { foo: array, anotherField: 42 - } + } as any }, fields: { 'foo[4]': { @@ -286,7 +285,7 @@ describe('removeBatch', () => { focus: focus4, touched: true, error: 'E Error' - }, + } as any, 'foo[0]': { name: 'foo[0]', blur: blur0, @@ -294,7 +293,7 @@ describe('removeBatch', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', blur: blur1, @@ -302,7 +301,7 @@ describe('removeBatch', () => { focus: focus1, touched: false, error: 'B Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -310,7 +309,7 @@ describe('removeBatch', () => { focus: focus2, touched: true, error: 'C Error' - }, + } as any, 'foo[3]': { name: 'foo[3]', blur: blur3, @@ -318,14 +317,14 @@ describe('removeBatch', () => { focus: focus3, touched: false, error: 'D Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = removeBatch(['foo', [1, 3]], state, { changeValue }) + const returnValue = removeBatch(['foo', [1, 3]], state, createMockTools({ changeValue })) expect(returnValue).toEqual(['b', 'd']) expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -333,7 +332,7 @@ describe('removeBatch', () => { values: { foo: ['a', 'c', 'e'], anotherField: 42 - } + } as any }, fields: { 'foo[2]': { @@ -374,32 +373,32 @@ describe('removeBatch', () => { it('should adjust higher indexes when removing (nested arrays)', () => { const array = ['a', 'b', 'c', 'd', 'e'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - function blur0() {} - function blur1() {} - function blur2() {} - function blur3() {} - function blur4() {} - function change0() {} - function change1() {} - function change2() {} - function change3() {} - function change4() {} - function focus0() {} - function focus1() {} - function focus2() {} - function focus3() {} - function focus4() {} - const state = { + function blur0() { } + function blur1() { } + function blur2() { } + function blur3() { } + function blur4() { } + function change0() { } + function change1() { } + function change2() { } + function change3() { } + function change4() { } + function focus0() { } + function focus1() { } + function focus2() { } + function focus3() { } + function focus4() { } + const state: MutableState = { formState: { values: { foo: [array], anotherField: 42 - } + } as any }, fields: { 'foo[0][4]': { @@ -409,7 +408,7 @@ describe('removeBatch', () => { focus: focus4, touched: true, error: 'E Error' - }, + } as any, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -417,7 +416,7 @@ describe('removeBatch', () => { focus: focus0, touched: true, error: 'A Error' - }, + } as any, 'foo[0][1]': { name: 'foo[0][1]', blur: blur1, @@ -425,7 +424,7 @@ describe('removeBatch', () => { focus: focus1, touched: false, error: 'B Error' - }, + } as any, 'foo[0][2]': { name: 'foo[0][2]', blur: blur2, @@ -433,7 +432,7 @@ describe('removeBatch', () => { focus: focus2, touched: true, error: 'C Error' - }, + } as any, 'foo[0][3]': { name: 'foo[0][3]', blur: blur3, @@ -441,16 +440,14 @@ describe('removeBatch', () => { focus: focus3, touched: false, error: 'D Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = removeBatch(['foo[0]', [1, 3]], state, { - changeValue - }) + const returnValue = removeBatch(['foo[0]', [1, 3]], state, createMockTools({ changeValue })) expect(returnValue).toEqual(['b', 'd']) expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -458,7 +455,7 @@ describe('removeBatch', () => { values: { foo: [['a', 'c', 'e']], anotherField: 42 - } + } as any }, fields: { 'foo[0][2]': { @@ -495,4 +492,10 @@ describe('removeBatch', () => { } }) }) + + it('should return undefined when removing all elements', () => { + const op = getOp([0, 1]) + const result = op(['a', 'b']) + expect(result).toBeUndefined() + }) }) diff --git a/src/removeBatch.js b/src/removeBatch.ts similarity index 86% rename from src/removeBatch.js rename to src/removeBatch.ts index 48057f1..34198b6 100644 --- a/src/removeBatch.js +++ b/src/removeBatch.ts @@ -1,5 +1,4 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' import copyField from './copyField' import { escapeRegexTokens } from './utils' @@ -29,7 +28,7 @@ const removeBatch: Mutator = ( [name, indexes]: any[], state: MutableState, { changeValue }: Tools -) => { +): any[] => { if (indexes.length === 0) { return [] } @@ -44,10 +43,10 @@ const removeBatch: Mutator = ( } } - let returnValue = [] - changeValue(state, name, (array: ?(any[])): ?(any[]) => { + let returnValue: any[] = [] + changeValue(state, name, (array?: any[]): any[] | undefined => { // use original order of indexes for return value - returnValue = indexes.map(index => array && array[index]) + returnValue = indexes.map((index: number) => array && array[index]) if (!array) { return array @@ -67,7 +66,7 @@ const removeBatch: Mutator = ( // now we have to remove any subfields for our indexes, // and decrement all higher indexes. const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const newFields = {} + const newFields: { [key: string]: any } = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { @@ -80,9 +79,8 @@ const removeBatch: Mutator = ( if (fieldIndex > sortedIndexes[0]) { // Shift all higher indices down - const decrementedKey = `${name}[${fieldIndex - ~indexOfFieldIndex}]${ - tokens[2] - }` + const decrementedKey = `${name}[${fieldIndex - ~indexOfFieldIndex}]${tokens[2] + }` copyField(state.fields, key, newFields, decrementedKey) return } @@ -97,4 +95,4 @@ const removeBatch: Mutator = ( return returnValue } -export default removeBatch +export default removeBatch \ No newline at end of file diff --git a/src/shift.js b/src/shift.js deleted file mode 100644 index eb15224..0000000 --- a/src/shift.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' -import remove from './remove' - -const shift: Mutator = ( - [name]: any[], - state: MutableState, - tools: Tools -) => remove([name, 0], state, tools) - -export default shift diff --git a/src/shift.test.js b/src/shift.test.ts similarity index 78% rename from src/shift.test.js rename to src/shift.test.ts index 84950cc..590218e 100644 --- a/src/shift.test.js +++ b/src/shift.test.ts @@ -1,29 +1,30 @@ import shift from './shift' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('shift', () => { it('should call changeValue once', () => { const changeValue = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } - } - const result = shift(['foo'], state, { changeValue, getIn, setIn }) + } as any + const result = shift(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -34,15 +35,15 @@ describe('shift', () => { it('should treat undefined like an empty array', () => { const changeValue = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: undefined } - }, + } as any, fields: {} - } - const returnValue = shift(['foo'], state, { changeValue, getIn, setIn }) + } as any + const returnValue = shift(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) @@ -52,46 +53,46 @@ describe('shift', () => { it('should remove first value from array and return it', () => { const array = ['a', 'b', 'c', 'd'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - const state = { + const state: MutableState = { formState: { values: { foo: array, anotherField: 42 } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'B Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', touched: true, error: 'C Error' - }, + } as any, 'foo[3]': { name: 'foo[3]', touched: false, error: 'D Error' - }, + } as any, anotherField: { name: 'anotherField', touched: false - } + } as any } - } - const returnValue = shift(['foo'], state, { changeValue, getIn, setIn }) + } as any + const returnValue = shift(['foo'], state, createMockTools({ changeValue, getIn, setIn })) expect(returnValue).toBe('a') expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -127,4 +128,4 @@ describe('shift', () => { } }) }) -}) +}) \ No newline at end of file diff --git a/src/shift.ts b/src/shift.ts new file mode 100644 index 0000000..c76fa03 --- /dev/null +++ b/src/shift.ts @@ -0,0 +1,10 @@ +import { MutableState, Mutator, Tools } from 'final-form' +import remove from './remove' + +const shift: Mutator = ( + [name]: any[], + state: MutableState, + tools: Tools +): any => remove([name, 0], state, tools) + +export default shift \ No newline at end of file diff --git a/src/swap.test.js b/src/swap.test.ts similarity index 88% rename from src/swap.test.js rename to src/swap.test.ts index b74dd7b..2314a1b 100644 --- a/src/swap.test.js +++ b/src/swap.test.ts @@ -1,24 +1,25 @@ import swap from './swap' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('swap', () => { - const getOp = (from, to) => { + const getOp = (from, to: any) => { const changeValue = jest.fn() - swap(['foo', from, to], { fields: {} }, { changeValue }) + swap(['foo', from, to], { fields: {} }, createMockTools({ changeValue })) return changeValue.mock.calls[0][2] } it('should do nothing if indexA and indexB are equal', () => { const changeValue = jest.fn() - const result = swap(['foo', 1, 1], { fields: {} }, { changeValue }) + const result = swap(['foo', 1, 1], { fields: {} }, createMockTools({ changeValue })) expect(result).toBeUndefined() expect(changeValue).not.toHaveBeenCalled() }) it('should call changeValue once', () => { const changeValue = jest.fn() - const state = { fields: {} } - const result = swap(['foo', 0, 2], state, { changeValue }) + const state: MutableState = { fields: {} } + const result = swap(['foo', 0, 2], state, createMockTools({ changeValue })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -47,16 +48,16 @@ describe('swap', () => { it('should swap field state as well as values', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: ['apple', 'banana', 'carrot', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -85,12 +86,12 @@ describe('swap', () => { } } } - swap(['foo', 0, 2], state, { changeValue }) + swap(['foo', 0, 2], state, createMockTools({ changeValue })) expect(state).toEqual({ formState: { values: { foo: ['carrot', 'banana', 'apple', 'date'] - } + } as any }, fields: { 'foo[2]': { @@ -123,12 +124,12 @@ describe('swap', () => { it('should swap field state for deep fields and different shapes', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: [ @@ -202,7 +203,7 @@ describe('swap', () => { } } } - swap(['foo', 0, 2], state, { changeValue }) + swap(['foo', 0, 2], state, createMockTools({ changeValue })) expect(state).toEqual({ formState: { values: { @@ -281,16 +282,16 @@ describe('swap', () => { it('should preserve functions in field state', () => { // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) - state.formState.values = setIn(state.formState.values, name, after) || {} + state.formState.values = setIn(state.formState.values, name, after) || {} as any } - const state = { + const state: MutableState = { formState: { values: { foo: ['apple', 'banana', 'carrot', 'date'] - } + } as any }, fields: { 'foo[0]': { @@ -323,7 +324,7 @@ describe('swap', () => { } } } - swap(['foo', 0, 2], state, { changeValue }) + swap(['foo', 0, 2], state, createMockTools({ changeValue })) expect(state.fields['foo[0]'].change()).toBe('foo[0]') expect(state.fields['foo[1]'].change()).toBe('foo[1]') expect(state.fields['foo[2]'].change()).toBe('foo[2]') diff --git a/src/swap.js b/src/swap.ts similarity index 85% rename from src/swap.js rename to src/swap.ts index 736adec..3c1a50d 100644 --- a/src/swap.js +++ b/src/swap.ts @@ -1,16 +1,15 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' import copyField from './copyField' const swap: Mutator = ( [name, indexA, indexB]: any[], state: MutableState, { changeValue }: Tools -) => { +): void => { if (indexA === indexB) { return } - changeValue(state, name, (array: ?(any[])): any[] => { + changeValue(state, name, (array?: any[]): any[] => { const copy = [...(array || [])] const a = copy[indexA] copy[indexA] = copy[indexB] @@ -21,7 +20,7 @@ const swap: Mutator = ( // swap all field state that begin with "name[indexA]" with that under "name[indexB]" const aPrefix = `${name}[${indexA}]` const bPrefix = `${name}[${indexB}]` - const newFields = {} + const newFields: { [key: string]: any } = {} Object.keys(state.fields).forEach(key => { if (key.substring(0, aPrefix.length) === aPrefix) { const suffix = key.substring(aPrefix.length) @@ -40,4 +39,4 @@ const swap: Mutator = ( state.fields = newFields } -export default swap +export default swap \ No newline at end of file diff --git a/src/testUtils.test.ts b/src/testUtils.test.ts new file mode 100644 index 0000000..361f9bb --- /dev/null +++ b/src/testUtils.test.ts @@ -0,0 +1,37 @@ +import { createMockState, createMockTools } from './testUtils' + +describe('testUtils', () => { + describe('createMockState', () => { + it('should create a mock state object', () => { + const state = createMockState() + expect(state).toHaveProperty('fieldSubscribers') + expect(state).toHaveProperty('fields') + expect(state).toHaveProperty('formState') + expect(state.formState).toHaveProperty('values') + }) + }) + + describe('createMockTools', () => { + it('should create mock tools with default functions', () => { + const tools = createMockTools() + expect(tools.changeValue).toBeDefined() + expect(tools.getIn).toBeDefined() + expect(tools.setIn).toBeDefined() + expect(tools.shallowEqual).toBeDefined() + expect(tools.renameField).toBeDefined() + expect(tools.resetFieldState).toBeDefined() + }) + + it('should allow overriding specific tools', () => { + const customChangeValue = jest.fn() + const tools = createMockTools({ changeValue: customChangeValue }) + expect(tools.changeValue).toBe(customChangeValue) + expect(tools.getIn).toBeDefined() + }) + + it('should work with no overrides', () => { + const tools = createMockTools() + expect(typeof tools.changeValue).toBe('function') + }) + }) +}) \ No newline at end of file diff --git a/src/testUtils.ts b/src/testUtils.ts new file mode 100644 index 0000000..fcc4981 --- /dev/null +++ b/src/testUtils.ts @@ -0,0 +1,19 @@ +import { MutableState, Tools } from 'final-form' + +export const createMockState = (): MutableState => ({ + fieldSubscribers: {}, + fields: {}, + formState: { + values: {} + } +} as any) + +export const createMockTools = (overrides: Partial> = {}): Tools => ({ + changeValue: jest.fn(), + getIn: jest.fn(), + setIn: jest.fn(), + shallowEqual: jest.fn(), + renameField: jest.fn(), + resetFieldState: jest.fn(), + ...overrides +} as unknown as Tools) \ No newline at end of file diff --git a/src/unshift.js b/src/unshift.js deleted file mode 100644 index 62a99ac..0000000 --- a/src/unshift.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' -import insert from './insert' - -const unshift: Mutator = ( - [name, value]: any[], - state: MutableState, - tools: Tools -) => insert([name, 0, value], state, tools) - -export default unshift diff --git a/src/unshift.test.js b/src/unshift.test.ts similarity index 76% rename from src/unshift.test.js rename to src/unshift.test.ts index 0dbf91b..1b57cb2 100644 --- a/src/unshift.test.js +++ b/src/unshift.test.ts @@ -1,59 +1,57 @@ import unshift from './unshift' -import { getIn, setIn } from 'final-form' +import { getIn, setIn, MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('unshift', () => { - const getOp = value => { + const getOp = (value: any) => { const changeValue = jest.fn() const resetFieldState = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } - } - unshift(['foo', value], state, { changeValue, resetFieldState }) + } as any + unshift(['foo', value], state, createMockTools({ changeValue, resetFieldState })) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() const resetFieldState = jest.fn() - const state = { + const state: MutableState = { formState: { values: { foo: ['one', 'two'] } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'First Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'Second Error' - } + } as any } - } - const result = unshift(['foo', 'bar'], state, { - changeValue, - resetFieldState - }) + } as any + const result = unshift(['foo', 'bar'], state, createMockTools({ changeValue, resetFieldState })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -73,42 +71,39 @@ describe('unshift', () => { it('should insert value to beginning of array', () => { const array = ['a', 'b', 'c'] // implementation of changeValue taken directly from Final Form - const changeValue = (state, name, mutate) => { + const changeValue = (state: any, name: string, mutate: (value: any) => any) => { const before = getIn(state.formState.values, name) const after = mutate(before) state.formState.values = setIn(state.formState.values, name, after) || {} } - const resetFieldState = name => { + const resetFieldState = (name: string) => { state.fields[name].touched = false } - const state = { + const state: MutableState = { formState: { values: { foo: array } - }, + } as any, fields: { 'foo[0]': { name: 'foo[0]', touched: true, error: 'A Error' - }, + } as any, 'foo[1]': { name: 'foo[1]', touched: false, error: 'B Error' - }, + } as any, 'foo[2]': { name: 'foo[2]', touched: true, error: 'C Error' - } + } as any } - } - const returnValue = unshift(['foo', 'NEWVALUE'], state, { - changeValue, - resetFieldState - }) + } as any + const returnValue = unshift(['foo', 'NEWVALUE'], state, createMockTools({ changeValue, resetFieldState })) expect(returnValue).toBeUndefined() expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ @@ -139,4 +134,4 @@ describe('unshift', () => { } }) }) -}) +}) \ No newline at end of file diff --git a/src/unshift.ts b/src/unshift.ts new file mode 100644 index 0000000..f575849 --- /dev/null +++ b/src/unshift.ts @@ -0,0 +1,10 @@ +import { MutableState, Mutator, Tools } from 'final-form' +import insert from './insert' + +const unshift: Mutator = ( + [name, value]: any[], + state: MutableState, + tools: Tools +): void => insert([name, 0, value], state, tools) + +export default unshift \ No newline at end of file diff --git a/src/update.test.js b/src/update.test.ts similarity index 63% rename from src/update.test.js rename to src/update.test.ts index 0084e1b..5419eab 100644 --- a/src/update.test.js +++ b/src/update.test.ts @@ -1,16 +1,31 @@ import update from './update' +import { MutableState } from 'final-form' +import { createMockTools } from './testUtils' describe('update', () => { - const getOp = (index, value) => { + const getOp = (index: number, value: any) => { const changeValue = jest.fn() - update(['foo', index, value], {}, { changeValue }) + const mockState: MutableState = { + fieldSubscribers: {}, + fields: {}, + formState: { + values: {} + } + } as any + update(['foo', index, value], mockState, createMockTools({ changeValue })) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} - const result = update(['foo', 0, 'bar'], state, { changeValue }) + const state: MutableState = { + fieldSubscribers: {}, + fields: {}, + formState: { + values: {} + } + } as any + const result = update(['foo', 0, 'bar'], state, createMockTools({ changeValue })) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -35,4 +50,4 @@ describe('update', () => { expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'd', 'c']) }) -}) +}) \ No newline at end of file diff --git a/src/update.js b/src/update.ts similarity index 57% rename from src/update.js rename to src/update.ts index dfbc5db..df77bdd 100644 --- a/src/update.js +++ b/src/update.ts @@ -1,16 +1,15 @@ -// @flow -import type { MutableState, Mutator, Tools } from 'final-form' +import { MutableState, Mutator, Tools } from 'final-form' const update: Mutator = ( [name, index, value]: any[], state: MutableState, { changeValue }: Tools -) => { - changeValue(state, name, (array: ?(any[])): any[] => { +): void => { + changeValue(state, name, (array?: any[]): any[] => { const copy = [...(array || [])] copy.splice(index, 1, value) return copy }) } -export default update +export default update \ No newline at end of file diff --git a/src/utils.js b/src/utils.ts similarity index 88% rename from src/utils.js rename to src/utils.ts index 592f664..e1294a8 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,5 +1,3 @@ -// @flow - // From MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping export const escapeRegexTokens = (string: string): string => - string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string + string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index da0f67f..63bce14 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,26 @@ { "compilerOptions": { - "baseUrl": ".", - "noEmit": true, - "strict": true + "target": "ES2018", + "module": "ESNext", + "lib": ["ES2018"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["jest"] }, - "include": ["./src/**/*"] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] }