hocvietcode.com
  • Trang chủ
  • Học lập trình
    • Lập trình C/C++
    • Lập trình HTML
    • Lập trình Javascript
      • Javascript cơ bản
      • ReactJS framework
      • AngularJS framework
      • Typescript cơ bản
      • Angular
    • Lập trình Mobile
      • Lập Trình Dart Cơ Bản
        • Dart Flutter Framework
    • Cơ sở dữ liệu
      • MySQL – MariaDB
      • Micrsoft SQL Server
      • Extensible Markup Language (XML)
      • JSON
    • Lập trình PHP
      • Lập trình PHP cơ bản
      • Laravel Framework
    • Lập trình Java
      • Java Cơ bản
    • Cấu trúc dữ liệu và giải thuật
    • Lập Trình C# Cơ Bản
    • Machine Learning
  • WORDPRESS
    • WordPress cơ bản
    • WordPress nâng cao
    • Chia sẻ WordPress
  • Kiến thức hệ thống
    • Microsoft Azure
    • Docker
    • Linux
  • Chia sẻ IT
    • Tin học văn phòng
      • Microsoft Word
      • Microsoft Excel
    • Marketing
      • Google Adwords
      • Facebook Ads
      • Kiến thức khác
    • Chia sẻ phần mềm
    • Review công nghệ
    • Công cụ – tiện ích
      • Kiểm tra bàn phím online
      • Kiểm tra webcam online
Đăng nhập
  • Đăng nhập / Đăng ký

Please enter key search to display results.

Home
  • ReactJS
Xử lý Form (biểu mẫu) trong React

Xử lý Form (biểu mẫu) trong React

  • 15-10-2022
  • Toanngo92
  • 0 Comments

Form là một thành phần quan trọng của các ứng dụng web React. Chúng cho phép người dùng trực tiếp nhập và gửi dữ liệu trong các component khác nhau, từ màn hình đăng nhập đến trang thanh toán. Vì hầu hết các ứng dụng React là các ứng dụng không chuyển trang (SPA) hoặc các ứng dụng web tải một trang duy nhất qua đó dữ liệu mới được hiển thị động, bạn sẽ không gửi thông tin trực tiếp từ biểu mẫu đến máy chủ. Thay vào đó, chúng ta sẽ nắm bắt thông tin biểu mẫu ở phía máy khách và gửi hoặc hiển thị nó bằng cách sử dụng mã JavaScript bổ sung .

Các form React đưa ra một thách thức duy nhất vì bạn có thể cho phép trình duyệt xử lý hầu hết các phần tử biểu mẫu và thu thập dữ liệu thông qua các sự kiện thay đổi trong React hoặc có thể sử dụng React để kiểm soát hoàn toàn phần tử bằng cách đặt và cập nhật trực tiếp giá trị đầu vào. Cách tiếp cận đầu tiên được gọi là component không được kiểm soát (uncontrolled component) vì React không thiết lập giá trị. Cách tiếp cận thứ hai được gọi là thành phần được kiểm soát (controlled component) vì React tham gia cập nhật đầu vào.

Ở bài hướng dẫn này, mình sẽ tiếp tục xây dựng tính năng checkout thanh toán giỏ hàng, bằng cách sử dụng form. Chúng ta cũng sẽ tìm hiểu những ưu điểm và nhược điểm của các component được kiểm soát và không được kiểm soát. Cuối cùng, bạn sẽ tự động đặt các thuộc tính biểu mẫu để bật và tắt các trường tùy thuộc vào state biểu mẫu. Để ví dụ thêm sinh động, mình sẽ bổ sung thêm các input như date time, radiobox, checkbox trong form.

Mục lục

  • Khởi tạo dự án
    • Tạo các component layout Header, Footer, ProductsPage
    • Tạo Ultils
    • Khởi tạo dữ liệu mẫu
    • Tạo các file context
    • Tạo các Components
  • Tạo form với JSX
  • Thu thập dữ liệu trong form sử dụng uncontrolled components
  • Thu thập dữ liệu trong form sử dụng controlled components
  • Cập nhật động các thuộc tính form

Khởi tạo dự án

npx create-react-app reactformdemo
cd reactformdemo
npm install react-proptypes
npm install react-jss

Tạo các component layout Header, Footer, ProductsPage

Tạo file layout/Header/Header.js:

import React, { useContext } from "react"
import { createUseStyles } from "react-jss"
import UserContext from "../../context/UserContext";

const useStyles = createUseStyles({
    header: {
        background: '#47b7e5',
        color: '#fff',
        justifyContent: 'space-between',
        padding: 15,
        textTransform: 'uppercase',
        fontWeight: 'bold',
        fontSize: 12,
        '& nav': {
            '& ul': {
                listStyle: 'none',
                padding: 0,
                margin: '0px -5px',
                justifyContent: 'flex-end',
                '& li': {
                    margin: '0px 5px'
                }
            }
        }
    }
});
const Header = () => {
    const classes = useStyles();
    const user = useContext(UserContext)

    return (<header className={classes.header + ' d-flex'}>
        <div className="logo">web888.vn</div>
        <nav>
            <ul className="d-flex">
                <li>Home page</li>
                <li>About</li>
                <li>Contact</li>
                <li>Hello, {user.name}</li>
            </ul>
        </nav>
    </header>)
}

export default Header

Tạo file layout/Footer/Footer.js:

import React from "react"
import { createUseStyles } from "react-jss"

const useStyles = createUseStyles({
    footer: {
        padding: 15,
        fontSize: 12,
        color: '#fff',
        background: '#fa726c'
    }
});

const Footer = () => {
    const classes = useStyles()

    return (<footer className={classes.footer}>
        Copyright @ web888.vn
    </footer>)
}

export default Footer

Tạo file layout/ProductPage/ProductPage.js:

import React, { createContext, useReducer } from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/Cart/Cart";
import Products from "../../components/Products/Products";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";

const useStyles = createUseStyles({

})

const cartReducer = (state, item) => {
    const { product, type } = item;
    const cart = [...state];
    switch (type) {
        default:
        case 'add':
            // state value is array of product object
            if (cart.length === 0) {
                product.quantity = 1;
            }
            let checkExist = false;
            for (let i = 0; i < cart.length; i++) {
                if (product.id === cart[i].id) {
                    checkExist = true;
                    break;
                }
            }
            product.quantity = checkExist ? product.quantity + 1 : 1;
            if (product.quantity > 1) {
                return [...state];
            }
            return [...state, product];
        // break;
        case 'remove':
            for (let i = 0; i < cart.length; i++) {
                if (cart[i].id === product.id) {
                    cart.splice(i, 1);
                    break;
                }
            }
            return [...cart]
            break;
    }
}

const totalCartReducer = (state, item) => {
    const { product, type,productsCart } = item;
    const cartTotal = state;
    let checkExist = false;
    for (let i = 0; i < productsCart.length; i++) {
        if (product.id === productsCart[i].id) {
            checkExist = true;
            break;
        }
    }
    switch (type) {
        default:
        case 'add':
           
            cartTotal.totalPrice += product.price;
            cartTotal.totalQuantity  = checkExist ? productsCart.length : productsCart.length+1;
            return {...cartTotal}
            break;
        case 'remove':
            cartTotal.totalPrice -= product.price*product.quantity;
            cartTotal.totalQuantity  = productsCart.length-1;
            return  {...cartTotal}
            break;
    }
    return { ...state };
}


