Browse Source

Refactored the form demo component structure.

Snow 8 years ago
parent
commit
5a16687ed4
3 changed files with 266 additions and 238 deletions
  1. 237 0
      src/components/Form/Form.js
  2. 27 236
      src/components/Form/index.js
  3. 2 2
      src/routes.js

+ 237 - 0
src/components/Form/Form.js

@@ -0,0 +1,237 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Motion, spring } from 'react-motion';
+
+import './style.css';
+
+export default class Form extends React.Component {
+  static placeHolder = {
+    name: { message: 'Enter your name', color: 'grey', warnings: [] },
+    password: { message: '12345678', color: 'grey', warnings: [] },
+    password2: { message: '12345678', color: 'grey', warnings: [] },
+    comments: { message: 'Enter your comment', color: 'grey', warnings: [] }
+  };
+
+  state = {
+    ...this.constructor.placeHolder
+  };
+
+  validateRequired = name => {
+    const warning = (
+      <Motion defaultStyle={{ x: 0 }} style={{ x: spring(1) }} key="isRequired">
+        {({ x }) =>
+          <span className="warning" style={{ opacity: x }}>
+            This field is required.
+          </span>}
+      </Motion>
+    );
+    const color = this.state[name].color;
+    return this.state[name].message.length > 0 && color !== 'grey'
+      ? null
+      : warning;
+  };
+
+  validateLength = (name, length) => {
+    const warning = (
+      <Motion
+        defaultStyle={{ x: 0 }}
+        style={{ x: spring(1) }}
+        key="isMinLength"
+      >
+        {({ x }) =>
+          <span className="warning" style={{ opacity: x }}>
+            This field should at least have {length} characters.
+          </span>}
+      </Motion>
+    );
+    const color = this.state[name].color;
+    return this.state[name].message.length >= length && color !== 'grey'
+      ? null
+      : warning;
+  };
+
+  validatePasswordMatch = (name1, name2) => {
+    const password1 = this.state[name1].message;
+    const password2 = this.state[name2].message;
+    const warning = (
+      <span className="warning" key="isPwdMatch">
+        Password don't match.
+      </span>
+    );
+    const color1 = this.state[name1].color;
+    return password1 === password2 && color1 !== 'grey' ? null : warning;
+  };
+
+  handleSubmit = event => {
+    // force validate all fields
+    const keys = Object.keys(this.state);
+
+    keys.forEach(name => {
+      const warnings = this.validateItem(name);
+      if (name === 'password' || name === 'password2') {
+        // match two password input behavior
+        this.setState({
+          password: Object.assign({}, this.state.password, {
+            warnings
+          }),
+          password2: Object.assign({}, this.state.password2, {
+            warnings
+          })
+        });
+      } else {
+        this.setState({
+          [name]: Object.assign({}, this.state[name], {
+            warnings
+          })
+        });
+      }
+    });
+
+    const inValidFields = keys.filter(
+      name => this.state[name].warnings.length !== 0
+    );
+    if (inValidFields.length !== 0 || this.state.password.color === 'grey') {
+      event.preventDefault();
+    }
+  };
+
+  handleFocus = event => {
+    const target = event.target;
+    const name = target.name;
+    const color = this.state[name].color;
+    if (color === 'grey') {
+      this.setState({
+        [name]: Object.assign({}, this.state[name], {
+          message: '',
+          color: 'black'
+        })
+      });
+    }
+  };
+
+  wrapWarnings = (...theArgs) => {
+    const validWarnings = theArgs.filter(arg => arg !== null);
+    return validWarnings;
+  };
+
+  validateItem = name => {
+    let warnings;
+    switch (name) {
+      case 'name':
+        warnings = this.wrapWarnings(
+          this.validateLength(name, 5),
+          this.validateRequired(name)
+        );
+        break;
+      case 'password':
+      case 'password2':
+        warnings = this.wrapWarnings(
+          this.validatePasswordMatch('password', 'password2'),
+          this.validateRequired(name)
+        );
+        break;
+      case 'comments':
+        warnings = this.wrapWarnings(
+          this.validateLength(name, 20),
+          this.validateRequired(name)
+        );
+        break;
+      default:
+        break;
+    }
+    return warnings;
+  };
+
+  handleBlur = event => {
+    const target = event.target;
+    const name = target.name;
+    const message = this.constructor.placeHolder[name].message;
+    const warnings = this.validateItem(name);
+    if (target.value.length === 0) {
+      this.setState({
+        [name]: Object.assign({}, this.state[name], {
+          message: message,
+          color: 'grey',
+          warnings
+        })
+      });
+    } else if (name === 'password' || name === 'password2') {
+      // match two password inputs behavior
+      this.setState({
+        password: {
+          ...this.state.password,
+          warnings
+        },
+        password2: {
+          ...this.state.password2,
+          warnings
+        }
+      });
+    } else {
+      this.setState({
+        [name]: {
+          ...this.state[name],
+          warnings
+        }
+      });
+    }
+  };
+
+  handleChange = event => {
+    const target = event.target;
+    const value = target.type === 'checkbox' ? target.checked : target.value;
+    const name = target.name;
+    this.setState({
+      [name]: Object.assign({}, this.state[name], { message: value })
+    });
+  };
+
+  renderInput = (type, name) => {
+    const borderColor = this.state[name].warnings.length ? 'red' : null;
+    const props = {
+      type: type,
+      name: name,
+      id: name,
+      value: this.state[name].message,
+      onFocus: this.handleFocus,
+      onChange: this.handleChange,
+      style: { color: this.state[name].color, borderColor: borderColor },
+      onBlur: this.handleBlur,
+      className: name
+    };
+
+    if (type === 'textarea') {
+      return <textarea {...props} />;
+    }
+    return <input {...props} />;
+  };
+
+  render() {
+    return (
+      <form onSubmit={this.handleSubmit} action={<Link to="/tabss" />}>
+        <label htmlFor="name">
+          Name:
+        </label>
+        {this.renderInput('text', 'name')}
+        {this.state.name.warnings}
+        <label htmlFor="password">
+          Password:
+        </label>
+        {this.renderInput('password', 'password')}
+        {this.state.password.warnings}
+        <label htmlFor="password2">
+          Password:
+        </label>
+        {this.renderInput('password', 'password2')}
+        {this.state.password2.warnings}
+
+        <label htmlFor="comments">
+          Comments:
+        </label>
+        {this.renderInput('textarea', 'comments')}
+        {this.state.comments.warnings}
+        <input type="submit" value="Submit" />
+      </form>
+    );
+  }
+}

