Hướng dẫn xây dựng form hiển thị độ khó mật khẩu trong React

Với sự kiểm tra độ mạnh yếu của password, chúng ta sẽ cho người dùng sự trải nghiệm tốt hơn. Để có thể cải thiên password của người dùng.11 min


1079
1.8k shares, 1079 points

Cài đặt App React

Đầu tiên, chúng ta sử dụng package create-react-app để tạo app React. Sử dụng câu lệnh sau trong terminal.

npm install -g create-react-app

Chúng ta tạo app với tên react-password-strength, hoặc theo tên bạn muốn.

create-react-app react-password-strength

Tiếp theo, cần đặt đặt dependencies mà chúng ta cần cho app. Bằng câu lệnh sau

yarn add zxcvbn isemail prop-types node-sass bootstrap

Câu lệnh này sẽ cài những dependencies sau:

  • zxcvbn – Thư viện kiểm tra độ mạnh yếu của mật khẩu
  • isemail – Thư viện kiểm tra định dạng email
  • prop-types –  xác thực đầu vào của component
  • node-sass – Được sử dụng để biên dịch các tệp Sass thành CSS.

Như bạn có thể nhận thấy, bạn đã cài đặt package bootstrap để có được một số style mặc định.

import 'bootstrap/dist/css/bootstrap.min.css';

Start app React bằng câu lệnh sau

yarn start

Sau khi app được start, và được đồng bộ với code. Khi có bất kì sự thay đổi nào của code, sẽ được đồng bộ ngay lập tức

Màn hình hiển thị trên browser

Initial View

Xây dựng Components

Viết form với 3 input chính full name, email, và password. Và tạo message cho từng loại input riêng. Trong phần nàyn, bạn tạo các components trong React:

  • FormField – Wraps a form input field with its attributes and change event handler.
  • EmailField – Wraps the email FormField and adds email validation logic to it.
  • PasswordField – Wraps the password FormField and adds the password validation logic to it. Also attaches the password strength meter and some other visual cues to the field.
  • JoinForm – The fictitious Join Support Team form that houses the form fields.

Tạo thư mụccomponents bên trong thư mục src của ứng dụng để chứa tất cả các components.

Component FormField

Tạo file FormField.js trong src/components và thêm đoạn code sau

import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
class FormField extends Component {
  // initialize state
  state = { value: '', dirty: false, errors: [] }
  hasChanged = e => {
    e.preventDefault();
    // destructure props - assign default dummy functions to validator and onStateChanged props
    const { label, required = false, validator = f => f, onStateChanged = f => f } = this.props;
    const value = e.target.value;
    const isEmpty = value.length === 0;
    const requiredMissing = this.state.dirty && required && isEmpty;
    let errors = [];
    if (requiredMissing) {
      // if required and is empty, add required error to state
      errors = [ ...errors, `${label} is required` ];
    } else if ('function' === typeof validator) {
      try {
        validator(value);
      } catch (e) {
        // if validator throws error, add validation error to state
        errors = [ ...errors, e.message ];
      }
    }
    // update state and call the onStateChanged callback fn after the update
    // dirty is only changed to true and remains true on and after the first state update
    this.setState(({ dirty = false }) => ({ value, errors, dirty: !dirty || dirty }), () => onStateChanged(this.state));
  }
  render() {
    const { value, dirty, errors } = this.state;
    const { type, label, fieldId, placeholder, children } = this.props;
    const hasErrors = errors.length > 0;
    const controlClass = ['form-control', dirty ? hasErrors ? 'is-invalid' : 'is-valid' : '' ].join(' ').trim();
    return (
      <Fragment>
        <div className="form-group px-3 pb-2">
          <div className="d-flex flex-row justify-content-between align-items-center">
            <label htmlFor={fieldId} className="control-label">{label}</label>
            {/** Render the first error if there are any errors **/}
            { hasErrors && <div className="error form-hint font-weight-bold text-right m-0 mb-2">{ errors[0] }</div> }
          </div>
          {/** Render the children nodes passed to component **/}
          {children}
          <input type={type} className={controlClass} id={fieldId} placeholder={placeholder} value={value} onChange={this.hasChanged} />
        </div>
      </Fragment>
    );
  }
}
FormField.propTypes = {
  type: PropTypes.oneOf(["text", "password"]).isRequired,
  label: PropTypes.string.isRequired,
  fieldId: PropTypes.string.isRequired,
  placeholder: PropTypes.string.isRequired,
  required: PropTypes.bool,
  children: PropTypes.node,
  validator: PropTypes.func,
  onStateChanged: PropTypes.func
};
export default FormField;

Input State: Trước tiên, bạn đã khởi tạo state cho field component để theo dõi giá trị hiện tại của input, trạng thái dirty và mọi lỗi hiện có.