const ProductPage = () => {
    const classes = useStyles();

    const [productsCart, setCart] = useReducer(cartReducer, [])
    const [totalCart, setTotalCart] = useReducer(totalCartReducer, { totalQuantity: 0, totalPrice: 0,productsCart: []})

    return (

        <ProductContext.Provider value={{ productsCart, setCart }}>
            <CartContext.Provider value={{ totalCart, setTotalCart }}>
                <Cart />
                <Products />
            </CartContext.Provider>
        </ProductContext.Provider >

    )
}

export default ProductPage;

Tạo Ultils

Tạo file ultils/ultils.js:

const currencyOptions = {
    minimumFractionDigits: 0,
    maximumFractionDigits: 2,
}

const getLocalePrice = (price) => {
    return price.toLocaleString(price, currencyOptions)
}

export default getLocalePrice

Khởi tạo dữ liệu mẫu

Tạo file data/products_data.js:

const data = [
	{
		"id": "0001",
		"type": "Laptop",
		"name": "Laptop HP Notebook 15 bs1xx",
		"price": 8500000,
        "thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660891888837.jpg",
        "source": "https://laptopcutot.com/product/laptop-hp-notebook-15-bs1xx-i5-8250u-8g-ssd-240g-vga-on-15-6hd/",
		"specs": {
            "cpu": "Intel Core i5 8250U",
            "Ram": 8,
            "storages": [
                {
                    "storage": 240,
                    "storage_type": "ssd"
                }
            ]
         
        }
    },
    {
		"id": "0002",
		"type": "Laptop",
		"name": "Laptop HP Probook 450G4",
		"price": 8200000,
        "thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660882014527.jpg",
        "source": "https://laptopcutot.com/product/laptop-hp-probook-450g4-i5-7200u-8g-ssd-240g-vga-on-15-6hd/",
		"specs": {
            "cpu": "Intel Core i5 7200U",
            "Ram": 8,
            "storages": [
                {
                    "storage": 240,
                    "storage_type": "ssd"
                }
            ]         
        },
        "description": "The machine uses the 7th generation core i5 KabyLake to help deliver stronger and faster performance than many core i5 lines at the same price today. \nThe new 8 GB DDR4 RAM reduces battery power consumption by 20% and improves performance, upgradable to 16 GB."
    },
    {
		"id": "0003",
		"type": "Laptop",
		"name": "Laptop Dell Latitude E7270",
		"price": 7100000,
        "thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660891888837.jpg",
        "source": "https://laptopcutot.com/product/laptop-cu-dell-latitude-e7270-core-i7-6600u-ram-4gb-ssd-128gb-vga-hd-man-125inch-fhd/",
		"specs": {
            "cpu": "Intel Core i5 8250U",
            "Ram": 4,
            "storages": [
                {
                    "storage": 128,
                    "storage_type": "ssd"
                }
            ]
         
        },
        "description": "Dell Latitude E7270 Core i7-6600U is equipped with Intel's 6th generation processor, giving the machine a stable processing power and effective power saving. With 4 GB DDR4 RAM memory for good processing performance as well as smooth multitasking applications. The Intel HD Graphics 520 graphics chipset allows to run graphics applications well. In addition, Dell also equips the machine with a 128GB or 256GB M.2 SATA SSD to help the machine operate smoother and boot faster."
    },
    {
		"id": "0004",
		"type": "Laptop",
		"name": "Laptop HP 340s G7",
		"price": 7500000,
        "thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660114914748.jpg",
        "source": "https://laptopcutot.com/product/laptop-hp-340s-g7-i3-1005g1-4gb-ssd-512gb/",
		"specs": {
            "cpu": "Intel Core i3 1005G1",
            "Ram": 4,
            "storages": [
                {
                    "storage": 500,
                    "storage_type": "ssd"
                }
            ]
         
        }
    },
    {
		"id": "0005",
		"type": "Laptop",
		"name": "Laptop HP Elitebook 840 G3",
		"price": 8200000,
        "thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/03/auto-draft-1648029011162.jpg",
        "source": "https://laptopcutot.com/product/laptop-cu-hp-elitebook-840-g3-i7-6600u-ram8gb-ssd-256gb-intel-hd-graphic-520-mh-14-full-hd/",
		"specs": {
            "cpu": "Intel Core i7 6600U",
            "Ram": 8,
            "storages": [
                {
                    "storage": 240,
                    "storage_type": "ssd"
                }
            ]         
        },
        "description": "This is already the 3rd version in the Elitebook 840 series and HP continues to keep this name and add the G3 symbol at the back. In general, the overall design of the Elitebook 840 G3 is not much different from the previous 2 generations when the machine still possesses an ultra-thin and light design when it is only 18.9 mm thick and weighs only 1.5 kg.\nThe back of the Elitebook 840 G3 is made of silver-white magnesium alloy with scratch resistance, so it looks quite luxurious and high-class. Besides, although it is quite thin and light, when held in the hand, the device feels sturdy and durable thanks to the US military standard MIL-STD 810G with very good impact resistance, and it is also resistant to shocks. Extreme temperature, high pressure, drop resistance, impact resistance and moisture resistance are much better than conventional laptops."
    },
    {
		"id": "0006",
		"type": "Laptop",
		"name": "Laptop Dell Inspiron 5493",
		"price": 11900000,
        "thumbnail": "https://laptopcutot.com/wp-content/uploads/2021/11/5493-2048x2048.jpg",
        "source": "https://laptopcutot.com/product/laptop-cu-dell-inspiron-5493-i3-1005g1-ram-4gb-ssd-128gb-vga-microsoft-basic-display-adapter-14fhd/",
		"specs": {
            "cpu": "Intel Core i3 1005G1",
            "Ram": 4,
            "storages": [
                {
                    "storage": 128,
                    "storage_type": "ssd"
                }
            ]
         
        },
        "description": "Machines sold are strictly guaranteed for 3 months.\nThe machine is pre-installed with Windows 10 Pro OS and necessary software.\nComes with AP2REP headset\nIncluded accessories: laptop bag and wired mouse"
    }
];

export default data

Tạo file data/user_data.js:

const User = {
    id: 1,
    name: 'toanngo123',
    wishlist: ['0001','0003','0005']
}

export default User

Tạo các file context

Tạo file context CartContext.js:

import React, { createContext } from "react";

const CartContext = createContext();
export default CartContext

Tạo file context ProductContext.js:

import { createContext } from "react";

const UserContext = createContext();
export default UserContext;

Tạo file context UserContext.js:

import { createContext } from "react";

const UserContext = createContext();
export default UserContext;

Tạo các Components

Tạo file components/Products/Products.js

import React, { useState } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import products_data from '../../data/products_data'
import ProductItem from "../ProductItem/ProductItem"

const useStyles = createUseStyles({
    productsOuter: {
        maxWidth: 425,
        margin: 'auto'
    },
    productsWrap: {
        marginTop: 10,
        marginLeft: -5,
        marginRight: -5
    }
}) 