+ 27 - 236
src/components/Form/index.js

@@ -1,237 +1,28 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import { Link } from 'react-router-dom';
-import { Motion, spring } from 'react-motion';
-import './style.css';
-
-export default class Form extends React.Component {
-  static placeHolder = {
-    name: { message: 'Enter your name', color: 'grey', warnings: [] },
-    password: { message: '12345678', color: 'grey', warnings: [] },
-    password2: { message: '12345678', color: 'grey', warnings: [] },
-    comments: { message: 'Enter your comment', color: 'grey', warnings: [] }
-  };
-
-  state = {
-    ...this.constructor.placeHolder
-  };
-
-  validateRequired = name => {
-    const warning = (
-      <Motion defaultStyle={{ x: 0 }} style={{ x: spring(1) }} key="isRequired">
-        {({ x }) =>
-          <span className="warning" style={{ opacity: x }}>
-            This field is required.
-          </span>}
-      </Motion>
-    );
-    const color = this.state[name].color;
-    return this.state[name].message.length > 0 && color !== 'grey'
-      ? null
-      : warning;
-  };
-
-  validateLength = (name, length) => {
-    const warning = (
-      <Motion
-        defaultStyle={{ x: 0 }}
-        style={{ x: spring(1) }}
-        key="isMinLength"
-      >
-        {({ x }) =>
-          <span className="warning" style={{ opacity: x }}>
-            This field should at least have {length} characters.
-          </span>}
-      </Motion>
-    );
-    const color = this.state[name].color;
-    return this.state[name].message.length >= length && color !== 'grey'
-      ? null
-      : warning;
-  };
-
-  validatePasswordMatch = (name1, name2) => {
-    const password1 = this.state[name1].message;
-    const password2 = this.state[name2].message;
-    const warning = (
-      <span className="warning" key="isPwdMatch">
-        Password don't match.
-      </span>
-    );
-    const color1 = this.state[name1].color;
-    return password1 === password2 && color1 !== 'grey' ? null : warning;
-  };
-
-  handleSubmit = event => {
-    // force validate all fields
-    const keys = Object.keys(this.state);
-
-    keys.forEach(name => {
-      const warnings = this.validateItem(name);
-      if (name === 'password' || name === 'password2') {
-        // match two password input behavior
-        this.setState({
-          password: Object.assign({}, this.state.password, {
-            warnings
-          }),
-          password2: Object.assign({}, this.state.password2, {
-            warnings
-          })
-        });
-      } else {
-        this.setState({
-          [name]: Object.assign({}, this.state[name], {
-            warnings
-          })
-        });
-      }
-    });
-
-    const inValidFields = keys.filter(
-      name => this.state[name].warnings.length !== 0
-    );
-    if (inValidFields.length !== 0 || this.state.password.color === 'grey') {
-      event.preventDefault();
-    }
-  };
-
-  handleFocus = event => {
-    const target = event.target;
-    const name = target.name;
-    const color = this.state[name].color;
-    if (color === 'grey') {
-      this.setState({
-        [name]: Object.assign({}, this.state[name], {
-          message: '',
-          color: 'black'
-        })
-      });
-    }
-  };
-
-  wrapWarnings = (...theArgs) => {
-    const validWarnings = theArgs.filter(arg => arg !== null);
-    return validWarnings;
-  };
-
-  validateItem = name => {
-    let warnings;
-    switch (name) {
-      case 'name':
-        warnings = this.wrapWarnings(
-          this.validateLength(name, 5),
-          this.validateRequired(name)
-        );
-        break;
-      case 'password':
-      case 'password2':
-        warnings = this.wrapWarnings(
-          this.validatePasswordMatch('password', 'password2'),
-          this.validateRequired(name)
-        );
-        break;
-      case 'comments':
-        warnings = this.wrapWarnings(
-          this.validateLength(name, 20),
-          this.validateRequired(name)
-        );
-        break;
-      default:
-        break;
-    }
-    return warnings;
-  };
-
-  handleBlur = event => {
-    const target = event.target;
-    const name = target.name;
-    const message = this.constructor.placeHolder[name].message;
-    const warnings = this.validateItem(name);
-    if (target.value.length === 0) {
-      this.setState({
-        [name]: Object.assign({}, this.state[name], {
-          message: message,
-          color: 'grey',
-          warnings
-        })
-      });
-    } else if (name === 'password' || name === 'password2') {
-      // match two password inputs behavior
-      this.setState({
-        password: {
-          ...this.state.password,
-          warnings
-        },
-        password2: {
-          ...this.state.password2,
-          warnings
-        }
-      });
-    } else {
-      this.setState({
-        [name]: {
-          ...this.state[name],
-          warnings
-        }
-      });
-    }
-  };
-
-  handleChange = event => {
-    const target = event.target;
-    const value = target.type === 'checkbox' ? target.checked : target.value;
-    const name = target.name;
-    this.setState({
-      [name]: Object.assign({}, this.state[name], { message: value })
-    });
-  };
-
-  renderInput = (type, name) => {
-    const borderColor = this.state[name].warnings.length ? 'red' : null;
-    const props = {
-      type: type,
-      name: name,
-      id: name,
-      value: this.state[name].message,
-      onFocus: this.handleFocus,
-      onChange: this.handleChange,
-      style: { color: this.state[name].color, borderColor: borderColor },
-      onBlur: this.handleBlur,
-      className: name
-    };
-
-    if (type === 'textarea') {
-      return <textarea {...props} />;
-    }
-    return <input {...props} />;
-  };
-
-  render() {
-    return (
-      <form onSubmit={this.handleSubmit} action={<Link to="/tabss" />}>
-        <label htmlFor="name">
-          Name:
-        </label>
-        {this.renderInput('text', 'name')}
-        {this.state.name.warnings}
-        <label htmlFor="password">
-          Password:
-        </label>
-        {this.renderInput('password', 'password')}
-        {this.state.password.warnings}
-        <label htmlFor="password2">
-          Password:
-        </label>
-        {this.renderInput('password', 'password2')}
-        {this.state.password2.warnings}
-
-        <label htmlFor="comments">
-          Comments:
-        </label>
-        {this.renderInput('textarea', 'comments')}
-        {this.state.comments.warnings}
-        <input type="submit" value="Submit" />
-      </form>
-    );
-  }
-}
+import Form from './Form';
+
+const FormDemo = () => {
+  return (
+    <div>
+      <h2>Comments</h2>
+      <p>
+        This form is constructed purely in React without any external library.
+        The placeholders and validation are implemented in pure Javascript, thus
+        no HTML5 is required and more importantly, the style of placeholders and
+        warnings are easy to control. However, implementing good validation
+        logic on large forms is still challegning in react as the code could
+        become extremely verbose and repeative. A library is still required to
+        handle more professional and long forms with complex validation process.
+        In addition, using <code>setState</code> multiple times in one function
+        can lead to sublte bugs as this <code>setState</code> is asynchronous.
+        Updating the state with <code>setState</code> then checking certain
+        conditions based on the new state in one function does not work. Use a
+        callback function in <code>setState</code> instead.
+      </p>
+      <hr />
+      <Form />
+    </div>
+  );
+};
+
+export default FormDemo;

+ 2 - 2
src/routes.js

@@ -10,7 +10,7 @@ import TabView from './components/Tabs';
 import Modal from './components/Modal';
 import SlideShow from './components/SlideShow';
 import Counter from './components/Counter';
-import Form from './components/Form';
+import FormDemo from './components/Form';
 import NotFound from './components/NotFound';
 
 const Routes = props =>
@@ -26,7 +26,7 @@ const Routes = props =>
           <Route path="/counter" component={Counter} />
           <Route path="/modal" component={Modal} />
           <Route path="/slideshow" component={SlideShow} />
-          <Route path="/form" component={Form} />
+          <Route path="/form" component={FormDemo} />
           <Route component={NotFound} />
         </Switch>
       </main>