/test"],
5 |
6 | // Jest transformations -- this adds support for TypeScript
7 | // using ts-jest
8 | transform: {
9 | "^.+\\.tsx?$": "ts-jest"
10 | },
11 |
12 | // Runs special logic, such as cleaning up components
13 | // when using React Testing Library and adds special
14 | // extended assertions to Jest
15 | setupFilesAfterEnv: [
16 | "@testing-library/jest-dom/extend-expect"
17 | ],
18 |
19 | // Test spec file resolution pattern
20 | // Matches parent folder `__tests__` and filename
21 | // should contain `test` or `spec`.
22 | testRegex: "(/test/.*|(\\.|/)(test|spec))\\.tsx?$",
23 | // testRegex: "./test/store/reducers/control.test.ts",
24 | // testRegex: "test/**/*.[jt]s?(x)?$",
25 |
26 | // Module file extensions for importing
27 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
28 | };
29 |
--------------------------------------------------------------------------------
/cypress/integration/validation.spec.js:
--------------------------------------------------------------------------------
1 | // This recipe shows how to interact with a range input (slider)
2 |
3 | // Eventually, this will be expanded to includes examples of interacting
4 | // with various form elements
5 |
6 | describe('Form Interactions', function () {
7 | beforeEach(function () {
8 | cy.server();
9 | })
10 |
11 | it('updates range value when moving slider', function () {
12 | // To interact with a range input (slider), we need to set its value and
13 | // then trigger the appropriate event to signal it has changed
14 |
15 | // Here, we invoke jQuery's val() method to set the value
16 | // and trigger the 'change' event
17 |
18 | // Note that some implementations may rely on the 'input' event,
19 | // which is fired as a user moves the slider, but is not supported
20 | // by some browsers
21 | cy.get('input[type=range]').as('range')
22 | .invoke('val', 25)
23 | .trigger('change')
24 |
25 | cy.get('@range').siblings('p')
26 | .should('have.text', '25')
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/examples/example2/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #20262E;
3 | padding: 20px;
4 | font-family: Helvetica;
5 | }
6 |
7 | label {
8 | margin-right: 5px;
9 | }
10 |
11 | #root {
12 | background: #fff;
13 | border-radius: 4px;
14 | padding: 20px;
15 | transition: all 0.2s;
16 | }
17 |
18 | .fieldError {
19 | color: #a94442;
20 | }
21 |
22 | .react-chloroform-error {
23 | border-color: #a94442 !important;
24 | }
25 |
26 | .submitButton {
27 | transition: all ease 1.8s;
28 | }
29 |
30 | .loading:after {
31 | content: ' .';
32 | animation: dots 1s steps(5, end) infinite;
33 | }
34 |
35 | @keyframes dots {
36 | 0%,
37 | 20% {
38 | color: rgba(0, 0, 0, 0);
39 | text-shadow: .25em 0 0 rgba(0, 0, 0, 0), .5em 0 0 rgba(0, 0, 0, 0);
40 | }
41 | 40% {
42 | color: white;
43 | text-shadow: .25em 0 0 rgba(0, 0, 0, 0), .5em 0 0 rgba(0, 0, 0, 0);
44 | }
45 | 60% {
46 | text-shadow: .25em 0 0 white, .5em 0 0 rgba(0, 0, 0, 0);
47 | }
48 | 80%,
49 | 100% {
50 | text-shadow: .25em 0 0 white, .5em 0 0 white;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Dependency directories
33 | node_modules/
34 | jspm_packages/
35 |
36 | # Typescript v1 declaration files
37 | typings/
38 |
39 | # Optional npm cache directory
40 | .npm
41 |
42 | # Optional eslint cache
43 | .eslintcache
44 |
45 | # Optional REPL history
46 | .node_repl_history
47 |
48 | # Output of 'npm pack'
49 | *.tgz
50 |
51 | # Yarn Integrity file
52 | .yarn-integrity
53 |
54 | # dotenv environment variables file
55 | .env
56 |
57 | .DS_Store
58 |
59 | .idea
60 | es
61 | dist
62 |
--------------------------------------------------------------------------------
/src/store/reducers/form.ts:
--------------------------------------------------------------------------------
1 | import {FAILED, HAS_ERRORS, SUBMITTED, SUBMITTING} from '../../constants/form';
2 | import {
3 | RESET_SUBMIT,
4 | SET_SUBMITTED,
5 | SET_SUBMITTING,
6 | SET_SUBMIT_FAILED,
7 | INITIALIZE_STATE,
8 | } from '../action-types';
9 |
10 | const initialState = {
11 | status: undefined,
12 | initialized: false,
13 | };
14 |
15 | export default (state: Store.FormState = initialState, action: Store.Action): Store.FormState => {
16 | switch (action.type) {
17 | case SET_SUBMITTED:
18 | return {...state, status: SUBMITTED};
19 | case SET_SUBMITTING:
20 | return {...state, status: SUBMITTING};
21 | case SET_SUBMIT_FAILED:
22 | return {...state, status: FAILED};
23 | case RESET_SUBMIT:
24 | return initialState;
25 | case INITIALIZE_STATE:
26 | return {...state, initialized: true};
27 | default:
28 | return state;
29 | }
30 | };
31 |
32 | export const getStatus = (state: Store.FormState, hasFormErrors: boolean) =>
33 | state.status || (hasFormErrors ? HAS_ERRORS : '');
34 |
35 | export const getInitialized = (state: Store.FormState) => state.initialized;
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Darri Steinn Konráðsson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "rootDir": "./src",
5 | "outDir": "dist",
6 | "allowJs": false,
7 | "allowSyntheticDefaultImports": true,
8 | "alwaysStrict": true,
9 | "baseUrl": "./",
10 | "declaration": true,
11 | "emitDecoratorMetadata": false,
12 | "esModuleInterop": true,
13 | "experimentalDecorators": false,
14 | "forceConsistentCasingInFileNames": true,
15 | "importHelpers": true,
16 | "jsx": "react",
17 | "lib": ["dom", "esnext"],
18 | "module": "esnext",
19 | "moduleResolution": "node",
20 | "noFallthroughCasesInSwitch": true,
21 | "noImplicitAny": true,
22 | "noImplicitReturns": true,
23 | "noImplicitThis": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "pretty": true,
27 | "sourceMap": true,
28 | "strict": true,
29 | "strictFunctionTypes": true,
30 | "strictNullChecks": true,
31 | "strictPropertyInitialization": true,
32 | "stripInternal": true,
33 | "target": "es5",
34 | "paths": {
35 | "*": ["src/*", "node_modules/*"]
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
2 | type ExplicitAny = any;
3 |
4 | type Scalar = string | number | boolean;
5 |
6 | type Primitive = boolean | null | undefined | number | bigint | string | symbol;
7 |
8 | type ValueControl = {
9 | validateOn: string,
10 | validated: boolean,
11 | errors: string[],
12 | parseValue: Function,
13 | };
14 |
15 | type ScalarValue = ValueControl & {
16 | value: Scalar
17 | };
18 |
19 | type ObjectValue = ValueControl & {
20 | value: {[key: string]: ArrayValue | ObjectValue | ScalarValue}
21 | };
22 |
23 | type ArrayValue = ValueControl & {
24 | maxIndex: number,
25 | value: ArrayValue[] | ObjectValue[] | ScalarValue[]
26 | };
27 |
28 | declare namespace Store {
29 | type ControlState = {
30 | blueprint: {[key: string]: {} | undefined},
31 | store: {[key: string]: ScalarValue | ArrayValue | ObjectValue},
32 | validators: {[key: string]: Function},
33 | };
34 |
35 | type FormState = {
36 | status?: string,
37 | initialized: boolean,
38 | };
39 |
40 | interface CombinedState {
41 | form: FormState;
42 | control: ControlState;
43 | }
44 |
45 | type Action = {
46 | type: string,
47 | payload: ExplicitAny,
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "react": {
4 | "version": "16.8.0"
5 | }
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "prettier",
11 | "prettier/react",
12 | "plugin:@typescript-eslint/recommended"
13 | ],
14 | "parser": "babel-eslint",
15 | "plugins": [
16 | "react",
17 | "@typescript-eslint",
18 | "import",
19 | "prettier"
20 | ],
21 | "parserOptions": {
22 | "ecmaVersion": 2016,
23 | "sourceType": "module",
24 | "ecmaFeatures": {
25 | "jsx": true
26 | }
27 | },
28 | "env": {
29 | "es6": true,
30 | "node": true,
31 | "browser": true,
32 | "jest": true
33 | },
34 | "rules": {
35 | "arrow-body-style": "off",
36 | "arrow-parens": "off",
37 | "comma-dangle": ["error", "always-multiline"],
38 | "spaced-comment": "warn",
39 | "global-require": "off",
40 | "new-cap": "off",
41 | "no-console": "warn",
42 | "no-mixed-operators": "off",
43 | "no-underscore-dangle": "off",
44 | "function-paren-newline": "off",
45 | "operator-assignment": ["error", "never"],
46 | "camelcase": "off",
47 | "object-curly-spacing": ["error", "never"],
48 | "object-curly-newline": "off",
49 | "quotes": ["error", "single", {"avoidEscape": true}],
50 |
51 | "react/jsx-filename-extension": "off",
52 | "react/no-multi-comp": ["error", { "ignoreStateless": true }],
53 | "react/require-default-props": "off",
54 |
55 | "prettier/prettier": "error",
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/store/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import {Provider, connect as reduxConnect} from 'react-redux';
3 | // import {Store} from 'redux';
4 |
5 | export const Store = React.createContext({});
6 |
7 | // export const withLocalStore = (store: Store) => (WrappedComponent: React.ComponentType
| ExplicitAny) =>
8 | // (props: P) =>
9 | //
10 | //
11 | // ;
12 | //
13 | // export const connect = reduxConnect;
14 |
15 | export const compose = (...funcs: Function[]) => {
16 | if (funcs.length === 0) {
17 | // infer the argument type so it is usable in inference down the line
18 | return (arg: ExplicitAny) => arg
19 | }
20 |
21 | if (funcs.length === 1) {
22 | return funcs[0]
23 | }
24 |
25 | return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
26 | };
27 |
28 | /*
29 | // import React from 'react'
30 | import {
31 | // Provider,
32 | createStoreHook,
33 | createDispatchHook,
34 | createSelectorHook
35 | } from 'react-redux'
36 |
37 | const MyContext = React.createContext(null)
38 |
39 | // Export your custom hooks if you wish to use them in other files.
40 | export const useStore = createStoreHook(MyContext)
41 | export const useDispatch = createDispatchHook(MyContext)
42 | export const useSelector = createSelectorHook(MyContext)
43 |
44 | const myStore = createStore(rootReducer)
45 |
46 | export function MyProvider({ children }) {
47 | return (
48 |
49 | {children}
50 |
51 | )
52 | }
53 | */
54 |
--------------------------------------------------------------------------------
/src/store/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import {createSelector} from 'reselect';
2 |
3 | import {HAS_ERRORS, SUBMITTING} from '../../constants/form';
4 |
5 | import control, * as fromControl from './control';
6 | import form, * as fromForm from './form';
7 |
8 | const combineReducers = (reducers: {[key: string]: typeof control | typeof form}) => (
9 | state: {[key: string]: Store.FormState & Store.ControlState} = {},
10 | action: Store.Action
11 | ) =>
12 | Object.keys(reducers).reduce(
13 | (nextState, key) => ({
14 | ...nextState,
15 | [key]: reducers[key](state[key], action),
16 | }),
17 | {}
18 | );
19 |
20 | export default combineReducers({
21 | control,
22 | form,
23 | });
24 |
25 | /*
26 | * Form
27 | */
28 | export const getFormStatus = (state: Store.CombinedState) =>
29 | fromForm.getStatus(state.form, hasFormErrors(state));
30 |
31 | export const canBeSubmitted = (state: Store.CombinedState) =>
32 | [HAS_ERRORS, SUBMITTING].includes(getFormStatus(state));
33 |
34 | export const isFormInitialized = (state: Store.CombinedState) =>
35 | fromForm.getInitialized(state.form);
36 |
37 | /*
38 | * Control
39 | */
40 | export const getError = (state: Store.CombinedState, model: string) =>
41 | fromControl.getError(state.control, model);
42 |
43 | export const getFormValues = (state: Store.CombinedState) => fromControl.getValues(state.control.store);
44 |
45 | export const getValue = () => createSelector(
46 | (state: Store.CombinedState): {[_: string]: ExplicitAny} => state.control.store,
47 | (_: ExplicitAny, model: string): string => model,
48 | (store: {[_: string]: Function}, model: string) => fromControl.getValue(store, model.split('.')),
49 | )
50 |
51 | export const getValidators = () => createSelector(
52 | (state: Store.CombinedState): {[_: string]: Function} => state.control.validators,
53 | (_: ExplicitAny, model: string): string => model,
54 | (validators: {[_: string]: Function}, model: string) => fromControl.getValidators(validators, model),
55 | )
56 |
57 | export const hasBeenValidated = (state: Store.CombinedState, model: string) =>
58 | fromControl.hasBeenValidated(state.control, model);
59 |
60 | export const hasError = (state: Store.CombinedState, model: string) =>
61 | fromControl.hasError(state.control, model);
62 |
63 | export const hasFormErrors = (state: Store.CombinedState): boolean => fromControl.hasErrors(state.control);
64 |
--------------------------------------------------------------------------------
/cypress/app/src/components/ValidationTest.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | Form,
5 | Input,
6 | Select,
7 | TextArea,
8 | } from 'react-chloroform';
9 |
10 | function ValidationTest() {
11 | const attachCypressOrConsoleLog = model => {
12 | if (window.Cypress) {
13 | window.model = model;
14 | } else {
15 | console.log(model);
16 | }
17 | };
18 |
19 | return (
20 |
79 | );
80 | }
81 |
82 | export default ValidationTest;
83 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const isString = (val: ExplicitAny) => typeof val === 'string';
2 |
3 | export const isObject = (obj: ExplicitAny) => obj && typeof obj === 'object';
4 |
5 | export const isArray = (arr: ExplicitAny) => Array.isArray(arr);
6 |
7 | export const isNumber = (number: ExplicitAny) => !isNaN(number);
8 |
9 | export const getIndex = (str: string): string => {
10 | return str.split('.').pop() as string;
11 | };
12 |
13 | export const merge = (arr: [], value: Primitive, path: string) => {
14 | const pos: string = getIndex(path);
15 | const size = Math.max(isArray(arr) ? arr.length : 0, isNumber(pos) ? parseInt(pos) + 1 : 0);
16 | if (pos === '*') {
17 | return Array(size).fill(value);
18 | } else if (!isNumber(pos)) {
19 | return value;
20 | }
21 |
22 | const iPos: number = parseInt(pos as string);
23 | if (iPos >= size) {
24 | return arr;
25 | }
26 |
27 | const array = arr || new Array(size);
28 | return [
29 | ...array.slice(0, iPos),
30 | value,
31 | ...array.slice(iPos + 1)
32 | ];
33 | };
34 |
35 | export const mergeDeep = (...objects: {[key: string]: ExplicitAny}[]) =>
36 | objects.reduce((prev: {[key: string]: ExplicitAny}, obj: {[key: string]: ExplicitAny}) => {
37 | if (!obj) {
38 | return prev;
39 | }
40 | Object.keys(obj).forEach((key: string) => {
41 | const pVal: ExplicitAny = prev[key];
42 | const oVal: ExplicitAny = obj[key];
43 |
44 | if (isArray(pVal) && isArray(oVal)) {
45 | prev[key] = pVal.concat(...oVal);
46 | } else if (isObject(pVal) && isObject(oVal)) {
47 | prev[key] = mergeDeep(pVal, oVal);
48 | } else {
49 | prev[key] = oVal;
50 | }
51 | });
52 |
53 | return prev;
54 | }, {});
55 |
56 | export const getIn = (obj: {[key: string]: ExplicitAny}, ...path: string[]): ExplicitAny =>
57 | path.reduce(
58 | (accumulator: {[key: string]: ExplicitAny}, next: string) => {
59 | if (!accumulator) {
60 | return undefined;
61 | }
62 | return accumulator[next];
63 | }, obj);
64 |
65 | export const arrayToObject = (array: [], value: Scalar | []) =>
66 | array.reduceRight(
67 | (obj: {}, next: string, i: number) => {
68 | if (next === '*') {
69 | return i === array.length - 1 ? obj : [obj];
70 | }
71 | return {[next]: obj};
72 | }, isArray(value) ? (value as []).filter((x: Scalar) => x !== undefined) : value
73 | );
74 |
75 | export const fromDotProp = (name: string): string => name && name.replace(/\.[0-9]+/g, '.*');
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-chloroform",
3 | "version": "0.2.8",
4 | "description": "A framework for building forms in React applications",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/darrikonn/react-chloroform.git"
8 | },
9 | "keywords": [
10 | "form",
11 | "form-validation",
12 | "react",
13 | "react-chloroform",
14 | "reactjs"
15 | ],
16 | "main": "dist/index.js",
17 | "module": "dist/index.js",
18 | "author": "Darri Steinn Konradsson",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/darrikonn/react-chloroform/issues"
22 | },
23 | "homepage": "https://github.com/darrikonn/react-chloroform#readme",
24 | "scripts": {
25 | "build": "tsc",
26 | "build:watch": "tsc-watch",
27 | "test": "jest",
28 | "test:watch": "yarn test -- --watch",
29 | "clean": "rm -rf dist",
30 | "lint": "eslint src --ext ts,tsx",
31 | "lint:fix": "eslint src --ext ts,tsx --fix",
32 | "tsc": "tsc --noEmit",
33 | "format": "prettier --config .prettierrc --write \"src/**/*.ts*\"",
34 | "cypress:run": "cypress run",
35 | "cypress:open": "cypress open",
36 | "prepublish": "tsc"
37 | },
38 | "types": "./dist/index.d.ts",
39 | "peerDependencies": {
40 | "react": "^16.8.0"
41 | },
42 | "dependencies": {
43 | "prop-types": "^15.7.2",
44 | "react-redux": "^7.1.3",
45 | "redux-thunk": "^2.3.0",
46 | "reselect": "^4.0.0"
47 | },
48 | "devDependencies": {
49 | "@babel/cli": "^7.8.4",
50 | "@babel/core": "^7.8.4",
51 | "@babel/plugin-proposal-class-properties": "^7.1.0",
52 | "@babel/preset-env": "^7.8.4",
53 | "@babel/preset-react": "^7.0.0",
54 | "@testing-library/jest-dom": "^5.1.1",
55 | "@types/jest": "^25.1.2",
56 | "@types/node": "^13.7.0",
57 | "@types/react": "^16.9.19",
58 | "@types/react-redux": "^7.1.7",
59 | "@typescript-eslint/eslint-plugin": "^2.18.0",
60 | "@typescript-eslint/parser": "^2.18.0",
61 | "babel-core": "^7.0.0-bridge.0",
62 | "babel-eslint": "^10.0.3",
63 | "cypress": "^4.0.0",
64 | "eslint": "^6.8.0",
65 | "eslint-config-prettier": "^6.10.0",
66 | "eslint-config-react": "^1.1.7",
67 | "eslint-plugin-import": "^2.7.0",
68 | "eslint-plugin-prettier": "^3.0.0",
69 | "eslint-plugin-react": "^7.4.0",
70 | "eslint-plugin-react-hooks": "^2.3.0",
71 | "jest": "^25.1.0",
72 | "prettier": "^1.7.4",
73 | "ts-jest": "^25.2.0",
74 | "tsc-watch": "^4.1.0",
75 | "typescript": "^3.7.5"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/cypress/app/src/components/DataStructureTest.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Button,
4 | Checkbox,
5 | DataList,
6 | Form,
7 | Input,
8 | RadioButton,
9 | Select,
10 | TextArea,
11 | } from 'react-chloroform';
12 |
13 | function DataStructureTest() {
14 | const attachCypressOrConsoleLog = model => {
15 | if (window.Cypress) {
16 | window.model = model;
17 | } else {
18 | console.log(model);
19 | }
20 | };
21 |
22 | return (
23 |
24 |
82 |
83 | );
84 | }
85 |
86 | export default DataStructureTest;
87 |
--------------------------------------------------------------------------------
/src/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {useDispatch} from 'react-redux';
3 |
4 | import {useCachedSelector, useGetErrors, useWillMount} from '../hooks';
5 | import controlActions from '../actions/controls';
6 | import {BLUR, FOCUS, INPUT, MOUNT} from '../constants/events';
7 | import {getValue/*, hasBeenValidated, isFormInitialized*/} from '../store/reducers';
8 |
9 | interface PropTypes {
10 | autoFocus?: boolean;
11 | className?: string;
12 | disabled?: boolean;
13 | id?: string;
14 | model: string;
15 | onChange?: Function;
16 | parseValue?: Function;
17 | validator?: Function;
18 | style?: React.CSSProperties;
19 | validateOn?: typeof BLUR | typeof FOCUS | typeof INPUT | typeof MOUNT;
20 | }
21 |
22 | function Checkbox({
23 | autoFocus,
24 | className,
25 | disabled,
26 | id,
27 | model,
28 | onChange = () => {},
29 | parseValue,
30 | style,
31 | validateOn,
32 | validator = () => {},
33 | }: PropTypes) {
34 | useWillMount(() => controlActions.mountModel(model, parseValue, validateOn === MOUNT, validator));
35 |
36 | const dispatch = useDispatch();
37 | const value = useCachedSelector(getValue, model) || '';
38 | const errors: string[] = useGetErrors(model, value);
39 | // console.log('RENDERING: checkbox', model, errors);
40 |
41 | const handleOnChange = (e: React.ChangeEvent): void => {
42 | dispatch(controlActions.setValue(model, e.target.checked));
43 | onChange(model, e.target.checked);
44 | };
45 |
46 | const getClassName: () => string = () => {
47 | return [className, errors.length < 1] // && isValidated ? `CHCl3Error ${model}-CHCl3Error` : undefined]
48 | .join(' ')
49 | .trim();
50 | };
51 |
52 | const markValidated = () => {}; // isValidated || setValidated(model);
53 |
54 | return (
55 |
68 | );
69 | }
70 |
71 | /*
72 | const mapStateToProps = (state: Store.CombinedState, {model}: PropTypes) => ({
73 | value: getValue(state, model),
74 | errors: [],
75 | // initialized: isFormInitialized(state),
76 | });
77 |
78 | const mapDispatchToProps = {
79 | deleteValue: controlActions.deleteValue,
80 | markValidated: controlActions.markValidated,
81 | mountModel: controlActions.mountModel,
82 | setErrors: controlActions.setErrors,
83 | setValue: controlActions.setValue,
84 | updateValue: controlActions.updateValue,
85 | };
86 | */
87 |
88 | export default memo(Checkbox);
89 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {useDispatch} from 'react-redux';
3 |
4 | import {useCachedSelector, useGetErrors, useWillMount} from '../hooks';
5 | import controlActions from '../actions/controls';
6 | import {BLUR, FOCUS, INPUT, MOUNT} from '../constants/events';
7 | import {getValue/*, hasBeenValidated, isFormInitialized*/} from '../store/reducers';
8 |
9 | interface PropTypes {
10 | autoFocus?: boolean;
11 | className?: string;
12 | disabled?: boolean;
13 | id?: string;
14 | model: string;
15 | onChange?: Function;
16 | parseValue?: Function;
17 | placeholder?: string;
18 | style?: React.CSSProperties;
19 | type?: 'text' | 'email' | 'password' | 'number';
20 | validateOn?: typeof BLUR | typeof FOCUS | typeof INPUT | typeof MOUNT;
21 | validator?: Function;
22 | }
23 |
24 | function Input({
25 | autoFocus,
26 | className,
27 | disabled,
28 | id,
29 | model,
30 | onChange = () => {},
31 | parseValue,
32 | placeholder,
33 | style,
34 | type,
35 | validateOn,
36 | validator,
37 | }: PropTypes) {
38 | useWillMount(() => controlActions.mountModel(model, parseValue, validateOn === MOUNT, validator));
39 |
40 | const dispatch = useDispatch();
41 | const value = useCachedSelector(getValue, model) || '';
42 | const errors: string[] = useGetErrors(model, value);
43 |
44 | // console.log('RENDERING: input', model, value);
45 | // console.log("ERRORS", model, errors);
46 |
47 | const handleOnChange = (e: React.ChangeEvent): void => {
48 | dispatch(controlActions.setValue(model, e.target.value));
49 | onChange(model, e.target.value);
50 | };
51 |
52 | const getClassName: () => string = () => {
53 | return [className, errors.length < 1] // && isValidated ? `CHCl3Error ${model}-CHCl3Error` : undefined]
54 | .join(' ')
55 | .trim();
56 | };
57 |
58 | const markValidated = () => {}; // isValidated || setValidated(model);
59 |
60 | return (
61 |
75 | );
76 | }
77 |
78 | /*
79 | const mapStateToProps = (state: Store.CombinedState, {model}: PropTypes) => ({
80 | initialized: isFormInitialized(state),
81 | isValidated: hasBeenValidated(state, model),
82 | // value: getValue(state, model),
83 | gValidators: () => getValidators(state, model),
84 | });
85 |
86 | const mapDispatchToProps = {
87 | markValidated: controlActions.markValidated,
88 | mountModel: controlActions.mountModel,
89 | setErrors: controlActions.setErrors,
90 | // setValue: controlActions.setValue,
91 | };
92 | */
93 |
94 | export default memo(Input);
95 |
--------------------------------------------------------------------------------
/src/components/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {useDispatch} from 'react-redux';
3 |
4 | import {useCachedSelector, useGetErrors, useWillMount} from '../hooks';
5 | import controlActions from '../actions/controls';
6 | import {BLUR, FOCUS, INPUT, MOUNT} from '../constants/events';
7 | //import {getValue2 as getValue/*, hasError*/} from '../store/reducers';
8 | import {getValue/*, hasBeenValidated, isFormInitialized*/} from '../store/reducers';
9 |
10 | interface PropTypes {
11 | autoFocus?: boolean;
12 | className?: string;
13 | disabled?: boolean;
14 | id?: string;
15 | model: string;
16 | onChange?: Function;
17 | parseValue?: Function;
18 | placeholder?: string;
19 | style?: React.CSSProperties;
20 | validateOn?: typeof BLUR | typeof FOCUS | typeof INPUT | typeof MOUNT;
21 | validator?: Function;
22 | value: string | number;
23 | }
24 |
25 | function RadioButton({
26 | autoFocus,
27 | className,
28 | disabled,
29 | id,
30 | model,
31 | onChange = () => {},
32 | parseValue,
33 | placeholder,
34 | style,
35 | validateOn,
36 | validator,
37 | value,
38 | }: PropTypes) {
39 | useWillMount(() => controlActions.mountModel(model, parseValue, validateOn === MOUNT, validator));
40 |
41 | const dispatch = useDispatch();
42 | const checked = useCachedSelector(getValue, model) || '';
43 | const errors: string[] = useGetErrors(model, value);
44 | // console.log('RENDERING: radiobutton', model);
45 |
46 | const handleOnChange = (e: React.ChangeEvent): void => {
47 | dispatch(controlActions.setValue(model, e.target.value));
48 | onChange(model, e.target.value);
49 | };
50 |
51 | const getClassName: () => string = () => {
52 | return [className, errors.length < 1] // && isValidated ? `CHCl3Error ${model}-CHCl3Error` : undefined]
53 | .join(' ')
54 | .trim();
55 | };
56 |
57 | const markValidated = () => {}; // isValidated || setValidated(model);
58 |
59 | return (
60 |
75 | );
76 | }
77 |
78 | /*
79 | const mapStateToProps = (state: Store.CombinedState, {model}: PropTypes) => ({
80 | checked: getValue(state, model),
81 | // hasError: hasError(state, model),
82 | // parseValue: (x: string | number) => x, // disable parseValue for radio-buttons
83 | });
84 |
85 | const mapDispatchToProps = {
86 | markValidated: controlActions.markValidated,
87 | mountModel: controlActions.mountModel,
88 | setErrors: controlActions.setErrors,
89 | setValue: controlActions.setValue,
90 | };
91 | */
92 |
93 | export default memo(RadioButton);
94 |
--------------------------------------------------------------------------------
/src/components/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {useDispatch} from 'react-redux';
3 |
4 | import {useCachedSelector, useGetErrors, useWillMount} from '../hooks';
5 | import controlActions from '../actions/controls';
6 | import {BLUR, FOCUS, INPUT, MOUNT} from '../constants/events';
7 | // import {getValue2 as getValue, hasBeenValidated, hasError, isFormInitialized} from '../store/reducers';
8 | import {getValue/*, hasBeenValidated, isFormInitialized*/} from '../store/reducers';
9 |
10 | interface PropTypes {
11 | autoFocus?: boolean;
12 | className?: string;
13 | cols?: number;
14 | disabled?: boolean;
15 | id?: string;
16 | model: string;
17 | onChange?: Function;
18 | parseValue?: Function;
19 | placeholder?: string;
20 | rows?: number;
21 | style?: React.CSSProperties;
22 | validateOn?: typeof BLUR | typeof FOCUS | typeof INPUT | typeof MOUNT;
23 | validator?: Function;
24 | }
25 |
26 | function TextArea({
27 | autoFocus,
28 | className,
29 | cols = 30,
30 | disabled,
31 | id,
32 | model,
33 | onChange = () => {},
34 | parseValue,
35 | placeholder,
36 | rows = 10,
37 | style,
38 | validateOn,
39 | validator,
40 | }: PropTypes) {
41 | useWillMount(() => controlActions.mountModel(model, parseValue, validateOn === MOUNT, validator));
42 |
43 | const dispatch = useDispatch();
44 | const value = useCachedSelector(getValue, model) || '';
45 | const errors: string[] = useGetErrors(model, value);
46 | // console.log('RENDERING: textarea', model);
47 |
48 | const handleOnChange = (e: React.ChangeEvent): void => {
49 | dispatch(controlActions.setValue(model, e.target.value));
50 | onChange(model, e.target.value);
51 | };
52 |
53 | const getClassName: () => string = () => {
54 | return [className, errors.length < 1] // && isValidated ? `CHCl3Error ${model}-CHCl3Error` : undefined]
55 | .join(' ')
56 | .trim();
57 | };
58 |
59 | const markValidated = () => {}; // isValidated || setValidated(model);
60 |
61 | return (
62 |
77 | );
78 | }
79 |
80 | /*
81 | const mapStateToProps = (state: Store.CombinedState, {model}: PropTypes) => ({
82 | hasError: hasError(state, model),
83 | initialized: isFormInitialized(state),
84 | isValidated: hasBeenValidated(state, model),
85 | value: getValue(state, model),
86 | });
87 |
88 | const mapDispatchToProps = {
89 | markValidated: controlActions.markValidated,
90 | mountModel: controlActions.mountModel,
91 | setErrors: controlActions.setErrors,
92 | setValue: controlActions.setValue,
93 | };
94 | */
95 |
96 | export default memo(TextArea);
97 |
--------------------------------------------------------------------------------
/examples/example1/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactDOM from "react-dom";
3 | import {
4 | Form,
5 | FormInput,
6 | Button,
7 | ChloroformError,
8 | TextArea,
9 | Checkbox
10 | } from "react-chloroform";
11 |
12 | const isRequired = val => (val && val.length > 0) || "This field is required";
13 |
14 | class App extends Component {
15 | handleSubmit = model => console.log(model);
16 |
17 | render() {
18 | const initialState = {
19 | email: "example@example.com",
20 | name: "John Doe"
21 | };
22 |
23 | return (
24 |
25 |
89 |
90 | );
91 | }
92 | }
93 |
94 | ReactDOM.render(, document.getElementById("root"));
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-chloroform
2 |
3 | ---
4 |
5 | [](https://www.npmjs.com/package/react-chloroform)
6 | [](https://npmcharts.com/compare/react-chloroform?minimal=true)
7 | [](https://travis-ci.org/darrikonn/react-chloroform)
8 | [](https://github.com/prettier/prettier)
9 | [](https://github.com/darrikonn/react-chloroform/blob/master/LICENSE)
10 |
11 | ## Why
12 | > "No hooking up, super lightweight, and easy to use".
13 |
14 | A form validation library with only `react` and `prop-types` as its dependency.
15 |
16 | ## Install
17 | To install this package, run:
18 | ```
19 | npm install --save react-chloroform
20 | ```
21 |
22 | `yarn` users can use:
23 | ```
24 | yarn add react-chloroform
25 | ```
26 |
27 | You can also use the `UMD` build:
28 | ```
29 |
30 | ```
31 |
32 | Or alternatively the `es5 commonjs` build:
33 | ```
34 |
35 | ```
36 |
37 | ## Examples
38 | Refer to [`/examples`](https://github.com/darrikonn/react-chloroform/tree/master/examples) for all example source code.
39 |
40 | ### Quick start
41 | ```jsx
42 | import React from 'react';
43 | import {Form, FormInput, Button, ChloroformError} from 'react-chloroform';
44 |
45 | const YourFormComponent = () => {
46 | const handleSubmit = model => console.log(model);
47 |
48 | const initialState = {
49 | email: 'example@example.com',
50 | name: 'John Doe',
51 | };
52 |
53 | const isRequired = val => (val && val.length > 0) || 'This field is required';
54 |
55 | return (
56 |
72 | );
73 | };
74 |
75 | export default YourFormComponent;
76 | ```
77 |
78 | ### Fiddles
79 | - [`Example 1`](https://jsfiddle.net/darrikonn/gshkfp9v/)
80 | - [`Example 2`](https://jsfiddle.net/darrikonn/61z0exLq/)
81 |
82 | ## API
83 | Check out the [`api`](https://github.com/darrikonn/react-chloroform/blob/master/API.md).
84 |
85 | ## Contribution
86 | React-Chloroform is open for contributions by the community.
87 | Read the [`contributing guidelines`](https://github.com/darrikonn/react-chloroform/blob/master/CONTRIBUTING.md).
88 |
89 | ## Testing
90 | ```bash
91 | npm install
92 | ```
93 |
94 | Link from the react-chloroform git repo:
95 |
96 | ```bash
97 | npm link
98 | ```
99 |
100 | And then link from your test project with:
101 | ```bash
102 | npm link react-chloroform
103 | ```
104 |
105 | Build es with:
106 | ```bash
107 | npm run build:es:watch
108 | ```
109 |
110 | ## License
111 | MIT
112 |
--------------------------------------------------------------------------------
/src/components/DataList.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {useDispatch} from 'react-redux';
3 |
4 | import {useCachedSelector, useGetErrors, useWillMount} from '../hooks';
5 | import controlActions from '../actions/controls';
6 | import {BLUR, FOCUS, INPUT, MOUNT} from '../constants/events';
7 | // import {getValue2 as getValue, isFormInitialized, hasBeenValidated, hasError} from '../store/reducers';
8 | import {getValue/*, hasBeenValidated, isFormInitialized*/} from '../store/reducers';
9 |
10 | interface PropTypes {
11 | autoFocus?: boolean;
12 | className?: string;
13 | disabled?: boolean;
14 | id?: string;
15 | model: string;
16 | onChange?: Function;
17 | options: {
18 | disabled?: boolean,
19 | name?: string,
20 | value: string | number,
21 | }[];
22 | parseValue?: Function;
23 | placeholder?: string;
24 | style?: React.CSSProperties;
25 | validateOn?: typeof BLUR | typeof FOCUS | typeof INPUT | typeof MOUNT;
26 | validator?: Function;
27 | }
28 |
29 | function DataList({
30 | autoFocus,
31 | className,
32 | disabled,
33 | id,
34 | model,
35 | onChange = () => {},
36 | options,
37 | parseValue,
38 | placeholder,
39 | style,
40 | validateOn,
41 | validator,
42 | }: PropTypes) {
43 | useWillMount(() => controlActions.mountModel(model, parseValue, validateOn === MOUNT, validator));
44 |
45 | const dispatch = useDispatch();
46 | const value = useCachedSelector(getValue, model) || '';
47 | const errors: string[] = useGetErrors(model, value);
48 | // console.log('RENDERING: datalist', model);
49 |
50 | const handleOnChange = (e: React.ChangeEvent): void => {
51 | dispatch(controlActions.setValue(model, e.target.value));
52 | onChange(model, e.target.value);
53 | };
54 |
55 | const getClassName: () => string = () => {
56 | return [className, errors.length < 1] // && isValidated ? `CHCl3Error ${model}-CHCl3Error` : undefined]
57 | .join(' ')
58 | .trim();
59 | };
60 |
61 | const markValidated = () => {}; // isValidated || setValidated(model);
62 |
63 | return (
64 |
65 |
79 |
86 |
87 | );
88 | }
89 |
90 | /*
91 | const mapStateToProps = (state: Store.CombinedState, {model}: PropTypes) => ({
92 | hasError: hasError(state, model),
93 | initialized: isFormInitialized(state),
94 | isValidated: hasBeenValidated(state, model),
95 | value: getValue(state, model),
96 | });
97 |
98 | const mapDispatchToProps = {
99 | markValidated: controlActions.markValidated,
100 | mountModel: controlActions.mountModel,
101 | setErrors: controlActions.setErrors,
102 | setValue: controlActions.setValue,
103 | };
104 | */
105 |
106 | export default memo(DataList);
107 |
--------------------------------------------------------------------------------
/src/components/Form.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from 'react';
2 | import {useDispatch, useSelector, Provider} from 'react-redux';
3 |
4 | import controlActions from '../actions/controls';
5 | import formActions from '../actions/form';
6 | import reducers, {getFormValues/*, hasFormErrors*/} from '../store/reducers';
7 | import thunk from 'redux-thunk';
8 | import {createStore, applyMiddleware} from 'redux';
9 |
10 | interface PropTypes {
11 | afterSubmitState?: {} | undefined;
12 | children?: React.ReactNode;
13 | className?: string;
14 | id?: string;
15 | initialState?: {};
16 | onChange?: (_: React.ChangeEvent) => void;
17 | onReset?: Function;
18 | onResetState?: {} | undefined;
19 | onSubmit: Function;
20 | onSubmitFailed?: Function;
21 | style?: React.CSSProperties;
22 | validators?: {[key: string]: Function};
23 | }
24 |
25 | function Form({
26 | afterSubmitState,
27 | children,
28 | className,
29 | id,
30 | initialState = {},
31 | onChange = () => {},
32 | onReset = () => {},
33 | onResetState,
34 | onSubmit,
35 | onSubmitFailed = () => {},
36 | style,
37 | validators = {},
38 | }: PropTypes) {
39 | // console.log('RENDERING: form');
40 | const dispatch = useDispatch();
41 | useEffect(() => {
42 | dispatch(controlActions.initializeState(initialState, validators));
43 | }, []);
44 | const values = useSelector((state: Store.CombinedState) => getFormValues(state));
45 |
46 | const handleSubmit = (event: React.ChangeEvent) => {
47 | event.preventDefault();
48 | event.stopPropagation();
49 |
50 | // if (hasFormErrors) {
51 | // showErrors();
52 | // return;
53 | // }
54 |
55 | // this.props.setPending(this.props.model);
56 | // setSubmitting();
57 | Promise.resolve()
58 | .then(() => onSubmit(values))
59 | .then(() => {
60 | if (afterSubmitState && false) {
61 | dispatch(controlActions.initializeState(afterSubmitState, validators));
62 | }
63 | // setSubmitted();
64 | })
65 | .catch(err => {
66 | // setSubmitFailed();
67 | onSubmitFailed(err);
68 | })
69 | .finally(() => dispatch(formActions.resetSubmit()));
70 | };
71 |
72 | const handleReset = (event: React.ChangeEvent) => {
73 | event.preventDefault();
74 | event.stopPropagation();
75 |
76 | dispatch(controlActions.initializeState(onResetState || initialState, validators));
77 | onReset();
78 | };
79 |
80 | return (
81 |
91 | );
92 | }
93 |
94 | /*
95 | const mapStateToProps = (state: Store.CombinedState) => ({
96 | // hasFormErrors: hasFormErrors(state),
97 | getValues: () => getFormValues(state),
98 | });
99 |
100 | const mapDispatchToProps = {
101 | initializeState: controlActions.initializeState,
102 | // setPending: controlActions.setPending,
103 | // showErrors: controlActions.showErrors,
104 |
105 | resetSubmit: formActions.resetSubmit,
106 | // setSubmitFailed: formActions.setSubmitFailed,
107 | // setSubmitted: formActions.setSubmitted,
108 | // setSubmitting: formActions.setSubmitting,
109 | };
110 | */
111 |
112 | function StoreProvider(props: PropTypes) {
113 | const [store] = useState(createStore(reducers, applyMiddleware(thunk)));
114 |
115 | return (
116 |
117 |
118 |
119 | );
120 | }
121 |
122 | export default StoreProvider;
123 |
--------------------------------------------------------------------------------
/src/components/Select.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {useDispatch} from 'react-redux';
3 |
4 | import {useCachedSelector, useGetErrors, useWillMount} from '../hooks';
5 | import controlActions from '../actions/controls';
6 | import {BLUR, FOCUS, INPUT, MOUNT} from '../constants/events';
7 | // import {getValue2 as getValue, isFormInitialized, hasBeenValidated, hasError} from '../store/reducers';
8 | import {getValue/*, hasBeenValidated, isFormInitialized*/} from '../store/reducers';
9 |
10 | interface PropTypes {
11 | options: ({
12 | disabled?: boolean,
13 | name: string,
14 | value: string | number,
15 | } | {
16 | disabled?: boolean,
17 | name: string,
18 | group: {
19 | disabled?: boolean,
20 | name: string,
21 | value: string | number,
22 | }[],
23 | })[]
24 | placeholder?: string;
25 | value?: string;
26 | autoFocus?: boolean;
27 | className?: string;
28 | disabled?: boolean;
29 | id?: string;
30 | model: string;
31 | onChange?: Function;
32 | parseValue?: Function;
33 | style?: React.CSSProperties;
34 | type?: 'text' | 'email' | 'password' | 'number';
35 | validateOn?: typeof BLUR | typeof FOCUS | typeof INPUT | typeof MOUNT;
36 | validator?: Function;
37 | }
38 |
39 | function Select({
40 | autoFocus,
41 | className,
42 | disabled,
43 | id,
44 | model,
45 | onChange = () => {},
46 | options,
47 | parseValue,
48 | placeholder,
49 | style,
50 | validateOn,
51 | validator,
52 | }: PropTypes) {
53 | useWillMount(() => controlActions.mountModel(model, parseValue, validateOn === MOUNT, validator));
54 |
55 | const dispatch = useDispatch();
56 | const value = useCachedSelector(getValue, model) || '';
57 | const errors: string[] = useGetErrors(model, value);
58 | // console.log('RENDERING: select', model);
59 |
60 | const handleOnChange = (e: React.ChangeEvent): void => {
61 | dispatch(controlActions.setValue(model, e.target.value));
62 | onChange(model, e.target.value);
63 | };
64 |
65 | const getClassName: () => string = () => {
66 | return [className, errors.length < 1] // && isValidated ? `CHCl3Error ${model}-CHCl3Error` : undefined]
67 | .join(' ')
68 | .trim();
69 | };
70 |
71 | const markValidated = () => {}; // isValidated || setValidated(model);
72 |
73 | const mappedOptions = options.map(option => {
74 | if ('group' in option) {
75 | return (
76 |
87 | );
88 | }
89 |
90 | return (
91 |
94 | );
95 | });
96 |
97 | if (placeholder) {
98 | mappedOptions.unshift(
99 |
102 | );
103 | }
104 |
105 | return (
106 |
121 | );
122 | }
123 |
124 | /*
125 | const mapStateToProps = (state: Store.CombinedState, {model}: PropTypes) => ({
126 | hasError: hasError(state, model),
127 | initialized: isFormInitialized(state),
128 | isValidated: hasBeenValidated(state, model),
129 | value: getValue(state, model),
130 | });
131 |
132 | const mapDispatchToProps = {
133 | markValidated: controlActions.markValidated,
134 | mountModel: controlActions.mountModel,
135 | setErrors: controlActions.setErrors,
136 | setValue: controlActions.setValue,
137 | };
138 | */
139 |
140 | export default memo(Select);
141 |
--------------------------------------------------------------------------------
/examples/example2/index.jsx:
--------------------------------------------------------------------------------
1 | const {
2 | Form,
3 | Button,
4 | ChloroformError,
5 | Checkbox,
6 | withReactChloroform,
7 | } = ReactChloroform;
8 |
9 | const isRequired = val => (val && val.length > 0) || "You must have a favourite pet!";
10 |
11 | class SubmitButton extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | success: false,
16 | failure: false,
17 | text: 'Submit',
18 | };
19 | }
20 | componentDidUpdate(oldProps) {
21 | if (this.props.chloroformStatus && oldProps.chloroformStatus !== this.props.chloroformStatus) {
22 | this.onFormSubmit(this.props.chloroformStatus);
23 | }
24 | }
25 |
26 | onFormSubmit = result => {
27 | this.setState({
28 | success: result === 'submitted',
29 | failure: result === 'failed',
30 | text: this.renderText(),
31 | });
32 | setTimeout(() => {
33 | this.setState({
34 | success: false,
35 | failure: false,
36 | text: 'Submit',
37 | });
38 | }, 2000);
39 | };
40 |
41 | renderText = () => {
42 | const {chloroformStatus} = this.props;
43 | if (chloroformStatus === 'submitted') {
44 | return 'Submitted';
45 | } else if (chloroformStatus === 'failed') {
46 | return 'Failed';
47 | } else if (chloroformStatus === 'submitting') {
48 | return ;
49 | }
50 | return 'Submit';
51 | };
52 |
53 | getClassName = () => {
54 | const {success, failure} = this.state;
55 | let buttonClass = 'btn-primary';
56 | if (success) {
57 | buttonClass = 'btn-success';
58 | } else if (failure) {
59 | buttonClass = 'btn-danger';
60 | }
61 |
62 | return `btn btn-block ${buttonClass} submitButton`;
63 | };
64 |
65 | render() {
66 | const {chloroformStatus} = this.props;
67 |
68 | return (
69 |
76 | );
77 | }
78 | }
79 |
80 | const CustomSubmitButton = withReactChloroform(SubmitButton);
81 |
82 | class App extends React.Component {
83 | sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
84 |
85 | handleSubmit = model =>
86 | Promise.resolve()
87 | .then(() => this.sleep(2000))
88 | .then(() => console.log(model));
89 |
90 | render() {
91 | const initialState = {
92 | dog: true,
93 | };
94 |
95 | return (
96 |
97 |
What are your favourite pets?
98 |
153 |
154 | );
155 | }
156 | }
157 |
158 | ReactDOM.render(, document.getElementById("root"));
159 |
--------------------------------------------------------------------------------
/test/reducers/index.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import {createStore, applyMiddleware} from 'redux';
4 | import thunk from 'redux-thunk';
5 |
6 | import controlActions from '../../src/actions/controls';
7 | import reducers, {getValue, getValidators, getFormValues} from '../../src/store/reducers';
8 |
9 | describe('Control selectors tests', () => {
10 | let store;
11 |
12 | beforeEach(() => {
13 | store = createStore(reducers, applyMiddleware(thunk));
14 | });
15 |
16 | it('gets value from objects', () => {
17 | // Arrange
18 | const initialState = {
19 | 'drinks.*': 'allset',
20 | 'foo.bar': 1337,
21 | 'drinks.0.type': 'a bit different',
22 | };
23 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
24 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
25 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
26 | store.dispatch(controlActions.mountModel('foo.bar', undefined, false, undefined));
27 | store.dispatch(controlActions.initializeState(initialState, {}));
28 |
29 | // Act
30 | const drinks0TypeValue = getValue()(store.getState(), 'drinks.0.type');
31 | const drinks1TypeValue = getValue()(store.getState(), 'drinks.1.type');
32 | const drinks0FooValue = getValue()(store.getState(), 'drinks.0.foo');
33 | const drinks1FooValue = getValue()(store.getState(), 'drinks.1.foo');
34 | const fooBarValue = getValue()(store.getState(), 'foo.bar');
35 |
36 | // Assert
37 | expect(drinks0TypeValue).toEqual('a bit different');
38 | expect(drinks1TypeValue).toEqual('allset');
39 | expect(drinks0FooValue).toEqual('allset');
40 | expect(drinks1FooValue).toEqual('allset');
41 | expect(fooBarValue).toEqual(1337);
42 | });
43 |
44 | it('returns undefined if not all values are the same', () => {
45 | // Arrange
46 | const initialState = {
47 | 'drinks.*': 'allset',
48 | 'drinks.0.type': 'a bit different',
49 | };
50 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
51 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
52 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
53 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
54 | store.dispatch(controlActions.initializeState(initialState, {}));
55 |
56 | // Act
57 | const drinksValue = getValue()(store.getState(), 'drinks.*');
58 |
59 | // Assert
60 | expect(drinksValue).toEqual(undefined);
61 | });
62 |
63 | it('returns the value if all values are the same', () => {
64 | // Arrange
65 | const initialState = {
66 | 'drinks.*': 'allset',
67 | };
68 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
69 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
70 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
71 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
72 | store.dispatch(controlActions.initializeState(initialState, {}));
73 |
74 | // Act
75 | const drinksValue = getValue()(store.getState(), 'drinks.*');
76 |
77 | // Assert
78 | expect(drinksValue).toEqual('allset');
79 | });
80 | });
81 |
82 | describe('Form selectors tests', () => {
83 | let store;
84 |
85 | beforeEach(() => {
86 | store = createStore(reducers, applyMiddleware(thunk));
87 | });
88 |
89 | it('gets value from objects', () => {
90 | // Arrange
91 | const initialState = {
92 | 'drinks.*': 'allset',
93 | 'foo.bar': 1337,
94 | 'drinks.0.type': 'a bit different',
95 | };
96 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
97 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
98 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
99 | store.dispatch(controlActions.mountModel('foo.bar', undefined, false, undefined));
100 | store.dispatch(controlActions.initializeState(initialState, {}));
101 |
102 | // Act
103 | const result = getFormValues()(store.getState());
104 |
105 | // Assert
106 | expect(result).toEqual({
107 | drinks: [
108 | {
109 | type: 'a bit different',
110 | foo: 'allset',
111 | },
112 | {
113 | type: 'allset',
114 | foo: 'allset',
115 | },
116 | ],
117 | foo: {
118 | bar: 1337,
119 | },
120 | });
121 | });
122 |
123 | it('returns undefined for unset values', () => {
124 | // Arrange
125 | const initialState = {
126 | 'drinks.1': 'isset',
127 | foo: 1337,
128 | };
129 | store.dispatch(controlActions.mountModel('drinks.0', undefined, false, undefined));
130 | store.dispatch(controlActions.mountModel('drinks.1', undefined, false, undefined));
131 | store.dispatch(controlActions.mountModel('foo', undefined, false, undefined));
132 | store.dispatch(controlActions.initializeState(initialState, {}));
133 |
134 | // Act
135 | const result = getFormValues()(store.getState());
136 |
137 | // Assert
138 | expect(result).toEqual({
139 | drinks: [undefined, 'isset'],
140 | foo: 1337,
141 | });
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/test/reducers/control.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 |
3 | import {createStore, applyMiddleware} from 'redux';
4 | import thunk from 'redux-thunk';
5 |
6 | import controlActions from '../../src/actions/controls';
7 | import reducers from '../../src/store/reducers';
8 |
9 | describe('Blueprint tests', () => {
10 | let store;
11 |
12 | beforeEach(() => {
13 | store = createStore(reducers, applyMiddleware(thunk));
14 | });
15 |
16 | it('dispatches mountModel action and returns blueprint', () => {
17 | // Arrange & Act
18 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
19 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
20 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
21 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
22 |
23 | // Assert
24 | expect(store.getState().control.blueprint).toEqual({
25 | drinks: {
26 | '*': {
27 | length: 2,
28 | value: {
29 | type: undefined,
30 | foo: undefined,
31 | },
32 | },
33 | },
34 | });
35 | });
36 | });
37 |
38 | describe('Store tests', () => {
39 | let store;
40 |
41 | beforeEach(() => {
42 | store = createStore(reducers, applyMiddleware(thunk));
43 | });
44 |
45 | it('dispatches initializeStore after mount and returns store', () => {
46 | // Arrange
47 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
48 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
49 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
50 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
51 | store.dispatch(controlActions.mountModel('foo.bar', undefined, false, undefined));
52 | const initialState = {
53 | 'drinks.*': 'allset',
54 | 'foo.bar': 1337,
55 | 'drinks.0.type': 'a bit different',
56 | };
57 |
58 | // Act
59 | store.dispatch(controlActions.initializeState(initialState, {}));
60 |
61 | // Assert
62 | expect(store.getState().control.store).toEqual({
63 | drinks: {
64 | value: [
65 | {
66 | value: {
67 | type: {
68 | value: 'a bit different',
69 | },
70 | foo: {
71 | value: 'allset',
72 | },
73 | },
74 | },
75 | {
76 | value: {
77 | type: {
78 | value: 'allset',
79 | },
80 | foo: {
81 | value: 'allset',
82 | },
83 | },
84 | },
85 | ],
86 | },
87 | foo: {
88 | value: {
89 | bar: {
90 | value: 1337,
91 | },
92 | },
93 | },
94 | });
95 | });
96 |
97 | it('dispatches initializeStore after mount and keeps order', () => {
98 | // Arrange
99 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
100 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
101 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
102 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
103 | store.dispatch(controlActions.mountModel('foo.bar', undefined, false, undefined));
104 | const initialState = {
105 | 'drinks.0.type': 'a bit different',
106 | 'foo.bar': 1337,
107 | 'drinks.*': 'allset',
108 | };
109 |
110 | // Act
111 | store.dispatch(controlActions.initializeState(initialState, {}));
112 |
113 | // Assert
114 | expect(store.getState().control.store).toEqual({
115 | drinks: {
116 | value: [
117 | {
118 | value: {
119 | type: {
120 | value: 'a bit different',
121 | },
122 | foo: {
123 | value: 'allset',
124 | },
125 | },
126 | },
127 | {
128 | value: {
129 | type: {
130 | value: 'allset',
131 | },
132 | foo: {
133 | value: 'allset',
134 | },
135 | },
136 | },
137 | ],
138 | },
139 | foo: {
140 | value: {
141 | bar: {
142 | value: 1337,
143 | },
144 | },
145 | },
146 | });
147 | });
148 |
149 | it('sets values dot all objects in array', () => {
150 | // Arrange
151 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
152 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
153 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
154 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
155 | store.dispatch(controlActions.mountModel('foo.bar', undefined, false, undefined));
156 | store.dispatch(controlActions.initializeState({}, {}));
157 |
158 | // Act
159 | store.dispatch(controlActions.setValue('drinks.*', 'common value'));
160 |
161 | // Assert
162 | expect(store.getState().control.store).toEqual({
163 | drinks: {
164 | value: [
165 | {
166 | value: {
167 | type: {
168 | value: 'common value',
169 | },
170 | foo: {
171 | value: 'common value',
172 | },
173 | },
174 | },
175 | {
176 | value: {
177 | type: {
178 | value: 'common value',
179 | },
180 | foo: {
181 | value: 'common value',
182 | },
183 | },
184 | },
185 | ],
186 | },
187 | foo: {
188 | value: {
189 | bar: {
190 | value: undefined,
191 | },
192 | },
193 | },
194 | });
195 | });
196 |
197 | it('sets individual values', () => {
198 | // Arrange
199 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, undefined));
200 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
201 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, undefined));
202 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, undefined));
203 | store.dispatch(controlActions.mountModel('foo.bar', undefined, false, undefined));
204 | store.dispatch(controlActions.initializeState({}, {}));
205 |
206 | // Act
207 | store.dispatch(controlActions.setValue('drinks.0.type', 'first'));
208 | store.dispatch(controlActions.setValue('drinks.1.foo', 'second'));
209 | store.dispatch(controlActions.setValue('foo.bar', 'third'));
210 |
211 | // Assert
212 | expect(store.getState().control.store).toEqual({
213 | drinks: {
214 | value: [
215 | {
216 | value: {
217 | type: {
218 | value: 'first',
219 | },
220 | foo: {
221 | value: undefined,
222 | },
223 | },
224 | },
225 | {
226 | value: {
227 | type: {
228 | value: undefined,
229 | },
230 | foo: {
231 | value: 'second',
232 | },
233 | },
234 | },
235 | ],
236 | },
237 | foo: {
238 | value: {
239 | bar: {
240 | value: 'third',
241 | },
242 | },
243 | },
244 | });
245 | });
246 | });
247 |
248 | describe('Validator tests', () => {
249 | let store;
250 |
251 | beforeEach(() => {
252 | store = createStore(reducers, applyMiddleware(thunk));
253 | });
254 |
255 | it('mounts models with validators', () => {
256 | // Arrange
257 | const validator1 = () => {};
258 | const validator2 = () => {};
259 | const validator3 = () => {};
260 |
261 | // Act
262 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, validator1));
263 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
264 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, validator2));
265 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, validator3));
266 |
267 | // Assert
268 | expect(store.getState().control.validators).toEqual({
269 | 'drinks.*': validator1,
270 | 'drinks.1.type': validator2,
271 | 'drinks.0.foo': validator3,
272 | });
273 | });
274 |
275 | it('initializes validators with mount precedence', () => {
276 | // Arrange
277 | const validator1 = () => {};
278 | const validator2 = () => {};
279 | const validator3 = () => {};
280 | const validators = {
281 | 'drinks.0.foo': validator2,
282 | 'drinks.0.type': validator3,
283 | };
284 | store.dispatch(controlActions.mountModel('drinks.*', undefined, false, validator1));
285 | store.dispatch(controlActions.mountModel('drinks.0.type', undefined, false, undefined));
286 | store.dispatch(controlActions.mountModel('drinks.1.type', undefined, false, validator2));
287 | store.dispatch(controlActions.mountModel('drinks.0.foo', undefined, false, validator3));
288 |
289 | // Act
290 | store.dispatch(controlActions.initializeState({}, validators));
291 |
292 | // Assert
293 | expect(store.getState().control.validators).toEqual({
294 | 'drinks.*': validator1,
295 | 'drinks.0.type': validator3,
296 | 'drinks.1.type': validator2,
297 | 'drinks.0.foo': validator2,
298 | });
299 | });
300 | });
301 |
--------------------------------------------------------------------------------
/src/store/reducers/control.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MOUNT_MODEL,
3 | INITIALIZE_STATE,
4 | MARK_VALIDATED,
5 | SET_VALUE,
6 | SHOW_ERRORS,
7 | } from '../action-types';
8 | import {fromDotProp, getIn, isNumber, isObject, isArray} from '../../utils';
9 |
10 | const constructBlueprint = (arr: string[], state: ExplicitAny = {}): ExplicitAny => {
11 | if (arr.length == 0) {
12 | return undefined;
13 | } else if (arr[0] === "*" || isNumber(arr[0])) {
14 | return {
15 | ...state,
16 | '*': {
17 | length: Math.max(isNumber(arr[0]) ? parseInt(arr[0]) + 1 : 0, getIn(state, '*', 'length') || 0),
18 | value: constructBlueprint(arr.slice(1), getIn(state, '*', 'value')),
19 | }
20 | };
21 | } else {
22 | if ('*' in state) {
23 | if (process.env.NODE_ENV !== 'production') {
24 | console.error("Usage Error!\nYou're mixing Arrays and Objects within the same model value");
25 | }
26 | }
27 |
28 | return {
29 | ...state,
30 | [arr[0]]: constructBlueprint(arr.slice(1), state[arr[0]]),
31 | } as ObjectValue;
32 | }
33 | };
34 |
35 | const escapeRegExp = (str: string): string => {
36 | return str.replace(/[.*]/g, '\\$&');
37 | };
38 |
39 | const findBestMatch = (map: {[key: string]: ExplicitAny}, match: string): ExplicitAny => {
40 | const regex = escapeRegExp(`^(${match.replace(/\.(\d+)\b/g, '.($1|*))?(')})?$`);
41 | const matches: {[_: string]: ExplicitAny} = Object.keys(map).reduce(
42 | (acc: {[_: string]: ExplicitAny}, key: string) => {
43 | if (key.match(regex)) {
44 | return {...acc, [key]: map[key]};
45 | }
46 | return acc;
47 | }, {});
48 |
49 | // parse result
50 | const keys = Object.keys(matches);
51 | if (keys.length === 0) {
52 | return undefined;
53 | }
54 |
55 | let index: number = -2;
56 | return keys.reduce((acc: ExplicitAny, k: string) => {
57 | const lastDigit = k.match(/\d+(?![\s\S]*\.\d+)/);
58 | if (lastDigit) {
59 | const lastIndex = k.split('.').lastIndexOf(lastDigit.toString());
60 | if (lastIndex > index) {
61 | index = lastIndex;
62 | return matches[k];
63 | }
64 | }
65 | return acc;
66 | }, matches[keys[0]])
67 | };
68 |
69 | const initializeState = (state: ExplicitAny = {}, initialState: {}, currentPath: string = ''): {} => {
70 | const keys: string[] = Object.keys(state);
71 | if (keys.length === 0) {
72 | return findBestMatch(initialState, currentPath);
73 | } else {
74 | return keys.reduce((acc: ExplicitAny, next: ExplicitAny) => {
75 | if (next === '*') {
76 | return Array.from({length: state['*'].length}).map((_, i: number) => ({
77 | value: initializeState(state['*'].value, initialState, `${currentPath}.${i}`),
78 | }));
79 | } else {
80 | return {
81 | ...acc,
82 | [next]: {
83 | ...acc[next],
84 | value: initializeState(state[next], initialState, currentPath ? `${currentPath}.${next}` : next),
85 | }
86 | };
87 | }
88 | }, {});
89 | }
90 | };
91 |
92 | const setInEveryLeaf = (state: ExplicitAny, newValue: ExplicitAny): ExplicitAny => {
93 | if (isArray(state)) {
94 | return state.map((s: ExplicitAny) => ({
95 | value: setInEveryLeaf(s.value, newValue),
96 | }));
97 | } else if (isObject(state)) {
98 | return Object.keys(state).reduce((acc: ExplicitAny, next: ExplicitAny) => {
99 | return {
100 | ...acc,
101 | [next]: {
102 | ...acc[next],
103 | value: setInEveryLeaf(state[next].value, newValue),
104 | },
105 | };
106 | }, {});
107 | } else {
108 | return newValue;
109 | }
110 | };
111 |
112 | const setDeep: ExplicitAny = (arr: string[], state: ExplicitAny = {}, newValue: ExplicitAny) => {
113 | if (arr.length == 0) {
114 | return {
115 | ...state,
116 | value: setInEveryLeaf(state.value, newValue),
117 | }
118 | } else if (arr[0] === "*") {
119 | return {
120 | ...state,
121 | value: (state.value || []).map((v: ExplicitAny) => setDeep(arr.slice(1), v, newValue)),
122 | };
123 | } else if (isNumber(arr[0])) {
124 | return {
125 | ...state,
126 | value: [
127 | ...(state.value || []).slice(0, parseInt(arr[0])),
128 | setDeep(arr.slice(1), state.value[arr[0]], newValue),
129 | ...(state.value || []).slice(parseInt(arr[0]) + 1),
130 | ]
131 | };
132 | } else {
133 | return {
134 | ...state,
135 | value: {
136 | ...state.value,
137 | [arr[0]]: setDeep(arr.slice(1), state.value[arr[0]], newValue),
138 | },
139 | } as ObjectValue;
140 | }
141 | };
142 |
143 | export default (state: Store.ControlState = {blueprint: {}, store: {}, validators: {}}, action: Store.Action): Store.ControlState => {
144 | const {payload} = action;
145 | switch (action.type) {
146 | case MOUNT_MODEL: {
147 | const model = payload.model.split('.');
148 | return {
149 | ...state,
150 | blueprint: {
151 | ...state.blueprint,
152 | ...constructBlueprint(model, state.blueprint),
153 | },
154 | validators: {
155 | ...state.validators,
156 | [payload.model]: payload.validator,
157 | },
158 | };
159 | }
160 | case INITIALIZE_STATE:
161 | return {
162 | ...state,
163 | store: initializeState(state.blueprint, payload.initialState),
164 | validators: {...state.validators, ...payload.validators},
165 | };
166 | case SET_VALUE: {
167 | const model = payload.model.split('.');
168 | return {
169 | ...state,
170 | store: {
171 | ...state.store,
172 | [model[0]]: setDeep(model.slice(1), state.store[model[0]], payload.value),
173 | },
174 | };
175 | }
176 | case MARK_VALIDATED: { // combine with show_errors
177 | return state;
178 | // const model = fromDotProp(payload.model);
179 | // return {
180 | // ...state,
181 | // [model]: {
182 | // ...state[model],
183 | // validated: true,
184 | // },
185 | // };
186 | }
187 | case SHOW_ERRORS:
188 | return state;
189 | // return {
190 | // ...Object.keys(state).reduce((nextState, next) => {
191 | // const model = fromDotProp(next);
192 | // return {
193 | // ...nextState,
194 | // [model]: {
195 | // ...state[model],
196 | // validated: true
197 | // },
198 | // };
199 | // }, {}),
200 | // };
201 | default:
202 | return state;
203 | }
204 | };
205 |
206 | export const getError = (state: Store.ControlState, model: string) =>
207 | getIn(state, fromDotProp(model), 'validation', model, 'errors');
208 |
209 | const flattenObject = (obj: ExplicitAny, result: ExplicitAny = []) => {
210 | for (const i in obj) {
211 | if (isObject(obj[i])) {
212 | result = [...flattenObject(obj[i], result)];
213 | } else {
214 | result = [...result, obj[i]];
215 | }
216 | };
217 | return result;
218 | };
219 |
220 | export const getValidators = (
221 | validators: {[key: string]: Function},
222 | model: string,
223 | modelValidators: Function[] = [],
224 | ): Function[] => {
225 | let combinedValidators = modelValidators;
226 | if (validators[model]) {
227 | combinedValidators = [...modelValidators, validators[model]];
228 | }
229 |
230 | if (model && model.split('.').some(m => isNumber(m))) {
231 | return getValidators(validators, model.replace(/\.\d+(?![\s\S]*\.\d+)/, '.*'), combinedValidators)
232 | }
233 | return combinedValidators;
234 | };
235 |
236 | export const getValue = (state: ExplicitAny, arr: string[]): ExplicitAny => {
237 | if (arr.length == 0) {
238 | return state;
239 | } else if (!state || Object.keys(state).length === 0) {
240 | return undefined;
241 | } else if (arr[0] === "*") {
242 | const values: ExplicitAny = flattenObject((state || []).map((v: ExplicitAny) => getValue(v.value, arr.slice(1))));
243 | if (values.some((v: ExplicitAny) => v !== values[0])) {
244 | return undefined;
245 | } else {
246 | return values[0];
247 | }
248 | } else {
249 | return getValue(getIn(state, arr[0], 'value'), arr.slice(1));
250 | }
251 | };
252 |
253 | // const parseValues = (value: Scalar, parseValue: {[key: string]: Function}, model: string) => {
254 | // if (Array.isArray(value)) {
255 | // return value.map((v, i) => {
256 | // const parse = parseValue['*'] || parseValue[i] || ((x: Scalar) => x);
257 | // return v ? parse(v) : undefined;
258 | // });
259 | // }
260 | //
261 | // const key = getIndex(model);
262 | // const parse = parseValue[key] || ((x: Scalar) => x);
263 | // return value ? parse(value) : value;
264 | // };
265 | //
266 |
267 | const getStateValue = (state: ExplicitAny = {}): {} => {
268 | if (isArray(state.value)) {
269 | return state.value.map((s: ExplicitAny) => getStateValue(s));
270 | } else if (isObject(state.value)) {
271 | return Object.keys(state.value).reduce((acc, v) => ({
272 | ...acc,
273 | [v]: getStateValue(state.value[v]),
274 | }), {});
275 | } else {
276 | return state.value;
277 | }
278 | };
279 |
280 | export const getValues = (store: {[key: string]: ScalarValue | ArrayValue | ObjectValue}) => {
281 | return Object.keys(store).reduce((acc, model) => ({
282 | ...acc,
283 | [model]: getStateValue(store[model]),
284 | }), {});
285 | };
286 |
287 | export const hasBeenValidated = (state: Store.ControlState, model: string) =>
288 | getIn(state, fromDotProp(model), 'validation', model, 'validated');
289 |
290 | export const hasError = (state: Store.ControlState, model: string): boolean => {
291 | const errors = getError(state, model);
292 | return Boolean(getIn(errors, 'length'));
293 | };
294 |
295 | export const hasErrors = (state: Store.ControlState) =>
296 | Object.keys(state).filter(model => hasError(state, model)).length > 0;
297 |
--------------------------------------------------------------------------------
/cypress/integration/datastructure.spec.js:
--------------------------------------------------------------------------------
1 | // This recipe shows how to interact with a range input (slider)
2 |
3 | // Eventually, this will be expanded to includes examples of interacting
4 | // with various form elements
5 |
6 | describe('Datastructure interaction test', () => {
7 | before(() => cy.server());
8 | beforeEach(() => {
9 | cy.visit('/');
10 | });
11 |
12 | /* GETTERS */
13 | const getRoot = i =>
14 | cy
15 | .get('#data-structure-test > div')
16 | .as('root')
17 | .eq(i);
18 |
19 | const getDrink = i =>
20 | getRoot(0)
21 | .find('input[type=checkbox]')
22 | .eq(i);
23 |
24 | const getHumanInterest = (i, j) =>
25 | getRoot(1)
26 | .find('div')
27 | .eq(i)
28 | .find('input')
29 | .eq(j);
30 |
31 | const getHumanGender = i =>
32 | getRoot(2)
33 | .find('input[type=radio]')
34 | .eq(i);
35 |
36 | const getVehicle = i =>
37 | getRoot(3)
38 | .find('input')
39 | .eq(i);
40 |
41 | const getRestItem = item => getRoot(4).find(item);
42 |
43 | /* PRE-CONDITIONS */
44 | const drinksPrecondition = () => {
45 | getDrink(0).should('not.be.checked');
46 | getDrink(1).should('be.checked');
47 | getDrink(2).should('not.be.checked');
48 | getDrink(3).should('not.be.checked');
49 | getDrink(4).should('not.be.checked');
50 | getDrink(5).should('not.be.checked');
51 | getDrink(6).should('not.be.checked');
52 | };
53 |
54 | const humansInterestsPrecondition = () => {
55 | getHumanInterest(0, 0).should('have.value', 'ALL');
56 | getHumanInterest(0, 1).should('have.value', 'ALL');
57 | getHumanInterest(1, 0).should('have.value', 'ALL');
58 | getHumanInterest(1, 1).should('have.value', 'ALL');
59 | };
60 |
61 | const humansGenderPrecondition = () => {
62 | getHumanGender(0).should('not.be.checked');
63 | getHumanGender(1).should('be.checked');
64 | };
65 |
66 | const vehiclePrecondition = () => {
67 | getVehicle(0).should('have.value', '');
68 | getVehicle(1).should('have.value', '');
69 | };
70 |
71 | const restPrecondition = (selector, value = '') => {
72 | getRestItem(selector).should('have.value', value);
73 | };
74 |
75 | /* DRINKS */
76 | it('should set all drinks as checked', () => {
77 | // Arrange
78 | drinksPrecondition();
79 |
80 | // Act
81 | getDrink(0).check();
82 |
83 | // Assert
84 | getDrink(0).should('be.checked');
85 | getDrink(1).should('be.checked');
86 | getDrink(2).should('be.checked');
87 | getDrink(3).should('be.checked');
88 | getDrink(4).should('be.checked');
89 | getDrink(5).should('be.checked');
90 | getDrink(6).should('be.checked');
91 | });
92 |
93 | it('should set all drinks as unchecked', () => {
94 | // Arrange
95 | drinksPrecondition();
96 | getDrink(0).check();
97 |
98 | // Act
99 | getDrink(0).uncheck();
100 |
101 | // Assert
102 | getDrink(0).should('not.be.checked');
103 | getDrink(1).should('not.be.checked');
104 | getDrink(2).should('not.be.checked');
105 | getDrink(3).should('not.be.checked');
106 | getDrink(4).should('not.be.checked');
107 | getDrink(5).should('not.be.checked');
108 | getDrink(6).should('not.be.checked');
109 | });
110 |
111 | it('should only check the 5th checkbox', () => {
112 | // Arrange
113 | drinksPrecondition();
114 |
115 | // Act
116 | getDrink(5).check();
117 |
118 | // Assert
119 | getDrink(0).should('not.be.checked');
120 | getDrink(1).should('be.checked');
121 | getDrink(2).should('not.be.checked');
122 | getDrink(3).should('not.be.checked');
123 | getDrink(4).should('not.be.checked');
124 | getDrink(5).should('be.checked');
125 | getDrink(6).should('not.be.checked');
126 | });
127 |
128 | it('should check the last checkbox', () => {
129 | // Arrange
130 | drinksPrecondition();
131 |
132 | // Act
133 | getDrink(2).check();
134 | getDrink(3).check();
135 | getDrink(4).check();
136 |
137 | // Assert
138 | getDrink(0).should('not.be.checked');
139 | getDrink(1).should('be.checked');
140 | getDrink(2).should('be.checked');
141 | getDrink(3).should('be.checked');
142 | getDrink(4).should('be.checked');
143 | getDrink(5).should('not.be.checked');
144 | getDrink(6).should('be.checked');
145 | });
146 |
147 | /* HUMANS */
148 | it('should update human interests accordingly and leave gender intact', () => {
149 | // Arrange
150 | humansInterestsPrecondition();
151 |
152 | // Act
153 | getHumanInterest(0, 0).clear().type('Changing');
154 | getHumanInterest(0, 1).clear().type('the');
155 | getHumanInterest(1, 0).clear().type('input');
156 | getHumanInterest(1, 1).clear().type('value');
157 |
158 | // Assert
159 | getHumanInterest(0, 0).should('have.value', 'Changing');
160 | getHumanInterest(0, 1).should('have.value', 'the');
161 | getHumanInterest(1, 0).should('have.value', 'input');
162 | getHumanInterest(1, 1).should('have.value', 'value');
163 | getHumanGender(0).should('not.be.checked');
164 | getHumanGender(1).should('be.checked');
165 | });
166 |
167 | it('should update human gender accordingly and leave interests intact', () => {
168 | // Arrange
169 | humansGenderPrecondition();
170 |
171 | // Act
172 | getHumanGender(0).check();
173 |
174 | // Assert
175 | getHumanInterest(0, 0).should('have.value', 'ALL');
176 | getHumanInterest(0, 1).should('have.value', 'ALL');
177 | getHumanInterest(1, 0).should('have.value', 'ALL');
178 | getHumanInterest(1, 1).should('have.value', 'ALL');
179 | getHumanGender(0).should('be.checked');
180 | getHumanGender(1).should('not.be.checked');
181 | });
182 |
183 | /* VEHICLE */
184 | it('should update vehicles accordingly', () => {
185 | // Arrange
186 | vehiclePrecondition();
187 |
188 | // Act
189 | getVehicle(0).clear().type('1337');
190 | getVehicle(1).clear().type('42');
191 |
192 | // Assert
193 | getVehicle(0).should('have.value', '1337');
194 | getVehicle(1).should('have.value', '42');
195 | });
196 |
197 | /* REST */
198 | it('should update datalist by typing', () => {
199 | // Arrange
200 | const selector = 'input[list=cat]';
201 | restPrecondition(selector);
202 |
203 | // Act
204 | getRestItem(selector).type('foo');
205 |
206 | // Assert
207 | getRestItem(selector).should('have.value', 'foo');
208 | });
209 |
210 | it('should update select correctly', () => {
211 | // Arrange
212 | const selector = 'select';
213 | restPrecondition(selector, 'barfoo');
214 |
215 | // Act
216 | getRestItem(selector).select('foobar');
217 |
218 | // Assert
219 | getRestItem(selector).should('have.value', 'foobar');
220 | });
221 |
222 | it('should update textarea by typing', () => {
223 | // Arrange
224 | const selector = 'textarea';
225 | restPrecondition(selector);
226 |
227 | // Act
228 | getRestItem(selector).type('wabbalabbadubdub');
229 |
230 | // Assert
231 | getRestItem(selector).should('have.value', 'wabbalabbadubdub');
232 | });
233 |
234 | /* BUTTONS */
235 | it('should update the whole store and log to console on submit', () => {
236 | // Arrange
237 | drinksPrecondition();
238 | humansInterestsPrecondition();
239 | humansGenderPrecondition();
240 | vehiclePrecondition();
241 | restPrecondition('input[list=cat]');
242 | restPrecondition('select', 'barfoo');
243 | restPrecondition('textarea');
244 |
245 | // Act
246 | getDrink(1).uncheck();
247 | getDrink(5).check();
248 | getHumanInterest(0, 0).clear().type('first first');
249 | getHumanInterest(1, 0).clear().type('second first');
250 | getHumanGender(0).check();
251 | getHumanGender(1).check();
252 | getVehicle(0).type('love tesla');
253 | getRestItem('input[list=cat]').type('Rick');
254 | getRestItem('select').select('foobar');
255 | getRestItem('textarea').type('and Morty');
256 |
257 | getRoot(5)
258 | .find('button[type=submit]')
259 | .click();
260 |
261 | // Assert
262 | cy.window()
263 | .its('model')
264 | .should('deep.equal', {
265 | drinks: [
266 | {type: false, foo: true},
267 | {type: undefined, foo: undefined},
268 | {type: undefined, foo: undefined},
269 | {type: undefined, foo: undefined},
270 | ],
271 | human: [
272 | {
273 | interests: ['first first', 'ALL'],
274 | gender: 'male',
275 | },
276 | {
277 | interests: ['second first', 'ALL'],
278 | gender: 'male',
279 | },
280 | ],
281 | vehicle: {
282 | car: {
283 | tesla: 'love tesla',
284 | porsche: undefined,
285 | },
286 | },
287 | cat: 'Rick',
288 | dog: 'foobar',
289 | ape: 'and Morty',
290 | });
291 | });
292 |
293 | it('should update the whole store and reset to original state', () => {
294 | // Arrange
295 | drinksPrecondition();
296 | humansInterestsPrecondition();
297 | humansGenderPrecondition();
298 | vehiclePrecondition();
299 | restPrecondition('input[list=cat]');
300 | restPrecondition('select', 'barfoo');
301 | restPrecondition('textarea');
302 |
303 | // Act
304 | getDrink(1).uncheck();
305 | getDrink(5).check();
306 | getHumanInterest(0, 0).clear().type('first first');
307 | getHumanInterest(1, 0).clear().type('second first');
308 | getHumanGender(0).check();
309 | getHumanGender(1).check();
310 | getVehicle(0).type('love tesla');
311 | getRestItem('input[list=cat]').type('Rick');
312 | getRestItem('select').select('foobar');
313 | getRestItem('textarea').type('and Morty');
314 |
315 | getRoot(5)
316 | .find('button[type=reset]')
317 | .click();
318 | getRoot(5)
319 | .find('button[type=submit]')
320 | .click();
321 |
322 | // Assert
323 | cy.window()
324 | .its('model')
325 | .should('deep.equal', {
326 | drinks: [
327 | {type: true, foo: undefined},
328 | {type: undefined, foo: undefined},
329 | {type: undefined, foo: undefined},
330 | {type: undefined, foo: undefined},
331 | ],
332 | human: [
333 | {
334 | interests: ['ALL', 'ALL'],
335 | gender: 'male',
336 | },
337 | {
338 | interests: ['ALL', 'ALL'],
339 | gender: 'male',
340 | },
341 | ],
342 | vehicle: {
343 | car: {
344 | tesla: undefined,
345 | porsche: undefined,
346 | },
347 | },
348 | cat: undefined,
349 | dog: 'barfoo',
350 | ape: undefined,
351 | });
352 | });
353 | });
354 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | ## API
2 | All controls share the following properties:
3 | - **model**: a required string that will determine the name of your control, e.g.
4 | ```jsx
5 | model="username"
6 | ```
7 | - **validator**: an optional array of validator functions that will validate your input. Every validator must return a string when the validation fails e.g.
8 | ```jsx
9 | // multi line
10 | const isRequired = val => {
11 | if (val && val.length > 0) {
12 | return; // can also return false
13 | }
14 | return 'This field is required';
15 | };
16 | validator={[isRequired]}
17 |
18 | // single line
19 | validator={[val => (val && val.length > 0) || 'This field is required']}
20 | ```
21 | Since the validator prop accepts an array of validators, you can have multiple validators per control.
22 | - **validateOn**: an optional enum (`blur`, `focus`, `input`, `mount`) that detirmines at what point the validator starts to validate. Default is `mount`, e.g.
23 | ```jsx
24 | validateOn="blur"
25 | ```
26 | - **className**: optional for class style and compatible with CSS modules, e.g.
27 | ```jsx
28 | className={styles.button}
29 | ```
30 | - **style**: optional for inline style, e.g.
31 | ```jsx
32 | style={{color: 'red'}}
33 | ```
34 | - **id**: optional string to attach a label to the control, e.g.
35 | ```jsx
36 | id="age_1337"
37 | ```
38 | - **disabled**: optional boolean if the control should be disabled, e.g.
39 | ```jsx
40 | disabled={true}
41 | ```
42 | - **onChange**: an optional function that allows you to catch the on change event, e.g.
43 | ```jsx
44 | onChange={(model, value) => console.log(model, value)}
45 | ```
46 |
47 |
48 |
49 | ### Button
50 | `import {Button} from 'react-chloroform';`
51 |
52 | #### Attributes
53 | - **type**: optional string descriping the button type, default `button`, e.g.
54 | ```jsx
55 | type="submit"
56 | ```
57 | - **text**: required string for the button text, e.g.
58 | ```jsx
59 | text="Submit form"
60 | ```
61 | - **onClick**: optional event handler function to catch when the button was clicked, e.g.
62 | ```jsx
63 | onClick={() => console.log('Button was clicked')}
64 | ```
65 |
66 | #### Example
67 | ```jsx
68 |
73 |
74 |
78 | ```
79 |
80 |
81 |
82 | ### Checkbox
83 | `import {Checkbox} from 'react-chloroform';`
84 |
85 | #### Attributes
86 | - **group**: an optional string for when you want to group checkboxes together with the reserved `all` checkbox, e.g.
87 | ```jsx
88 | group="drinks"
89 | ```
90 | - **validation**: grouped checkboxes offer a validation. The validation must be set on the reserved `all` checkbox, e.g.
91 | ```jsx
92 |
93 |
100 | ```
101 | #### Example
102 | ```jsx
103 |
104 |
109 |
110 |
111 |
116 | ```
117 |
118 |
119 |
120 | ### ChloroformError
121 | `import {ChloroformError} from 'react-chloroform'`;
122 |
123 | This component renders the errors from your control validation.
124 |
125 | #### Attributes
126 | - **component**: a required node/func that will render your errors, e.g.
127 | ```jsx
128 | component={({error}) => {error}
}
129 | ```
130 |
131 | #### Example
132 | ```jsx
133 |
137 | {error}
}
140 | />
141 | ```
142 |
143 |
144 |
145 | ### DataList
146 | `import {DataList} from 'react-chloroform';`
147 |
148 | #### Attributes
149 | - **options**: an array of options that will be rendered by the datalist. The option attributes are:
150 | - **name**: an optional string that further describes this option.
151 | - **value**: a required string or a number that determines the value of the option.
152 | - **disabled**: an optional boolean if this option should be disabled.
153 | ```jsx
154 | options={[{name: 'Name', value: 'Value'}]}
155 | ```
156 | - **placeholder**: an optional placeholder for the input, e.g.
157 | ```jsx
158 | placeholder="Select gender"
159 | ```
160 | #### Example
161 | ```jsx
162 | const options = [
163 | {
164 | name: 'React',
165 | value: 'react',
166 | },
167 | {
168 | name: 'Angular',
169 | value: 'angular',
170 | disabled: true,
171 | },
172 | ];
173 |
174 |
179 | ```
180 |
181 |
182 |
183 | ### FormInput
184 | `import {FormInput} from 'react-chloroform';`
185 |
186 | #### Attributes
187 | - **placeholder**: an optional placeholder for the input, e.g.
188 | ```jsx
189 | placeholder="Write something..."
190 | ```
191 | - **type**: your text input type, e.g.
192 | ```jsx
193 | type="password"
194 | ```
195 | #### Example
196 | ```jsx
197 |
202 | ```
203 |
204 |
205 |
206 | ### Form
207 | `import {Form} from 'react-chloroform';`
208 |
209 | This is the wrapper form needed to collect the user input.
210 |
211 | #### Attributes
212 | - **afterSubmitState**: an optional object that allows you to re-initialise your state with prefilled values after submit, e.g.
213 | ```jsx
214 | afterSubmitState={{email: '', jobTitle: 'developer'}}
215 | ```
216 | - **initialState**: an optional object that allows you to initialise your state with prefilled values, e.g.
217 | ```jsx
218 | initialState={{email: 'react_chloroform@reactchloroform.com', jobTitle: 'developer'}}
219 | ```
220 | - **onReset**: an optional function that allows you to catch the on reset event, e.g.
221 | ```jsx
222 | onReset={() => console.log('Form reset called')}
223 | ```
224 | - **onResetState**: an optional object that allows you reset your state to a predefined object. The default is the initialState, e.g.
225 | ```jsx
226 | onResetState={{}}
227 | ```
228 | - **onSubmit**: a required function that allows you to catch the on submit event, e.g.
229 | ```jsx
230 | onSubmit={model => console.log(model)}
231 | ```
232 | - **onSubmitFailed**: an optional function that allows you to catch if the form submit fails, e.g.
233 | ```jsx
234 | onSubmitFailed={error => console.log(error)}
235 | ```
236 | - **onChange**: an optional function that allows you to catch the on change event, e.g.
237 | ```jsx
238 | onChange={model => console.log(model)}
239 | ```
240 |
241 | #### Example
242 | ```jsx
243 | const initialState = {
244 | name: 'Darri',
245 | age: 1337,
246 | };
247 |
248 | const handleSubmit = model => console.log(model);
249 |
250 |
256 | ```
257 |
258 |
259 |
260 | ### RadioButton
261 | `import {RadioButton} from 'react-chloroform';`
262 |
263 | All radio buttons in the same group need to share the same model name.
264 |
265 | #### Attributes
266 | - **value**: a required string/number representing the value of the radiobutton, e.g.
267 | ```jsx
268 | value="true"
269 | ```
270 |
271 | #### Example
272 | ```jsx
273 |
274 |
279 |
280 |
281 |
286 | ```
287 |
288 |
289 |
290 | ### Select
291 | `import {Select} from 'react-chloroform';`
292 |
293 | #### Attributes
294 | - **options**: an array of options that will be rendered by the select. The option attributes are:
295 | - **name**: a required string that further describes this option.
296 | - **value**: a required string or a number that determines the value of the option.
297 | - **disabled**: an optional boolean if this option should be disabled.
298 | ```jsx
299 | options={[{name: 'Name', value: 'Value'}]}
300 | ```
301 | - The options can also be grouped together. The following are the attributes when grouping is needed:
302 | - **name**: a required string that further describes this group.
303 | - **disabled**: an optional boolean if this group should be disabled.
304 | - **group**: a required array of options.
305 | ```jsx
306 | options={[{name: 'drinks', group: [{name: 'Pepsi', value: 'pepsi'}]}
307 | ```
308 | - **placeholder**: an optional placeholder for the select, e.g.
309 | ```jsx
310 | placeholder="Choose your option"
311 | ```
312 | #### Example
313 | ```jsx
314 | const options = [
315 | {
316 | name: 'React',
317 | value: 'react',
318 | },
319 | {
320 | name: 'Other',
321 | disabled: true,
322 | group: [
323 | {
324 | name: 'Angular',
325 | value: 'angular',
326 | }
327 | ],
328 | },
329 | ];
330 |
331 |
336 | ```
337 |
338 |
339 |
340 | ### TextArea
341 | `import {TextArea} from 'react-chloroform';`
342 |
343 | #### Attributes
344 | - **cols**: an optional number specifying the width in avg character width, default 30, e.g.
345 | ```jsx
346 | cols={10}
347 | ```
348 | - **rows**: an optional number specifying the height in lines, default 10, e.g.
349 | ```jsx
350 | rows={5}
351 | ```
352 | - **placeholder**: an optional placeholder for the text area, e.g.
353 | ```jsx
354 | placeholder="Write here..."
355 | ```
356 |
357 | #### Example
358 | ```jsx
359 |
364 | ```
365 |
366 |
367 |
368 | ### withReactChloroform
369 | `import {withReactChloroform} from 'react-chloroform';`
370 |
371 | A HOC allowing you to write your own component with react-chloroform support and behaviour.
372 |
373 | #### Attributes
374 | - **chloroformStatus**: a string representing the status of the form. The status is `undefined` when nothing is occurring. Otherwise it's one of:
375 | - **submitted**: when the form is submitting
376 | - **submitting**: if submission succeeded
377 | - **failed**: if submission failed
378 | - **hasErrors**: if the form has errors
379 | e.g.
380 | ```jsx
381 |
382 | ```
383 | - **error**: contains the errors from your validation, e.g.
384 | ```jsx
385 | {error && {error}
}
386 | ```
387 | - **value**: a string/number that the HOC passes down as a prop, e.g.
388 | ```jsx
389 | value={value}
390 | ```
391 | - **onChange**: a function that the HOC passes down as a prop, e.g.
392 | ```jsx
393 | onChange={e => onChange(e.target.value)}
394 | ```
395 | - **startValidating**: you can optionally bind the startValidating function to an event handler. By default, the validation will be done on mount, e.g.
396 | ```jsx
397 | onBlur={startValidating}
398 | ```
399 | - **showError**: a boolean representing if your component should show the error. This depends on the startValidating property, e.g.
400 | ```jsx
401 | {showError && error && {error}
}
402 | ```
403 |
404 | #### Example
405 | ```jsx
406 | ---
407 | const MyInput = ({value, onChange, startValidating}) =>
408 |
409 | onChange(e.target.value)}
412 | onFocus={startValidating}
413 | />
414 |
;
415 |
416 | export default withReactChloroform(MyInput);
417 | ---
418 |
419 | // then use your custom component
420 |
424 | ```
425 |
--------------------------------------------------------------------------------
/cypress/app/report.20200130.151727.75820.0.001.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "header": {
4 | "reportVersion": 1,
5 | "event": "Allocation failed - JavaScript heap out of memory",
6 | "trigger": "FatalError",
7 | "filename": "report.20200130.151727.75820.0.001.json",
8 | "dumpEventTime": "2020-01-30T15:17:27Z",
9 | "dumpEventTimeStamp": "1580397447405",
10 | "processId": 75820,
11 | "cwd": "/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app",
12 | "commandLine": [
13 | "node",
14 | "/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app/node_modules/react-scripts/scripts/start.js"
15 | ],
16 | "nodejsVersion": "v13.7.0",
17 | "wordSize": 64,
18 | "arch": "x64",
19 | "platform": "darwin",
20 | "componentVersions": {
21 | "node": "13.7.0",
22 | "v8": "7.9.317.25-node.28",
23 | "uv": "1.34.1",
24 | "zlib": "1.2.11",
25 | "brotli": "1.0.7",
26 | "ares": "1.15.0",
27 | "modules": "79",
28 | "nghttp2": "1.40.0",
29 | "napi": "5",
30 | "llhttp": "2.0.1",
31 | "openssl": "1.1.1d",
32 | "cldr": "35.1",
33 | "icu": "64.2",
34 | "tz": "2019a",
35 | "unicode": "12.1"
36 | },
37 | "release": {
38 | "name": "node",
39 | "headersUrl": "https://nodejs.org/download/release/v13.7.0/node-v13.7.0-headers.tar.gz",
40 | "sourceUrl": "https://nodejs.org/download/release/v13.7.0/node-v13.7.0.tar.gz"
41 | },
42 | "osName": "Darwin",
43 | "osRelease": "18.7.0",
44 | "osVersion": "Darwin Kernel Version 18.7.0: Sun Dec 1 18:59:03 PST 2019; root:xnu-4903.278.19~1/RELEASE_X86_64",
45 | "osMachine": "x86_64",
46 | "cpus": [
47 | {
48 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
49 | "speed": 2600,
50 | "user": 4571700,
51 | "nice": 0,
52 | "sys": 2762030,
53 | "idle": 12794090,
54 | "irq": 0
55 | },
56 | {
57 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
58 | "speed": 2600,
59 | "user": 288010,
60 | "nice": 0,
61 | "sys": 310960,
62 | "idle": 19528340,
63 | "irq": 0
64 | },
65 | {
66 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
67 | "speed": 2600,
68 | "user": 4175510,
69 | "nice": 0,
70 | "sys": 2264710,
71 | "idle": 13687100,
72 | "irq": 0
73 | },
74 | {
75 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
76 | "speed": 2600,
77 | "user": 302140,
78 | "nice": 0,
79 | "sys": 265660,
80 | "idle": 19559500,
81 | "irq": 0
82 | },
83 | {
84 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
85 | "speed": 2600,
86 | "user": 3989090,
87 | "nice": 0,
88 | "sys": 1944010,
89 | "idle": 14194210,
90 | "irq": 0
91 | },
92 | {
93 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
94 | "speed": 2600,
95 | "user": 304400,
96 | "nice": 0,
97 | "sys": 232020,
98 | "idle": 19590880,
99 | "irq": 0
100 | },
101 | {
102 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
103 | "speed": 2600,
104 | "user": 3747200,
105 | "nice": 0,
106 | "sys": 1654110,
107 | "idle": 14726000,
108 | "irq": 0
109 | },
110 | {
111 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
112 | "speed": 2600,
113 | "user": 327150,
114 | "nice": 0,
115 | "sys": 207070,
116 | "idle": 19593080,
117 | "irq": 0
118 | },
119 | {
120 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
121 | "speed": 2600,
122 | "user": 3615900,
123 | "nice": 0,
124 | "sys": 1451590,
125 | "idle": 15059820,
126 | "irq": 0
127 | },
128 | {
129 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
130 | "speed": 2600,
131 | "user": 344500,
132 | "nice": 0,
133 | "sys": 187080,
134 | "idle": 19595720,
135 | "irq": 0
136 | },
137 | {
138 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
139 | "speed": 2600,
140 | "user": 3431750,
141 | "nice": 0,
142 | "sys": 1287120,
143 | "idle": 15408440,
144 | "irq": 0
145 | },
146 | {
147 | "model": "Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz",
148 | "speed": 2600,
149 | "user": 368560,
150 | "nice": 0,
151 | "sys": 173300,
152 | "idle": 19585440,
153 | "irq": 0
154 | }
155 | ],
156 | "networkInterfaces": [
157 | {
158 | "name": "lo0",
159 | "internal": true,
160 | "mac": "00:00:00:00:00:00",
161 | "address": "127.0.0.1",
162 | "netmask": "255.0.0.0",
163 | "family": "IPv4"
164 | },
165 | {
166 | "name": "lo0",
167 | "internal": true,
168 | "mac": "00:00:00:00:00:00",
169 | "address": "::1",
170 | "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
171 | "family": "IPv6",
172 | "scopeid": 0
173 | },
174 | {
175 | "name": "lo0",
176 | "internal": true,
177 | "mac": "00:00:00:00:00:00",
178 | "address": "fe80::1",
179 | "netmask": "ffff:ffff:ffff:ffff::",
180 | "family": "IPv6",
181 | "scopeid": 1
182 | },
183 | {
184 | "name": "en5",
185 | "internal": false,
186 | "mac": "ac:de:48:00:11:22",
187 | "address": "fe80::aede:48ff:fe00:1122",
188 | "netmask": "ffff:ffff:ffff:ffff::",
189 | "family": "IPv6",
190 | "scopeid": 11
191 | },
192 | {
193 | "name": "en0",
194 | "internal": false,
195 | "mac": "f0:18:98:98:ac:b1",
196 | "address": "fe80::c96:1993:3250:a113",
197 | "netmask": "ffff:ffff:ffff:ffff::",
198 | "family": "IPv6",
199 | "scopeid": 13
200 | },
201 | {
202 | "name": "en0",
203 | "internal": false,
204 | "mac": "f0:18:98:98:ac:b1",
205 | "address": "10.134.118.4",
206 | "netmask": "255.255.252.0",
207 | "family": "IPv4"
208 | },
209 | {
210 | "name": "awdl0",
211 | "internal": false,
212 | "mac": "62:79:11:91:de:3c",
213 | "address": "fe80::6079:11ff:fe91:de3c",
214 | "netmask": "ffff:ffff:ffff:ffff::",
215 | "family": "IPv6",
216 | "scopeid": 15
217 | },
218 | {
219 | "name": "utun0",
220 | "internal": false,
221 | "mac": "00:00:00:00:00:00",
222 | "address": "fe80::539c:3ffb:c09f:2d69",
223 | "netmask": "ffff:ffff:ffff:ffff::",
224 | "family": "IPv6",
225 | "scopeid": 21
226 | }
227 | ],
228 | "host": "Darri.local"
229 | },
230 | "javascriptStack": {
231 | "message": "No stack.",
232 | "stack": [
233 | "Unavailable."
234 | ]
235 | },
236 | "nativeStack": [
237 | {
238 | "pc": "0x000000010015cafb",
239 | "symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__1::basic_string, std::__1::allocator > const&, v8::Local) [/usr/local/bin/node]"
240 | },
241 | {
242 | "pc": "0x0000000100084c5c",
243 | "symbol": "node::OnFatalError(char const*, char const*) [/usr/local/bin/node]"
244 | },
245 | {
246 | "pc": "0x000000010017fcf5",
247 | "symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]"
248 | },
249 | {
250 | "pc": "0x000000010017fc9f",
251 | "symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]"
252 | },
253 | {
254 | "pc": "0x0000000100299e57",
255 | "symbol": "v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/usr/local/bin/node]"
256 | },
257 | {
258 | "pc": "0x000000010029b1f4",
259 | "symbol": "v8::internal::Heap::MarkCompactPrologue() [/usr/local/bin/node]"
260 | },
261 | {
262 | "pc": "0x0000000100298dac",
263 | "symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/local/bin/node]"
264 | },
265 | {
266 | "pc": "0x0000000100297853",
267 | "symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/bin/node]"
268 | },
269 | {
270 | "pc": "0x000000010029f2a0",
271 | "symbol": "v8::internal::Heap::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/bin/node]"
272 | },
273 | {
274 | "pc": "0x000000010029f2f6",
275 | "symbol": "v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/bin/node]"
276 | },
277 | {
278 | "pc": "0x000000010027ce67",
279 | "symbol": "v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/usr/local/bin/node]"
280 | },
281 | {
282 | "pc": "0x00000001004e1de7",
283 | "symbol": "v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/usr/local/bin/node]"
284 | },
285 | {
286 | "pc": "0x00000001007505b9",
287 | "symbol": "Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/usr/local/bin/node]"
288 | }
289 | ],
290 | "javascriptHeap": {
291 | "totalMemory": 2165067776,
292 | "totalCommittedMemory": 2161808536,
293 | "usedMemory": 1950738616,
294 | "availableMemory": 181297600,
295 | "memoryLimit": 2197815296,
296 | "heapSpaces": {
297 | "read_only_space": {
298 | "memorySize": 262144,
299 | "committedMemory": 33328,
300 | "capacity": 33040,
301 | "used": 33040,
302 | "available": 0
303 | },
304 | "new_space": {
305 | "memorySize": 33554432,
306 | "committedMemory": 31197672,
307 | "capacity": 16758784,
308 | "used": 2762992,
309 | "available": 13995792
310 | },
311 | "old_space": {
312 | "memorySize": 2041978880,
313 | "committedMemory": 2041482384,
314 | "capacity": 1978410128,
315 | "used": 1860614624,
316 | "available": 117795504
317 | },
318 | "code_space": {
319 | "memorySize": 1740800,
320 | "committedMemory": 1657152,
321 | "capacity": 1489440,
322 | "used": 1489440,
323 | "available": 0
324 | },
325 | "map_space": {
326 | "memorySize": 4460544,
327 | "committedMemory": 4367024,
328 | "capacity": 2953280,
329 | "used": 2953280,
330 | "available": 0
331 | },
332 | "large_object_space": {
333 | "memorySize": 82448384,
334 | "committedMemory": 82448384,
335 | "capacity": 82340696,
336 | "used": 82340696,
337 | "available": 0
338 | },
339 | "code_large_object_space": {
340 | "memorySize": 622592,
341 | "committedMemory": 622592,
342 | "capacity": 544544,
343 | "used": 544544,
344 | "available": 0
345 | },
346 | "new_large_object_space": {
347 | "memorySize": 0,
348 | "committedMemory": 0,
349 | "capacity": 16758784,
350 | "used": 0,
351 | "available": 16758784
352 | }
353 | }
354 | },
355 | "resourceUsage": {
356 | "userCpuSeconds": 503.504,
357 | "kernelCpuSeconds": 91.8606,
358 | "cpuConsumptionPercent": 18.1846,
359 | "maxRss": 2438115360768,
360 | "pageFaults": {
361 | "IORequired": 22,
362 | "IONotRequired": 5778826
363 | },
364 | "fsActivity": {
365 | "reads": 0,
366 | "writes": 0
367 | }
368 | },
369 | "libuv": [
370 | ],
371 | "environmentVariables": {
372 | "npm_config_save_dev": "",
373 | "npm_config_legacy_bundling": "",
374 | "npm_config_dry_run": "",
375 | "npm_package_dependencies_react_chloroform": "file:../..",
376 | "AUTOJUMP_ERROR_PATH": "/Users/dkonradsson/Library/autojump/errors.log",
377 | "npm_config_viewer": "man",
378 | "npm_config_only": "",
379 | "npm_config_commit_hooks": "true",
380 | "npm_config_browser": "",
381 | "npm_config_also": "",
382 | "npm_config_sign_git_commit": "",
383 | "npm_config_rollback": "true",
384 | "TERM_PROGRAM": "iTerm.app",
385 | "NODE": "/usr/local/Cellar/node/13.7.0/bin/node",
386 | "npm_config_usage": "",
387 | "npm_config_audit": "true",
388 | "INIT_CWD": "/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app",
389 | "PYENV_ROOT": "/Users/dkonradsson/.pyenv",
390 | "AUTOJUMP_SOURCED": "1",
391 | "npm_config_globalignorefile": "/usr/local/etc/npmignore",
392 | "DO_AUTH_TOKEN": "a2deb365cf316de5c2c629f9432158edbf0b891a8f09da39a72e94d11071aca2",
393 | "TERM": "xterm-256color",
394 | "SHELL": "/bin/zsh",
395 | "npm_config_shell": "/bin/zsh",
396 | "npm_config_maxsockets": "50",
397 | "npm_config_init_author_url": "",
398 | "npm_config__bit_registry": "https://node.bit.dev",
399 | "CLICOLOR": "1",
400 | "npm_config_shrinkwrap": "true",
401 | "npm_config_parseable": "",
402 | "npm_config_metrics_registry": "https://registry.npmjs.org/",
403 | "TMPDIR": "/var/folders/6p/x5hpghyd2tz5wlpg51ykv8pr0000gn/T/",
404 | "npm_config_timing": "",
405 | "npm_config_init_license": "ISC",
406 | "Apple_PubSub_Socket_Render": "/private/tmp/com.apple.launchd.SFaY31rVbx/Render",
407 | "npm_config_if_present": "",
408 | "TERM_PROGRAM_VERSION": "3.3.7",
409 | "npm_config_sign_git_tag": "",
410 | "npm_config_init_author_email": "",
411 | "npm_config_cache_max": "Infinity",
412 | "npm_config_preid": "",
413 | "npm_config_long": "",
414 | "npm_config_local_address": "",
415 | "npm_config_git_tag_version": "true",
416 | "npm_config_cert": "",
417 | "TERM_SESSION_ID": "w5t0p0:C75D0981-261C-438A-9570-C34B99CE5B01",
418 | "npm_config_registry": "https://registry.npmjs.org/",
419 | "npm_config_noproxy": "",
420 | "npm_config_fetch_retries": "2",
421 | "npm_package_private": "true",
422 | "npm_package_dependencies_react_dom": "^16.9.0",
423 | "LC_ALL": "en_US.UTF-8",
424 | "ZSH": "/Users/dkonradsson/.oh-my-zsh",
425 | "npm_config_versions": "",
426 | "npm_config_message": "%s",
427 | "npm_config_key": "",
428 | "USER": "dkonradsson",
429 | "COMMAND_MODE": "unix2003",
430 | "npm_config_globalconfig": "/usr/local/etc/npmrc",
431 | "npm_config_prefer_online": "",
432 | "npm_config_logs_max": "10",
433 | "npm_config_always_auth": "",
434 | "npm_package_eslintConfig_extends": "react-app",
435 | "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.cAx6DjLShn/Listeners",
436 | "__CF_USER_TEXT_ENCODING": "0x1F5:0x0:0x0",
437 | "npm_execpath": "/usr/local/lib/node_modules/npm/bin/npm-cli.js",
438 | "npm_config_global_style": "",
439 | "npm_config_cache_lock_retries": "10",
440 | "PYTHON_CONFIGURE_OPTS": "--enable-framework",
441 | "npm_config_update_notifier": "true",
442 | "npm_config_cafile": "",
443 | "PAGER": "less",
444 | "npm_config_heading": "npm",
445 | "npm_config_audit_level": "low",
446 | "LSCOLORS": "Gxfxcxdxbxegedabagacad",
447 | "npm_config_searchlimit": "20",
448 | "npm_config_read_only": "",
449 | "npm_config_offline": "",
450 | "npm_config_fetch_retry_mintimeout": "10000",
451 | "npm_config_json": "",
452 | "npm_config_access": "",
453 | "npm_config_argv": "{\"remain\":[],\"cooked\":[\"start\"],\"original\":[\"start\"]}",
454 | "PATH": "/usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app/node_modules/.bin:/Users/dkonradsson/.pyenv/shims:/Users/dkonradsson/bin:/usr/local/bin:/home/darris/.gem/ruby/2.3.0/bin:/usr/local/Cellar/node/10.8.0/bin:/usr/local/opt/pyenv/shims:/Users/dkonradsson/.pyenv/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/TeX/texbin:/usr/local/go/bin",
455 | "npm_config_allow_same_version": "",
456 | "npm_config_https_proxy": "",
457 | "npm_config_engine_strict": "",
458 | "npm_config_description": "true",
459 | "_": "/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app/node_modules/.bin/react-scripts",
460 | "npm_config_userconfig": "/Users/dkonradsson/.npmrc",
461 | "npm_config_init_module": "/Users/dkonradsson/.npm-init.js",
462 | "npm_config_cidr": "",
463 | "PWD": "/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app",
464 | "npm_config_user": "",
465 | "npm_config_node_version": "13.7.0",
466 | "npm_lifecycle_event": "start",
467 | "EDITOR": "nvim",
468 | "npm_config_save": "true",
469 | "npm_config_ignore_prepublish": "",
470 | "npm_config_editor": "nvim",
471 | "npm_config_auth_type": "legacy",
472 | "npm_package_name": "app",
473 | "LANG": "en_US.UTF-8",
474 | "BASE16_THEME": ".base16_theme",
475 | "npm_config_tag": "latest",
476 | "npm_config_script_shell": "",
477 | "ITERM_PROFILE": "Default",
478 | "npm_config_progress": "true",
479 | "npm_config_global": "",
480 | "npm_config_before": "",
481 | "npm_package_scripts_build": "react-scripts build",
482 | "npm_package_scripts_start": "react-scripts start",
483 | "npm_config_searchstaleness": "900",
484 | "npm_config_optional": "true",
485 | "npm_config_ham_it_up": "",
486 | "WTF_GITHUB_TOKEN": "3ec0b6becc878524acf0f1a6b497ed1699ac91e3",
487 | "XPC_FLAGS": "0x0",
488 | "npm_config_save_prod": "",
489 | "npm_config_force": "",
490 | "npm_config_bin_links": "true",
491 | "npm_config_searchopts": "",
492 | "npm_package_dependencies_classnames": "^2.2.6",
493 | "PYTHONDONTWRITEBYTECODE": "1",
494 | "npm_config_node_gyp": "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js",
495 | "npm_config_depth": "Infinity",
496 | "npm_config_sso_poll_frequency": "500",
497 | "npm_config_rebuild_bundle": "true",
498 | "npm_package_version": "0.1.0",
499 | "XPC_SERVICE_NAME": "0",
500 | "npm_config_unicode": "true",
501 | "PYENV_SHELL": "zsh",
502 | "SHLVL": "2",
503 | "HOME": "/Users/dkonradsson",
504 | "COLORFGBG": "7;0",
505 | "npm_config_fetch_retry_maxtimeout": "60000",
506 | "npm_package_scripts_test": "react-scripts test",
507 | "npm_config_tag_version_prefix": "v",
508 | "npm_config_strict_ssl": "true",
509 | "npm_config_sso_type": "oauth",
510 | "npm_config_scripts_prepend_node_path": "warn-only",
511 | "npm_config_save_prefix": "^",
512 | "npm_config_loglevel": "notice",
513 | "npm_config_ca": "",
514 | "LC_TERMINAL_VERSION": "3.3.7",
515 | "npm_config_save_exact": "",
516 | "npm_config_group": "20",
517 | "npm_config_fetch_retry_factor": "10",
518 | "npm_config_dev": "",
519 | "npm_package_browserslist_3": "not op_mini all",
520 | "npm_config_version": "",
521 | "npm_config_prefer_offline": "",
522 | "npm_config_cache_lock_stale": "60000",
523 | "npm_package_browserslist_2": "not ie <= 11",
524 | "npm_config_otp": "",
525 | "npm_config_cache_min": "10",
526 | "npm_package_browserslist_1": "not dead",
527 | "ITERM_SESSION_ID": "w5t0p0:C75D0981-261C-438A-9570-C34B99CE5B01",
528 | "npm_config_searchexclude": "",
529 | "npm_config_cache": "/Users/dkonradsson/.npm",
530 | "npm_package_browserslist_0": ">0.2%",
531 | "LESS": "-R",
532 | "LOGNAME": "dkonradsson",
533 | "npm_lifecycle_script": "react-scripts start",
534 | "npm_config_color": "true",
535 | "npm_config_proxy": "",
536 | "npm_config_package_lock": "true",
537 | "CHEAT_HOME": "~/.config/cheat",
538 | "LC_CTYPE": "UTF-8",
539 | "npm_config_package_lock_only": "",
540 | "npm_config_fund": "true",
541 | "npm_package_dependencies_react": "^16.9.0",
542 | "npm_config_save_optional": "",
543 | "PGDATA": "/usr/local/var/postgres",
544 | "npm_config_ignore_scripts": "",
545 | "npm_config_user_agent": "npm/6.13.6 node/v13.7.0 darwin x64",
546 | "npm_config_cache_lock_wait": "10000",
547 | "npm_config_production": "",
548 | "LC_TERMINAL": "iTerm2",
549 | "npm_config_send_metrics": "",
550 | "npm_config_save_bundle": "",
551 | "npm_config_umask": "0022",
552 | "npm_config_node_options": "",
553 | "npm_config_init_version": "1.0.0",
554 | "npm_package_devDependencies_react_scripts": "3.1.1",
555 | "npm_config_init_author_name": "",
556 | "npm_config_git": "git",
557 | "npm_config_scope": "",
558 | "npm_package_scripts_eject": "react-scripts eject",
559 | "SECURITYSESSIONID": "186a9",
560 | "npm_config_unsafe_perm": "true",
561 | "npm_config_tmp": "/var/folders/6p/x5hpghyd2tz5wlpg51ykv8pr0000gn/T",
562 | "npm_config_onload_script": "",
563 | "npm_node_execpath": "/usr/local/Cellar/node/13.7.0/bin/node",
564 | "npm_config_link": "",
565 | "npm_config_format_package_lock": "true",
566 | "npm_config_prefix": "/usr/local",
567 | "COLORTERM": "truecolor",
568 | "BABEL_ENV": "development",
569 | "NODE_ENV": "development",
570 | "SKIP_PREFLIGHT_CHECK": "true",
571 | "NODE_PATH": ""
572 | },
573 | "userLimits": {
574 | "core_file_size_blocks": {
575 | "soft": 0,
576 | "hard": "unlimited"
577 | },
578 | "data_seg_size_kbytes": {
579 | "soft": "unlimited",
580 | "hard": "unlimited"
581 | },
582 | "file_size_blocks": {
583 | "soft": "unlimited",
584 | "hard": "unlimited"
585 | },
586 | "max_locked_memory_bytes": {
587 | "soft": "unlimited",
588 | "hard": "unlimited"
589 | },
590 | "max_memory_size_kbytes": {
591 | "soft": "unlimited",
592 | "hard": "unlimited"
593 | },
594 | "open_files": {
595 | "soft": 49152,
596 | "hard": "unlimited"
597 | },
598 | "stack_size_bytes": {
599 | "soft": 8388608,
600 | "hard": 67104768
601 | },
602 | "cpu_time_seconds": {
603 | "soft": "unlimited",
604 | "hard": "unlimited"
605 | },
606 | "max_user_processes": {
607 | "soft": 2837,
608 | "hard": 2837
609 | },
610 | "virtual_memory_kbytes": {
611 | "soft": "unlimited",
612 | "hard": "unlimited"
613 | }
614 | },
615 | "sharedObjects": [
616 | "/usr/local/bin/node",
617 | "/usr/local/opt/icu4c/lib/libicui18n.64.dylib",
618 | "/usr/local/opt/icu4c/lib/libicuuc.64.dylib",
619 | "/usr/local/opt/icu4c/lib/libicudata.64.dylib",
620 | "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation",
621 | "/usr/lib/libSystem.B.dylib",
622 | "/usr/lib/libc++.1.dylib",
623 | "/usr/lib/system/libcache.dylib",
624 | "/usr/lib/system/libcommonCrypto.dylib",
625 | "/usr/lib/system/libcompiler_rt.dylib",
626 | "/usr/lib/system/libcopyfile.dylib",
627 | "/usr/lib/system/libcorecrypto.dylib",
628 | "/usr/lib/system/libdispatch.dylib",
629 | "/usr/lib/system/libdyld.dylib",
630 | "/usr/lib/system/libkeymgr.dylib",
631 | "/usr/lib/system/liblaunch.dylib",
632 | "/usr/lib/system/libmacho.dylib",
633 | "/usr/lib/system/libquarantine.dylib",
634 | "/usr/lib/system/libremovefile.dylib",
635 | "/usr/lib/system/libsystem_asl.dylib",
636 | "/usr/lib/system/libsystem_blocks.dylib",
637 | "/usr/lib/system/libsystem_c.dylib",
638 | "/usr/lib/system/libsystem_configuration.dylib",
639 | "/usr/lib/system/libsystem_coreservices.dylib",
640 | "/usr/lib/system/libsystem_darwin.dylib",
641 | "/usr/lib/system/libsystem_dnssd.dylib",
642 | "/usr/lib/system/libsystem_info.dylib",
643 | "/usr/lib/system/libsystem_m.dylib",
644 | "/usr/lib/system/libsystem_malloc.dylib",
645 | "/usr/lib/system/libsystem_networkextension.dylib",
646 | "/usr/lib/system/libsystem_notify.dylib",
647 | "/usr/lib/system/libsystem_sandbox.dylib",
648 | "/usr/lib/system/libsystem_secinit.dylib",
649 | "/usr/lib/system/libsystem_kernel.dylib",
650 | "/usr/lib/system/libsystem_platform.dylib",
651 | "/usr/lib/system/libsystem_pthread.dylib",
652 | "/usr/lib/system/libsystem_symptoms.dylib",
653 | "/usr/lib/system/libsystem_trace.dylib",
654 | "/usr/lib/system/libunwind.dylib",
655 | "/usr/lib/system/libxpc.dylib",
656 | "/usr/lib/libobjc.A.dylib",
657 | "/usr/lib/libc++abi.dylib",
658 | "/usr/lib/libDiagnosticMessagesClient.dylib",
659 | "/usr/lib/libicucore.A.dylib",
660 | "/usr/lib/libz.1.dylib",
661 | "/Users/dkonradsson/Documents/repos/react-chloroform/cypress/app/node_modules/fsevents/build/Release/fse.node",
662 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices",
663 | "/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork",
664 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/FSEvents",
665 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore",
666 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata",
667 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/OSServices.framework/Versions/A/OSServices",
668 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SearchKit.framework/Versions/A/SearchKit",
669 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/AE.framework/Versions/A/AE",
670 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices",
671 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/DictionaryServices.framework/Versions/A/DictionaryServices",
672 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/SharedFileList.framework/Versions/A/SharedFileList",
673 | "/usr/lib/libauto.dylib",
674 | "/usr/lib/libbsm.0.dylib",
675 | "/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit",
676 | "/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration",
677 | "/System/Library/Frameworks/Security.framework/Versions/A/Security",
678 | "/usr/lib/libsqlite3.dylib",
679 | "/usr/lib/libxml2.2.dylib",
680 | "/usr/lib/libnetwork.dylib",
681 | "/usr/lib/libapple_nghttp2.dylib",
682 | "/usr/lib/libenergytrace.dylib",
683 | "/usr/lib/system/libkxld.dylib",
684 | "/usr/lib/libpcap.A.dylib",
685 | "/usr/lib/libcoretls.dylib",
686 | "/usr/lib/libcoretls_cfhelpers.dylib",
687 | "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
688 | "/System/Library/PrivateFrameworks/AppleFSCompression.framework/Versions/A/AppleFSCompression",
689 | "/usr/lib/libOpenScriptingUtil.dylib",
690 | "/usr/lib/libpam.2.dylib",
691 | "/usr/lib/libxar.1.dylib",
692 | "/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration",
693 | "/usr/lib/libarchive.2.dylib",
694 | "/usr/lib/liblangid.dylib",
695 | "/usr/lib/libCRFSuite.dylib",
696 | "/usr/lib/libbz2.1.0.dylib",
697 | "/usr/lib/liblzma.5.dylib",
698 | "/System/Library/Frameworks/NetFS.framework/Versions/A/NetFS",
699 | "/System/Library/PrivateFrameworks/NetAuth.framework/Versions/A/NetAuth",
700 | "/System/Library/PrivateFrameworks/login.framework/Versions/A/Frameworks/loginsupport.framework/Versions/A/loginsupport",
701 | "/System/Library/PrivateFrameworks/TCC.framework/Versions/A/TCC",
702 | "/System/Library/PrivateFrameworks/CoreNLP.framework/Versions/A/CoreNLP",
703 | "/System/Library/PrivateFrameworks/MetadataUtilities.framework/Versions/A/MetadataUtilities",
704 | "/usr/lib/libmecabra.dylib",
705 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
706 | "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
707 | "/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText",
708 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO",
709 | "/System/Library/Frameworks/ColorSync.framework/Versions/A/ColorSync",
710 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/ATS",
711 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSyncLegacy.framework/Versions/A/ColorSyncLegacy",
712 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/HIServices",
713 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/LangAnalysis.framework/Versions/A/LangAnalysis",
714 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/PrintCore",
715 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/QD.framework/Versions/A/QD",
716 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/SpeechSynthesis.framework/Versions/A/SpeechSynthesis",
717 | "/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
718 | "/System/Library/Frameworks/IOSurface.framework/Versions/A/IOSurface",
719 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",
720 | "/usr/lib/libcompression.dylib",
721 | "/System/Library/Frameworks/CoreDisplay.framework/Versions/A/CoreDisplay",
722 | "/System/Library/Frameworks/Metal.framework/Versions/A/Metal",
723 | "/System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders",
724 | "/System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/A/MultitouchSupport",
725 | "/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore",
726 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vImage.framework/Versions/A/vImage",
727 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib",
728 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvMisc.dylib",
729 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib",
730 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib",
731 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib",
732 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libLinearAlgebra.dylib",
733 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparseBLAS.dylib",
734 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libQuadrature.dylib",
735 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBNNS.dylib",
736 | "/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libSparse.dylib",
737 | "/System/Library/PrivateFrameworks/GPUWrangler.framework/Versions/A/GPUWrangler",
738 | "/System/Library/PrivateFrameworks/IOAccelerator.framework/Versions/A/IOAccelerator",
739 | "/System/Library/PrivateFrameworks/IOPresentment.framework/Versions/A/IOPresentment",
740 | "/System/Library/PrivateFrameworks/DSExternalDisplay.framework/Versions/A/DSExternalDisplay",
741 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreFSCache.dylib",
742 | "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSCore.framework/Versions/A/MPSCore",
743 | "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSImage.framework/Versions/A/MPSImage",
744 | "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSNeuralNetwork.framework/Versions/A/MPSNeuralNetwork",
745 | "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSMatrix.framework/Versions/A/MPSMatrix",
746 | "/System/Library/Frameworks/MetalPerformanceShaders.framework/Frameworks/MPSRayIntersector.framework/Versions/A/MPSRayIntersector",
747 | "/System/Library/PrivateFrameworks/MetalTools.framework/Versions/A/MetalTools",
748 | "/System/Library/PrivateFrameworks/AggregateDictionary.framework/Versions/A/AggregateDictionary",
749 | "/usr/lib/libMobileGestalt.dylib",
750 | "/System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage",
751 | "/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo",
752 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL",
753 | "/System/Library/PrivateFrameworks/GraphVisualizer.framework/Versions/A/GraphVisualizer",
754 | "/System/Library/PrivateFrameworks/FaceCore.framework/Versions/A/FaceCore",
755 | "/System/Library/Frameworks/OpenCL.framework/Versions/A/OpenCL",
756 | "/usr/lib/libFosl_dynamic.dylib",
757 | "/System/Library/PrivateFrameworks/OTSVG.framework/Versions/A/OTSVG",
758 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontParser.dylib",
759 | "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Resources/libFontRegistry.dylib",
760 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJPEG.dylib",
761 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libTIFF.dylib",
762 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libPng.dylib",
763 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libGIF.dylib",
764 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libJP2.dylib",
765 | "/System/Library/Frameworks/ImageIO.framework/Versions/A/Resources/libRadiance.dylib",
766 | "/System/Library/PrivateFrameworks/AppleJPEG.framework/Versions/A/AppleJPEG",
767 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGFXShared.dylib",
768 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLU.dylib",
769 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib",
770 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGLImage.dylib",
771 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCVMSPluginSupport.dylib",
772 | "/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libCoreVMClient.dylib",
773 | "/usr/lib/libcups.2.dylib",
774 | "/System/Library/Frameworks/Kerberos.framework/Versions/A/Kerberos",
775 | "/System/Library/Frameworks/GSS.framework/Versions/A/GSS",
776 | "/usr/lib/libresolv.9.dylib",
777 | "/usr/lib/libiconv.2.dylib",
778 | "/System/Library/PrivateFrameworks/Heimdal.framework/Versions/A/Heimdal",
779 | "/usr/lib/libheimdal-asn1.dylib",
780 | "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/OpenDirectory",
781 | "/System/Library/PrivateFrameworks/CommonAuth.framework/Versions/A/CommonAuth",
782 | "/System/Library/Frameworks/OpenDirectory.framework/Versions/A/Frameworks/CFOpenDirectory.framework/Versions/A/CFOpenDirectory",
783 | "/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation",
784 | "/System/Library/PrivateFrameworks/APFS.framework/Versions/A/APFS",
785 | "/usr/lib/libutil.dylib",
786 | "/usr/lib/libcharset.1.dylib",
787 | "/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio",
788 | "/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox",
789 | "/System/Library/PrivateFrameworks/AppleSauce.framework/Versions/A/AppleSauce",
790 | "/System/Library/PrivateFrameworks/AssertionServices.framework/Versions/A/AssertionServices",
791 | "/System/Library/PrivateFrameworks/BaseBoard.framework/Versions/A/BaseBoard",
792 | "/usr/lib/libmecab.1.0.0.dylib",
793 | "/usr/lib/libgermantok.dylib",
794 | "/usr/lib/libThaiTokenizer.dylib",
795 | "/usr/lib/libChineseTokenizer.dylib",
796 | "/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling",
797 | "/System/Library/PrivateFrameworks/CoreEmoji.framework/Versions/A/CoreEmoji",
798 | "/System/Library/PrivateFrameworks/Lexicon.framework/Versions/A/Lexicon",
799 | "/System/Library/PrivateFrameworks/LinguisticData.framework/Versions/A/LinguisticData",
800 | "/usr/lib/libcmph.dylib",
801 | "/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData",
802 | "/System/Library/Frameworks/ServiceManagement.framework/Versions/A/ServiceManagement",
803 | "/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/BackgroundTaskManagement",
804 | "/usr/lib/libxslt.1.dylib"
805 | ]
806 | }
--------------------------------------------------------------------------------