Handle Input Change: Tiếp theo, bạn đã thêm trình xử lý sự kiện hasChanged (e) để cập nhật giá trị state  thành giá trị đầu input hiện tại trên mỗi thay đổi đối với input React

Rendering and Props: Ở đây bạn đang hiển thị filed input và label của nó. Bạn cũng có điều kiện đưa ra lỗi đầu tiên trong mảng state lỗi (nếu có bất kỳ lỗi nào).

Component EmailField

Tạo file EmailField.js trong thư mục src/componentsvà thêm đoạn code sau

import React from 'react';
import PropTypes from 'prop-types';
import { validate } from 'isemail';
import FormField from './FormField';
const EmailField = props => {
  // prevent passing type and validator props from this component to the rendered form field component
  const { type, validator, ...restProps } = props;
  // validateEmail function using the validate() method of the isemail package
  const validateEmail = value => {
    if (!validate(value)) throw new Error('Email is invalid');
  };
  // pass the validateEmail to the validator prop
  return <FormField type="text" validator={validateEmail} {...restProps} />
};
EmailField.propTypes = {
  label: PropTypes.string.isRequired,
  fieldId: PropTypes.string.isRequired,
  placeholder: PropTypes.string.isRequired,
  required: PropTypes.bool,
  children: PropTypes.node,
  onStateChanged: PropTypes.func
};
export default EmailField;

Trong component EmailField , bạn đang render component FormField và truyền hàm email validation tới prop validator . Bạn sử dụng validate()của package isemailđể validate email.

Component PasswordField

Tạo file PasswordField.js trong thư mục src/componentsvà thêm đoạn code sau

import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import zxcvbn from 'zxcvbn';
import FormField from './FormField';
class PasswordField extends Component {
  constructor(props) {
    super(props);
    const { minStrength = 3, thresholdLength = 7 } = props;
    // set default minStrength to 3 if not a number or not specified
    // minStrength must be a a number between 0 - 4
    this.minStrength = typeof minStrength === 'number'
      ? Math.max( Math.min(minStrength, 4), 0 )
      : 3;
    // set default thresholdLength to 7 if not a number or not specified
    // thresholdLength must be a minimum value of 7
    this.thresholdLength = typeof thresholdLength === 'number'
      ? Math.max(thresholdLength, 7)
      : 7;
    // initialize internal component state
    this.state = { password: '', strength: 0 };
  };
  stateChanged = state => {
    // update the internal state using the updated state from the form field
    this.setState({
      password: state.value,
      strength: zxcvbn(state.value).score
    }, () => this.props.onStateChanged(state));
  };
  validatePasswordStrong = value => {
    // ensure password is long enough
    if (value.length <= this.thresholdLength) throw new Error("Password is short");
    // ensure password is strong enough using the zxcvbn library
    if (zxcvbn(value).score < this.minStrength) throw new Error("Password is weak");
  };
  render() {
    const { type, validator, onStateChanged, children, ...restProps } = this.props;
    const { password, strength } = this.state;
    const passwordLength = password.length;
    const passwordStrong = strength >= this.minStrength;
    const passwordLong = passwordLength > this.thresholdLength;
    // dynamically set the password length counter class
    const counterClass = ['badge badge-pill', passwordLong ? passwordStrong ? 'badge-success' : 'badge-warning' : 'badge-danger'].join(' ').trim();
    // password strength meter is only visible when password is not empty
    const strengthClass = ['strength-meter mt-2', passwordLength > 0 ? 'visible' : 'invisible'].join(' ').trim();
    return (
      <Fragment>
        <div className="position-relative">
          {/** Pass the validation and stateChanged functions as props to the form field **/}
          <FormField type="password" validator={this.validatePasswordStrong} onStateChanged={this.stateChanged} {...restProps}>
            <span className="d-block form-hint">To conform with our Strong Password policy, you are required to use a sufficiently strong password. Password must be more than 7 characters.</span>
            {children}
            {/** Render the password strength meter **/}
            <div className={strengthClass}>
              <div className="strength-meter-fill" data-strength={strength}></div>
            </div>
          </FormField>
          <div className="position-absolute password-count mx-3">
            {/** Render the password length counter indicator **/}
            <span className={counterClass}>{ passwordLength ? passwordLong ? `${this.thresholdLength}+` : passwordLength : '' }</span>
          </div>
        </div>
      </Fragment>
    );
  }
}
PasswordField.propTypes = {
  label: PropTypes.string.isRequired,
  fieldId: PropTypes.string.isRequired,
  placeholder: PropTypes.string.isRequired,
  required: PropTypes.bool,
  children: PropTypes.node,
  onStateChanged: PropTypes.func,
  minStrength: PropTypes.number,
  thresholdLength: PropTypes.number
};
export default PasswordField;

