├── .nvmrc
├── demo-app
├── .env
├── README.md
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── src
│ ├── App.test.js
│ ├── formValidator.js
│ ├── index.js
│ ├── index.css
│ ├── App.css
│ ├── App.js
│ ├── ReactoFormExample.js
│ ├── ReactoFormHookExample.js
│ ├── ReactoFormExampleMUI.js
│ ├── ReactoFormHookExampleMUI.js
│ ├── ReactoFormHookExampleUpdateMUI.js
│ └── serviceWorker.js
├── .gitignore
└── package.json
├── .eslintignore
├── .gitignore
├── lib
├── shared
│ ├── bracketsToDots.js
│ ├── propTypes.js
│ ├── bracketsToDots.test.js
│ ├── filterErrorsForNames.js
│ ├── getDateTimeValuesFromDate.js
│ ├── getDateFromDateTimeValues.js
│ └── recursivelyCloneElements.js
├── index.js
├── muiCheckboxOptions.js
├── muiOptions.js
├── FormList.test.js
├── __snapshots__
│ ├── Form.test.js.snap
│ └── FormList.test.js.snap
├── Form.test.js
├── useReactoForm.js
├── FormList.js
└── Form.js
├── jestSetup.js
├── LICENSE
├── CHANGELOG.md
├── babel.config.js
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.13.0
2 |
--------------------------------------------------------------------------------
/demo-app/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | cjs
2 | esm
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | cjs
3 | esm
4 | .DS_Store
5 | node_modules
6 | npm-debug.log
7 | .idea
8 |
--------------------------------------------------------------------------------
/demo-app/README.md:
--------------------------------------------------------------------------------
1 | A small app demoing ReactoForm for example code and testing purposes. Run it with `npm start`.
2 |
--------------------------------------------------------------------------------
/demo-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/longshotlabs/reacto-form/HEAD/demo-app/public/favicon.ico
--------------------------------------------------------------------------------
/lib/shared/bracketsToDots.js:
--------------------------------------------------------------------------------
1 | import toPath from "lodash/toPath";
2 |
3 | export default function bracketsToDots(pathString) {
4 | return toPath(pathString).join(".");
5 | }
6 |
--------------------------------------------------------------------------------
/demo-app/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-named-as-default,import/no-named-as-default-member */
2 | import Form from "./Form";
3 | import FormList from "./FormList";
4 | import muiOptions from "./muiOptions";
5 | import useReactoForm from "./useReactoForm";
6 |
7 | export { Form, FormList, muiOptions, useReactoForm };
8 |
--------------------------------------------------------------------------------
/lib/shared/propTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | const customPropTypes = {
4 | errors: PropTypes.arrayOf(
5 | PropTypes.shape({
6 | message: PropTypes.string.isRequired,
7 | name: PropTypes.string.isRequired,
8 | })
9 | ),
10 | };
11 |
12 | export default customPropTypes;
13 |
--------------------------------------------------------------------------------
/lib/muiCheckboxOptions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | nullValue: false,
3 | onChangeGetValue: (event) => event.target.checked || false,
4 | propNames: {
5 | errors: false,
6 | hasBeenValidated: false,
7 | isReadOnly: "disabled",
8 | onChanging: false,
9 | onSubmit: false,
10 | value: "checked",
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/lib/shared/bracketsToDots.test.js:
--------------------------------------------------------------------------------
1 | import bracketsToDots from "./bracketsToDots";
2 |
3 | test("bracketsToDots", () => {
4 | expect(bracketsToDots("a[0].b")).toBe("a.0.b");
5 | expect(bracketsToDots("a[0].b[2]")).toBe("a.0.b.2");
6 | expect(bracketsToDots("a[0].")).toBe("a.0.");
7 | expect(bracketsToDots("a.[0].")).toBe("a.0.");
8 | });
9 |
--------------------------------------------------------------------------------
/lib/muiOptions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | nullValue: "",
3 | onChangeGetValue: (event) => event.target.value,
4 | onChangingGetValue: (event) => event.target.value,
5 | propNames: {
6 | errors: false,
7 | hasBeenValidated: false,
8 | isReadOnly: "disabled",
9 | onChange: "onBlur",
10 | onChanging: "onChange",
11 | onSubmit: false,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/demo-app/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": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/jestSetup.js:
--------------------------------------------------------------------------------
1 | // This file is loaded from the jest.setupFiles config in package.json
2 |
3 | import "core-js/stable"; // eslint-disable-line import/no-extraneous-dependencies
4 | import Enzyme from "enzyme"; // eslint-disable-line import/no-extraneous-dependencies
5 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; // eslint-disable-line import/no-extraneous-dependencies
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
--------------------------------------------------------------------------------
/demo-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/demo-app/src/formValidator.js:
--------------------------------------------------------------------------------
1 | import SimpleSchema from "simpl-schema";
2 |
3 | const formSchema = new SimpleSchema({
4 | firstName: {
5 | type: String,
6 | min: 4
7 | },
8 | lastName: {
9 | type: String,
10 | min: 2
11 | },
12 | isMarried: {
13 | type: Boolean,
14 | optional: true
15 | }
16 | });
17 |
18 | const validator = formSchema.getFormValidator();
19 |
20 | export default validator;
21 |
--------------------------------------------------------------------------------
/demo-app/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 * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/demo-app/src/index.css:
--------------------------------------------------------------------------------
1 | html, body, div#root { height: 100%; }
2 |
3 | div#root {
4 | background-color: #444444;
5 | padding-top: 20px;
6 | padding-bottom: 20px;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
12 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
13 | sans-serif;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | code {
19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
20 | monospace;
21 | }
22 |
--------------------------------------------------------------------------------
/lib/shared/filterErrorsForNames.js:
--------------------------------------------------------------------------------
1 | import bracketsToDots from "./bracketsToDots";
2 |
3 | export default function filterErrorsForNames(errors, names, exact) {
4 | if (!Array.isArray(errors) || !Array.isArray(names)) return [];
5 |
6 | // Accept paths that may contain brackets or dots, making them all dots
7 | names = names.map((name) => bracketsToDots(name));
8 |
9 | return errors.filter((error) => {
10 | const errorName = bracketsToDots(error.name);
11 | return names.some((name) => {
12 | if (name === errorName) return true;
13 | return !exact && errorName.startsWith(`${name}.`);
14 | });
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/demo-app/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/demo-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.2",
7 | "react": "^16.8.6",
8 | "react-dom": "^16.8.6",
9 | "react-scripts": "^5.0.1",
10 | "reacto-form": "^1.5.0",
11 | "reacto-form-inputs": "^1.1.0",
12 | "simpl-schema": "^1.10.2"
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 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/shared/getDateTimeValuesFromDate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Convert a Date instance to an object with properties for the various pieces
3 | *
4 | * @param {Function} propsFunc - Takes in element and returns a props object
5 | * @param {Moment} moment - An instance of moment.tz
6 | * @param {String} timezone - A timezone identifier
7 | */
8 | export default function getDateTimeValuesFromDate(date, moment, timezone) {
9 | if (
10 | !date ||
11 | !(date instanceof Date) ||
12 | !moment ||
13 | typeof timezone !== "string"
14 | ) {
15 | // The defaults have to be "" rather than undefined or null so that React knows they are "controlled" inputs
16 | return {
17 | dayValue: "",
18 | monthValue: "",
19 | timeValue: "",
20 | yearValue: "",
21 | };
22 | }
23 | const m = moment(date).tz(timezone);
24 | return {
25 | dayValue: m.format("DD"),
26 | monthValue: m.format("M"),
27 | timeValue: m.format("HH:mm"),
28 | yearValue: m.format("YYYY"),
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-2020 Eric Dobbertin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # reacto-form CHANGELOG
2 |
3 | ## 1.5.1
4 |
5 | Update `@babel/runtime` and `lodash` dependencies to their latest versions
6 |
7 | ## 1.5.0
8 |
9 | Add support for MUI `Checkbox` along with examples of it in demo app.
10 |
11 | ## 1.4.2
12 |
13 | Fix form `value` prop data getting lost after a successful submission.
14 |
15 | ## 1.4.1
16 |
17 | Upgrade transitive dependencies to fix vulnerabilities
18 |
19 | ## 1.4.0
20 |
21 | Both `form.submit()` and `form.validate()` now reliably return a Promise that resolves with the updated errors array. This allows you to await form submission and easily check the errors after.
22 |
23 | ## 1.3.0
24 |
25 | - Update `Form` component to work with MUI in a way similar to `useReactoForm`
26 | - Allow settings keys to `false` in `propNames` to omit those input props
27 |
28 | ## 1.2.0
29 |
30 | - The `getInputProps` function returned by `useReactoForm` hook now returns a `hasBeenValidated` boolean prop.
31 | - `useReactoForm` hook now includes `resetValue` in returned object.
32 |
33 | ## 1.1.0
34 |
35 | Introduce React Hook: `useReactoForm`
36 |
37 | ## 1.0.0
38 |
39 | Various non-breaking changes
40 |
41 | ## 0.0.1
42 |
43 | Initial release
44 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // See https://babeljs.io/docs/en/config-files#root-babelconfigjs-files
2 | module.exports = function getBabelConfig(api) {
3 | const isTest = api.env("test");
4 |
5 | // Config for when running Jest tests
6 | if (isTest) {
7 | return {
8 | plugins: ["@babel/plugin-proposal-class-properties"],
9 | presets: ["@babel/env", "@babel/preset-react"],
10 | };
11 | }
12 |
13 | // We set this in the `build:modules` package.json script
14 | const esmodules = process.env.BABEL_MODULES === "1";
15 |
16 | const babelEnvOptions = {
17 | modules: esmodules ? false : "auto",
18 | // https://babeljs.io/docs/en/babel-preset-env#targets
19 | targets: {
20 | // 'browsers' target is ignored when 'esmodules' is true
21 | esmodules,
22 | },
23 | };
24 |
25 | return {
26 | ignore: ["**/*.test.js", "__snapshots__"],
27 | plugins: [
28 | "@babel/plugin-proposal-class-properties",
29 | [
30 | "@babel/plugin-transform-runtime",
31 | {
32 | useESModules: esmodules,
33 | },
34 | ],
35 | ],
36 | presets: [["@babel/env", babelEnvOptions], "@babel/preset-react"],
37 | sourceMaps: true,
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/lib/shared/getDateFromDateTimeValues.js:
--------------------------------------------------------------------------------
1 | const oneOrTwoDigits = /^\d{1,2}$/;
2 | const upToFourDigits = /^\d{1,4}$/;
3 | const timeFormat = /^\d{1,2}:\d{2}$/;
4 |
5 | /**
6 | * Convert a Date instance to an object with properties for the various pieces
7 | *
8 | * @param {Object} obj - Object with properties for date/time pieces
9 | * @param {Moment} moment - An instance of moment.tz
10 | * @param {String} timezone - A timezone identifier
11 | */
12 | export default function getDateFromDateTimeValues(obj, moment, timezone) {
13 | if (!moment || typeof timezone !== "string") return null;
14 |
15 | const { dayValue, monthValue, timeValue, yearValue } = obj;
16 |
17 | if (typeof dayValue !== "string" || !dayValue.match(oneOrTwoDigits))
18 | return null;
19 | if (
20 | typeof monthValue !== "string" ||
21 | !monthValue.match(oneOrTwoDigits) ||
22 | Number(monthValue) > 12 ||
23 | Number(monthValue) < 1
24 | )
25 | return null;
26 | if (typeof timeValue !== "string" || !timeValue.match(timeFormat))
27 | return null;
28 | if (typeof yearValue !== "string" || !yearValue.match(upToFourDigits))
29 | return null;
30 |
31 | const dateString = `${yearValue}-${monthValue}-${dayValue} ${timeValue}`;
32 | return moment.tz(dateString, "YYYY-M-DD H:mm", timezone).toDate();
33 | }
34 |
--------------------------------------------------------------------------------
/lib/shared/recursivelyCloneElements.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | /**
4 | * Recursively clone elements
5 | *
6 | * @param {React node} elements - React elements to traverse through. This can be an array or a single
7 | * element or null, so it's ok to pass the `children` prop directly.
8 | * @param {Function} [getProps] - A function that takes an element and returns props to be applied to the clone of that element
9 | * @param {Function} [shouldStopRecursing] - A function that takes an element and returns a truthy value if `recursivelyCloneElements`
10 | * should not be called for that element's children
11 | */
12 | export default function recursivelyCloneElements(
13 | elements,
14 | getProps,
15 | shouldStopRecursing
16 | ) {
17 | const newElements = React.Children.map(elements, (element) => {
18 | if (!element || typeof element === "string" || !element.props)
19 | return element;
20 |
21 | if (typeof getProps !== "function") getProps = () => ({});
22 | if (typeof shouldStopRecursing !== "function")
23 | shouldStopRecursing = () => false;
24 |
25 | const children = shouldStopRecursing(element)
26 | ? element.props.children
27 | : recursivelyCloneElements(element.props.children, getProps);
28 |
29 | return React.cloneElement(element, getProps(element), children);
30 | });
31 |
32 | return Array.isArray(newElements) && newElements.length === 1
33 | ? newElements[0]
34 | : newElements;
35 | }
36 |
--------------------------------------------------------------------------------
/demo-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/lib/FormList.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import renderer from "react-test-renderer";
3 | import { Input } from "reacto-form-inputs";
4 | import Form from "./Form";
5 | import FormList from "./FormList";
6 |
7 | test("has isFormList property set to true", () => {
8 | expect(FormList.isFormList).toBe(true);
9 | });
10 |
11 | test("snapshot Input child", () => {
12 | const component = renderer.create(
13 |
14 |
15 |
16 | );
17 |
18 | const tree = component.toJSON();
19 | expect(tree).toMatchSnapshot();
20 | });
21 |
22 | test("snapshot Form child", () => {
23 | const component = renderer.create(
24 |
28 |
31 |
32 | );
33 |
34 | const tree = component.toJSON();
35 | expect(tree).toMatchSnapshot();
36 | });
37 |
38 | test("snapshot Input child - fixed count", () => {
39 | const component = renderer.create(
40 |
46 |
47 |
48 | );
49 |
50 | const tree = component.toJSON();
51 | expect(tree).toMatchSnapshot();
52 | });
53 |
54 | test("snapshot Form child - fixed count", () => {
55 | const component = renderer.create(
56 |
62 |
65 |
66 | );
67 |
68 | const tree = component.toJSON();
69 | expect(tree).toMatchSnapshot();
70 | });
71 |
--------------------------------------------------------------------------------
/demo-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Paper from '@material-ui/core/Paper';
3 | import Tabs from '@material-ui/core/Tabs';
4 | import Tab from '@material-ui/core/Tab';
5 | import ReactoFormExample from "./ReactoFormExample";
6 | import ReactoFormExampleMUI from "./ReactoFormExampleMUI";
7 | import ReactoFormHookExample from "./ReactoFormHookExample";
8 | import ReactoFormHookExampleMUI from "./ReactoFormHookExampleMUI";
9 | import ReactoFormHookExampleUpdateMUI from "./ReactoFormHookExampleUpdateMUI";
10 | import './App.css';
11 |
12 | function App() {
13 | const [currentTab, setCurrentTab] = useState(0);
14 | const [updateFormData, setUpdateFormData] = useState({
15 | firstName: "Existing",
16 | lastName: "Name",
17 | isMarried: true
18 | });
19 |
20 | return (
21 |
22 | setCurrentTab(newValue)}
26 | textColor="primary"
27 | value={currentTab}
28 | style={{
29 | borderBottomColor: "#cccccc",
30 | borderBottomStyle: "solid",
31 | borderBottomWidth: 1
32 | }}
33 | >
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {currentTab === 0 && }
42 | {currentTab === 1 && }
43 | {currentTab === 2 && }
44 | {currentTab === 3 && }
45 | {currentTab === 4 && }
46 |
47 |
48 | );
49 | }
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/demo-app/src/ReactoFormExample.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import { Form } from "reacto-form";
5 | import { ErrorsBlock, Field, Input } from "reacto-form-inputs";
6 | import validator from "./formValidator";
7 |
8 | const useStyles = makeStyles(theme => ({
9 | button: {
10 | marginTop: theme.spacing(1),
11 | },
12 | errors: {
13 | color: theme.palette.error.main
14 | },
15 | field: {
16 | marginBottom: theme.spacing(2),
17 | marginTop: theme.spacing(2),
18 | },
19 | input: {
20 | display: "block",
21 | lineHeight: 2,
22 | marginTop: theme.spacing(0.5),
23 | width: "100%"
24 | },
25 | root: {
26 | marginLeft: "auto",
27 | marginRight: "auto",
28 | width: "50%",
29 | }
30 | }));
31 |
32 | async function mySubmissionFunction(...args) {
33 | console.log("Submit", ...args);
34 | }
35 |
36 | export default function ReactoFormExample() {
37 | const classes = useStyles();
38 | const formRef = useRef(null);
39 |
40 | return (
41 |
42 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/demo-app/src/ReactoFormHookExample.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import { ErrorsBlock, Field, Input } from "reacto-form-inputs";
5 | import useReactoForm from "reacto-form/esm/useReactoForm";
6 | import validator from "./formValidator";
7 |
8 | const useStyles = makeStyles(theme => ({
9 | button: {
10 | marginTop: theme.spacing(1),
11 | },
12 | errors: {
13 | color: theme.palette.error.main
14 | },
15 | field: {
16 | marginBottom: theme.spacing(2),
17 | marginTop: theme.spacing(2),
18 | },
19 | input: {
20 | display: "block",
21 | lineHeight: 2,
22 | marginTop: theme.spacing(0.5),
23 | width: "100%"
24 | },
25 | root: {
26 | marginLeft: "auto",
27 | marginRight: "auto",
28 | width: "50%",
29 | }
30 | }));
31 |
32 | async function mySubmissionFunction(...args) {
33 | console.log("Submit", ...args);
34 | }
35 |
36 | export default function ReactoFormHookExample() {
37 | const classes = useStyles();
38 |
39 | const {
40 | getErrors,
41 | getInputProps,
42 | submitForm
43 | } = useReactoForm({
44 | logErrorsOnSubmit: true,
45 | onChange: (val) => { console.log("onChangeForm", val); },
46 | onChanging: (val) => { console.log("onChangingForm", val); },
47 | onSubmit: mySubmissionFunction,
48 | validator,
49 | });
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/demo-app/src/ReactoFormExampleMUI.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import TextField from '@material-ui/core/TextField';
5 | import Form from "reacto-form/esm/Form";
6 | import muiOptions from "reacto-form/esm/muiOptions";
7 | import validator from "./formValidator";
8 |
9 | TextField.isFormInput = true;
10 |
11 | const useStyles = makeStyles(theme => ({
12 | button: {
13 | marginTop: theme.spacing(1),
14 | },
15 | errors: {
16 | color: theme.palette.error.main
17 | },
18 | field: {
19 | marginBottom: theme.spacing(2),
20 | marginTop: theme.spacing(2),
21 | },
22 | input: {
23 | display: "block",
24 | lineHeight: 2,
25 | marginTop: theme.spacing(0.5),
26 | width: "100%"
27 | },
28 | root: {
29 | marginLeft: "auto",
30 | marginRight: "auto",
31 | width: "50%",
32 | }
33 | }));
34 |
35 | export default function ReactoFormExample() {
36 | const classes = useStyles();
37 | const formRef = useRef(null);
38 |
39 | return (
40 |
41 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/demo-app/src/ReactoFormHookExampleMUI.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import Checkbox from '@material-ui/core/Checkbox';
5 | import FormControlLabel from '@material-ui/core/FormControlLabel';
6 | import FormGroup from '@material-ui/core/FormGroup';
7 | import TextField from '@material-ui/core/TextField';
8 | import muiOptions from "reacto-form/esm/muiOptions";
9 | import muiCheckboxOptions from "reacto-form/esm/muiCheckboxOptions";
10 | import useReactoForm from "reacto-form/esm/useReactoForm";
11 | import validator from "./formValidator";
12 |
13 | const useStyles = makeStyles(theme => ({
14 | button: {
15 | marginTop: theme.spacing(1),
16 | },
17 | root: {
18 | marginLeft: "auto",
19 | marginRight: "auto",
20 | width: "50%",
21 | }
22 | }));
23 |
24 | async function mySubmissionFunction(...args) {
25 | console.log("Submit", ...args);
26 | }
27 |
28 | export default function ReactoFormHookExampleMUI() {
29 | const classes = useStyles();
30 |
31 | const {
32 | getFirstErrorMessage,
33 | getInputProps,
34 | hasErrors,
35 | submitForm
36 | } = useReactoForm({
37 | logErrorsOnSubmit: true,
38 | onChange: (val) => { console.log("onChangeForm", val); },
39 | onChanging: (val) => { console.log("onChangingForm", val); },
40 | onSubmit: mySubmissionFunction,
41 | validator,
42 | isReadOnly: true
43 | });
44 |
45 | return (
46 |
47 | {
53 | if (event.key === "Enter") submitForm();
54 | }}
55 | {...getInputProps("firstName", muiOptions)}
56 | />
57 | {
63 | if (event.key === "Enter") submitForm();
64 | }}
65 | {...getInputProps("lastName", muiOptions)}
66 | />
67 |
68 |
71 | }
72 | label="Are you married?"
73 | {...getInputProps("isMarried", muiCheckboxOptions)}
74 | />
75 |
76 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/demo-app/src/ReactoFormHookExampleUpdateMUI.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import Checkbox from '@material-ui/core/Checkbox';
5 | import FormControlLabel from '@material-ui/core/FormControlLabel';
6 | import FormGroup from '@material-ui/core/FormGroup';
7 | import TextField from '@material-ui/core/TextField';
8 | import muiOptions from "reacto-form/esm/muiOptions";
9 | import muiCheckboxOptions from "reacto-form/esm/muiCheckboxOptions";
10 | import useReactoForm from "reacto-form/esm/useReactoForm";
11 | import validator from "./formValidator";
12 |
13 | const useStyles = makeStyles(theme => ({
14 | button: {
15 | marginTop: theme.spacing(1),
16 | },
17 | root: {
18 | marginLeft: "auto",
19 | marginRight: "auto",
20 | width: "50%",
21 | }
22 | }));
23 |
24 | export default function ReactoFormHookExampleUpdateMUI(props) {
25 | const {
26 | setUpdateFormData,
27 | updateFormData
28 | } = props;
29 |
30 | const classes = useStyles();
31 |
32 | const {
33 | getFirstErrorMessage,
34 | getInputProps,
35 | hasErrors,
36 | submitForm
37 | } = useReactoForm({
38 | logErrorsOnSubmit: true,
39 | onChange: (val) => { console.log("onChangeForm", val); },
40 | onChanging: (val) => { console.log("onChangingForm", val); },
41 | onSubmit(formData) {
42 | setUpdateFormData(formData);
43 | },
44 | validator,
45 | value: updateFormData,
46 | });
47 |
48 | return (
49 |
50 | {
56 | if (event.key === "Enter") submitForm();
57 | }}
58 | {...getInputProps("firstName", muiOptions)}
59 | />
60 | {
66 | if (event.key === "Enter") submitForm();
67 | }}
68 | {...getInputProps("lastName", muiOptions)}
69 | />
70 |
71 |
74 | }
75 | label="Are you married?"
76 | {...getInputProps("isMarried", muiCheckboxOptions)}
77 | />
78 |
79 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/lib/__snapshots__/Form.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`form snapshot 1 1`] = `
4 |
8 |
12 |
17 |
18 | Text above
19 |
20 |
30 |
31 | Text below
32 |
33 |
34 |
35 | `;
36 |
37 | exports[`form snapshot 2 - complex nesting 1`] = `
38 |
42 |
46 |
51 |
52 | Text above
53 |
54 |
64 |
65 | Text below
66 |
67 |
68 |
71 |
72 | Inner Form
73 |
74 |
78 |
82 |
87 |
88 | Text above
89 |
90 |
100 |
101 | Text below
102 |
103 |
104 |
108 |
113 |
114 | Text above
115 |
116 |
126 |
127 | Text below
128 |
129 |
130 |
131 |
132 |
133 | `;
134 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reacto-form",
3 | "version": "1.5.1",
4 | "description": "A reference implementation of the Composable Form Specification for React (see https://composableforms.netlify.app)",
5 | "author": "Long Shot Labs (https://www.longshotlabs.co/)",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/longshotlabs/reacto-form.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/longshotlabs/reacto-form/issues"
13 | },
14 | "homepage": "https://github.com/longshotlabs/reacto-form",
15 | "files": [
16 | "CHANGELOG.md",
17 | "cjs",
18 | "esm",
19 | "LICENSE",
20 | "README.md"
21 | ],
22 | "browserslist": [
23 | "last 2 version",
24 | "> 1%",
25 | "maintained node versions",
26 | "not dead"
27 | ],
28 | "eslintConfig": {
29 | "extends": [
30 | "airbnb-base",
31 | "plugin:jsx-a11y/recommended",
32 | "plugin:react/recommended",
33 | "prettier"
34 | ],
35 | "parser": "@babel/eslint-parser",
36 | "env": {
37 | "browser": true,
38 | "jest": true
39 | },
40 | "settings": {
41 | "react": {
42 | "version": "detect"
43 | }
44 | },
45 | "rules": {
46 | "arrow-body-style": 0,
47 | "consistent-return": 0,
48 | "max-len": 0,
49 | "no-param-reassign": 0,
50 | "no-underscore-dangle": 0,
51 | "no-use-before-define": [
52 | 2,
53 | "nofunc"
54 | ],
55 | "no-unused-expressions": 0,
56 | "no-console": 0,
57 | "space-before-function-paren": 0,
58 | "react/prefer-stateless-function": 0,
59 | "react/destructuring-assignment": 0,
60 | "react/no-multi-comp": 0,
61 | "react/jsx-filename-extension": 0,
62 | "jsx-a11y/href-no-hash": "off",
63 | "jsx-a11y/anchor-is-valid": [
64 | "warn",
65 | {
66 | "aspects": [
67 | "invalidHref"
68 | ]
69 | }
70 | ]
71 | }
72 | },
73 | "jest": {
74 | "setupFilesAfterEnv": [
75 | "raf/polyfill",
76 | "/jestSetup.js"
77 | ],
78 | "testEnvironment": "jsdom"
79 | },
80 | "main": "./cjs/index.js",
81 | "module": "./esm/index.js",
82 | "scripts": {
83 | "build": "npm run build:modules && npm run build:common",
84 | "build:common": "rm -rf cjs/** && BABEL_ENV=production babel lib --out-dir cjs",
85 | "build:modules": "rm -rf esm/** && BABEL_ENV=production BABEL_MODULES=1 babel lib --out-dir esm",
86 | "lint": "BABEL_ENV=test eslint ./lib",
87 | "prepublishOnly": "npm run lint && npm test && npm run build",
88 | "test": "jest ./lib"
89 | },
90 | "peerDependencies": {
91 | "react": ">=16.8 || >=17"
92 | },
93 | "dependencies": {
94 | "@babel/runtime": "^7.27.0",
95 | "clone": "^2.1.2",
96 | "lodash": "^4.17.20",
97 | "prop-types": "^15.7.2"
98 | },
99 | "devDependencies": {
100 | "@babel/cli": "^7.12.8",
101 | "@babel/core": "^7.16.0",
102 | "@babel/eslint-parser": "^7.16.3",
103 | "@babel/plugin-proposal-class-properties": "^7.16.0",
104 | "@babel/plugin-transform-runtime": "^7.16.0",
105 | "@babel/preset-env": "^7.16.0",
106 | "@babel/preset-react": "^7.16.0",
107 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
108 | "composable-form-tests": "^1.1.0",
109 | "core-js": "^3.19.1",
110 | "enzyme": "^3.11.0",
111 | "eslint": "^8.2.0",
112 | "eslint-config-airbnb": "^19.0.0",
113 | "eslint-config-prettier": "^8.3.0",
114 | "eslint-plugin-import": "^2.25.3",
115 | "eslint-plugin-jsx-a11y": "^6.5.1",
116 | "eslint-plugin-react": "^7.27.0",
117 | "jest": "^27.3.1",
118 | "jsdom": "^18.1.0",
119 | "raf": "^3.4.1",
120 | "react": "^17.0.2",
121 | "react-dom": "^17.0.2",
122 | "react-test-renderer": "^17.0.2",
123 | "reacto-form-inputs": "^1.2.0"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/lib/Form.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import renderer from "react-test-renderer";
4 | import { Field, Input } from "reacto-form-inputs";
5 | import Form from "./Form";
6 |
7 | test("has isForm property set to true", () => {
8 | expect(Form.isForm).toBe(true);
9 | });
10 |
11 | test("form snapshot 1", () => {
12 | const component = renderer.create(
13 |
20 | );
21 |
22 | const tree = component.toJSON();
23 | expect(tree).toMatchSnapshot();
24 | });
25 |
26 | test("form snapshot 2 - complex nesting", () => {
27 | const component = renderer.create(
28 |
50 | );
51 |
52 | const tree = component.toJSON();
53 | expect(tree).toMatchSnapshot();
54 | });
55 |
56 | test("sets value prop on child input for simple name", () => {
57 | const wrapper = mount(
58 |
61 | );
62 |
63 | expect(wrapper.find("input").prop("value")).toBe("BAR");
64 | });
65 |
66 | test("sets value prop on child input for path name", () => {
67 | const wrapper = mount(
68 |
71 | );
72 |
73 | expect(wrapper.find("input").prop("value")).toBe("VAL");
74 | });
75 |
76 | test("keeps child input value prop if present", () => {
77 | const wrapper = mount(
78 |
81 | );
82 |
83 | expect(wrapper.find("input").prop("value")).toBe("DEFAULT");
84 | });
85 |
86 | test("sets value prop on nested descendant input", () => {
87 | const wrapper = mount(
88 |
97 | );
98 |
99 | expect(wrapper.find("input").prop("value")).toBe("BAR");
100 | });
101 |
102 | test("simple form value is updated after user enters input", () => {
103 | const wrapper = mount(
104 |
107 | );
108 |
109 | expect(wrapper.instance().getValue()).toEqual({ foo: "BAR" });
110 |
111 | wrapper.find("input").simulate("change", { target: { value: "NEW" } });
112 |
113 | expect(wrapper.instance().getValue()).toEqual({ foo: "NEW" });
114 | });
115 |
116 | test("path form value is updated after user enters input", () => {
117 | const wrapper = mount(
118 |
121 | );
122 |
123 | expect(wrapper.instance().getValue()).toEqual({ foo: [{ a: "VAL" }] });
124 |
125 | wrapper.find("input").simulate("change", { target: { value: "NEW" } });
126 |
127 | expect(wrapper.instance().getValue()).toEqual({ foo: [{ a: "NEW" }] });
128 | });
129 |
130 | test("blurring input triggers form onChanging and onChange", () => {
131 | const onChange = jest.fn().mockName("onChange");
132 | const onChanging = jest.fn().mockName("onChanging");
133 |
134 | const wrapper = mount(
135 |
138 | );
139 |
140 | expect(onChange).toHaveBeenCalledTimes(1);
141 | expect(onChanging).toHaveBeenCalledTimes(1);
142 |
143 | expect(onChange.mock.calls[0][0]).toEqual({ foo: null });
144 | expect(onChanging.mock.calls[0][0]).toEqual({ foo: null });
145 |
146 | onChange.mockClear();
147 | onChanging.mockClear();
148 |
149 | wrapper.find("input").simulate("blur", { target: { value: "NEW" } });
150 |
151 | expect(onChange).toHaveBeenCalledTimes(1);
152 | expect(onChanging).toHaveBeenCalledTimes(1);
153 |
154 | expect(onChange.mock.calls[0][0]).toEqual({ foo: "NEW" });
155 | expect(onChanging.mock.calls[0][0]).toEqual({ foo: "NEW" });
156 | });
157 |
--------------------------------------------------------------------------------
/demo-app/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/lib/useReactoForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import clone from "clone";
3 | import get from "lodash/get";
4 | import isEqual from "lodash/isEqual";
5 | import set from "lodash/set";
6 | import bracketsToDots from "./shared/bracketsToDots";
7 | import filterErrorsForNames from "./shared/filterErrorsForNames";
8 |
9 | /**
10 | * @summary To ensure we do not mutate objects passed in, we'll do a deep clone.
11 | * @param {Any} value Any value
12 | * @return {Object} Cloned value
13 | */
14 | function cloneValue(value) {
15 | return value ? clone(value) : {};
16 | }
17 |
18 | /**
19 | * @summary Main ReactoForm hook
20 | */
21 | export default function useReactoForm(props) {
22 | const {
23 | hasBeenValidated: hasBeenValidatedProp,
24 | logErrorsOnSubmit = false,
25 | onChange = () => {},
26 | onChanging = () => {},
27 | onSubmit = () => {},
28 | revalidateOn = "changing",
29 | shouldSubmitWhenInvalid = false,
30 | validateOn = "submit",
31 | validator,
32 | value: valueProp,
33 | } = props;
34 |
35 | const [errors, setErrors] = useState([]);
36 | const [hasBeenValidated, setHasBeenValidated] = useState(
37 | hasBeenValidatedProp || false
38 | );
39 | const [forceReset, setForceReset] = useState(0);
40 | const [formData, setFormData] = useState({});
41 |
42 | // isReadOnly can be passed as a function, which is then called with
43 | // the current form data to determine whether it should be read only.
44 | let { isReadOnly } = props;
45 | if (typeof isReadOnly === "function") {
46 | isReadOnly = !!isReadOnly(formData);
47 | }
48 |
49 | /**
50 | * @summary Set field value in state using lodash set
51 | * @return {Object} A copy of formData, mutated
52 | */
53 | function setFieldValueInFormData(fieldPath, fieldValue) {
54 | const formDataCopy = clone(formData);
55 | set(formDataCopy, fieldPath, fieldValue === undefined ? null : fieldValue);
56 | setFormData(formDataCopy);
57 | return formDataCopy;
58 | }
59 |
60 | // Whenever a changed value prop comes in, we reset state to that, thus becoming clean.
61 | useEffect(() => {
62 | setErrors([]);
63 | setHasBeenValidated(false);
64 | setFormData(cloneValue(valueProp));
65 | }, [valueProp, forceReset]);
66 |
67 | // Let props override the `hasBeenValidated` state
68 | useEffect(() => {
69 | if (
70 | typeof hasBeenValidatedProp === "boolean" &&
71 | hasBeenValidatedProp !== hasBeenValidated
72 | ) {
73 | setHasBeenValidated(hasBeenValidatedProp);
74 | }
75 | }, [hasBeenValidatedProp]);
76 |
77 | /**
78 | * @summary Validate the form
79 | * @return {Promise