const Products  = () => {
    const classes = useStyles();
    const [products,seProducts] = useState(products_data)

    return(<div className={classes.productsOuter}>
        <div className={classes.productsWrap+' d-flex'}>
            {
                products.map((item) => {
                    return (<ProductItem key={item.id} product={item} />)
                })
            }
        </div>
    </div>)
}

export default Products

Tạo file components/ProductItem/ProductItem.js

import React, { createContext, useContext } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import UserContext from "../../context/UserContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";
import CartContext from "../../context/CartContext";

const useStyles = createUseStyles({
    productWrap: {
        width: '50%',
        padding: '0px 5px',
        boxSizing: 'border-box',
        marginBottom: 15,
        position: 'relative'
    },
    productThumbnail: {
        display: 'block',
        position: 'relative',
        overflow: 'hidden',
        paddingTop: '100%',
        border: '1px solid #e1e1e1',
        '& img': {
            width: '100%',
            objectFit: 'cover',
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            margin: 'auto'
        }
    },
    productInfo: {
        '& h3': {
            margin: 0,
            '& a': {
                fontSize: 14,
                textDecoration: 'none',
                color: '#333'
            }
        },
        '& p': {
            margin: 0,
            marginBottom: 10
        }
    },
    btnAddCart: {
        background: '#ffc856',
        color: '#fff',
        display: 'inline-block',
        border: 5,
        borderRadius: 5,
        padding: 10,
        cursor: 'pointer'
    },
    wishList : {
        '&:before':{
            content: '"\\2665"',
            fontSize: 24,
            color: 'red',
            display: 'block',
            position: 'absolute',
            right: 15,
            top: 0
        }
    }
})

const ProductItem = (props) => {
    const classes = useStyles();
    const user = useContext(UserContext);
    const item = props.product;

    const {productsCart,setCart} = useContext(ProductContext)
    const {setTotalCart} = useContext(CartContext)

    const addCart = (event,item) =>{
        setCart({product: item,type: 'add'})
        setTotalCart({product:item,type:'add',productsCart: productsCart})
    }

    return (<div key={item.id} className={classes.productWrap+' '+(user.wishlist.includes(item.id) ? classes.wishList : '')}>
        <a href={item.source} target="_blank" rel="noreferrer" className={classes.productThumbnail}>
            <img alt={item.name} src={item.thumbnail} />
        </a>
        <div className={classes.productInfo}>
            <h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
            <p>Price: {getLocalePrice(item.price)} VND</p>
            <button type="button" onClick={(event) => addCart(event,item)} className={classes.btnAddCart}>Add cart</button>
        </div>
    </div>)
}
export default ProductItem

Tạo file components/Cart/Cart.js:

import React, { useContext, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    cartWrap: {
        border: '1px solid #e1e1e1',
        padding: 15,
        marginBottom: 10
    },
    cartTitle: {
        margin: 0
    },
    cartContent: {
        margin: 0
    },
    cartItems: {
        position: 'relative'
    },
    productCart: {
        position: 'relative',
        marginBottom: 15,
        '& h3': {
            margin: '0px 0px 5px 0px'
        }
    },
    removeProduct: {
        position: 'absolute',
        right: 0,
        top: 0,
        width: 30,
        height: 30,
        background: 'red',
        color: '#fff',
        border: 'none'
    }
})



const Cart = () => {
    const classes = useStyles();
    const {productsCart,setCart} = useContext(ProductContext);
    const {totalCart,setTotalCart} = useContext(CartContext);
    const {totalQuantity,totalPrice} = totalCart;

    const removeProductCart = (event,item) => {
        setCart({product: item,type: 'remove'})
        setTotalCart({product:item,type:'remove',productsCart: productsCart})
    }

    return (<div className={classes.cartWrap}>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>total: {getLocalePrice(totalPrice)} VND</p>
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <button onClick={(event) => removeProductCart(event,item)} className={classes.removeProduct}>x</button>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
    </div>)
}

export default Cart

Sửa file App.js

import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/Header/Header';
import Footer from './layout/Footer/Footer';
import User from './data/user_data';
import UserContext from './context/UserContext';
import ProductPage from './layout/ProductPage/ProductPage';

const user = User

const useStyles = createUseStyles({
  App: {
    maxWidth: 425,
    margin: 'auto'
  }
});

function App() {
  const classes = useStyles()
  return (
    <UserContext.Provider value={user}>
      <div className={classes.App}>
        <Header />
        <ProductPage />
        <Footer />
      </div>
    </UserContext.Provider>

  );
}

export default App;

Sửa file App.css

.d-flex{
  display: flex;
  flex-wrap: wrap;
}

Output:

Tạo form với JSX

Ở bước này, chúng ta bắt đầu tiến hành làm form checkout và cho phép khách hàng thanh toán. Chúng ta tiến hành thêm nút checkout vào Cart component như sau:

import React, { useContext, useEffect, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    cartWrap: {
        border: '1px solid #e1e1e1',
        padding: 15,
        marginBottom: 10
    },
    cartTitle: {
        margin: 0
    },
    cartContent: {
        margin: 0
    },
    cartItems: {
        position: 'relative'
    },
    productCart: {
        position: 'relative',
        marginBottom: 15,
        '& h3': {
            margin: '0px 0px 5px 0px'
        }
    },
    removeProduct: {
        position: 'absolute',
        right: 0,
        top: 0,
        width: 30,
        height: 30,
        background: 'red',
        color: '#fff',
        border: 'none'
    },
    btnCheckOut:{
        color: '#fff',
        border: 5,
        cursor: 'pointer',
        display: 'inline-block',
        padding: 10,
        background: '#ffc856',
        borderRadius: 5
    }
})



const Cart = () => {
    const classes = useStyles();
    const {productsCart,setCart} = useContext(ProductContext);
    const {totalCart,setTotalCart} = useContext(CartContext);
    const {totalQuantity,totalPrice} = totalCart;

    const removeProductCart = (event,item) => {
        setCart({product: item,type: 'remove'})
        setTotalCart({product:item,type:'remove',productsCart: productsCart})
    }

    return (<div className={classes.cartWrap}>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>total: {getLocalePrice(totalPrice)} VND</p>
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <button onClick={(event) => removeProductCart(event,item)} className={classes.removeProduct}>x</button>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
        {productsCart.length > 0 && <button className={classes.btnCheckOut}>checkout</button>}
    </div>)
}

export default Cart

Output:

Bước tiếp theo, tạo một component Checkout như sau:

import React from "react";
import { createUseStyles } from "react-jss";

const useStyles = createUseStyles({
    formCheckOut: {
        border: '1px solid #e1e1e1',
        padding: 15
    },
    inputWrap: {
        '& input': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        },
        '& select': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        }
    },
    checkboxWrap: {
        marginBottom: 5
    }
});

