Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
07.03.2022
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
При редактировании товара нужно иметь возможность редактировать и характеристики. В принципе, мы могли бы поступить просто — отправить все характаристики на сервер вместе с остальными данными товара. В этом случае сервер удалит все существующие характеристики и создаст новые из полученных. Но это не слишком изящное решение — возможно, только одна характеристика была исправлена, а мы удаляем все подряд и создаем заново. Так что будем работать с каждой характеристикой по отдельности, тем более, что на сервере мы создали модель для работы с характеристиками. Для этого на клиенте для каждой характеристики добавим свойства append
, change
и remove
.
- при добавлении новой хар-ки свойство
append
принимает значениеtrue
- при изменении старой хар-ки свойство
change
принимает значениеtrue
- при удалении старой хар-ки свойство
remove
принимает значениеtrue
При обновлении товара мы можем для каждой хар-ки выполнить подходящий http-запрос на сервер — чтобы добавить, изменить или удалить характеристику. Но, прежде чем начинать работать с хар-ками, нужно добавить в http/catalogAPI.js
функции для работы с хар-ками.
/* * Создание, обновление и удаление характеристик товара */ export const createProperty = async (productId, property) => { const { data } = await authInstance.post(`product/${productId}/property/create`, property) return data } export const updateProperty = async (productId, id, property) => { const { data } = await authInstance.put(`product/${productId}/property/update/${id}`, property) return data } export const deleteProperty = async (productId, id) => { const { data } = await authInstance.delete(`product/${productId}/property/delete/${id}`) return data }
При добавлении нового товара, чтобы обращаться к нужной хар-ке, мы в качестве идентификатора использовали timestamp
, но лучше установим пакет react-uuid
:
> npm install react-uuid
Компонент UpdateProperties.js
получился не сложным, он просто получает от родителя массив хар-тик и функцию для изменения этого массива:
import { useEffect, useState } from 'react' import { Row, Col, Button, Form } from 'react-bootstrap' import { createProperty, updateProperty, deleteProperty } from '../http/catalogAPI.js' import uuid from 'react-uuid' const UpdateProperties = (props) => { const { properties, setProperties } = props const append = () => { setProperties([...properties, {id: null, name: '', value: '', unique: uuid(), append: true}]) } const remove = (unique) => { // новую хар-ку надо просто удалить из массива properties, а старую — оставить, но // изменить remove на true, чтобы потом выполнить http-запрос на сервер для удаления const item = properties.find(elem => elem.unique === unique) if (item.id) { // старая хар-ка setProperties(properties.map( elem => elem.unique === unique ? {...elem, change: false, remove: true} : elem )) } else { // новая хар-ка setProperties(properties.filter(elem => elem.unique === unique)) } } const change = (key, value, unique) => { setProperties(properties.map( item => item.unique === unique ? {...item, [key]: value, change: !item.append} : item )) } return ( <> <h5>Характеристики</h5> <Button onClick={append} variant="outline-primary" size="sm" className="mb-2"> Добавить </Button> {properties.map(item => <Row key={item.unique} className="mb-2" style={{display: item.remove ? 'none': 'flex'}}> <Col> <Form.Control name={'name_' + item.unique} value={item.name} onChange={e => change('name', e.target.value, item.unique)} placeholder="Название..." size="sm" /> </Col> <Col> <Form.Control name={'value_' + item.unique} value={item.value} onChange={e => change('value', e.target.value, item.unique)} placeholder="Значение..." size="sm" /> </Col> <Col> <Button onClick={() => remove(item.unique)} size="sm" variant="outline-danger"> Удалить </Button> {item.change && ' *'} </Col> </Row> )} </> ) } export default UpdateProperties
А вот компонент UpdateProduct.js
стал немного сложнее. Добавилась функция updateProperties
, которая проходит по всему массиву properties
и для каждой хар-ки выполняет подходящий http-запрос. Причем, для каждого http-запроса мы ждем ответ, так что к моменту возврата из функции все хар-ки уже обновились на сервере. И когда мы выполним еще один запрос, чтобы обновить название, цену, категорию и бренд товара — то в ответе получим уже обновленные хар-ки.
import { Modal, Button, Form, Row, Col } from 'react-bootstrap' import { fetchOneProduct, updateProduct, fetchCategories, fetchBrands } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' import uuid from 'react-uuid' import UpdateProperties from './UpdateProperties.js' import { createProperty, updateProperty, deleteProperty } from '../http/catalogAPI.js' const defaultValue = {name: '', price: '', category: '', brand: ''} const defaultValid = {name: null, price: null, category: null, brand: null} const isValid = (value) => { const result = {} const pattern = /^[1-9][0-9]*$/ for (let key in value) { if (key === 'name') result.name = value.name.trim() !== '' if (key === 'price') result.price = pattern.test(value.price.trim()) if (key === 'category') result.category = pattern.test(value.category) if (key === 'brand') result.brand = pattern.test(value.brand) } return result } const updateProperties = async (properties, productId) => { for (const prop of properties) { const empty = prop.name.trim() === '' || prop.value.trim() === '' // если вдруг старая хар-ка оказалась пустая — удалим ее на сервере if (empty && prop.id) { try { await deleteProperty(productId, prop) } catch(error) { alert(error.response.data.message) } continue } /* * Если у объекта prop свойство append равно true — это новая хар-ка, ее надо создать. * Если у объекта prop свойство change равно true — хар-ка изменилась, ее надо обновить. * Если у объекта prop свойство remove равно true — хар-ку удалили, ее надо удалить. */ if (prop.append && !empty) { try { await createProperty(productId, prop) } catch(error) { alert(error.response.data.message) } continue } if (prop.change && !prop.remove) { try { await updateProperty(productId, prop.id, prop) } catch(error) { alert(error.response.data.message) } continue } if (prop.remove) { try { await deleteProperty(productId, prop.id) } catch(error) { alert(error.response.data.message) } continue } } } const UpdateProduct = (props) => { const { id, show, setShow, setChange } = props const [value, setValue] = useState(defaultValue) const [valid, setValid] = useState(defaultValid) // список категорий и список брендов для возможности выбора const [categories, setCategories] = useState(null) const [brands, setBrands] = useState(null) // выбранное для загрузки изображение товара const [image, setImage] = useState(null) // список характеристик товара const [properties, setProperties] = useState([]) useEffect(() => { if(id) { // нужно получить с сервера данные товара для редактирования fetchOneProduct(id) .then( data => { const prod = { name: data.name, price: data.price.toString(), category: data.categoryId.toString(), brand: data.brandId.toString() } setValue(prod) setValid(isValid(prod)) // для удобства работы с хар-ми зададим для каждой уникальный идентификатор // и доп.свойства, которые подскажут нам, какой http-запрос на сервер нужно // выполнить — добавления, обновления или удаления характеристики setProperties(data.props.map(item => { // при добавлении новой хар-ки свойство append принимает значение true // при изменении старой хар-ки свойство change принимает значение true // при удалении старой хар-ки свойство remove принимает значение true return {...item, unique: uuid(), append: false, remove: false, change: false} })) } ) .catch( error => alert(error.response.data.message) ) // нужно получить с сервера список категорий и список брендов fetchCategories() .then( data => setCategories(data) ) fetchBrands() .then( data => setBrands(data) ) } }, [id]) const handleInputChange = (event) => { const data = {...value, [event.target.name]: event.target.value} setValue(data) setValid(isValid(data)) } const handleImageChange = (event) => { setImage(event.target.files[0]) } const handleSubmit = async (event) => { event.preventDefault() /* * На первый взгляд кажется, что переменная correct не нужна, можно обойтись valid, но это * не так. Нельзя использовать значение valid сразу после изменения этого значения — ф-ция * setValid не изменяет значение состояния мгновенно. Вызов функции лишь означает — React * «принял к сведению» наше сообщение, что состояние нужно изменить. */ const correct = isValid(value) setValid(correct) // если введенные данные прошли проверку — можно отправлять их на сервер if (correct.name && correct.price && correct.category && correct.brand) { const data = new FormData() data.append('name', value.name.trim()) data.append('price', value.price.trim()) data.append('categoryId', value.category) data.append('brandId', value.brand) if (image) data.append('image', image, image.name) // нужно обновить, добавить или удалить характеристики и обязательно дождаться // ответа сервера — поэтому функция updateProperties() объявлена как async, а // в теле функции для выполнения действия с каждой хар-кой используется await if (properties.length) { await updateProperties(properties, id) } updateProduct(id, data) .then( data => { // изменяем состояние, чтобы обновить список товаров setChange(state => !state) // сбрасываем поле загрузки изображения, чтобы при сохранении товара, // когда новое изображение не выбрано, не загружать старое повтороно event.target.image.value = '' // в принципе, мы могли бы сбросить все поля формы на дефолтные значения, но // если пользователь решит отредатировать тот же товар повтороно, то увидит // пустые поля формы — http-запрос на получение данных для редактирования мы // выполняем только тогда, когда выбран новый товар (изменился id товара) const prod = { name: data.name, price: data.price.toString(), category: data.categoryId.toString(), brand: data.brandId.toString() } setValue(prod) setValid(isValid(prod)) // мы получим актуальные значения хар-тик с сервера, потому что обновление // хар-тик завершилось еще до момента отправки этого http-запроса на сервер setProperties(data.props.map(item => { return {...item, unique: uuid(), append: false, remove: false, change: false} })) // закрываем модальное окно редактирования товара setShow(false) } ) .catch( error => alert(error.response.data.message) ) } } return ( <Modal show={show} onHide={() => setShow(false)} size="lg"> <Modal.Header closeButton> <Modal.Title>Редактирование товара</Modal.Title> </Modal.Header> <Modal.Body> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={value.name} onChange={e => handleInputChange(e)} isValid={valid.name === true} isInvalid={valid.name === false} placeholder="Название товара..." className="mb-3" /> <Row className="mb-3"> <Col> <Form.Select name="category" value={value.category} onChange={e => handleInputChange(e)} isValid={valid.category === true} isInvalid={valid.category === false} > <option value="">Категория</option> {categories && categories.map(item => <option key={item.id} value={item.id}>{item.name}</option> )} </Form.Select> </Col> <Col> <Form.Select name="brand" value={value.brand} onChange={e => handleInputChange(e)} isValid={valid.brand === true} isInvalid={valid.brand === false} > <option value="">Бренд</option> {brands && brands.map(item => <option key={item.id} value={item.id}>{item.name}</option> )} </Form.Select> </Col> </Row> <Row className="mb-3"> <Col> <Form.Control name="price" value={value.price} onChange={e => handleInputChange(e)} isValid={valid.price === true} isInvalid={valid.price === false} placeholder="Цена товара..." /> </Col> <Col> <Form.Control name="image" type="file" onChange={e => handleImageChange(e)} placeholder="Фото товара..." /> </Col> </Row> <UpdateProperties properties={properties} setProperties={setProperties} /> <Row> <Col> <Button type="submit">Сохранить</Button> </Col> </Row> </Form> </Modal.Body> </Modal> ) } export default UpdateProduct
Рефакторинг
В какой-то момент свернул на кривую дорожку и долго откладывал возвращение на правильный путь. Так что будет новая версия клиента в директории client.v2
— с исправлением допущенных ошибок.
Первый этап
Во-первых, данные о пользователе следует получать в компоненте App
. Потому что эти данные требуются компоненту NavBar
, чтобы показать нужные ссылки — авторизации или личного кабинета. Чтобы получить эти данные — пришлось создавать HOC-компонент CheckAuth
, который показывает loader, пока выполняется запрос на сервер. Если данные будут получены в App
и сохранены в хранилище — HOC-компонент CheckAuth
будет не нужен.
import { BrowserRouter } from 'react-router-dom' import AppRouter from './components/AppRouter.js' import NavBar from './components/NavBar.js' import 'bootstrap/dist/css/bootstrap.min.css' import { AppContext } from './components/AppContext.js' import { check as checkAuth } from './http/userAPI.js' import { useState, useContext, useEffect } from 'react' import { observer } from 'mobx-react-lite' import Loader from './components/Loader.js' const App = observer(() => { const { user } = useContext(AppContext) const [loading, setLoading] = useState(true) useEffect(() => { checkAuth() .then(data => { if (data) { user.login(data) } }) .finally( () => setLoading(false) ) }, []) // показываем loader, пока получаем с сервера данные пользователя if (loading) { return <Loader /> } return ( <BrowserRouter> <NavBar /> <AppRouter /> </BrowserRouter> ) }) export default App
import { Container, Spinner } from 'react-bootstrap' const Loader = () => { const style = { width: '100%', height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' } return ( <div style={style}> <Spinner animation="grow" variant="primary" /> </div> ) } export default Loader
Теперь можно удалить как сам HOC-компонент CheckAuth
, так и его использование в компоненте NavBar
:
import { Container, Navbar, Nav } from 'react-bootstrap' import { NavLink } from 'react-router-dom' import { AppContext } from './AppContext.js' import { useContext } from 'react' import { observer } from 'mobx-react-lite' import FetchBasket from './FetchBasket.js' const NavBar = observer(() => { const { user, basket } = useContext(AppContext) return ( <Navbar bg="dark" variant="dark"> <Container> <NavLink to="/" className="navbar-brand">Магазин</NavLink> <Nav className="ml-auto"> <NavLink to="/delivery" className="nav-link">Доставка</NavLink> <NavLink to="/contacts" className="nav-link">Контакты</NavLink> {user.isAuth ? ( <NavLink to="/user" className="nav-link">Личный кабинет</NavLink> ) : ( <> <NavLink to="/login" className="nav-link">Войти</NavLink> <NavLink to="/signup" className="nav-link">Регистрация</NavLink> </> )} {user.isAdmin && ( <NavLink to="/admin" className="nav-link">Панель управления</NavLink> )} <FetchBasket> <NavLink to="/basket" className="nav-link"> Корзина {!!basket.count && <span>({basket.count})</span>} </NavLink> </FetchBasket> </Nav> </Container> </Navbar> ) }) export default NavBar
Второй этап
Во-вторых, аналогичная ситуация с корзиной. Чтобы правильно ее показать, используется HOC-компонент FetchBasket
, который показывает loader, пока идет запрос на сервер. Так что содержимое корзины тоже будем получать в компоненте App
— причем, нам надо дождаться от сервера как данных пользователя, так и данных корзины.
import { BrowserRouter } from 'react-router-dom' import AppRouter from './components/AppRouter.js' import NavBar from './components/NavBar.js' import 'bootstrap/dist/css/bootstrap.min.css' import { AppContext } from './components/AppContext.js' import { check as checkAuth } from './http/userAPI.js' import { useState, useContext, useEffect } from 'react' import { observer } from 'mobx-react-lite' import Loader from './components/Loader.js' import { fetchBasket } from './http/basketAPI.js' const App = observer(() => { const { user, basket } = useContext(AppContext) const [userAuthLoading, setUserAuthLoading] = useState(true) const [basketLoading, setBasketLoading] = useState(true) useEffect(() => { checkAuth() .then(data => { if (data) { user.login(data) } }) .finally( () => setUserAuthLoading(false) ) fetchBasket() .then( data => basket.products = data.products ) .finally( () => setBasketLoading(false) ) }, []) // показываем loader, пока получаем пользователя и корзину if (userAuthLoading || basketLoading) { return <Loader /> } return ( <BrowserRouter> <NavBar /> <AppRouter /> </BrowserRouter> ) }) export default App
Теперь можно удалить как сам HOC-компонент FetchBasket
, так и его использование в компоненте NavBar
. Но пока получилось не слишком изящно, так что можно немного доработать, используя Promise.all
. Будем отправлять сразу два http-запроса и ждать, пока будут получены ответы сервера на оба.
import { BrowserRouter } from 'react-router-dom' import AppRouter from './components/AppRouter.js' import NavBar from './components/NavBar.js' import 'bootstrap/dist/css/bootstrap.min.css' import { AppContext } from './components/AppContext.js' import { check as checkAuth } from './http/userAPI.js' import { useState, useContext, useEffect } from 'react' import { observer } from 'mobx-react-lite' import Loader from './components/Loader.js' import { fetchBasket } from './http/basketAPI.js' import axios from 'axios' const App = observer(() => { const { user, basket } = useContext(AppContext) const [loading, setLoading] = useState(true) useEffect(() => { Promise.all([checkAuth(), fetchBasket()]) .then( axios.spread((userData, basketData) => { if (userData) { user.login(userData) } basket.products = basketData.products }) ) .finally( () => setLoading(false) ) }, []) // показываем loader, пока получаем пользователя и корзину if (loading) { return <Loader /> } return ( <BrowserRouter> <NavBar /> <AppRouter /> </BrowserRouter> ) }) export default App
Третий этап
Этот баг заметил еще в 14-ой части. Если перезагрузить страницу корзины кнопкой F5, то на какое-то мгновение было видно сообщение «Ваша корзина пуста». Это происходило потому, что изначально при создании экземпляра BasketStore
масссив _products
пустой. Потом HOC-компонент FetchBasket
получал с сервера содержимое корзины, массив _products
изменялся, а поскольку это observable-значение и BasketList
обернут в observer
— это вызывало новый рендер.
Чтобы избавиться от этого бага — пришлось добавить в компонент BasketList
код, который отправлял на сервер запрос для получения корзины. А до тех пор, пока не получен ответ — компонент показывал loader. Теперь, когда корзину мы получаем в компоненте App
— нам этот код не нужен. К моменту рендера компонента BasketList
данные корзины уже получены с сервера и сохранены в хранилище — так что можно удалить лишний код.
import { useContext, useState } from 'react' import { AppContext } from './AppContext.js' import { increment, decrement, remove } from '../http/basketAPI.js' import { Table, Spinner, Button } from 'react-bootstrap' import { useNavigate } from 'react-router-dom' import BasketItem from './BasketItem.js' import { observer } from 'mobx-react-lite' const BasketList = observer(() => { const { basket } = useContext(AppContext) const [fetching, setFetching] = useState(false) const navigate = useNavigate() const handleIncrement = (id) => { setFetching(true) increment(id) .then( data => basket.products = data.products ) .finally( () => setFetching(false) ) } const handleDecrement = (id) => { setFetching(true) decrement(id) .then( data => basket.products = data.products ) .finally( () => setFetching(false) ) } const handleRemove = (id) => { setFetching(true) remove(id) .then( data => basket.products = data.products ) .finally( () => setFetching(false) ) } if (fetching) { return <Spinner animation="border" /> } return ( <> {basket.count ? ( <> <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Наименование</th> <th>Количество</th> <th>Цена</th> <th>Сумма</th> <th>Удалить</th> </tr> </thead> <tbody> {basket.products.map(item => <BasketItem key={item.id} increment={handleIncrement} decrement={handleDecrement} remove={handleRemove} {...item} /> )} <tr> <th colSpan="3">Итого</th> <th>{basket.sum}</th> <th>руб.</th> </tr> </tbody> </Table> <Button onClick={() => navigate('/checkout')}>Оформить заказ</Button> </> ) : ( <p>Ваша корзина пуста</p> )} </> ) }) export default BasketList
Четвертый этап
Для создания и редактирования категории предназначены компоненты CreateCategory
и UpdateCategory
, но эти компоненты очень похожи, так что есть смысл объединить их в один компонент EditCategory
.
import { useState, useEffect } from 'react' import { fetchCategories, deleteCategory } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import EditCategory from '../components/EditCategory.js' const AdminCategories = () => { const [categories, setCategories] = useState(null) // список загруженных категорий const [fetching, setFetching] = useState(true) // загрузка списка категорий с сервера const [show, setShow] = useState(false) // модальное окно создания-редактирования // для обновления списка после добавления, редактирования, удаления — изменяем состояние const [change, setChange] = useState(false) // id категории, которую будем редактировать — для передачи в <EditCategory id={…} /> const [categoryId, setCategoryId] = useState(null) const handleCreateClick = () => { setCategoryId(0) setShow(true) } const handleUpdateClick = (id) => { setCategoryId(id) setShow(true) } const handleDeleteClick = (id) => { deleteCategory(id) .then( data => { setChange(!change) alert(`Категория «${data.name}» удалена`) } ) .catch( error => alert(error.response.data.message) ) } useEffect(() => { fetchCategories() .then( data => setCategories(data) ) .finally( () => setFetching(false) ) }, [change]) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Категории</h1> <Button onClick={() => handleCreateClick()}>Создать категорию</Button> <EditCategory id={categoryId} show={show} setShow={setShow} setChange={setChange} /> {categories.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {categories.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => handleUpdateClick(item.id)}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => handleDeleteClick(item.id)}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список категорий пустой</p> )} </Container> ) } export default AdminCategories
import { Modal, Button, Form } from 'react-bootstrap' import { createCategory, fetchCategory, updateCategory } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' const EditCategory = (props) => { const { id, show, setShow, setChange } = props const [name, setName] = useState('') const [valid, setValid] = useState(null) useEffect(() => { if(id) { fetchCategory(id) .then( data => { setName(data.name) setValid(data.name !== '') } ) .catch( error => alert(error.response.data.message) ) } else { setName('') setValid(null) } }, [id]) const handleChange = (event) => { setName(event.target.value) setValid(event.target.value.trim() !== '') } const handleSubmit = (event) => { event.preventDefault() /* * На первый взгляд кажется, что переменная correct не нужна, можно обойтись valid, но это * не так. Нельзя использовать значение valid сразу после изменения этого значения — ф-ция * setValid не изменяет значение состояния мгновенно. Вызов функции лишь означает — React * «принял к сведению» наше сообщение, что состояние нужно изменить. */ const correct = name.trim() !== '' setValid(correct) if (correct) { const data = { name: name.trim() } const success = (data) => { // закрываем модальное окно создания-редактирования категории setShow(false) // изменяем состояние родителя, чтобы обновить список категорий setChange(state => !state) } const error = (error) => alert(error.response.data.message) id ? updateCategory(id, data).then(success).catch(error) : createCategory(data).then(success).catch(error) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>{id ? 'Редактирование' : 'Создание'} категории</Modal.Title> </Modal.Header> <Modal.Body> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={name} onChange={e => handleChange(e)} isValid={valid === true} isInvalid={valid === false} placeholder="Название категории..." className="mb-3" /> <Button type="submit">Сохранить</Button> </Form> </Modal.Body> </Modal> ) } export default EditCategory
Пятый этап
Для создания и редактирования бренда предназначены компоненты CreateBrand
и UpdateBrand
, но эти компоненты очень похожи, так что есть смысл объединить их в один компонент EditBrand
.
import { useState, useEffect } from 'react' import { fetchBrands, deleteBrand } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import EditBrand from '../components/EditBrand.js' const AdminBrands = () => { const [brands, setBrands] = useState(null) // список загруженных брендов const [fetching, setFetching] = useState(true) // загрузка списка брендов с сервера const [show, setShow] = useState(false) // модальное окно создания-редактирования // для обновления списка после добавления, редактирования, удаления — изменяем состояние const [change, setChange] = useState(false) // id бренда, который будем редактировать — для передачи в <EditBrand id={…} /> const [brandId, setBrandId] = useState(0) const handleCreateClick = () => { setBrandId(0) setShow(true) } const handleUpdateClick = (id) => { setBrandId(id) setShow(true) } const handleDeleteClick = (id) => { deleteBrand(id) .then( data => { setChange(!change) alert(`Бренд «${data.name}» удален`) } ) .catch( error => alert(error.response.data.message) ) } useEffect(() => { fetchBrands() .then( data => setBrands(data) ) .finally( () => setFetching(false) ) }, [change]) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Бренды</h1> <Button onClick={() => handleCreateClick()}>Создать бренд</Button> <EditBrand id={brandId} show={show} setShow={setShow} setChange={setChange} /> {brands.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {brands.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => handleUpdateClick(item.id)}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => handleDeleteClick(item.id)}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список брендов пустой</p> )} </Container> ) } export default AdminBrands
import { Modal, Button, Form } from 'react-bootstrap' import { createBrand, fetchBrand, updateBrand } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' const EditBrand = (props) => { const { id, show, setShow, setChange } = props const [name, setName] = useState('') const [valid, setValid] = useState(null) useEffect(() => { if(id) { fetchBrand(id) .then( data => { setName(data.name) setValid(data.name !== '') } ) .catch( error => alert(error.response.data.message) ) } else { setName('') setValid(null) } }, [id]) const handleChange = (event) => { setName(event.target.value) setValid(event.target.value.trim() !== '') } const handleSubmit = (event) => { event.preventDefault() /* * На первый взгляд кажется, что переменная correct не нужна, можно обойтись valid, но это * не так. Нельзя использовать значение valid сразу после изменения этого значения — ф-ция * setValid не изменяет значение состояния мгновенно. Вызов функции лишь означает — React * «принял к сведению» наше сообщение, что состояние нужно изменить. */ const correct = name.trim() !== '' setValid(correct) if (correct) { const data = { name: name.trim() } const success = (data) => { // закрываем модальное окно создания-редактирования бренда setShow(false) // изменяем состояние родителя, чтобы обновить список брендов setChange(state => !state) } const error = (error) => alert(error.response.data.message) id ? updateBrand(id, data).then(success).catch(error) : createBrand(data).then(success).catch(error) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>{id ? 'Редактирование' : 'Создание'} бренда</Modal.Title> </Modal.Header> <Modal.Body> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={name} onChange={e => handleChange(e)} isValid={valid === true} isInvalid={valid === false} placeholder="Название бренда..." className="mb-3" /> <Button type="submit">Сохранить</Button> </Form> </Modal.Body> </Modal> ) } export default EditBrand
Шестой этап
Надо бы и компоненты создания-редактирования товаров и характеристик переделать, но честно говоря, уже устал — никак не ожидал, что разработка учебного проекта так затянется. Так что на этом — все, game over.
Вместо заключения
Исходные коды — здесь, демо работы приложения — ниже:
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
- Магазин на JavaScript, часть 11 из 19. Компоненты Signup и Login, компоненты Shop и Basket
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк