Walidacja formularza w React

Walidacja formularza jest przeprowadzona za pomocą mojego hooka useForm. Obsługiwana jest walidacja:

  • required – sprawdzane jest czy pole formularza zawiera jakieś dane
  • isEmail – sprawdzane jest czy wpisane dane są poprawnym adresem email
  • min – sprawdzane jest czy wprowadzone dane mają odpowiednią liczbę znaków
  • match – sprawdzane jest czy wprowadzone dane są takie same jak w innym polu formularza (czy hasło i powtórzenie hasła są takie same)

Aby użyć hook w komponencie zawierającym formularz należy:

const {values, errors, validate, handleSubmit} = useForm(callback)

Przekazane z hooka wartości to:

  • values – obiekt zawierający wartości wprowadzone do formularza i (po walidacji) przekazane do funkcji callback(), w której jest dokonywane zapytanie do zewnętrznego API
  • errors – obiekt zawierający wartości błędów walidacji, które będą wyświetlane pod polami formularza
  • validate – funkcja dokonująca walidacji wpisanych wartości podczas pisania
  • handleSubmit – funkcja dokonująca walidacji podczas wysyłania formularza (onSubmit)

W formularzu podajemy jako pierwszy argument obiekt event, a jako drugi argument listę nazw elementów formularza tzn.

<form onSubmit={(event) => 
            handleSubmit(event, ['role', 'username', 'email', 'pass', 'passConfirm'])}>

Przykładowy element formularza o nazwie passConfirm wygląda następująco:

<div>
  <input name='passConfirm' 
           placeholder='Retype Password' 
           type='password' 
           value={values.passConfirm || ''}
           onChange={(event) => validate( event, {'match' : 'pass', 'required': true})}/>
  <p className='help is-danger'>{errors.passConfirm}</p>
</div>

Hook useForm.js

import { useState } from 'react'