Component này đang sử dụng packagezxcvbn để ước tính độ mạnh của password. Package có zxcvbn() lấy chuỗi mật khẩu làm đối số đầu tiên của nó và trả về một đối tượng có một số thuộc tính để ước tính cường độ mật khẩu.

Đây lànhững gì đang xảy ra trong component PasswordField:

Initialization: Trong constructor(), bạn đã tạo hai thuộc tính, thresholdLangth và minStrength, từ chỗ dựa tương ứng của chúng được truyền cho component.  thresholdLengthlà độ dài mật khẩu tối thiểu trước khi nó có thể được coi là đủ dài. Mặc định là 7. minStrengthlà điểm thấp nhất zxcvbn trước khi mật khẩu được coi là đủ mạnh. Giá trị của nó nằm trong khoảng từ 0-4. Nó mặc định là 3 nếu không được custom

Handling Password Changes: Bạn đã xác định function validation password sẽ được chuyển đến prop trình xác thực của component FormField bên dưới. Hàm đảm bảo rằng độ dài mật khẩu dài hơn thresholdLength và cũng có điểm zxcvbn() tối thiểu được chỉ định.

Rendering and Props: Tại đây, bạn đã render component FormField bên dưới cùng với một số component cho input đầu vào, máy đo mật khẩu và bộ đếm độ dài mật khẩu.

Đồng hồ đo cường độ mật khẩu cho biết độ mạnh của mật khẩu hiện tại dựa trên statei và được định cấu hình ở chế độ ẩn dưới dạng động nếu độ dài mật khẩu là 0. Đồng hồ sẽ chỉ ra các màu khác nhau cho các mức cường độ khác nhau.

Thành phần PasswordField chấp nhận hai fields, minStrength và thresholdLength, như được định nghĩa trong component’s  propTypes.

Component Joinform

Tạo file JoinForm.js trong thư mục src/components

import React, { Component } from 'react';
import FormField from './FormField';
import EmailField from './EmailField';
import PasswordField from './PasswordField';
class JoinForm extends Component {
  // initialize state to hold validity of form fields
  state = { fullname: false, email: false, password: false }
  // higher-order function that returns a state change watch function
  // sets the corresponding state property to true if the form field has no errors
  fieldStateChanged = field => state => this.setState({ [field]: state.errors.length === 0 });
  // state change watch functions for each field
  emailChanged = this.fieldStateChanged('email');
  fullnameChanged = this.fieldStateChanged('fullname');
  passwordChanged = this.fieldStateChanged('password');
  render() {
    const { fullname, email, password } = this.state;
    const formValidated = fullname && email && password;
    // validation function for the fullname
    // ensures that fullname contains at least two names separated with a space
    const validateFullname = value => {
      const regex = /^[a-z]{2,}(\s[a-z]{2,})+$/i;
      if (!regex.test(value)) throw new Error('Fullname is invalid');
    };
    return (
      <div className="form-container d-table-cell position-relative align-middle">
        <form action="/" method="POST" noValidate>
          <div className="d-flex flex-row justify-content-between align-items-center px-3 mb-5">
            <legend className="form-label mb-0">Support Team</legend>
            {/** Show the form button only if all fields are valid **/}
            { formValidated && <button type="button" className="btn btn-primary text-uppercase px-3 py-2">Join</button> }
          </div>
          <div className="py-5 border-gray border-top border-bottom">
            {/** Render the fullname form field passing the name validation fn **/}
            <FormField type="text" fieldId="fullname" label="Full Name" placeholder="Enter Full Name" validator={validateFullname} onStateChanged={this.fullnameChanged} required />
            {/** Render the email field component **/}
            <EmailField fieldId="email" label="Email" placeholder="Enter Email Address" onStateChanged={this.emailChanged} required />
            {/** Render the password field component using thresholdLength of 7 and minStrength of 3 **/}
            <PasswordField fieldId="password" label="Password" placeholder="Enter Password" onStateChanged={this.passwordChanged} thresholdLength={7} minStrength={3} required />
          </div>
        </form>
      </div>
    );
  }
}
export default JoinForm;

Component JoinForm bao bọc các components field form tạo nên form. Chúng ta đã khởi tạo state để giữ tính hợp lệ của ba fields: fullname, email và password. Tất cả đều false hoặc invalid.

Cuối cùng, form đượcrendered. Lưu ý rằng bạn đã thêm function validation vào field fullname  để đảm bảo rằng ít nhất có tên và họ, được phân tách bằng khoảng trắng và chỉ chứa các ký tự bảng chữ cái.

Component trong App

