├── tsconfig.json ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── common │ ├── helpers.js │ ├── strings.js │ ├── validation.d.ts │ └── validation.js ├── index.css ├── index.js ├── App.js ├── App.css ├── InputField.js ├── registerServiceWorker.js └── Form.js ├── .gitignore ├── .eslintrc.js ├── package.json └── README.md /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/**/*.d.ts" 4 | ] 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuf/validation/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/common/helpers.js: -------------------------------------------------------------------------------- 1 | export const poopJson = json => JSON.stringify(json, null, 2); 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/strings.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export const Msg = { 3 | mustEqualAbc: 'Field must equal \'abc\'', 4 | mustContainAbc: 'Field must contain \'abc\'', 5 | required: 'Field is required', 6 | }; 7 | -------------------------------------------------------------------------------- /src/common/validation.d.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | export type Validator = any; 4 | export type ValidatorPair = [string, Validator]; 5 | 6 | // 7 | 8 | export function newValidator(field: string, validator: Validator): ValidatorPair; 9 | export function combineValidatorPairs(...validators: ValidatorPair[]): any; 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Calmm Form Validation", 3 | "name": "Calmm Form Validation Example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import K, * as U from 'karet.util'; 4 | import * as L from 'partial.lenses'; 5 | import * as R from 'ramda'; 6 | import * as Kefir from 'kefir'; 7 | import * as Atom from 'kefir.atom'; 8 | 9 | import App from './App'; 10 | import registerServiceWorker from './registerServiceWorker'; 11 | import './index.css'; 12 | 13 | if (process.env.NODE_ENV === 'development') { 14 | Object.assign(window, { K, U, R, L, Kefir, Atom }); 15 | } 16 | 17 | ReactDOM.render(, document.getElementById('root')); 18 | registerServiceWorker(); 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "jest": true 5 | }, 6 | "extends": "airbnb", 7 | "rules": { 8 | "arrow-parens": 0, 9 | "indent": 0, 10 | "spaced-comment": 0, 11 | "react/jsx-wrap-multilines": 0, 12 | "react/jsx-filename-extension": 0, 13 | "react/jsx-indent": 0, 14 | "react/jsx-indent-props": 0, 15 | "react/jsx-first-prop-new-line": 0, 16 | "react/jsx-closing-bracket-location": 0, 17 | "react/require-default-props": 0, 18 | "react/forbid-prop-types": 0 19 | }, 20 | "plugins": [ 21 | "react", 22 | "jsx-a11y", 23 | "import" 24 | ] 25 | }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Calmm Form Validation Example 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'karet'; 2 | 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import './App.css'; 5 | 6 | import Form from './Form'; 7 | 8 | const App = () => 9 |
10 |
11 |
12 |
13 |

Calmm · Form Validation

14 |
15 |

16 | Validating form data in Calmm.js 17 |

18 |

19 | View on Github 20 |