const CheckOut = () => {
    const classes = useStyles();

    const handleSubmit = (event) =>{
        event.preventDefault();
        alert("Form submited")
    }

    return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
        <p><strong>Customer name:</strong></p>
        <div className={classes.inputWrap}>
            <input type="text" name="cusName" />
        </div>
        <p><strong>Customer phone:</strong></p>
        <div className={classes.inputWrap}>
            <input type="tel" name="cusPhone" />
        </div>
        <p><strong>Payment method:</strong></p>
        <div className={classes.inputWrap}>
            <select name="paymentMethod">
                <option value="paypal">Paypal</option>
                <option value="cod">Cash on delivery</option>
            </select>
        </div>
        <p><strong>Extra Order:</strong></p>
        <div className={classes.checkboxWrap}>
           <input type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
           <input  type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
           <input  type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
        </div>
        <div className={classes.inputWrap}>
            <input type="submit" />
        </div>
    </form>)
}

export default CheckOut

Sửa file layout/ProductPage/ProductPage.js:

import React, { createContext, useReducer } from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/Cart/Cart";
import CheckOut from "../../components/CheckOut/CheckOut";
import Products from "../../components/Products/Products";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";

const useStyles = createUseStyles({

})

const cartReducer = (state, item) => {
    const { product, type } = item;
    const cart = [...state];
    switch (type) {
        default:
        case 'add':
            // state value is array of product object
            if (cart.length === 0) {
                product.quantity = 1;
            }
            let checkExist = false;
            for (let i = 0; i < cart.length; i++) {
                if (product.id === cart[i].id) {
                    checkExist = true;
                    break;
                }
            }
            product.quantity = checkExist ? product.quantity + 1 : 1;
            if (product.quantity > 1) {
                return [...state];
            }
            return [...state, product];
        // break;
        case 'remove':
            for (let i = 0; i < cart.length; i++) {
                if (cart[i].id === product.id) {
                    cart.splice(i, 1);
                    break;
                }
            }
            return [...cart]
            break;
    }
}

const totalCartReducer = (state, item) => {
    const { product, type,productsCart } = item;
    const cartTotal = state;
    let checkExist = false;
    for (let i = 0; i < productsCart.length; i++) {
        if (product.id === productsCart[i].id) {
            checkExist = true;
            break;
        }
    }
    switch (type) {
        default:
        case 'add':
           
            cartTotal.totalPrice += product.price;
            cartTotal.totalQuantity  = checkExist ? productsCart.length : productsCart.length+1;
            return {...cartTotal}
            break;
        case 'remove':
            cartTotal.totalPrice -= product.price*product.quantity;
            cartTotal.totalQuantity  = productsCart.length-1;
            return  {...cartTotal}
            break;
    }
    return { ...state };
}


const ProductPage = () => {
    const classes = useStyles();

    const [productsCart, setCart] = useReducer(cartReducer, [])
    const [totalCart, setTotalCart] = useReducer(totalCartReducer, { totalQuantity: 0, totalPrice: 0,productsCart: []})

    return (

        <ProductContext.Provider value={{ productsCart, setCart }}>
            <CartContext.Provider value={{ totalCart, setTotalCart }}>
                <Cart />
                <CheckOut />
                <Products />
            </CartContext.Provider>
        </ProductContext.Provider >

    )
}

export default ProductPage;

Tạo file components/CheckOut/CheckOut.js:

import React, { useState } from "react";
import { createUseStyles } from "react-jss";

const useStyles = createUseStyles({
    formCheckOut: {
        border: '1px solid #e1e1e1',
        padding: 15
    },
    inputWrap: {
        '& input': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        },
        '& select': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        }
    },
    checkboxWrap: {
        marginBottom: 5
    }
});

const CheckOut = () => {
    const classes = useStyles();

    const [submitting, setSubmitting] = useState(false);

    const handleSubmit = (event) => {
        event.preventDefault();
        setSubmitting(true);
        setTimeout(() => {
            setSubmitting(false)
        }, 5000);
        alert("Form submited");
    }

    return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
        <p><strong>Customer name:</strong></p>
        <div className={classes.inputWrap}>
            <input type="text" name="cusName" />
        </div>
        <p><strong>Customer phone:</strong></p>
        <div className={classes.inputWrap}>
            <input type="tel" name="cusPhone" />
        </div>
        <p><strong>Payment method:</strong></p>
        <div className={classes.inputWrap}>
            <select name="paymentMethod">
                <option value="paypal">Paypal</option>
                <option value="cod">Cash on delivery</option>
            </select>
        </div>
        <p><strong>Extra Order:</strong></p>
        <div className={classes.checkboxWrap}>
            <input type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
            <input type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
            <input type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
        </div>
        <div className={classes.inputWrap}>
            <input type="submit" />
        </div>
        {submitting &&
            <div>Submtting Form...</div>
        }
    </form>)
}

export default CheckOut

Giải thích: với file CheckOut.js, chúng ta thấy sau khi submit mình có setState lại cho biết submitting, để mô phỏng việc gửi request lên API (có độ trễ nhất định).

Output và cấu trúc thư mục:

Thu thập dữ liệu trong form sử dụng uncontrolled components

Trong bước này, chúng thu thập dữ liệu từ form bằng cách sử dụng các component không được kiểm soát . Component không được kiểm soát là một thành phần không có gia trị được thiết lập bởi React. Thay vì đặt dữ liệu trên component, chúng ta sẽ kết nối với sự kiện onChange để thu thập thông tin đầu vào của người dùng. Khi bạn xây dựng các component, chúng ta sẽ học cách React xử lý các loại input khác nhau và cách tạo một hàm có thể sử dụng lại để thu thập dữ liệu biểu mẫu vào một object duy nhất .

Lưu ý: Trong hầu hết các trường hợp, bạn sẽ sử dụng các controlled components (thành phần được kiểm soát) cho ứng dụng React của mình. Nhưng chúng ta bắt đầu với các thành phần không được kiểm soát để có thể tránh các lỗi nhỏ hoặc các vòng lặp ngẫu nhiên mà bạn có thể đưa vào khi đặt sai giá trị.

Mình sẽ bắt đầu tiến hành sửa lần lượt các file. Bắt đầu từ components/CheckOut/CheckOut.js như sau:

import React, { useContext, useReducer, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    formCheckOut: {
        border: '1px solid #e1e1e1',
        padding: 15,
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        width: 425,
        margin: 'auto',
        zIndex: 1,
        boxSizing: 'border-box',
        background: '#fff'
    },
    inputWrap: {
        '& input': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        },
        '& select': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        }
    },
    checkboxWrap: {
        marginBottom: 5
    },
    closeCheckOut: {
        position: 'absolute',
        right: 15,
        background: 'red',
        color: '#fff',
        border: 'none',
        height: 30,
        width: 30,
        cursor: 'pointer'
    }
});

const formReducer = (state, item) => {
    return {...state, [item.name]: item.value}
}


const CheckOut = () => {
    const classes = useStyles();

    const [submitting, setSubmitting] = useState(false);
    const [formData,setFormData] = useReducer(formReducer,{});
    const {checkOut,setCheckOut} = useContext(CheckOutContext);
    const {totalCart} = useContext(CartContext);
    const {totalQuantity,totalPrice} = totalCart;
    const {productsCart} = useContext(ProductContext);

    const handleSubmit = (event) => {
        event.preventDefault();
        setSubmitting(true);
        setTimeout(() => {
            setSubmitting(false)
        }, 5000);
        console.log(formData);
        alert("Form submited");
    }

    const handleChange = (event) => {
        setFormData({
            name: event.target.name,
            value: event.target.value,
        });
    }

    const handleCloseCheckOut = (event) => {
        setCheckOut({type:'close'})
    }

    return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
        <button onClick={(event) => handleCloseCheckOut(event)} type="button" className={classes.closeCheckOut}>x</button>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>Total cart price: {getLocalePrice(totalPrice)} VND</p>
        <hr/>
        <p className={classes.cartContent}><strong>Total Order: {getLocalePrice(totalPrice)} VND</strong></p>
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
        <div>
            <p>Total order price: {0}</p>
        </div>
        <p><strong>Customer name:</strong></p>
        <div className={classes.inputWrap}>
            <input onChange={(event) => handleChange(event)} type="text" name="cusName" />
        </div>
        <p><strong>Customer phone:</strong></p>
        <div className={classes.inputWrap}>
            <input onChange={(event) => handleChange(event)}  type="tel" name="cusPhone" />
        </div>
        <p><strong>Payment method:</strong></p>
        <div className={classes.inputWrap}>
            <select  onChange={(event) => handleChange(event)}  name="paymentMethod">
                <option value="paypal">Paypal</option>
                <option value="cod">Cash on delivery</option>
            </select>
        </div>
        <p><strong>Extra Order:</strong></p>
        <div className={classes.checkboxWrap}>
            <input  onChange={(event) => handleChange(event)}  type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
            <input  onChange={(event) => handleChange(event)}  type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
            <input  onChange={(event) => handleChange(event)}  type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
        </div>
        <div className={classes.inputWrap}>
            <input type="submit" />
        </div>
        {submitting &&
            <div>Submtting Form...</div>
        }
    </form>)
}

export default CheckOut

Sửa file components/Cart/Cart.js:

import React, { useContext, useEffect, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    cartWrap: {
        border: '1px solid #e1e1e1',
        padding: 15,
        marginBottom: 10
    },
    cartTitle: {
        margin: 0
    },
    cartContent: {
        margin: 0
    },
    cartItems: {
        position: 'relative'
    },
    productCart: {
        position: 'relative',
        marginBottom: 15,
        '& h3': {
            margin: '0px 0px 5px 0px'
        }
    },
    removeProduct: {
        position: 'absolute',
        right: 0,
        top: 0,
        width: 30,
        height: 30,
        background: 'red',
        color: '#fff',
        border: 'none'
    },
    btnCheckOut:{
        color: '#fff',
        border: 5,
        cursor: 'pointer',
        display: 'inline-block',
        padding: 10,
        background: '#ffc856',
        borderRadius: 5
    }
})



const Cart = () => {
    const classes = useStyles();
    const {productsCart,setCart} = useContext(ProductContext);
    const {checkOut,setCheckOut} = useContext(CheckOutContext);
    const {totalCart,setTotalCart} = useContext(CartContext);
    const {totalQuantity,totalPrice} = totalCart;

    const removeProductCart = (event,item) => {
        setCart({product: item,type: 'remove'})
        setTotalCart({product:item,type:'remove',productsCart: productsCart})
    }

    const activeCheckOut = (event) => {
        setCheckOut({type:'active',item_obj: {activeCheckOut:true,totalQuantity: totalQuantity,totalPrice: totalPrice,productsCart: productsCart}});
    }

    return (<div className={classes.cartWrap}>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>total: {getLocalePrice(totalPrice)} VND</p>
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <button onClick={(event) => removeProductCart(event,item)} className={classes.removeProduct}>x</button>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
        {productsCart.length > 0 && <button onClick={(event) => activeCheckOut(event)} className={classes.btnCheckOut}>checkout</button>}
    </div>)
}

export default Cart

Sửa file layout/ProductPage/ProductsPage.js:

import React, { createContext, useReducer } from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/Cart/Cart";
import CheckOut from "../../components/CheckOut/CheckOut";
import Products from "../../components/Products/Products";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";

const useStyles = createUseStyles({

})

const cartReducer = (state, item) => {
    const { product, type } = item;
    const cart = [...state];
    switch (type) {
        default:
        case 'add':
            // state value is array of product object
            if (cart.length === 0) {
                product.quantity = 1;
            }
            let checkExist = false;
            for (let i = 0; i < cart.length; i++) {
                if (product.id === cart[i].id) {
                    checkExist = true;
                    break;
                }
            }
            product.quantity = checkExist ? product.quantity + 1 : 1;
            if (product.quantity > 1) {
                return [...state];
            }
            return [...state, product];
        // break;
        case 'remove':
            for (let i = 0; i < cart.length; i++) {
                if (cart[i].id === product.id) {
                    cart.splice(i, 1);
                    break;
                }
            }
            return [...cart]
            break;
    }
}

const totalCartReducer = (state, item) => {
    const { product, type, productsCart } = item;
    const cartTotal = state;
    let checkExist = false;
    for (let i = 0; i < productsCart.length; i++) {
        if (product.id === productsCart[i].id) {
            checkExist = true;
            break;
        }
    }
    switch (type) {
        default:
        case 'add':

            cartTotal.totalPrice += product.price;
            cartTotal.totalQuantity = checkExist ? productsCart.length : productsCart.length + 1;
            return { ...cartTotal }
            break;
        case 'remove':
            cartTotal.totalPrice -= product.price * product.quantity;
            cartTotal.totalQuantity = productsCart.length - 1;
            return { ...cartTotal }
            break;
    }
    return { ...state };
}

const checkOutReducer = (state, item) => {

    const { item_obj, type } = item;
    switch (type) {
        default:
        case 'close':
            return {...state,activeCheckOut:false}
            break;
        case 'active':
             const {activeCheckOut, totalQuantity, totalPrice, productsCart} = item_obj;
            return { ...state, activeCheckOut,activeCheckOut, totalQuantity, totalPrice, productsCart }

    }
}


const ProductPage = () => {
    const classes = useStyles();

    const [productsCart, setCart] = useReducer(cartReducer, [])
    const [totalCart, setTotalCart] = useReducer(totalCartReducer, { totalQuantity: 0, totalPrice: 0, productsCart: [] })
    const [checkOut, setCheckOut] = useReducer(checkOutReducer, { activeCheckOut: false, totalQuantity: 0, totalPrice: 0, productsCart: [] })

    return (

        <ProductContext.Provider value={{ productsCart, setCart }}>
            <CartContext.Provider value={{ totalCart, setTotalCart }}>
                <CheckOutContext.Provider value={{ checkOut, setCheckOut }}>
                    <Cart />
                    {checkOut.activeCheckOut && <CheckOut />}
                    <Products />
                </CheckOutContext.Provider>
            </CartContext.Provider>
        </ProductContext.Provider >

    )
}

export default ProductPage;

Output:

Thông tin giở hàng:

Dữ liệu form data sau khi submit:

Ở trong tình huống này, mình muốn tối ưu thêm mã nguồn cho 2 vấn đề sau:

  • Extra order hiện tại chỉ lấy dữ liệu của một checkbox.
  • Thông tin total order theo nghiệp vụ sẽ bằng tổng giá sản phẩm + extra order
  • Dữ liệu của đơn hàng thiếu thông tin sản phẩm, số lượng và giá bán

Mình sẽ tiến hành sửa file components/CheckOut/CheckOut.js như sau:

import React, { useContext, useReducer, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    formCheckOut: {
        border: '1px solid #e1e1e1',
        padding: 15,
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        width: 425,
        margin: 'auto',
        zIndex: 1,
        boxSizing: 'border-box',
        background: '#fff'
    },
    inputWrap: {
        '& input': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        },
        '& select': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        }
    },
    checkboxWrap: {
        marginBottom: 5
    },
    closeCheckOut: {
        position: 'absolute',
        right: 15,
        background: 'red',
        color: '#fff',
        border: 'none',
        height: 30,
        width: 30,
        cursor: 'pointer'
    }
});

const formReducer = (state, item) => {
    return { ...state, [item.name]: item.value }
}


const CheckOut = () => {
    const classes = useStyles();

    const [submitting, setSubmitting] = useState(false);
    const [formData, setFormData] = useReducer(formReducer, {});
    const { checkOut, setCheckOut } = useContext(CheckOutContext);
    const { totalCart } = useContext(CartContext);
    const { totalQuantity, totalPrice } = totalCart;
    const { productsCart } = useContext(ProductContext);
    const [totalPriceCheckOut, setTotalPriceCheckOut] = useState(totalPrice);

    const handleSubmit = (event) => {
        event.preventDefault();
        formData.products = productsCart
        setSubmitting(true);
        setTimeout(() => {
            setSubmitting(false)
        }, 5000);
        console.log(formData);
        alert("Form submited");
    }

    const handleChange = (event) => {
        let totalExtraPrice = 0;
        const isCheckbox = event.target.type == 'checkbox' ? true : false;
        if (!isCheckbox) {
            setFormData({
                name: event.target.name,
                value: event.target.value,
            });
        } else {
            const lst_checked = document.querySelectorAll(`input[name=${event.target.name}]:checked`);
            let lst_checked_data = [];
            lst_checked.forEach((elm,index) => {
                console.log(elm);
                lst_checked_data.push(elm.value)
            })
            lst_checked.forEach((elm, index) => {
                totalExtraPrice += parseInt(elm.value);
            })
            setFormData({
                name: event.target.name,
                value: lst_checked_data,
            });
        }
        /* set checkout price */
        setTotalPriceCheckOut(totalPrice + totalExtraPrice)
        setFormData({
            name: 'totalCheckOutPrice',
            value: totalPriceCheckOut,
        });
        /* end set checkout price */
    }

    const handleCloseCheckOut = (event) => {
        setCheckOut({ type: 'close' })
    }

    return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
        <button onClick={(event) => handleCloseCheckOut(event)} type="button" className={classes.closeCheckOut}>x</button>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>Total cart price: {getLocalePrice(totalPrice)} VND</p>
        <hr />
        <p className={classes.cartContent}><strong>Total Order: {getLocalePrice(totalPriceCheckOut)} VND</strong></p>
        <input type="hidden" name="totalPriceCheckOut" value={totalPriceCheckOut} />
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
        <div>
            <p>Total order price: {0}</p>
        </div>
        <p><strong>Customer name:</strong></p>
        <div className={classes.inputWrap}>
            <input onChange={(event) => handleChange(event)} type="text" name="cusName" />
        </div>
        <p><strong>Customer phone:</strong></p>
        <div className={classes.inputWrap}>
            <input onChange={(event) => handleChange(event)} type="tel" name="cusPhone" />
        </div>
        <p><strong>Payment method:</strong></p>
        <div className={classes.inputWrap}>
            <select onChange={(event) => handleChange(event)} name="paymentMethod">
                <option value="paypal">Paypal</option>
                <option value="cod">Cash on delivery</option>
            </select>
        </div>
        <p><strong>Extra Order:</strong></p>
        <div className={classes.checkboxWrap}>
            <input onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
            <input onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
            <input onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
        </div>
        <div className={classes.inputWrap}>
            <input type="submit" />
        </div>
        {submitting &&
            <div>Submtting Form...</div>
        }
    </form>)
}

export default CheckOut

Output:

Với ví dụ phía trên, sau khi bấm submit order, dữ liệu đơn hàng đã được log ra console bao gồm đầy đủ thông tin mình mong muốn.

Lưu ý: ở đây mình chỉ ví dụ cách này để chúng ta có hình dung về việc submit form với giải pháp sử dụng uncontrolled components, tuy nhiên đây không phải là giải pháp đầy đủ được sử dụng trong thực tế. Để tìm hiểu rõ hơn, chúng ta sẽ đến với phần tiếp theo.

Thu thập dữ liệu trong form sử dụng controlled components

Trong bước này, bạn sẽ tự động thiết lập và cập nhật dữ liệu bằng cách sử dụng các controlled components. Bạn sẽ thêm một props value vào mỗi component để thiết lập hoặc cập nhật dữ liệu form  và reset dữ liệu form sau khi gửi đi. Đến cuối bước này, bạn sẽ có thể kiểm soát động dữ liệu form bằng cách sử dụng React state và props.

Với các uncontrolled components, chúng ta không phải lo lắng về việc đồng bộ hóa dữ liệu. Ứng dụng của sẽ luôn tuân theo những thay đổi gần đây nhất. Nhưng có nhiều trường hợp chúng ta sẽ cần cả đọc và ghi vào một component input. Để làm điều này, bạn sẽ cần gán giá trị động cho component.

Ở bước trước, chúng ta đã gửi một form. Nhưng sau khi gửi form thành công, form vẫn chứa dữ liệu cũ. Để xóa dữ liệu khỏi mỗi input, chúng ta sẽ cần thay đổi các components từ các uncontrolled components thành các controlled components.

Một controlled component tương tự như một uncontrolled component, nhưng React cập nhật phần props value. Nhược điểm là nếu không cẩn thận và không cập nhật đúng cách value thì component sẽ bị hỏng và không cập nhật.

Trong form này, chúng ta đang lưu trữ dữ liệu, vì vậy để chuyển đổi các component, chúng ta sẽ cập nhật props value với dữ liệu từ state formData. Tuy nhiên, có một vấn đề: value không được phép mang giá trị undefined. Nếu giá trị của bạn là undefined, bạn sẽ nhận được lỗi trong console.

Vì state ban đầu của bạn là một object trống, bạn sẽ cần đặt giá trị thành giá trị từ formData hoặc giá trị mặc định, chẳng hạn như một chuỗi rỗng. Chúng ta tiến hành sửa file components/CheckOut/CheckOut.js như sau:

import React, { useContext, useReducer, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    formCheckOut: {
        border: '1px solid #e1e1e1',
        padding: 15,
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        width: 425,
        margin: 'auto',
        zIndex: 1,
        boxSizing: 'border-box',
        background: '#fff'
    },
    inputWrap: {
        '& input': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        },
        '& select': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        }
    },
    checkboxWrap: {
        marginBottom: 5
    },
    closeCheckOut: {
        position: 'absolute',
        right: 15,
        background: 'red',
        color: '#fff',
        border: 'none',
        height: 30,
        width: 30,
        cursor: 'pointer'
    }
});

const formReducer = (state, item) => {
    if(item.reset){
        return {
            cusName: '',
            cusphone: '',
            paymentMethod: 'cod',
            products: [],
            extraorder: [],
            totalCheckOutPrice: 0
        }
    }
    return { ...state, [item.name]: item.value }
}


const CheckOut = () => {
    const classes = useStyles();

    const [submitting, setSubmitting] = useState(false);
    const [formData, setFormData] = useReducer(formReducer, {
        cusName: '',
        cusphone: '',
        paymentMethod: 'cod',
        products: [],
        extraorder: [],
        totalCheckOutPrice: 0
    });
    const { checkOut, setCheckOut } = useContext(CheckOutContext);
    const { totalCart } = useContext(CartContext);
    const { totalQuantity, totalPrice } = totalCart;
    const { productsCart } = useContext(ProductContext);
    const [totalPriceCheckOut, setTotalPriceCheckOut] = useState(totalPrice);

    const handleSubmit = (event) => {
        event.preventDefault();
        formData.products = productsCart
        setSubmitting(true);
        setTimeout(() => {
            setSubmitting(false);
            setFormData({reset: true});
        }, 5000);
        console.log(formData);
        alert("Form submited");
    }

    const handleChange = (event) => {
        let totalExtraPrice = 0;
        const isCheckbox = event.target.type == 'checkbox' ? true : false;
        if (!isCheckbox) {
            setFormData({
                name: event.target.name,
                value: event.target.value,
            });
        } else {
            const lst_checked = document.querySelectorAll(`input[name=${event.target.name}]:checked`);
            let lst_checked_data = [];
            lst_checked.forEach((elm,index) => {
                console.log(elm);
                lst_checked_data.push(elm.value)
            })
            lst_checked.forEach((elm, index) => {
                totalExtraPrice += parseInt(elm.value);
            })
            setFormData({
                name: event.target.name,
                value: lst_checked_data,
            });
        }
        /* set checkout price */
        setTotalPriceCheckOut(totalPrice + totalExtraPrice)
        setFormData({
            name: 'totalCheckOutPrice',
            value: totalPriceCheckOut,
        });
        /* end set checkout price */
    }

    const handleCloseCheckOut = (event) => {
        setCheckOut({ type: 'close' })
    }

    return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
        <button onClick={(event) => handleCloseCheckOut(event)} type="button" className={classes.closeCheckOut}>x</button>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>Total cart price: {getLocalePrice(totalPrice)} VND</p>
        <hr />
        <p className={classes.cartContent}><strong>Total Order: {getLocalePrice(totalPriceCheckOut)} VND</strong></p>
        <input type="hidden" name="totalPriceCheckOut" value={totalPriceCheckOut} />
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
        <div>
            <p>Total order price: {0}</p>
        </div>
        <p><strong>Customer name:</strong></p>
        <div className={classes.inputWrap}>
            <input onChange={(event) => handleChange(event)} type="text" name="cusName" value={formData.cusName ? formData.cusName : ''} />
        </div>
        <p><strong>Customer phone:</strong></p>
        <div className={classes.inputWrap}>
            <input onChange={(event) => handleChange(event)} type="tel" name="cusPhone" value={formData.cusPhone ? formData.cusPhone : ''}  />
        </div>
        <p><strong>Payment method:</strong></p>
        <div className={classes.inputWrap}>
            <select defaultValue={formData.paymentMethod == 'paypal' ? 'paypal' : 'cod' } onChange={(event) => handleChange(event)} name="paymentMethod">
                <option value="paypal">Paypal</option>
                <option value="cod">Cash on delivery</option>
            </select>
        </div>
        <p><strong>Extra Order:</strong></p>
        <div className={classes.checkboxWrap}>
            <input checked={formData.extraorder.includes('100') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
            <input checked={formData.extraorder.includes('200') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
            <input checked={formData.extraorder.includes('300') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
        </div>
        <div className={classes.inputWrap}>
            <input type="submit" />
        </div>
        {submitting &&
            <div>Submtting Form...</div>
        }
    </form>)
}

export default CheckOut

Giải thích: dựa vào nội dung cập nhật, chúng ta thấy mặc định mình sẽ khởi tạo object với các thuộc tính sẵn có của form trong reducer, giá trị của những input trong form cũng được gán bằng state bên trong formData hoặc giá trị mặc định. sau khi submit, dữ liệu form được trả lại giá trị như ban đầu.

Cập nhật động các thuộc tính form

Phần này, chúng ta chỉ dành để nói về giai đoạn khi submit form, trong giai đoạn này, mình sẽ nên disable các nút submit, các input bên trong form để khóa không cho người dùng tương tác trong thời gian này. Mình sẽ sửa file như sau:

import React, { useContext, useReducer, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";

const useStyles = createUseStyles({
    formCheckOut: {
        border: '1px solid #e1e1e1',
        padding: 15,
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        width: 425,
        margin: 'auto',
        zIndex: 1,
        boxSizing: 'border-box',
        background: '#fff'
    },
    inputWrap: {
        '& input': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        },
        '& select': {
            width: '100%',
            display: 'block',
            padding: 10,
            border: '1px solid #e1e1e1',
            boxSizing: 'border-box',
            marginBottom: 5
        }
    },
    checkboxWrap: {
        marginBottom: 5
    },
    closeCheckOut: {
        position: 'absolute',
        right: 15,
        background: 'red',
        color: '#fff',
        border: 'none',
        height: 30,
        width: 30,
        cursor: 'pointer'
    }
});

const formReducer = (state, item) => {
    if(item.reset){
        return {
            cusName: '',
            cusphone: '',
            paymentMethod: 'cod',
            products: [],
            extraorder: [],
            totalCheckOutPrice: 0
        }
    }
    return { ...state, [item.name]: item.value }
}


const CheckOut = () => {
    const classes = useStyles();

    const [submitting, setSubmitting] = useState(false);
    const [formData, setFormData] = useReducer(formReducer, {
        cusName: '',
        cusphone: '',
        paymentMethod: 'cod',
        products: [],
        extraorder: [],
        totalCheckOutPrice: 0
    });
    const { checkOut, setCheckOut } = useContext(CheckOutContext);
    const { totalCart } = useContext(CartContext);
    const { totalQuantity, totalPrice } = totalCart;
    const { productsCart } = useContext(ProductContext);
    const [totalPriceCheckOut, setTotalPriceCheckOut] = useState(totalPrice);

    const handleSubmit = (event) => {
        event.preventDefault();
        formData.products = productsCart
        setSubmitting(true);
        setTimeout(() => {
            setSubmitting(false);
            setFormData({reset: true});
        }, 5000);
        console.log(formData);
        alert("Form submited");
    }

    const handleChange = (event) => {
        let totalExtraPrice = 0;
        const isCheckbox = event.target.type == 'checkbox' ? true : false;
        if (!isCheckbox) {
            setFormData({
                name: event.target.name,
                value: event.target.value,
            });
        } else {
            const lst_checked = document.querySelectorAll(`input[name=${event.target.name}]:checked`);
            let lst_checked_data = [];
            lst_checked.forEach((elm,index) => {
                console.log(elm);
                lst_checked_data.push(elm.value)
            })
            lst_checked.forEach((elm, index) => {
                totalExtraPrice += parseInt(elm.value);
            })
            setFormData({
                name: event.target.name,
                value: lst_checked_data,
            });
        }
        /* set checkout price */
        setTotalPriceCheckOut(totalPrice + totalExtraPrice)
        setFormData({
            name: 'totalCheckOutPrice',
            value: totalPriceCheckOut,
        });
        /* end set checkout price */
    }

    const handleCloseCheckOut = (event) => {
        setCheckOut({ type: 'close' })
    }

    return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
        <button onClick={(event) => handleCloseCheckOut(event)} type="button" className={classes.closeCheckOut}>x</button>
        <h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
        <p className={classes.cartContent}>Total cart price: {getLocalePrice(totalPrice)} VND</p>
        <hr />
        <p className={classes.cartContent}><strong>Total Order: {getLocalePrice(totalPriceCheckOut)} VND</strong></p>
        <div className={classes.cartItems}>
            {productsCart.map((item) => {
                return (<div key={item.id} className={classes.productCart}>
                    <h3>{item.name}</h3>
                    <div className="item-price">Price: {getLocalePrice(item.price)}</div>
                    <div className="item-quantity">Quantity: {item.quantity}</div>
                </div>)
            })}
        </div>
        <div>
            <p>Total order price: {0}</p>
        </div>
        <p><strong>Customer name:</strong></p>
        <div className={classes.inputWrap}>
            <input disabled={submitting ? true : false} onChange={(event) => handleChange(event)} type="text" name="cusName" value={formData.cusName ? formData.cusName : ''} />
        </div>
        <p><strong>Customer phone:</strong></p>
        <div className={classes.inputWrap}>
            <input disabled={submitting ? true : false} onChange={(event) => handleChange(event)} type="tel" name="cusPhone" value={formData.cusPhone ? formData.cusPhone : ''}  />
        </div>
        <p><strong>Payment method:</strong></p>
        <div className={classes.inputWrap}>
            <select disabled={submitting ? true : false} defaultValue={formData.paymentMethod == 'paypal' ? 'paypal' : 'cod' } onChange={(event) => handleChange(event)} name="paymentMethod">
                <option value="paypal">Paypal</option>
                <option value="cod">Cash on delivery</option>
            </select>
        </div>
        <p><strong>Extra Order:</strong></p>
        <div className={classes.checkboxWrap}>
            <input  disabled={submitting ? true : false} checked={formData.extraorder.includes('100') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
            <input  disabled={submitting ? true : false} checked={formData.extraorder.includes('200') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
            <input  disabled={submitting ? true : false} checked={formData.extraorder.includes('300') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
        </div>
        <div className={classes.inputWrap}>
            <input  disabled={submitting ? true : false} type="submit" />
        </div>
        {submitting &&
            <div>Submtting Form...</div>
        }
    </form>)
}

export default CheckOut

Dựa vào giá trị state submitting, button và các input sẽ được disable trong khi form đang trong tiến trình submit, và enable nếu form đang không trong tiến trình này.

Form là một trong những thành phần quan trọng trong bất kì ứng dụng web nào. Trong React, chúng ta có các tùy chọn khác nhau để kết nối và kiểm soát các form và phần tử. Giống như các component khác, bạn có thể cập nhật động các thuộc tính bao gồm các value input. Các uncontrolled component là tốt nhất để đơn giản hóa, nhưng có thể không phù hợp với các tình huống khi một component cần được xóa hoặc điền trước dữ liệu. Các controlled component cung cấp cho chúng ta nhiều cơ hội để cập nhật dữ liệu, nhưng có thể thêm một mức độ trừu tượng khác có thể gây ra lỗi hoặc re-render ảnh hưởng tới performance của ứng dụng. Bất kể cách tiếp cận của bạn là gì, React cung cấp cho bạn khả năng tự động cập nhật và điều chỉnh các form của bạn cho phù hợp với nhu cầu của ứng dụng và người dùng của bạn.

Bài tập

Bài 1:

Dựng form Login như hình:

Nút login mặc định không cho bấm (disable), User cần nhập liệu tối thiểu 4 ký tự, cho cả username và password, sau khi nhập đủ thì nút login cho bấm

Nếu user nhập đúng admin/123456, in thông báo login successfully, nếu không in thông báo login fail

Bài 2: 

  1. Tạo 1 bảng trên postmain gồm các cột name,username,email,password (tạo token phục vụ cho việc insert data vào bảng thông qua API)
  2. Dựng giao diện react như hình, yêu cầu validate trường name và username tối thiểu 6 ký tự, email address đáp ứng chuẩn email, password và confirm password phải giống nhau và tối thiểu 6 ký tự. Sau khi đáp ứng yêu cầu, bấm signup, thêm bản ghi mới vào bảng trên airtable

Bài viết liên quan:

useMemo trong ReactJS
Định tuyến trong ứng dụng React với React Router
Giới thiệu Redux và quản lý state với Redux trong React
Gọi API trong React với useEffect Hook
Xử lý tải dữ liệu bất đồng bộ (Async Data Loading), lazyload trong React
Xử lý các sự kiện trong DOM và Window bằng React
Debug (gỡ lỗi) React component thông qua React Developer Tools
khái niệm React Context và chia sẻ state qua các component với React Context
Cách quản lý state(trạng thái) bằng Hooks trên React Component
React strict mode (chế độ phát triển) trong React
Quản lý state (trạng thái) trong React Class Component
Hướng dẫn định kiểu (style) cho React Component

THÊM BÌNH LUẬN Cancel reply

Dịch vụ thiết kế Wesbite

NỘI DUNG MỚI CẬP NHẬT

2. PHÂN TÍCH VÀ ĐẶC TẢ HỆ THỐNG

1. TỔNG QUAN KIẾN THỨC THỰC HÀNH TRIỂN KHAI DỰ ÁN CÔNG NGHỆ THÔNG TIN

Hướng dẫn tự cài đặt n8n comunity trên CyberPanel, trỏ tên miền

Mẫu prompt tạo mô tả chi tiết bối cảnh

Một số cải tiến trong ASP.NET Core, Razor Page, Model Binding, Gabbage collection

Giới thiệu

hocvietcode.com là website chia sẻ và cập nhật tin tức công nghệ, chia sẻ kiến thức, kỹ năng. Chúng tôi rất cảm ơn và mong muốn nhận được nhiều phản hồi để có thể phục vụ quý bạn đọc tốt hơn !

Liên hệ quảng cáo: [email protected]

Kết nối với HỌC VIẾT CODE

© hocvietcode.com - Tech888 Co .Ltd since 2019

Đăng nhập

Trở thành một phần của cộng đồng của chúng tôi!
Registration complete. Please check your email.
Đăng nhập bằng google
Đăng kýBạn quên mật khẩu?

Create an account

Welcome! Register for an account
The user name or email address is not correct.
Registration confirmation will be emailed to you.
Log in Lost your password?

Reset password

Recover your password
Password reset email has been sent.
The email could not be sent. Possible reason: your host may have disabled the mail function.
A password will be e-mailed to you.
Log in Register
×