Cho đến thời điểm này, trình duyệt vẫn hiển thị ứng dụng React soạn sẵn. Bây giờ bạn sẽ sửa đổi file App.js trong thư mục src để hiển thị JoinForm  bên trong AppComponent.

Thêm đoạn code sau

import React from 'react';
import JoinForm from './components/JoinForm';
import './App.css';
function App() {
  return (
    <div className="main-container d-table position-absolute m-auto">
      <JoinForm />
    </div>
  );
}
export default App;

Styling với Sass

Bạn chỉ còn một bước nữa là hoàn thành ứng dụng của mình. Hiện tại, mọi thứ có vẻ chưa chặt chẽ. Trong bước này, bạn sẽ tiếp tục và define một số style cho form.

Để tận dụng sức mạnh của các biến Sass, cấu trúc lồng(nesting) và vòng lặp, trước đây chúng ta đã cài đặt dependency của node-sass. Bạn đang sử dụng Sass để tạo tệp CSS mà trình duyệt có thể hiểu được.

Có hai điều bạn sẽ cần thay đổi để sử dụng Sass trong ứng dụng của mình

  • Đổi tên file src/App.css thànhsrc/App.scss.
  • Chỉnh đoạn import trong file src/App.js

Sau khi đổi tên tệp src/App.css, hãy cập nhật file src/App.js của bạn như sau

import './App.scss';

Tiếp theo, thay thế code hiện có trong file App.scss bằng code sau để định dạng app React

/** Declare some variables **/
$primary: #007bff;
// Password strength meter color for the different levels
$strength-colors: (darkred, orangered, orange, yellowgreen, green);
// Gap width between strength meter bars
$strength-gap: 6px;
body {
  font-size: 62.5%;
}
.main-container {
  width: 400px;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.form-container {
  bottom: 100px;
}
legend.form-label {
  font-size: 1.5rem;
  color: desaturate(darken($primary, 10%), 60%);
}
.control-label {
  font-size: 0.8rem;
  font-weight: bold;
  color: desaturate(darken($primary, 10%), 80%);
}
.form-control {
  font-size: 1rem;
}
.form-hint {
  font-size: 0.6rem;
  line-height: 1.4;
  margin: -5px auto 5px;
  color: #999;
  &.error {
    color: #C00;
    font-size: 0.8rem;
  }
}
button.btn {
  letter-spacing: 1px;
  font-size: 0.8rem;
  font-weight: 600;
}
.password-count {
  bottom: 16px;
  right: 10px;
  font-size: 1rem;
}
.strength-meter {
  position: relative;
  height: 3px;
  background: #DDD;
  margin: 7px 0;
  border-radius: 2px;
  // Dynamically create the gap effect
  &:before,
  &:after {
    content: '';
    height: inherit;
    background: transparent;
    display: block;
    border-color: #FFF;
    border-style: solid;
    border-width: 0 $strength-gap 0;
    position: absolute;
    width: calc(20% + #{$strength-gap});
    z-index: 10;
  }
  // Dynamically create the gap effect
  &:before {
    left: calc(20% - #{($strength-gap / 2)});
  }
  // Dynamically create the gap effect
  &:after {
    right: calc(20% - #{($strength-gap / 2)});
  }
}
.strength-meter-fill {
  background: transparent;
  height: inherit;
  position: absolute;
  width: 0;
  border-radius: inherit;
  transition: width 0.5s ease-in-out, background 0.25s;
  // Dynamically generate strength meter color styles
  @for $i from 1 through 5 {
    &[data-strength='#{$i - 1}'] {
      width: (20% * $i);
      background: nth($strength-colors, $i);
    }
  }
}

Bạn đã thành công trong việc thêm các style theo ứng dụng của bạn. Lưu ý việc sử dụng nội dung CSS được tạo trong .strength-meter:before và .strength-meter:before các phần tử để thêm các khoảng trống vào máy đo cường độ mật khẩu.

Bạn cũng có thể sử dụng Sass @for để tự động tạo màu cho máy đo cường độ ở các mức cường độ mật khẩu khác nhau.

Đây là màn hình hoàn chỉnh:

react

Hiển thị messages khi báo lỗi nhìn như sau

react

Trong trường hợp không có lỗi, và độ mạnh cao

react

Kết luận

Qua bài viết chúng ta có thể kiểm tra độ mạnh yếu password của app React. Từ đó có thể thêm vào app để có trải nghiệm tốt hơn cho người dùng

Tham khảo về React : Bí quyết lập trình React cho người mới bắt đầu


Like it? Share with your friends!

1079
1.8k shares, 1079 points

What's Your Reaction?

hate hate
0
hate
confused confused
0
confused
fail fail
0
fail
fun fun
0
fun
geeky geeky
0
geeky
love love
1
love
lol lol
0
lol
omg omg
0
omg
win win
1
win