function useForm(callback) {
    
    const [values, setValues] = useState({})
    const [errors, setErrors] = useState({})

    const pattern = new RegExp(
        /^[a-zA-Z0-9]+@(?:[a-zA-Z0-9]+\.)+[A-Za-z]+$/
    )

Na wstępie inicjowane są obiekty: values – przechowujący wprowadzone wartości do pól formularza oraz errors – przechowujący komunikaty błędów. Następnie tworzony jest obiekt RegExp przechowujący wzorzec pasujący do prawidłowego adresu email.

Funkcja useForm pobiera jako argument nazwę funkcji zwrotnej, która będzie wywołana po przesłaniu formularza i wykonaniu funkcji handleSubmit:

    const handleSubmit = (event, controls) => {
        event.preventDefault()
        controls.map((value) => validateOnSubmit(value))
        setErrors({...errors})
        if (!Object.keys(errors).length) {
            callback()
        }
    }

Powyższa funkcja pobiera jako argumenty obiekt event oraz listę pól formularza. Następnie wywoływana jest funkcja preventDefault() zapobiegająca domyślnemu działaniu podczas przesłania formularza, a dalej wywoływana jest funkcja validateOnSubmit() dla każdego pola formularza tzn.

    const validateOnSubmit = (value) => {
        if (values[value] === undefined) {
            errors[[value]] =  'This field is required'
        }
    }

Powyższa funkcja sprawdza, czy w polu formularza są wprowadzone jakieś wartości. Jeśli nie generowany jest błąd.

Jeśli obiekt errors nie będzie pusty, tzn. będą obecne błędy, to nie nastąpi wykonanie funkcji zwrotnej.

Funkcja validate() dokonuje sprawdzania wprowadzonego tekstu do pola formularza podczas pisania, a także wyświetla odpowiednie komunikaty o błędach walidacji podczas pisania. Pobiera jako argumenty obiekt event oraz obiekt zawierający reguły walidacji.

const validate = (event, rules) => {

   setValues(values => ({...values, [event.target.name]: event.target.value}))

Dla dowolnego pola formularza wywołana jest funkcja setValues(), która uzupełnia obiekt values o obiekt o kluczu pola formularza, do którego wprowadza się aktualnie tekst. Następnie wywoływane są poszczególne walidacje dla określonych reguł, tzn.

  • Jeśli wybrana jest reguła 'required’:true w formularzu to hook sprawdza czy ma do czynienia z tą metodą, oraz czy wartość wprowadzonego tekstu do pola formularza jest równa 0. W takim przypadku obiekt errors jest uzupełniany o kolejny element o kluczu będącym nazwą pola formularza.
// is required validation
        if (rules.required === true && event.target.value.length === 0) {
            setErrors(errors => ({...errors, [event.target.name]: 'This field is required'}))
        } 
  • Jeśli wybrana regułą to 'isEmail’:true w formularzu, to hook sprawdza czy wprowadzony tekst jest zgodny ze wzorcem tzn.
// is valid email address validation
        else if (rules.isEmail === true && !pattern.test(event.target.value)) {
            setErrors(errors => ({...errors, [event.target.name]: 'This email address is invalid'}))
        }
  • Jeśli wybrana reguła to 'min’: liczba, to hook sprawdza czy wpisany tekst w polu formularza jest co najmniej o długości podanej jako wartość liczba np. 'min’: 6 zaakceptuje tekst o długości co najmniej 6 znaków. W przeciwnym przypadku nastąpi ustawienie odpowiedniego błędu. Do tekstu komunikatu błędu pobrana jest nazwa pola formularza tzn. tekst będzie się zmieniał w zależności jak będzie się nazywać zmienna.
// min value length validation
        else if (rules.min && event.target.value.length < rules.min) {
            setErrors(errors => ({...errors, 
                [event.target.name]:
                [event.target.name.charAt(0).toUpperCase()] 
                + [event.target.name.slice(1)]
                + '  is too short'}))
        } 
  • Jeśli wybrana reguła to match, to jako wartość dla klucza match podajemy nazwę pola formularza, z którym mają być porównywane wartości. Hook nie ma 'na stałe’ zapisanej nazwy porównywanego pola tzn.
 // match validation
        else if (rules.match && event.target.value!==values[rules.match]) {
            setErrors(errors => ({...errors, 
                [event.target.name]: "Passwords don't match"}))
        } 

Jeśli żadne z powyższych warunków nie zostało spełnione, to wywoływany jest blok else:

else {
            delete errors[event.target.name]
        }

Bulma – modal komponent + React +Flask-RESTful

We wpisie opiszę, jak uaktywnić komponent modal z frameworka Bulma, aby po wciśnięciu przycisku kasowania wiersza-kontraktu w tabeli kontraktów możliwe było potwierdzenie lub anulowanie usunięcia kontraktu.

<tbody>
  {cursor && Object.keys(cursor).map(
      (keyName, keyIndex) =>
      <tr  className="has-text-centered is-size-7"
           key={keyIndex}>
          <td>{keyIndex+1}</td>
          <td>{(cursor[keyName].status)}</td>
          <td>{(cursor[keyName].contract_number)}</td>
          <td>{(cursor[keyName].contractor)}</td>
          <td>{(cursor[keyName].customer)}</td>
          <td>{(cursor[keyName].date_of_order)}</td>
          <td>{(cursor[keyName].date_of_delivery)}</td>
          <td>{(cursor[keyName].pallets_position)}</td>
          <td>{(cursor[keyName].pallets_planned)}</td>
          <td>{(cursor[keyName].pallets_actual)}</td>
          <td>{(cursor[keyName].warehouse)}</td>
          <td>
            <FaEdit />
            <FaTrashAlt onClick={() => 
               showModal(cursor[keyName].id)}
               title={`Delete contract ${keyIndex+1}`} />
          </td>
      </tr>)}   
</tbody>
Deleting contract

Zamiast bezpośrednio wywołać funkcję deleteContract() zostaje wywołana funkcja showModal() pobierająca jako parametr id wybranego kontraktu.

Funkcja showModal() ustawia odpowiednio zmienne id oraz modal tzn.

const [modal, setModal] = useState(false)
const [id, setId] = useState()

const showModal = (id) => {
        setId(id)
        setModal(!modal)
    }

Zmienna id przechowuje numer id wybranego kontraktu, natomiast zmienna modal przechowuje wartość boolean określającą stan okna modalnego (wyświetlony lub niewidoczny-domyślnie). Wywołanie funkcji setModal() zmienia wartość domyślną – false zmiennej modal na true.

Kod okna modalnego wyświetlającego potwierdzenie kasowania kontraktu wygląda następująco:

<div class={`modal ${modal && 'is-active'}`}>
  <div class="modal-background"></div>
  <div class="modal-card column is-one-quarter">
     <header class="modal-card-head">
        <p class="modal-card-title has-text-danger">
           Delete Contract?
        </p>
      </header>
      <section class="modal-card-foot">
         <button class="button is-danger" 
             onClick={() => deleteContract(id)}>Delete
         </button>
         <button class="button"
             onClick={() => setModal(!modal)}>Cancel
         </button>
       </section>
  </div>
</div>

Wybranie przycisku Cancel powoduje, że okno modalne staje się niewidoczne. Wybranie przycisku Delete aktywuje właściwą funkcję kasowania wiersza tzn.

const deleteContract = (id) => {
  setCursor(Object.values(cursor)
    .filter((row) => row.id !== id))
  fetch('/api/contract/delete', {
    method: 'POST',
    headers: {
      'Content-type': 'application/json',
     },
       body: JSON.stringify({'id': id, 'token': token})
  })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch((err) => console.log(err));
  setModal(!modal)
}

Funkcja setCursor pobiera wartości obiektu cursor i filtruje je w taki sposób, że odrzucany jest wybrany kontrakt. Następnie następuje wywołanie api we Flasku, oprócz id kontraktu przesyłany jest również token użytkownika. Wynik pomyślnego kasowania wiersz wyświetlany jest w konsoli.

Jak wygląda API we Flasku opisałem w poprzednim wpisie.

W ostatnim wierszu funkcji deleteContract() usuwam okno modalne.

Kasowanie rekordu (Flask-RESTful, SQLAlchemy, React i Bulma CSS framework)

W tabeli kontraktów po naciśnięciu odpowiedniego przycisku kasowania wiersza w tabeli zostaje aktywowana funkcja deleteContract, która jako parametr przyjmuje wartość id kontraktu tzn.:

<td>
    <FaTrashAlt onClick={() => deleteContract(cursor[keyName].id)} />
</td>

Funkcja deleteContract filtruje dane zawarte w zmiennej cursor w taki sposób, że odrzucany jest wiersz, który chcemy usunąć. Następnie wywoływana jest zapytanie do backendu, gdzie w metodzie POST przesyłana jest wartość id kontraktu oraz token. Gdy usunięcie rekordu będzie zakończone sukcesem, to z API zostaje przesłana informacja w zmiennej message (w podanym przykładzie wartość tej zmiennej jest wyświetlana w konsoli).

const deleteContract = (id) => {
    setCursor(Object.values(cursor).filter((row) => row.id !== id))
    fetch('/api/contract/delete', {
            method: 'POST',
            headers: {
                'Content-type': 'application/json',
            },
            body: JSON.stringify({'id': id, 'token': token})
        })
        .then(res => res.json()).then(data => console.log(data)).catch((err) => console.log(err));
    }

Backend we Flask-RESTful z wykorzystaniem SQLAlchemy:

class DeleteContract(Resource):
    def post(self):
        req = request.json
        contract_id = req['id']
        token = req['token'].strip('"')
        user = User.query.filter_by(token=token).first()
        if user.role == 'Customer':
            contract = Contract.query.filter_by(customer=user.username).filter_by(id=contract_id).first()
            if contract:
                contract.status ='cancelled'
                db.session.commit()
                return {"message": "Row deleted"}

Przesłane za pomocą metody POST wartości id i token przypisuję do odpowiednich zmiennych contract_id oraz token. Następnie znajduję użytkownika w bazie danych na podstawie przesłanego tokena. Jeśli użytkownik należy do grupy Customer, to dokonywane jest wyszukiwanie kontraktu na podstawie nazwy użytkownika i numeru id kontraktu. Jeśli kontrakt ten istnieje to zamiast go usuwać z bazy, zmieniam jego status na cancelled.

Uaktualniłem również metodę post() klasy AllContracts(Resource), aby utworzony obiekt kursora nie zawierał rekordów ze statusem cancelled tzn.:

class AllContracts(Resource):
    def get(self, token):
        token = token.strip('"')
        user = User.query.filter_by(token=token).first()
        if user.role == 'Customer':
            contracts = Contract.query.filter(
                Contract.status!='cancelled').
                filter_by(customer=user.username).
                order_by(Contract.id.desc()).all()
        elif user.role == 'Contractor':
            contracts = Contract.query.
                    filter(Contract.status!='cancelled').
                    filter_by(contractor=user.username).
                    order_by(Contract.id.desc()).all()
        cursor = {}
        for i, contract in enumerate(contracts):
            cursor[i] = contract.serialize()
        return cursor

Hook useEffect() do wczytania kontraktów po wyborze opcji Contracts z menu wygląda następująco:

    useEffect( 
        () => {
            const token = sessionStorage.getItem('token')
            fetch('/api/contract/' + token)
            .then(res => res.json()).then(data => setCursor(data)).catch((err) => console.log(err))
        }, [token, setCursor],
    );

React, Flask-restful i SQLAlchemy – pobranie danych z bazy danych

Frontend:

Dane z backendu pobieram przy wybraniu z opcji menu komponentu Navbar frameworka Bulma. Aby dane były wczytane podczas randerowania komponentu używam hook useEffect. Tworzone jest wówczas zapytanie do backendu, a jako parametr przekazywany jest token, ustawiany podczas logowania użytkownika. Z backendu przesyłana jest odpowiedź w formie obiektu Promise, z którego jeśli zapytanie zostało zakończone sukcesem jest ustawiana zmienna cursor (zmiana stanu za pomocą setCursor). Cursor i funkcja setCursor przekazywane są do komponentu z komponentu nadrzędnego App.js jako parametry (props) tzn.

import LoginForm from '../Forms/LoginForm/LoginForm'
import { useEffect } from "react";

function Contracts({token, putToken, cursor, setCursor}) {

    useEffect( 
        () => {
            const token = sessionStorage.getItem('token')
            fetch('/api/contract/' + token)
            .then(res => res.json()).then(data => setCursor(data)).catch((err) => console.log(err))
        }, [token, setCursor],
    );
    
    if (!token) {
        return <LoginForm putToken={putToken} />
    };

Aby komponent odświeżał dane po poprawnym zalogowaniu się w dodatkowym parametrze metody useEffect w tablicy umieściłem zmienną token, która zostaje ustawiona w komponencie LoginForm.js

Aby komponent odświeżał dane po zmianie zawartości cursora np. poprzez dodanie nowego kontraktu jako dodatkowy parametr metody useEffect umieściłem funkcję setCursor.

Poszczególne wiersze danych są umieszczane w tabeli tzn.

<tbody>
    {cursor && Object.keys(cursor).map((keyName, keyIndex) => 
    <tr key={keyIndex}>
            <td>{keyIndex+1}</td>
            <td>{(cursor[keyName].status)}</td>
            <td>{(cursor[keyName].contract_number)}</td>
            <td>{(cursor[keyName].contractor)}</td>
            <td>{(cursor[keyName].customer)}</td>
            <td>{(cursor[keyName].date_of_order)}</td>
            <td>{(cursor[keyName].date_of_delivery)}</td>
            <td>{(cursor[keyName].pallets_position)}</td>
            <td>{(cursor[keyName].pallets_planned)}</td>
            <td>{(cursor[keyName].pallets_actual)}</td>
            <td>{(cursor[keyName].warehouse)}</td>
     </tr>)}   
</tbody>

Backend we Flask-restful:

W pliku __init__.py dodaję kolejną klasę zasobów:

from api.resources.contracts import AllContracts

api.add_resource(AllContracts, '/api/contract/<token>', endpoint='all_contracts')

W pliku contracts.py definiuję klasę zasobów:

from datetime import datetime
from flask import request
from flask_restful import Resource
from .. import db
from ..common.models import User, Contract

class AllContracts(Resource):
    def get(self, token):
        token = token.strip('"')
        user = User.query.filter_by(token=token).first()
        contracts = Contract.query.filter_by(customer=user.username).order_by(Contract.id.desc()).all()
        cursor = {}
        for i, contract in enumerate(contracts):
            cursor[i] = contract.serialize()
        return cursor

W pliku models.py definiuję m.in. jak klasa Contract dziedzicząca po klasie Model z SQLAlchemy ma być serializowana:

from .. import db
from datetime import datetime

class Contract(db.Model):
    '''Model of contract between a contractor and a customer'''

    id = db.Column(db.Integer, primary_key=True)
    status = db.Column(db.String(10), nullable=False, default='open')
    contract_number = db.Column(db.String(20), nullable=False)
    contractor = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
    customer = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
    date_of_order = db.Column(db.Date, nullable=False, default=datetime.utcnow)
    date_of_delivery = db.Column(db.Date, nullable=False)
    pallets_position = db.Column(db.Integer)
    pallets_planned = db.Column(db.Integer, nullable=False)
    pallets_actual = db.Column(db.Integer)    
    warehouse = db.Column(db.String(10), nullable=False)

    def serialize(self):
        return {'id': self.id,
                'status': self.status,
                'contract_number': self.contract_number,
                'contractor': self.contractor,
                'customer': self.customer,
                'date_of_order': datetime.strftime(self.date_of_order, '%Y-%m-%d'),
                'date_of_delivery': datetime.strftime(self.date_of_delivery, '%Y-%m-%d'),
                'pallets_position': self.pallets_position,
                'pallets_planned': self.pallets_planned,
                'pallets_actual': self.pallets_actual,
                'warehouse': self.warehouse 
                }

React i Bulma navbar

Rozwiązane problemy:

  1. Jak zwinąć burger menu przy wyborze opcji z w/w menu. (domyślnie wybór opcji z burger menu pozostawia rozwinięte menu)
  2. Jak zwinąć menu rozwijalne przy wyborze którejś z opcji. (domyślnie menu rozwijalne nie zamyka się, gdy wybrano którąś z opcji)

ad. 1

Menu nareszcie zwija się po wybraniu opcji
<div id="navbarBasicExample" className={`navbar-menu ${burgerActive? "is-active": ""}`}>
                <div class="navbar-start">
                <Link to="/" class="navbar-item" onClick={handleOnClick}>
                    Home
                </Link>

przy czym:

    const [burgerActive, setBurgerActive] = useState(false)

    const handleOnClick = () => {
        setBurgerActive(false)
    }

ad2.

Menu rozwijalne zwija się, gdy wybrano którąś z opcji.
<div class="navbar-item has-dropdown is-hoverable" key={location.pathname}>

przy czym:

import {
    Link, useLocation
  } from 'react-router-dom' 

const Navbar = () => {
    let location = useLocation();

Bulma navbar i React

Navbar jako komponent React.js

Dodałem obsługę przycisku hamburger.

plik: Navbar.js

import React, { useState } from 'react'
import {
    Link
  } from 'react-router-dom' 

const Navbar = () => {
    const [burgerActive, setBurgerActive] = useState(false)
    return (
        <nav class="navbar" role="navigation" aria-label="main navigation">
            <div class="navbar-brand">
                <a class="navbar-item" href="https://slawomirkwiatkowski.pl">
                <div class="title">Contracts</div>
                </a>

                <a role="button" className={`navbar-burger ${burgerActive? "is-active": ""}`} 
                    aria-label="menu" aria-expanded="false" 
                    data-target="navbarBasicExample"
                    onClick={() => setBurgerActive(!burgerActive)}
                >
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                </a>
            </div>

            <div id="navbarBasicExample" className={`navbar-menu ${burgerActive? "is-active": ""}`}>
                <div class="navbar-start">
                <Link to="/" class="navbar-item">
                    Home
                </Link>

                <a class="navbar-item">
                    Documentation
                </a>

                <div class="navbar-item has-dropdown is-hoverable">
                    <a class="navbar-link">
                    More
                    </a>

                    <div class="navbar-dropdown">
                    <Link to="/about" class="navbar-item">
                        About
                    </Link>
                    <a class="navbar-item">
                        Jobs
                    </a>
                    <a class="navbar-item">
                        Contact
                    </a>
                    <hr class="navbar-divider"/>
                    <a class="navbar-item">
                        Report an issue
                    </a>
                    </div>
                </div>
                </div>

                <div class="navbar-end">
                <div class="navbar-item">
                    <div class="buttons">
                    <Link to="/user/register" class="button is-link">
                        <strong>Sign up</strong>
                    </Link>
                    <Link to="/user/login" class="button is-light">
                        Sign In
                    </Link>
                    </div>
                </div>
                </div>
            </div>
        </nav>
    )
}

export default Navbar

React & Flask #1 – rejestracja użytkownika

Frontend:

import React, {useState} from 'react'
import { useHistory } from "react-router-dom";
import {useForm} from "react-hook-form";
import './RegisterForm.css'
function RegisterForm() {

    let history = useHistory();

    const {register, handleSubmit, getValues, formState: { errors } } = useForm();

    const [userRole, setUserRole] = useState('')

    const onSubmit = (data, e) => {
        e.preventDefault()
        data['role'] = userRole
        fetch('/api/user/register', {
            method: 'POST',
            headers: {
                'Content-type': 'application/json',
            },
            body: JSON.stringify(data)
        })
        .then(res => console.log(res.json()))
        history.push('/user/login')
    }
    return (
        <div className="columns">
            <div className="column is-narrow-desktop is-offset-5">
                <div className="card is-3 mt-5">
                    <header className="card-header">
                        <p className="card-header-title has-text-primary-light has-background-dark">
                            User Registration
                        </p>
                    </header>
                    <div className="card-content">
                        <div className="content">
                            <form onSubmit={handleSubmit(onSubmit)}>
                                <div className="field">
                                    <div className="control">
                                        <select name="role" required
                                            value={userRole}
                                            onChange={(e) => setUserRole(e.target.value)}>
                                            <option value="" disabled defaultValue="" hidden>Role</option>
                                            <option value="Contractor" >Contractor</option>
                                            <option value="Customer">Customer</option>
                                        </select>
                                    </div>
                                </div>
                                <div className="field control">
                                    <input placeholder="Username" name="username" 
                                        {...register("username", {required: true, minLength: 3})} />
                                    <p className="help is-danger">
                                        {errors.username?.type === 'required' && "This field is required"}
                                        {errors.username?.type === 'minLength' && "This field must contain at least 3 characters"}
                                    </p>
                                </div>
                                <div className="field control">
                                    <input placeholder="Email" id="email"
                                        autoComplete="email"
                                        {...register("email", {required: true, pattern: /\S+@\S+\.\S+/})} />
                                    <p className="help is-danger">
                                        {errors.email?.type === 'required' && "This field is required"}
                                        {errors.email?.type === 'pattern' && "Invalid email address"}
                                    </p>
                                </div>
                                <div className="field control">
                                    <input type="password" placeholder="Password" id="password" name="password"
                                        autoComplete="new-password"
                                        {...register("password",  {required: true, minLength: 6})} />
                                    <p className="help is-danger">
                                        {errors.password?.type === 'required' && "This field is required"}
                                        {errors.password?.type === 'minLength' && "This field must contain at least 6 characters"}
                                    </p>
                                </div>
                                <div className="field control">
                                    <input type="password" placeholder="Retype Password" id="confirm_password"
                                        autoComplete="new-password" 
                                        {...register("confirm_password",  {required: true, minLength: 6,
                                            validate: value => value === getValues('password')})} />
                                    <p className="help is-danger">
                                        {errors.confirm_password?.type === 'required' && "This field is required"}
                                        {errors.confirm_password?.type === 'minLength' && "This field must contain at least 6 characters"}
                                        {errors.confirm_password?.type === 'validate' && "Passwords don't match"}
                                    </p>
                                </div>

                                <div className="control">
                                    <input type="submit" value="Register" className="button is-link" />    
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        

    )
}

export default RegisterForm

Backend – główny plik: __init__py

def create_app(Config):
    app =  Flask(__name__, static_folder='../build', static_url_path='/')
    app.config.from_object(Config)
    api.init_app(app)
    db.init_app(app)
    app.db = db
    bcrypt = Bcrypt(app)
    app.bcrypt = bcrypt


    from .common import utils
    from .common import routes
    app.register_blueprint(api_bp)
    app.register_blueprint(utils.bp)
    app.register_blueprint(routes.bp)
    return app


from api.resources.auth import UserRegister
api.add_resource(UserRegister, '/api/user/register', endpoint='user_register')

Plik zawierający klasę Resource dla obsługi rejestracji użytkownika:

from flask import current_app, request
from flask_restful import Resource
from api.common.parsers import parser
from ..common.utils import send_email
from ..common.models import User
from .. import db
class UserRegister(Resource):

    def post(self):
        req = request.json
        username = req['username']
        password = req['password']
        email = req['email']
        role = req['role']
        bcrypt = current_app.bcrypt
        hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
        status = send_email(email, category='confirm_account')
        user = User(username=username, password=hashed_password, email=email, role=role)
        db.session.add(user)
        db.session.commit()
        return {"message": status}

Formularze w React z użyciem React Hook Form

Przykładowy mój formularz logowania:

import React from 'react'
import {useForm} from "react-hook-form"


function LoginForm() {

    const {register, handleSubmit,  formState: { errors } } = useForm();
    

    const onSubmit = (data, e) =>  {
        e.preventDefault();
        console.log(data)
        };


    return (
        <form onSubmit={handleSubmit(onSubmit)}>

            <input placeholder="Enter username" id="username" 
                {...register("username", {required: true, minLength: 3})} />
            {errors.username?.type === 'required' && "This field is required"}
            {errors.username?.type === 'minLength' && "This field must contain at least 3 characters."}
            {/* {errors.username && <span>This field is required and must contain at least 3 characters.</span>} */}
            
            <input type="password" placeholder="Password" id="password" 
                {...register("password",  {required: true, minLength: 6})} />
            {errors.password?.type === 'required' && "This field is required"}
            {errors.password?.type === 'minLength' && "This field must contain at least 6 characters."}


            <input type="submit" value="Login" />    

        </form>
    )
}

export default LoginForm

 

React i BrowserRouter – przykład

Przykład zastosowania routingu w React.js :

import './App.css';
import {
  BrowserRouter,
  Switch,
  Route,
  Link
} from 'react-router-dom' 
import LoginForm from './components/LoginForm/LoginForm'

function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Switch>
          <Route path="/" exact>
            <LoginForm />
            <Link to="/about">About</Link>
          </Route>
          <Route path="/about">
            <Link to="/">Go to main page</Link>
          </Route>
          <Route path="/to-component" component={LoginForm} />
        </Switch>
      </div>
    </BrowserRouter>
  );
}

export default App;