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
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:
- 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)
- 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