├── _config.yml
├── .coveralls.yml
├── demo
├── src
│ ├── lib
│ │ └── hoc-form.js
│ ├── index.css
│ ├── index.js
│ ├── App.js
│ ├── App.css
│ ├── Input.js
│ ├── logo.svg
│ ├── Form.js
│ └── registerServiceWorker.js
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── README.md
├── .gitignore
└── package.json
├── .gitignore
├── .npmignore
├── .travis.yml
├── index.js
├── jest.json
├── .babelrc
├── src
├── ContextedField.js
├── Field.js
└── HocForm.js
├── webpack.config.js
├── tests
├── helpers.js
├── HocForm.test.js
└── __snapshots__
│ └── HocForm.test.js.snap
├── CHANGELOG.md
├── LICENSE
├── package.json
└── README.md
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 |
--------------------------------------------------------------------------------
/demo/src/lib/hoc-form.js:
--------------------------------------------------------------------------------
1 | ../../../dist/index.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | dist/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo/
2 | node_modules/
3 | tests/
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pacdiv/hoc-form/HEAD/demo/public/favicon.ico
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import HocForm from './src/HocForm';
2 | import Field from './src/Field';
3 |
4 | export { Field };
5 | export default HocForm;
6 |
--------------------------------------------------------------------------------
/jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "coveragePathIgnorePatterns": [
3 | "/node_modules/",
4 | "/demo/",
5 | "/dist/",
6 | "tests/"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "transform-runtime"
4 | ],
5 | "presets": [
6 | "env",
7 | "react",
8 | "stage-0"
9 | ]
10 | }
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | html, body, #root, .App {
8 | height: 100%;
9 | width: 100%;
10 | }
11 |
--------------------------------------------------------------------------------
/src/ContextedField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function ContextedField({ component: CustomComponent, ...props }) {
4 | return ;
5 | }
6 |
7 | export default ContextedField;
8 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # Example of using hocForm
2 |
3 | First, install this demo by running `yarn` or `npm install`. Then, run the project through `yarn start` if you’re using **yarn** or `npm run start` if you prefer **npm**.
4 |
5 | Running those lines will open a tab in your web browser! 🚀
6 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/demo/.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 |
--------------------------------------------------------------------------------
/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0-beta",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.3.2",
7 | "react-dom": "^16.3.2",
8 | "react-scripts": "1.1.4"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test --env=jsdom",
14 | "eject": "react-scripts eject"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demo/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Form from './Form';
3 |
4 | import logo from './logo.svg';
5 | import './App.css';
6 |
7 | class App extends Component {
8 | render() {
9 | return (
10 |
11 |
15 | );
16 | }
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/demo/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 | width: 100%;
6 | }
7 |
8 | .App-logo {
9 | animation: App-logo-spin infinite 20s linear;
10 | height: 80px;
11 | }
12 |
13 | .App-header {
14 | background-color: #222;
15 | height: 150px;
16 | padding: 20px;
17 | color: white;
18 | }
19 |
20 | .App-title {
21 | font-size: 1.5em;
22 | }
23 |
24 | .App-intro {
25 | font-size: large;
26 | }
27 |
28 | @keyframes App-logo-spin {
29 | from { transform: rotate(0deg); }
30 | to { transform: rotate(360deg); }
31 | }
32 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
4 |
5 | module.exports = {
6 | mode: 'none',
7 | entry: ['./index.js'],
8 | module: {
9 | rules: [
10 | {
11 | test: /\.(js)$/,
12 | exclude: /node_modules/,
13 | use: ['babel-loader']
14 | }
15 | ]
16 | },
17 | plugins: [
18 | new UglifyJsPlugin({
19 | test: /\.js/
20 | }),
21 | new webpack.optimize.ModuleConcatenationPlugin(),
22 | new webpack.NoEmitOnErrorsPlugin()
23 | ],
24 | resolve: {
25 | extensions: ['*', '.js']
26 | },
27 | output: {
28 | filename: 'index.js',
29 | path: path.resolve(__dirname, 'dist'),
30 | libraryTarget: 'commonjs2'
31 | }
32 | };
--------------------------------------------------------------------------------
/tests/helpers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Field } from '../index';
4 | import Input from '../demo/src/Input';
5 |
6 | function singleField() {
7 | return (
8 |
17 | );
18 | }
19 |
20 | export function Form({ onSubmit }) {
21 | return (
22 |
28 | );
29 | }
30 |
31 | export function validate(values, props) {
32 | let errors = {};
33 |
34 | if (!values.login) {
35 | errors = {
36 | ...errors,
37 | login: 'Please enter a login',
38 | };
39 | }
40 |
41 | return Object.keys(errors).length
42 | ? Promise.reject(errors)
43 | : Promise.resolve();
44 | }
--------------------------------------------------------------------------------
/src/Field.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { FormContext } from './HocForm';
4 | import ContextedField from './ContextedField';
5 |
6 | function Field({ name, props, component }) {
7 | return (
8 |
9 | {({ state, setError, setValue, unsetError }) => (
10 | setValue(name, value),
16 | value: state.values[name] || undefined,
17 | ...(props.onBlur
18 | ? { onBlur: value => props.onBlur(value, state.values)
19 | .then(() => unsetError(name))
20 | .catch(error => setError(name, error))
21 | }
22 | : {}),
23 | }}
24 | meta={{ error: state.errors[name] || undefined }}
25 | />
26 | )}
27 |
28 | );
29 | }
30 |
31 | export default Field;
32 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.0.1-beta (July 30, 2018)
2 |
3 | ### Fixes
4 |
5 | * Fixed some links on the README.
6 |
7 | ## 2.0.0-beta (July 27, 2018)
8 |
9 | ### Features
10 |
11 | * `Field` allows an `onBlur` callback through its `props` component properties, which must return a Promise;
12 |
13 | ### Breaking changes
14 |
15 | * `hocForm.options.validate` must return a Promise;
16 | * `hocForm.options.asyncValidate` has been removed. All validation runs now through the `validate` option;
17 | * `hocForm.options.validateOnBlur` have been removed. Blur events are now running through `Field.props.onBlur`.
18 |
19 | ## 1.2.0 (June 5, 2018)
20 |
21 | ### Features
22 |
23 | * `hocForm` now provides its state to the wrapped form component through a property named `hocFormState`.
24 |
25 | ## 1.1.0 (June 5, 2018)
26 |
27 | ### Features
28 |
29 | * `hocForm` now accepts a new option: `validateOnBlur`, which allows to run sync validation on blur events. Defaults to `false`.
30 |
31 | ### Fixes
32 |
33 | * Resolved an issue on `multiple renderers concurrently rendering the same context provider`.
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 pacdiv
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hoc-form",
3 | "version": "2.0.1-beta",
4 | "description": "Concise form validation for React!",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "jest -c ./jest.json --coverage --coverageReporters=text-lcov | coveralls",
9 | "test:watch": "jest -c ./jest.json --watch --coverage"
10 | },
11 | "keywords": [],
12 | "author": "Loïc JEAN-LAMBERT (@pacdiv)",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "babel-core": "^6.26.0",
16 | "babel-jest": "^23.0.1",
17 | "babel-loader": "^7.1.4",
18 | "babel-plugin-transform-runtime": "^6.23.0",
19 | "babel-preset-env": "^1.6.1",
20 | "babel-preset-react": "^6.24.1",
21 | "babel-preset-stage-0": "^6.24.1",
22 | "compression-webpack-plugin": "^1.1.11",
23 | "coveralls": "^3.0.1",
24 | "enzyme": "^3.3.0",
25 | "enzyme-adapter-react-16": "^1.1.1",
26 | "jest": "^23.1.0",
27 | "react-dom": "^16.4.0",
28 | "uglifyjs-webpack-plugin": "^1.2.5",
29 | "webpack": "4.16.1",
30 | "webpack-cli": "^2.0.14"
31 | },
32 | "dependencies": {
33 | "react": "^16.3.1"
34 | },
35 | "peerDependencies": {
36 | "react": "^16.3.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo/src/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const style = {
4 | container: {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | position: 'relative',
8 | },
9 | error: {
10 | bottom: '1em',
11 | color: 'crimson',
12 | fontSize: '.8em',
13 | position: 'absolute',
14 | },
15 | field: {
16 | border: '1px solid lightgrey',
17 | borderColor: 'lightgrey',
18 | borderRadius: '.3em',
19 | fontSize: '1em',
20 | fontWeight: 300,
21 | height: '2em',
22 | marginBottom: '2em',
23 | padding: '.5em 1em',
24 | },
25 | invalidField: {
26 | borderColor: 'crimson',
27 | },
28 | label: {
29 | fontWeight: 300,
30 | marginBottom: '.5em',
31 | },
32 | };
33 |
34 | function Input({
35 | input = {},
36 | label = '',
37 | meta = {},
38 | placeholder = '',
39 | type = 'text',
40 | }) {
41 | return (
42 |
43 |
46 | input.onBlur && input.onBlur(e.target.value)}
51 | onChange={e => input.onChange(e.target.value)}
52 | style={{
53 | ...style.field,
54 | ...(meta.error ? style.invalidField : {}),
55 | }}
56 | />
57 | {meta.error &&
58 | {meta.error}
59 | }
60 |
61 | );
62 | }
63 |
64 | export default Input;
65 |
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | hoc-form | Demo
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tests/HocForm.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { mount, shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import hocForm, { Field } from '../index';
6 | import {
7 | Form,
8 | validate,
9 | } from './helpers';
10 |
11 | Enzyme.configure({ adapter: new Adapter() });
12 |
13 | const LoginForm = hocForm({
14 | initialValues: {
15 | login: 'hulk',
16 | },
17 | validate,
18 | })(Form);
19 |
20 | const LoginFormWithoutValidation = hocForm({})(Form);
21 |
22 | describe('HocForm()()', () => {
23 | const event = {
24 | preventDefault: value => value,
25 | };
26 | let spy;
27 | const onSubmit = jest.fn(values => values);
28 | function submitWithNewLogin(wrapper, form, event, login, times) {
29 | wrapper.setState({
30 | values: {
31 | login,
32 | },
33 | }, () => {
34 | Promise.resolve(form.simulate('submit', event))
35 | .then(() => expect(spy).toHaveBeenCalledTimes(times))
36 | .catch(() => ({}));
37 | });
38 | }
39 |
40 | afterEach(() => {
41 | spy.mockRestore();
42 | });
43 |
44 | it('shallows HocForm with validation', () => {
45 | const formWrapper = shallow(
46 | ,
49 | );
50 | spy = jest.spyOn(formWrapper.instance().props, 'onSubmit');
51 |
52 | formWrapper.instance().setError('login', 'Please enter a login');
53 | formWrapper.instance().unsetError('login');
54 | formWrapper.instance().setValue('login', '');
55 |
56 | const form = formWrapper.find(Form).first().dive();
57 | submitWithNewLogin(formWrapper, form, event, '', 1);
58 | submitWithNewLogin(formWrapper, form, event, 'starman', 2);
59 | expect(formWrapper.render()).toMatchSnapshot();
60 | });
61 |
62 | it('shallows HocForm without sync or async validation', () => {
63 | const onSubmit = jest.fn(values => values)
64 | const formWrapper = shallow(
65 | ,
68 | );
69 | const spy = jest.spyOn(formWrapper.instance().props, 'onSubmit');
70 |
71 | const form = formWrapper.find(Form).first().dive();
72 | formWrapper.instance().setValue('login', 'hulk');
73 | submitWithNewLogin(formWrapper, form, event, 'starman', 1);
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/demo/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/HocForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export const FormContext = React.createContext({});
4 |
5 | const HOC = hocProps => WrappedComponent => {
6 | return class Form extends Component {
7 | static removeObjectEntry = function(name, values) {
8 | return Object
9 | .entries(values)
10 | .reduce((acc, [key, value]) => ({
11 | ...acc,
12 | ...(key !== name ? { [key]: value } : {}),
13 | }), {});
14 | }
15 |
16 | constructor(props) {
17 | super(props);
18 | const initialValues = hocProps.initialValues || props.initialValues;
19 | this.state = {
20 | errors: {},
21 | isValid: false,
22 | values: {
23 | ...(initialValues ? { ...initialValues } : {})
24 | },
25 | };
26 |
27 | this.onSubmit = this.onSubmit.bind(this);
28 | this.setError = this.setError.bind(this);
29 | this.setValue = this.setValue.bind(this);
30 | this.unsetError = this.unsetError.bind(this);
31 |
32 | this.validate = hocProps.validate || this.props.validate;
33 | }
34 |
35 | onSubmit(e) {
36 | e && e.preventDefault();
37 | const { errors, values } = this.state;
38 |
39 | if (!this.validate) {
40 | this.props.onSubmit(values);
41 | return;
42 | }
43 |
44 | this.validate(values)
45 | .then(() => {
46 | this.setState({ errors: {}, isValid: true });
47 | this.props.onSubmit(values);
48 | })
49 | .catch(errors => this.setState({ errors, isValid: false }));
50 | }
51 |
52 | setError(key, error) {
53 | this.setState({
54 | errors: {
55 | ...this.state.errors,
56 | [key]: error,
57 | },
58 | });
59 | }
60 |
61 | setValue(key, value) {
62 | this.setState({
63 | values: {
64 | ...this.state.values,
65 | [key]: value,
66 | },
67 | });
68 | }
69 |
70 | unsetError(key) {
71 | const errors = Form.removeObjectEntry(key, this.state.errors);
72 | this.setState({ errors });
73 | }
74 |
75 | render() {
76 | return (
77 |
85 |
90 |
91 | );
92 | }
93 | }
94 | }
95 |
96 | export default HOC;
97 |
--------------------------------------------------------------------------------
/demo/src/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import hocForm, { Field } from './lib/hoc-form';
3 | import Input from './Input';
4 |
5 | const style = {
6 | field: {
7 | display: 'flex',
8 | flexDirection: 'column',
9 | },
10 | form: {
11 | margin: 'auto',
12 | width: '18em',
13 | },
14 | submitButton: {
15 | background: 'mediumseagreen',
16 | color: 'white',
17 | border: '0',
18 | borderRadius: '.3em',
19 | cursor: 'pointer',
20 | fontSize: '.9em',
21 | fontWeight: '300',
22 | height: '3.5em',
23 | width: '100%',
24 | },
25 | };
26 |
27 | const unavailableUsernames = [
28 | 'elonmusk',
29 | 'ironman',
30 | 'lukeskywalker',
31 | ];
32 |
33 | function validateLogin(value = '') {
34 | if (value.trim() === '') {
35 | return Promise.reject('Please enter an username');
36 | }
37 |
38 | return unavailableUsernames.includes(value)
39 | ? Promise.reject('This username is unavailable')
40 | : Promise.resolve()
41 | }
42 |
43 | function validatePassword(value = '') {
44 | if (value.trim() === '') {
45 | return Promise.reject('Please enter a password');
46 | } else if (value.trim().length < 6) {
47 | return Promise.reject('Password must contain 6 characters or more');
48 | } else {
49 | return Promise.resolve();
50 | }
51 | }
52 |
53 | function validatePasswordConfirmation(value = '', password = '') {
54 | if (value.trim() === '') {
55 | return Promise.reject('Please enter a password');
56 | } else if (value !== password) {
57 | return Promise.reject('Please enter the same password as below');
58 | } else {
59 | return Promise.resolve();
60 | }
61 | }
62 |
63 | export function Form({
64 | onSubmit,
65 | }) {
66 | return (
67 |
109 | );
110 | }
111 |
112 | export default hocForm({
113 | validate(values, props) {
114 | let errors = {};
115 | const errorCatcher = (key, callback, ...args) => (
116 | callback(values[key], args)
117 | .catch(error => ({ [key]: error }))
118 | );
119 |
120 | return Promise.all([
121 | errorCatcher('login', validateLogin),
122 | errorCatcher('pwd', validatePassword),
123 | errorCatcher('confirmPwd', validatePasswordConfirmation, values.pwd),
124 | ]).then((errors) => {
125 | if (!values.referrer) {
126 | errors = errors.concat({ referrer: 'Please give us something! 😇🙏' });
127 | }
128 |
129 | const results = errors.reduce((acc, item) => ({ ...acc, ...item }), {});
130 | return Object.keys(results).length ? Promise.reject(results) : Promise.resolve();
131 | });
132 | }
133 | })(Form);
134 |
--------------------------------------------------------------------------------
/demo/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 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # hoc-form •    
3 |
4 | Tired of writing custom form validation on every project you might work on? Let’s try `hocForm`, form validation on React has rarely been so simple! 🤗
5 |
6 | # Requirements
7 | hocForm needs at least react@16.3.1 and react-dom@16.3.1 to work.
8 |
9 | # Installation
10 | Using `npm`:
11 |
12 | ```
13 | npm install --save hoc-form
14 | ```
15 |
16 | `yarn add hoc-form` works fine too!
17 |
18 | # Usage
19 | ```javascript
20 | import React from 'react';
21 | import hocForm, { Field } from 'hoc-form';
22 |
23 | // First, we need a input component to render our text field
24 | function Input({
25 | input = {},
26 | label = '',
27 | meta = {},
28 | placeholder = '',
29 | type = 'text',
30 | }) {
31 | return (
32 |
33 |
36 | input.onBlur && input.onBlur(e.target.value)}
41 | onChange={e => input.onChange(e.target.value)}
42 | />
43 | {meta.error && {meta.error}}
44 |
45 | );
46 | }
47 |
48 | // Then, we need to create our form component and its helpers
49 | const unavailableUsernames = [
50 | 'elonmusk',
51 | 'ironman',
52 | 'lukeskywalker',
53 | ];
54 |
55 | function validateLogin(value = '') {
56 | if (value.trim() === '') {
57 | return Promise.reject('Please enter an username');
58 | }
59 |
60 | return unavailableUsernames.includes(value)
61 | ? Promise.reject('This username is unavailable')
62 | : Promise.resolve()
63 | }
64 |
65 | function validatePassword(value = '') {
66 | if (value.trim().length < 6) {
67 | return Promise.reject('Password must contain 6 characters or more');
68 | }
69 |
70 | return Promise.resolve();
71 | }
72 |
73 | function Form({ onSubmit }) {
74 | return (
75 |
96 | );
97 | }
98 |
99 | // Finally, an export of our wrapped form component
100 | // with a validation function
101 | export default hocForm({
102 | validate(values, props) {
103 | let errors = {};
104 | const errorCatcher = (key, callback, ...args) => (
105 | callback(values[key], args)
106 | .catch(error => ({ [key]: error }))
107 | );
108 |
109 | return Promise.all([
110 | errorCatcher('login', validateLogin),
111 | errorCatcher('pwd', validatePassword),
112 | ]).then((errors) => {
113 | const results = errors.reduce((acc, item) => ({ ...acc, ...item }), {});
114 | return Object.keys(results).length ? Promise.reject(results) : Promise.resolve();
115 | });
116 | }
117 | })(Form);
118 | ```
119 | Please check out the [complete demo](https://github.com/pacdiv/hoc-form/tree/master/demo)! 🚀
120 |
121 | # API
122 |
123 | ## `hocForm({ options })(MyFormComponent) => React.Component`
124 | Renders your `MyFormComponent` contexted with `hocForm`.
125 |
126 | Two arguments are required:
127 | - An `options` object to define the `validate` function and optional `initialValues`
128 | - A form `React.Component` to render.
129 |
130 | ### `options.validate(values, props) => Promise`
131 | Validates the form on submit.
132 |
133 | Arguments:
134 | 1. `values` (`Object`): An object containing all fields keys and their value.
135 | 2. `props` (`Object`): An object containing all props provided to `MyFormComponent`.
136 |
137 | Returns:
138 | - A promise:
139 | - On success, your must return `Promise.resolve()`
140 | - In case of failure, your must return `Promise.reject({})`. The object parameter must contain every field key with its error (type of string or else, depends on how your components used with `Field` are designed).
141 |
142 | Example, with errors as strings:
143 | ```javascript
144 | function validate(values, props) {
145 | let errors = {};
146 |
147 | if (values.username) {
148 | if (!props.isUsernameAvailable(values.username)) {
149 | errors = { ...errors, username: 'This username is unavailable' };
150 | }
151 | } else {
152 | errors = { ...errors, username: 'Please enter an username' };
153 | }
154 |
155 | if (!values.password) {
156 | errors = { ...errors, password: 'Please enter a password' };
157 | }
158 |
159 | return Object.keys(errors).length
160 | ? Promise.reject(errors)
161 | : Promise.resolve();
162 | }
163 | ```
164 |
165 | ### `options.initialValues: Object`
166 | Object containing all initial values following fields keys.
167 |
168 | Example:
169 | ```javascript
170 | initialValues: {
171 | country: 'United Kingdom',
172 | phone: '+44',
173 | }
174 | ```
175 |
176 | ### `MyFormComponent: React.Component`
177 | A `React.Component` rendering a form including some `hocForm.Field` items.
178 |
179 | Example:
180 | ```javascript
181 | function Form({ onSubmit }) {
182 | return (
183 |