├── cypress.json ├── .travis.yml ├── cypress ├── app │ ├── src │ │ ├── app.module.css │ │ ├── components │ │ │ ├── index.js │ │ │ ├── ValidationTest.js │ │ │ └── DataStructureTest.js │ │ ├── index.js │ │ └── App.js │ ├── .gitignore │ ├── public │ │ └── index.html │ ├── package.json │ └── report.20200130.151727.75820.0.001.json └── integration │ ├── validation.spec.js │ └── datastructure.spec.js ├── src ├── constants │ ├── events.ts │ └── form.ts ├── hooks │ ├── index.ts │ ├── useWillMount.ts │ ├── useCachedSelector.ts │ └── useGetErrors.ts ├── actions │ ├── form.ts │ ├── index.ts │ └── controls.ts ├── index.ts ├── store │ ├── action-types.ts │ ├── reducers │ │ ├── form.ts │ │ ├── index.ts │ │ └── control.ts │ └── index.tsx ├── components │ ├── Button.tsx │ ├── Checkbox.tsx │ ├── Input.tsx │ ├── RadioButton.tsx │ ├── TextArea.tsx │ ├── DataList.tsx │ ├── Form.tsx │ └── Select.tsx └── utils.ts ├── .babelrc ├── .npmignore ├── examples ├── example1 │ ├── package.json │ ├── style.css │ ├── index.html │ └── index.jsx └── example2 │ ├── index.html │ ├── style.css │ └── index.jsx ├── types ├── global.d.ts └── index.d.ts ├── .prettierrc ├── CONTRIBUTING.md ├── jest.config.js ├── .gitignore ├── LICENSE ├── tsconfig.json ├── .eslintrc ├── package.json ├── README.md ├── test └── reducers │ ├── index.test.ts │ └── control.test.ts └── API.md /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8080/", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.11.0" 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /cypress/app/src/app.module.css: -------------------------------------------------------------------------------- 1 | .section { 2 | display: flex; 3 | flex-direction: row; 4 | padding: 10px; 5 | border-bottom: 1px solid #eee; 6 | } 7 | -------------------------------------------------------------------------------- /cypress/app/src/components/index.js: -------------------------------------------------------------------------------- 1 | export {default as DataStructureTest} from './DataStructureTest'; 2 | export {default as ValidationTest} from './ValidationTest'; 3 | -------------------------------------------------------------------------------- /cypress/app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/constants/events.ts: -------------------------------------------------------------------------------- 1 | export const BLUR: string = 'blur'; 2 | export const FOCUS: string = 'focus'; 3 | export const INPUT: string = 'input'; 4 | export const MOUNT: string = 'mount'; 5 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export {default as useCachedSelector} from './useCachedSelector'; 2 | export {default as useGetErrors} from './useGetErrors'; 3 | export {default as useWillMount} from './useWillMount'; 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/react", "@babel/env"], 3 | "plugins": ["@babel/proposal-class-properties"], 4 | "env": { 5 | "commonjs": {}, 6 | 7 | "umd": {}, 8 | 9 | "es": {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | src/ 3 | example/ 4 | examples/ 5 | CONTRIBUTING.md 6 | cypress/ 7 | .babelrc 8 | .eslintrc 9 | .prettierrc 10 | .travis.yml 11 | types/ 12 | yarn* 13 | tsconfig.json 14 | .td.cfg 15 | jest.config.js 16 | cypress.json 17 | -------------------------------------------------------------------------------- /src/constants/form.ts: -------------------------------------------------------------------------------- 1 | export const FAILED: string = 'failed'; 2 | export const HAS_ERRORS: string = 'hasErrors'; 3 | export const SUBMIT: string = 'submit'; 4 | export const SUBMITTED: string = 'submitted'; 5 | export const SUBMITTING: string = 'submitting'; 6 | -------------------------------------------------------------------------------- /examples/example1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esnextbin-sketch", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "babel-runtime": "6.26.0", 6 | "react-chloroform": "0.0.9", 7 | "react": "16.2.0", 8 | "react-dom": "16.2.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Declare global variables for TypeScript and VSCode. 2 | // Do not rename this file or move these types into index.d.ts 3 | // @see https://code.visualstudio.com/docs/nodejs/working-with-javascript#_global-variables-and-type-checking 4 | declare const __DEV__: boolean; 5 | -------------------------------------------------------------------------------- /src/hooks/useWillMount.ts: -------------------------------------------------------------------------------- 1 | import {useLayoutEffect} from 'react'; 2 | import {useDispatch} from 'react-redux'; 3 | 4 | export default (func: Function) => { 5 | const dispatch = useDispatch(); 6 | 7 | return useLayoutEffect(() => { 8 | dispatch(func()); 9 | }, []); 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "insertPragma": false, 5 | "jsxBracketSameLine": false, 6 | "parser": "babel", 7 | "printWidth": 100, 8 | "proseWrap": "never", 9 | "requirePragma": false, 10 | "semi": true, 11 | "singleQuote": true, 12 | "tabWidth": 2, 13 | "trailingComma": "es5", 14 | "useTabs": false, 15 | } 16 | -------------------------------------------------------------------------------- /cypress/app/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {/*DataStructureTest, */ValidationTest} from './components'; 4 | 5 | import styles from './app.module.css'; 6 | 7 | function App() { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution 2 | React-Chloroform is open for contributions by the community. 3 | 4 | ### Issues 5 | Before submitting an issue, please check if the issue has been submitted before. 6 | 7 | ### Pull requests 8 | Before you create a PR, please submit an issue with your improvements before starting your work. That way you can avoid wasting your time on a PR that won't be approved. 9 | -------------------------------------------------------------------------------- /cypress/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/hooks/useCachedSelector.ts: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | import {useSelector} from 'react-redux'; 3 | 4 | export default (selector: () => ExplicitAny, ...args: ExplicitAny[]): ExplicitAny => { 5 | const cachedSelector = useMemo( 6 | selector, 7 | [] 8 | ) 9 | 10 | return useSelector((state: Store.CombinedState) => 11 | cachedSelector(state, ...args) 12 | ) 13 | }; 14 | -------------------------------------------------------------------------------- /src/actions/form.ts: -------------------------------------------------------------------------------- 1 | import createActions from '.'; 2 | import { 3 | RESET_SUBMIT, 4 | SET_SUBMITTED, 5 | SET_SUBMITTING, 6 | SET_SUBMIT_FAILED, 7 | } from '../store/action-types'; 8 | 9 | const actions = { 10 | [RESET_SUBMIT]: () => {}, 11 | [SET_SUBMITTED]: () => {}, 12 | [SET_SUBMITTING]: () => {}, 13 | [SET_SUBMIT_FAILED]: () => {}, 14 | }; 15 | 16 | export default createActions(actions); 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Button} from './components/Button'; 2 | export {default as Checkbox} from './components/Checkbox'; 3 | export {default as DataList} from './components/DataList'; 4 | export {default as Input} from './components/Input'; 5 | export {default as Form} from './components/Form'; 6 | export {default as RadioButton} from './components/RadioButton'; 7 | export {default as Select} from './components/Select'; 8 | export {default as TextArea} from './components/TextArea'; 9 | -------------------------------------------------------------------------------- /cypress/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React-Chloroform example 8 | 9 | 10 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/example1/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #20262E; 3 | padding: 20px; 4 | font-family: Helvetica; 5 | } 6 | 7 | #root { 8 | background: #fff; 9 | border-radius: 4px; 10 | padding: 20px; 11 | transition: all 0.2s; 12 | } 13 | 14 | .fieldError { 15 | color: #a94442; 16 | } 17 | 18 | .react-chloroform-error { 19 | border-color: #a94442 !important; 20 | -webkit-box-shadow: 0 0 0 0.2rem rgba(0,0,0,.075) !important; 21 | box-shadow: 0 0 0 0.2rem rgba(0,0,0,.075) !important; 22 | } 23 | -------------------------------------------------------------------------------- /src/store/action-types.ts: -------------------------------------------------------------------------------- 1 | export const INITIALIZE_GROUP = 'INITIALIZE_GROUP'; 2 | export const INITIALIZE_STATE = 'INITIALIZE_STATE'; 3 | export const MARK_VALIDATED = 'MARK_VALIDATED'; 4 | export const MOUNT_MODEL = 'MOUNT_MODEL'; 5 | export const RESET_SUBMIT = 'RESET_SUBMIT'; 6 | export const RESET_VALUES = 'RESET_VALUES'; 7 | export const SET_SUBMITTED = 'SET_SUBMITTED'; 8 | export const SET_SUBMITTING = 'SET_SUBMITTING'; 9 | export const SET_SUBMIT_FAILED = 'SET_SUBMIT_FAILED'; 10 | export const SET_VALUE = 'SET_VALUE'; 11 | export const SHOW_ERRORS = 'SHOW_ERRORS'; 12 | -------------------------------------------------------------------------------- /examples/example1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ESNextbin Sketch 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | const camelCase = (str: string) => str.toLowerCase().replace(/(_\w)/g, m => m[1].toUpperCase()); 2 | 3 | const actionCreator = ({type, payload}: Store.Action) => (dispatch: Function) => 4 | dispatch({ 5 | type, 6 | payload, 7 | }); 8 | 9 | const createActions = (actions: {[key: string]: Function}): {[key: string]: Function} => 10 | Object.keys(actions).reduce( 11 | (createdActions: {}, type: string) => ({ 12 | ...createdActions, 13 | [camelCase(type)]: (...args: Store.Action[]) => 14 | actionCreator({type, payload: actions[type](...args)}), 15 | }), 16 | {} 17 | ); 18 | 19 | export default createActions; 20 | -------------------------------------------------------------------------------- /src/hooks/useGetErrors.ts: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | 3 | import {getValidators} from '../store/reducers'; 4 | import {useCachedSelector} from '.'; 5 | 6 | export default (model: string, value: ExplicitAny): string[] => { 7 | const validators = useCachedSelector(getValidators, model); 8 | return useMemo( 9 | () => 10 | validators.reduce((acc: string[], validate: Function) => { 11 | const validation = validate(value); 12 | if (validation) { 13 | const [isValid, message] = validation; 14 | if (!isValid) { 15 | return [...acc, message]; 16 | } 17 | } 18 | return acc; 19 | }, []), 20 | [value] 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /cypress/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "classnames": "^2.2.6", 7 | "react": "^16.9.0", 8 | "react-chloroform": "file:../..", 9 | "react-dom": "^16.9.0" 10 | }, 11 | "devDependencies": { 12 | "react-scripts": "3.1.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/example2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ESNextbin Sketch 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | 3 | interface PropTypes { 4 | className?: string; 5 | disabled?: boolean; 6 | id?: string; 7 | onClick?: () => void; 8 | style?: React.CSSProperties; 9 | text: string; 10 | type?: 'button' | 'reset' | 'submit'; 11 | } 12 | 13 | function Button({className, disabled, onClick, id, style, text, type = 'button'}: PropTypes) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | /* 29 | const mapStateToProps = (state: Store.CombinedState, {type, disabled}: PropTypes) => ({ 30 | disabled: disabled === undefined ? type === SUBMIT && canBeSubmitted(state) : disabled, 31 | }); 32 | */ 33 | 34 | export default memo(Button); 35 | -------------------------------------------------------------------------------- /src/actions/controls.ts: -------------------------------------------------------------------------------- 1 | import createActions from '.'; 2 | import { 3 | INITIALIZE_GROUP, 4 | INITIALIZE_STATE, 5 | MARK_VALIDATED, 6 | MOUNT_MODEL, 7 | SET_VALUE, 8 | SHOW_ERRORS, 9 | } from '../store/action-types'; 10 | 11 | const actions = { 12 | [INITIALIZE_GROUP]: (group: string, validator: Function, validateOn: string): {} => ({ 13 | group, 14 | validator, 15 | validateOn, 16 | }), 17 | [INITIALIZE_STATE]: (initialState: {}, validators: {[key: string]: Function}): {} => ({initialState, validators}), 18 | [MARK_VALIDATED]: (model: string): {} => ({model}), 19 | [MOUNT_MODEL]: (model: string, parseValue: Function, validated: boolean, validator?: Function): {} => ({ 20 | model, 21 | parseValue, 22 | validated, 23 | validator, 24 | }), 25 | [SET_VALUE]: (model: string, value: Scalar): {} => ({model, value}), 26 | [SHOW_ERRORS]: () => {}, 27 | }; 28 | 29 | export default createActions(actions); 30 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // The root of your source code, typically /src 3 | // `` is a token Jest substitutes 4 | roots: ["/src", "/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 |

21 |
[val === 'BAR', `${val} is not a valid human interest`], 31 | 'human.*.interests.*': val => [val !== 'FOOBAR', 'FOOBAR is not a valid value'], 32 | }} 33 | > 34 | {/* list of list objects validation */} 35 |
36 |
37 | [ 40 | val !== null && val !== undefined && val !== '', 41 | 'This field is required', 42 | ]} // isRequired 43 | /> 44 | 45 |
46 |
47 | [val !== 'FOO', `${val} is not a valid human interest`]} 50 | /> 51 | 52 |
53 |
54 | 55 | {/* scalar validation */} 56 |
57 |