21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html body { 2 | font-family: 'PT Sans', sans-serif; 3 | height: 100rem; 4 | color: rgba(0, 0, 0, 0.87); 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | font-family: 'Karma', serif; 9 | } 10 | 11 | h1 { 12 | font-weight: 300; 13 | } 14 | 15 | .jumbotron { 16 | background-color: #c5c1c0; 17 | color: #0a1612; 18 | } 19 | 20 | header hr { 21 | width: 60%; 22 | border-top: solid 2px #c5c1c0; 23 | } 24 | 25 | header .content { 26 | width: 66vw; 27 | margin: 0 auto; 28 | background-color: #fff; 29 | padding: 2rem; 30 | } 31 | 32 | header .content p:last-child { 33 | margin-bottom: 0; 34 | } 35 | 36 | html code, 37 | input.form-control { 38 | font-family: 'Overpass Mono', monospace; 39 | } 40 | 41 | .card-inverse pre { 42 | color: #fff; 43 | margin-bottom: 0; 44 | } 45 | 46 | .card-success .validation-result { 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | font-size: 2rem; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/common/validation.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as L from 'partial.lenses'; 4 | import * as R from 'ramda'; 5 | 6 | // 7 | 8 | export const whenMatch = R.compose(L.when, R.test); 9 | export const whenNotMatch = R.compose(L.when, R.complement, R.test); 10 | 11 | export const mustNotMatch = regex => [L.defaults(''), whenMatch(regex)]; 12 | export const mustMatch = regex => [L.defaults(''), whenNotMatch(regex)]; 13 | 14 | export const required = L.when(R.isEmpty); 15 | export const mustEqual = R.compose(L.when, R.equals); 16 | export const mustNotEqual = R.compose(L.when, R.complement, R.equals); 17 | 18 | // 19 | 20 | export const newValidator = (name, schema) => [name, [name, L.pick(schema)]]; 21 | export const newValidatorFromP = ([name, schema]) => newValidator(name, schema); 22 | 23 | // 24 | 25 | export const combineValidatorPairs = (...validatorPairs) => R.fromPairs(validatorPairs); 26 | 27 | export const fromTemplate = template => 28 | R.compose(R.fromPairs, 29 | R.map(newValidatorFromP), 30 | R.toPairs)(template); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validation", 3 | "version": "0.1.0", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/stuf/validation.git" 8 | }, 9 | "homepage": "https://stuf.github.io/validation", 10 | "dependencies": { 11 | "bootstrap": "4.0.0-alpha.6", 12 | "karet": "^1.2.2", 13 | "karet.util": "^0.12.2", 14 | "kefir": "^3.7.2", 15 | "kefir.atom": "^5.3.4", 16 | "partial.lenses": "^11.8.0", 17 | "prop-types": "^15.5.10", 18 | "ramda": "^0.24.0", 19 | "react": "^15.5.4", 20 | "react-dom": "^15.5.4" 21 | }, 22 | "devDependencies": { 23 | "chai": "^4.0.0", 24 | "eslint": "^3.19.0", 25 | "eslint-config-airbnb": "^15.0.1", 26 | "eslint-plugin-import": "^2.3.0", 27 | "eslint-plugin-jsx-a11y": "^5.0.3", 28 | "eslint-plugin-react": "^7.0.1", 29 | "gh-pages": "^1.0.0", 30 | "react-scripts": "^1.0.7" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test --env=jsdom", 36 | "eject": "react-scripts eject", 37 | "predeploy": "npm run build", 38 | "deploy": "gh-pages -d build" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | This project contains an example implementation on how to perform simple form validation 4 | in [Calmm](https://github.com/calmm-js). 5 | 6 | The basic gist of the validation is to use [partial lenses](https://github.com/calmm-js/partial.lenses) 7 | to specify validators on the form data. 8 | 9 | ## Basic example 10 | 11 | A crude way of doing validation would be something like this: 12 | 13 | ```js 14 | import * as U from 'karet.util'; 15 | import * as R from 'ramda'; 16 | import * as L from 'partial.lenses'; 17 | 18 | const data = 19 | U.atom({ inputField: 'foo' }); 20 | 21 | const validationResult = 22 | data.map(L.get(L.pick({ 23 | inputField: ['inputField', { 24 | mandatory: [L.defaults(''), L.when(x => !x)], 25 | mustBeFoo: [L.defaults(''), L.when(x => x !== 'foo')] 26 | }] 27 | }))) 28 | 29 | const formIsValid = 30 | validationResult.map(R.or(R.isEmpty, R.isNil)); 31 | ``` 32 | 33 | The base idea is that we can use functions such as `L.when` to query data that we know is invalid. 34 | 35 | Because of how partial lenses work, the resulting view—or content for `validationResult`— 36 | will be `undefined` if we the data is valid, otherwise `validationResult` will contain a 37 | structure that tells us what checks fail, and with what kind of data. Based on this, we can build a simple DSL for specifying validation for the form. 38 | 39 | > Note: This is however a naïve implementation of validating data, and may not be fit for cases or forms with fields containing complex dependencies upon other form fields, but it should show an approach into performing validation through lenses. 40 | -------------------------------------------------------------------------------- /src/InputField.js: -------------------------------------------------------------------------------- 1 | import React from 'karet'; 2 | import * as L from 'partial.lenses'; 3 | import * as U from 'karet.util'; 4 | import * as P from 'prop-types'; 5 | 6 | // 7 | 8 | import { Msg } from './common/strings'; 9 | 10 | // 11 | 12 | const InputField = 13 | ({ label, inputId, value, validation, isValid = U.or(U.isNil, U.isEmpty)(validation) }) => 14 |
18 |
19 |
20 | 21 | 29 | {U.seq(validation, 30 | U.keys, 31 | U.map(key => 32 |
33 | {L.get([key, L.valueOr(`No text defined for validation \`${key}\``)], Msg)} 34 |
))} 35 |
36 |
37 |
; 38 | 39 | InputField.propTypes = { 40 | label: P.string, 41 | inputId: P.string, 42 | value: P.any, 43 | validation: P.any, 44 | isValid: P.any, 45 | }; 46 | 47 | // 48 | 49 | export default InputField; 50 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Form.js: -------------------------------------------------------------------------------- 1 | import * as React from 'karet'; 2 | import * as U from 'karet.util'; 3 | import * as L from 'partial.lenses'; 4 | 5 | // 6 | 7 | import InputField from './InputField'; 8 | 9 | import * as V from './common/validation'; 10 | import { poopJson } from './common/helpers'; 11 | 12 | // 13 | 14 | const validatorTemplate = 15 | V.fromTemplate({ 16 | inputField: { 17 | required: V.required, 18 | mustEqualAbc: V.mustMatch(/^abc$/), 19 | }, 20 | anotherInput: { 21 | required: V.required, 22 | }, 23 | }); 24 | 25 | const validation = L.pick(validatorTemplate); 26 | 27 | // 28 | 29 | const notEmptyOrNil = U.complement(U.or(U.isNil, U.isEmpty)); 30 | 31 | // 32 | 33 | const Form = () => { 34 | const formData = U.atom({ inputField: 'default', anotherInput: 'test' }); 35 | const formValidation = formData.map(L.get(validation)); 36 | const isInvalid = notEmptyOrNil(formValidation); 37 | 38 | return ( 39 |
40 | 41 | 45 | 46 | 50 | 51 |
52 | 59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 | Data 67 |
68 |
69 |
{formData.map(poopJson)}
70 |
71 |
72 | 73 |
76 |
77 | Validation result 78 |
79 |
80 | {U.ifte(isInvalid, 81 |
{formValidation.map(poopJson)}
, 82 |

Form is valid ♡

)} 83 |
84 |
85 |
86 | 87 |
88 | ); 89 | }; 90 | 91 | export default Form; 92 | --------------------------------------------------------------------------------