Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
06.02.2022
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Панель управления
Продолжаем работать с панелью управления — нужно добавить возможность редактирования и удаления категорий и брендов. Для этого создадим компоненты UpdateCategory.js
и UpdateBrand.js
. Этим компонентам нужны функции fetchCategory
и fetchBrand
, которые должны быть в файле http/catalogAPI.js
— но их там не оказалось. Так что с этого и начнем — редактируем http/catalogAPI.js
.
export const fetchCategory = async (id) => { const { data } = await guestInstance.get(`category/getone/${id}`) return data } export const fetchBrand = async (id) => { const { data } = await guestInstance.get(`brand/getone/${id}`) return data }
Редактирование категории
Создаем компонент components/UpdateCategory.js
:
import { Modal, Button, Form } from 'react-bootstrap' import { fetchCategory, updateCategory } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' const UpdateCategory = (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) ) .catch( error => alert(error.response.data.message) ) } }, [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() } updateCategory(id, data) .then( data => { // закрываем модальное окно редактирования категории setShow(false) // изменяем состояние компонента списка категорий setChange(state => !state) } ) .catch( error => alert(error.response.data.message) ) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>Редактирование категории</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 UpdateCategory
Редактируем компонент pages/AdminCategories.js
:
import { useState, useEffect } from 'react' import { fetchCategories, deleteCategory } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateCategory from '../components/CreateCategory.js' import UpdateCategory from '../components/UpdateCategory.js' const AdminCategories = () => { const [categories, setCategories] = useState(null) // список загруженных категорий const [fetching, setFetching] = useState(true) // загрузка списка категорий с сервера const [createShow, setCreateShow] = useState(false) // модальное окно создания категории const [updateShow, setUpdateShow] = useState(false) // модальное окно редактирования // для обновления списка после добавления, редактирования, удаления — изменяем состояние const [change, setChange] = useState(false) // id категории, которую будем редактировать — для передачи в <UpdateCategory id={…} /> const [category, setCategory] = useState(null) const handleUpdateClick = (id) => { setCategory(id) setUpdateShow(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={() => setCreateShow(true)}>Создать категорию</Button> <CreateCategory show={createShow} setShow={setCreateShow} setChange={setChange} /> <UpdateCategory id={category} show={updateShow} setShow={setUpdateShow} 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
Заодно реализовали удаление категории — функция deleteCategory
в файле http/catalogAPI.js
уже есть, нам только нужно ее вызвать, после чего обновить список категорий.
Редактирование бренда
Создаем компонент components/UpdateBrand.js
:
import { Modal, Button, Form } from 'react-bootstrap' import { fetchBrand, updateBrand } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' const UpdateBrand = (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) ) .catch( error => alert(error.response.data.message) ) } }, [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() } updateBrand(id, data) .then( data => setChange(!change) ) .catch( error => alert(error.response.data.message) ) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>Редактирование бренда</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 UpdateBrand
Редактируем компонент pages/AdminBrands.js
:
import { useState, useEffect } from 'react' import { fetchBrands, deleteBrand } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateBrand from '../components/CreateBrand.js' import UpdateBrand from '../components/UpdateBrand.js' const AdminBrands = () => { const [brands, setBrands] = useState(null) // список загруженных брендов const [fetching, setFetching] = useState(true) // загрузка списка брендов с сервера const [createShow, setCreateShow] = useState(false) // модальное окно создания бренда const [updateShow, setUpdateShow] = useState(false) // модальное окно редактирования // для обновления списка после добавления, редактирования, удаления — изменяем состояние const [change, setChange] = useState(false) // id бренда, который будем редактировать — для передачи в <UpdateBrand id={…} /> const [brand, setBrand] = useState(null) const handleUpdateClick = (id) => { setBrand(id) setUpdateShow(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={() => setCreateShow(true)}>Создать бренд</Button> <CreateBrand show={createShow} setShow={setCreateShow} setChange={setChange} /> <UpdateBrand id={brand} show={updateShow} setShow={setUpdateShow} 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
Заодно реализовали удаление бренда — функция deleteBrand
в файле http/catalogAPI.js
уже есть, нам только нужно ее вызвать, после чего обновить список брендов.
Список товаров
Компонент списка товаров AdminProducts.js
похож на компоненты AdminCategories.js
и AdminBrands.js
:
import { useState, useEffect } from 'react' import { fetchAllProducts, deleteProduct } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateProduct from '../components/CreateProduct.js' import UpdateProduct from '../components/UpdateProduct.js' const AdminProducts = () => { const [products, setProducts] = useState([]) // список загруженных товаров const [fetching, setFetching] = useState(true) // загрузка списка товаров с сервера const [createShow, setCreateShow] = useState(false) // модальное окно создания товара const [updateShow, setUpdateShow] = useState(false) // модальное окно редактирования // для обновления списка после добавления, редактирования, удаления — изменяем состояние const [change, setChange] = useState(false) // id товара, который будем редактировать — для передачи в <UpdateProduct id={…} /> const [product, setProduct] = useState(null) const handleUpdateClick = (id) => { setProduct(id) setUpdateShow(true) } const handleDeleteClick = (id) => { deleteProduct(id) .then( data => { setChange(!change) alert(`Товар «${data.name}» удален`) } ) .catch( error => alert(error.response.data.message) ) } useEffect(() => { fetchAllProducts() .then( data => setProducts(data.rows) ) .finally( () => setFetching(false) ) }, [change]) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Товары</h1> <Button onClick={() => setCreateShow(true)}>Создать товар</Button> <CreateProduct show={createShow} setShow={setCreateShow} setChange={setChange} /> <UpdateProduct id={product} show={updateShow} setShow={setUpdateShow} setChange={setChange} /> {products.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Категория</th> <th>Бренд</th> <th>Цена</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {products.map(item => <tr key={item.id}> <td>{item.name}</td> <td>{item.category.name}</td> <td>{item.brand.name}</td> <td>{item.price}</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 AdminProducts
Только нам еще нужно реализовать постраничную навигацию — но это мы уже тоже делали:
import { useState, useEffect } from 'react' import { fetchAllProducts, deleteProduct } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table, Pagination } from 'react-bootstrap' import CreateProduct from '../components/CreateProduct.js' import UpdateProduct from '../components/UpdateProduct.js' // количество товаров на страницу const ADMIN_PER_PAGE = 6 const AdminProducts = () => { const [products, setProducts] = useState([]) // список загруженных товаров const [fetching, setFetching] = useState(true) // загрузка списка товаров с сервера const [createShow, setCreateShow] = useState(false) // модальное окно создания товара const [updateShow, setUpdateShow] = useState(false) // модальное окно редактирования // для обновления списка после добавления, редактирования, удаления — изменяем состояние const [change, setChange] = useState(false) // id товара, который будем редактировать — для передачи в <UpdateProduct id={…} /> const [product, setProduct] = useState(null) // текущая страница списка товаров const [currentPage, setCurrentPage] = useState(1) // сколько всего страниц списка товаров const [totalPages, setTotalPages] = useState(1) // обработчик клика по номеру страницы const handlePageClick = (page) => { setCurrentPage(page) setFetching(true) } // содержимое компонента <Pagination> const pages = [] for (let page = 1; page <= totalPages; page++) { pages.push( <Pagination.Item key={page} active={page === currentPage} activeLabel="" onClick={() => handlePageClick(page)} > {page} </Pagination.Item> ) } const handleUpdateClick = (id) => { setProduct(id) setUpdateShow(true) } const handleDeleteClick = (id) => { deleteProduct(id) .then( data => { setChange(!change) alert(`Товар «${data.name}» удален`) } ) .catch( error => alert(error.response.data.message) ) } useEffect(() => { fetchAllProducts(null, null, currentPage, ADMIN_PER_PAGE) .then( data => { setProducts(data.rows) setTotalPages(Math.ceil(data.count / ADMIN_PER_PAGE)) } ) .finally( () => setFetching(false) ) }, [change, currentPage]) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Товары</h1> <Button onClick={() => setCreateShow(true)}>Создать товар</Button> <CreateProduct show={createShow} setShow={setCreateShow} setChange={setChange} /> <UpdateProduct id={product} show={updateShow} setShow={setUpdateShow} setChange={setChange} /> {products.length > 0 ? ( <> <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Категория</th> <th>Бренд</th> <th>Цена</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {products.map(item => <tr key={item.id}> <td>{item.name}</td> <td>{item.category?.name || 'NULL'}</td> <td>{item.brand?.name || 'NULL'}</td> <td>{item.price}</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> {totalPages > 1 && <Pagination>{pages}</Pagination>} </> ) : ( <p>Список товаров пустой</p> )} </Container> ) } export default AdminProducts
Добавление товара
Здесь многое аналогично созданию категории или бренда, но полей будет больше и нужно реализовать загрузку картинки товара:
import { Modal, Button, Form, Row, Col } from 'react-bootstrap' import { createProduct, fetchCategories, fetchBrands } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' import CreateProperties from './CreateProperties.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 CreateProduct = (props) => { const { show, setShow, setChange } = props const [value, setValue] = useState(defaultValue) const [valid, setValid] = useState(defaultValid) // выбранное для загрузки изображение товара const [image, setImage] = useState(null) // список категорий и список брендов для возможности выбора const [categories, setCategories] = useState(null) const [brands, setBrands] = useState(null) // нужно получить с сервера список категорий и список брендов useEffect(() => { fetchCategories() .then( data => setCategories(data) ) fetchBrands() .then( data => setBrands(data) ) }, []) 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 = (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) createProduct(data) .then( data => { // приводим форму в изначальное состояние event.target.image.value = '' setValue(defaultValue) setValid(defaultValid) // закрываем модальное окно создания товара setShow(false) // изменяем состояние компонента списка товаров, // чтобы в этом списке появился и новый товар setChange(state => !state) } ) .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> <Button type="submit">Сохранить</Button> </Form> </Modal.Body> </Modal> ) } export default CreateProduct
Редактирование товара
Здесь многое аналогично редактированию категории или бренда, но полей будет больше и нужно реализовать загрузку картинки товара:
import { Modal, Button, Form, Row, Col } from 'react-bootstrap' import { fetchOneProduct, updateProduct, fetchCategories, fetchBrands } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' import UpdateProperties from './UpdateProperties.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 UpdateProduct = (props) => { const { id, show, setShow, setChange } = props const [value, setValue] = useState(defaultValue) const [valid, setValid] = useState(defaultValid) // выбранное для загрузки изображение товара const [image, setImage] = useState(null) // список характеристик товара const [properties, setProperties] = useState([]) // список категорий и список брендов для возможности выбора const [categories, setCategories] = useState(null) const [brands, setBrands] = useState(null) 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)) setProperties(data.props) } ) .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 = (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) updateProduct(id, data) .then( data => { // нужно сбросить поле загрузки изображения, но не очищать все // поля формы для следующего товара, потому что при повтороном // редактировании того же товара все поля окажутся пустыми event.target.image.value = '' // закрываем модальное окно редактирования товара setShow(false) // изменяем состояние компонента списка товаров setChange(state => !state) } ) .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 productId={id} properties={properties} /> <Row> <Col> <Button type="submit">Сохранить</Button> </Col> </Row> </Form> </Modal.Body> </Modal> ) } export default UpdateProduct
Характеристики товара
При создании нового товара должна быть возможность добавить и характеристики товара, так что создадим еще компонент CreateProperties.js
. Он будет принимать пропсы properties
(массив объектов характеристик) и функцию setProperties
для изменения эти характеристик.
import { useState } from 'react' import { Row, Col, Button, Form } from 'react-bootstrap' const CreateProperties = (props) => { const { properties, setProperties } = props const append = () => { setProperties([...properties, {name: '', value: '', number: Date.now()}]) } const remove = (number) => { setProperties(properties.filter(item => item.number !== number)) } const change = (key, value, number) => { setProperties(properties.map(item => item.number === number ? {...item, [key]: value} : item)) } return ( <> <h5>Характеристики</h5> <Button onClick={append} variant="outline-primary" size="sm" className="mb-2"> Добавить </Button> {properties.map(item => <Row key={item.number} className="mb-2"> <Col> <Form.Control name={'name_' + item.number} value={item.name} onChange={e => change('name', e.target.value, item.number)} placeholder="Название..." size="sm" /> </Col> <Col> <Form.Control name={'value_' + item.number} value={item.value} onChange={e => change('value', e.target.value, item.number)} placeholder="Значение..." size="sm" /> </Col> <Col> <Button onClick={() => remove(item.number)} size="sm" variant="outline-danger"> Удалить </Button> </Col> </Row> )} </> ) } export default CreateProperties
Пропсы компонент будет получать от своего родителя — то есть от компонента CreateProduct
:
import { Modal, Button, Form, Row, Col } from 'react-bootstrap' import { createProduct, fetchCategories, fetchBrands } from '../http/catalogAPI.js' import { useState, useEffect } from 'react' import CreateProperties from './CreateProperties.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 CreateProduct = (props) => { const { show, setShow, setChange } = props const [value, setValue] = useState(defaultValue) const [valid, setValid] = useState(defaultValid) // выбранное для загрузки изображение товара const [image, setImage] = useState(null) // список характеристик товара const [properties, setProperties] = useState([]) // список категорий и список брендов для возможности выбора const [categories, setCategories] = useState(null) const [brands, setBrands] = useState(null) // нужно получить с сервера список категорий и список брендов useEffect(() => { fetchCategories() .then( data => setCategories(data) ) fetchBrands() .then( data => setBrands(data) ) }, []) 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 = (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) // характеристики нового товара if (properties.length) { const props = properties.filter( prop => prop.name.trim() !== '' && prop.value.trim() !== '' ) if (props.length) { data.append('props', JSON.stringify(props)) } } createProduct(data) .then( data => { // приводим форму в изначальное состояние event.target.image.value = '' setValue(defaultValue) setValid(defaultValid) setProperties([]) // закрываем модальное окно создания товара setShow(false) // изменяем состояние компонента списка товаров, // чтобы в этом списке появился и новый товар setChange(state => !state) } ) .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> <CreateProperties properties={properties} setProperties={setProperties} /> <Row> <Col> <Button type="submit">Сохранить</Button> </Col> </Row> </Form> </Modal.Body> </Modal> ) } export default CreateProduct
- Магазин на JavaScript, часть 19 из 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-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк