Last active
January 20, 2023 10:55
-
-
Save evgeniyworkbel/0e30cd388394f146fb8ab53a5b41203b to your computer and use it in GitHub Desktop.
Курс "JS: React Hooks"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
App.jsx | |
// @ts-check | |
import React, { useState } from 'react'; | |
import { useImmer } from 'use-immer'; | |
import getModal from './modals/index.js'; | |
// BEGIN (write your solution here) | |
const renderTask = (item, setActiveModal, updateClickedTaskId, updateClickedTaskName) => { | |
const { taskName, id } = item; | |
return ( | |
<React.Fragment key={id}> | |
<div> | |
<span className="mr-3">{taskName}</span> | |
<button | |
type="button" | |
className="border-0 btn btn-link mr-3 text-decoration-none" | |
data-testid="item-rename" | |
onClick={() => { | |
updateClickedTaskId(id); | |
updateClickedTaskName(taskName); | |
setActiveModal('renaming'); | |
}} | |
> | |
rename | |
</button> | |
<button | |
type="button" | |
className="border-0 btn btn-link mr-3 text-decoration-none" | |
data-testid="item-remove" | |
onClick={() => { | |
updateClickedTaskId(id); | |
setActiveModal('removing'); | |
}} | |
> | |
remove | |
</button> | |
</div> | |
</React.Fragment> | |
); | |
}; | |
const renderModal = (activeModal, props) => { | |
if (!activeModal) { | |
return null; | |
} | |
const Component = getModal(activeModal); | |
return <Component props={props} />; | |
}; | |
const handleClick = (modalType, setActiveModal) => (ev) => { | |
ev.preventDefault(); | |
setActiveModal(modalType); | |
}; | |
const App = () => { | |
const [activeModal, setActiveModal] = useState(null); | |
const [tasks, setTasks] = useImmer([]); | |
const [clickedTaskId, updateClickedTaskId] = useState(null); | |
const [clickedTaskName, updateClickedTaskName] = useState(null); | |
const onHide = () => setActiveModal(null); | |
return ( | |
<> | |
<div className="mb-3"> | |
<button | |
type="button" | |
data-testid="item-add" | |
className="btn btn-secondary" | |
onClick={handleClick('adding', setActiveModal)} | |
> | |
add | |
</button> | |
</div> | |
{tasks.length > 0 | |
&& tasks.map((item) => renderTask(item, setActiveModal, updateClickedTaskId, updateClickedTaskName))} | |
{renderModal(activeModal, { setActiveModal, setTasks, onHide, clickedTaskId, clickedTaskName })} | |
</> | |
); | |
}; | |
export default App; | |
// END | |
Add.jsx | |
import React, { useEffect, useRef } from 'react'; | |
import _ from 'lodash'; | |
import { useFormik } from 'formik'; | |
import { Modal, FormGroup, FormControl } from 'react-bootstrap'; | |
// BEGIN (write your solution here) | |
function Add(props) { | |
const { setActiveModal, setTasks, onHide } = props.props; | |
const inputEl = useRef(null); | |
const formik = useFormik({ | |
initialValues: { body: '' }, | |
onSubmit: (values) => { | |
const newTask = { taskName: values.body, id: _.uniqueId() }; | |
setActiveModal(null); | |
setTasks((prevTasks) => [...prevTasks, newTask]); | |
}, | |
}); | |
useEffect(() => { | |
inputEl.current.focus(); | |
}, []); | |
return ( | |
<Modal show onHide={onHide}> | |
<Modal.Dialog> | |
<Modal.Header closeButton> | |
<Modal.Title>Add</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form onSubmit={formik.handleSubmit}> | |
<FormGroup className="form-group" controlId="body"> | |
<FormControl | |
name="body" | |
type="text" | |
onChange={formik.handleChange} | |
value={formik.values.body} | |
ref={inputEl} | |
data-testid="input-body" | |
required | |
/> | |
</FormGroup> | |
<input className="btn btn-primary" type="submit" value="submit" /> | |
</form> | |
</Modal.Body> | |
</Modal.Dialog> | |
</Modal> | |
); | |
} | |
export default Add; | |
// END | |
Remove.jsx | |
import React from 'react'; | |
import { Modal, FormGroup } from 'react-bootstrap'; | |
// BEGIN (write your solution here) | |
const handleSubmit = ({ setActiveModal, setTasks, clickedTaskId }) => (ev) => { | |
ev.preventDefault(); | |
setActiveModal(null); | |
setTasks((items) => items.filter(({ id }) => id !== clickedTaskId)); | |
}; | |
function Remove(props) { | |
const { setActiveModal, setTasks, onHide, clickedTaskId } = props.props; | |
return ( | |
<Modal show onHide={onHide}> | |
<Modal.Dialog> | |
<Modal.Header closeButton> | |
<Modal.Title>Remove</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form onSubmit={handleSubmit({ setActiveModal, setTasks, clickedTaskId })}> | |
<FormGroup className="form-group"> | |
<input className="btn btn-danger" type="submit" value="remove" /> | |
</FormGroup> | |
</form> | |
</Modal.Body> | |
</Modal.Dialog> | |
</Modal> | |
); | |
} | |
export default Remove; | |
// END | |
Rename.jsx | |
import React, { useEffect, useRef } from 'react'; | |
import { useFormik } from 'formik'; | |
import { Modal, FormGroup, FormControl } from 'react-bootstrap'; | |
// BEGIN (write your solution here) | |
function Rename(props) { | |
const { setActiveModal, setTasks, onHide, clickedTaskId, clickedTaskName } = props.props; | |
const inputEl = useRef(null); | |
const formik = useFormik({ | |
initialValues: { body: clickedTaskName }, | |
onSubmit: (values) => { | |
setActiveModal(null); | |
setTasks((draft) => { | |
const index = draft.findIndex(({ id }) => id === clickedTaskId); | |
draft[index].taskName = values.body; | |
}); | |
}, | |
}); | |
useEffect(() => { | |
inputEl.current.focus(); | |
inputEl.current.select(); | |
}, []); | |
return ( | |
<Modal show onHide={onHide}> | |
<Modal.Dialog> | |
<Modal.Header closeButton> | |
<Modal.Title>Rename</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form onSubmit={formik.handleSubmit}> | |
<FormGroup className="form-group" controlId="body"> | |
<FormControl | |
name="body" | |
type="text" | |
onChange={formik.handleChange} | |
value={formik.values.body} | |
ref={inputEl} | |
data-testid="input-body" | |
required | |
/> | |
</FormGroup> | |
<input className="btn btn-primary" type="submit" value="submit" /> | |
</form> | |
</Modal.Body> | |
</Modal.Dialog> | |
</Modal> | |
); | |
} | |
export default Rename; | |
// END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
App.jsx | |
// @ts-check | |
import React, { useState } from 'react'; | |
import { useImmer } from 'use-immer'; | |
import getModal from './modals/index.js'; | |
// BEGIN | |
const renderItem = ({ item, showModal }) => ( | |
<div key={item.id}> | |
<span className="mr-3">{item.body}</span> | |
<button type="button" className="border-0 btn btn-link mr-3 text-decoration-none" data-testid="item-rename" onClick={() => showModal('renaming', item)}>rename</button> | |
<button type="button" className="border-0 btn btn-link text-decoration-none" data-testid="item-remove" onClick={() => showModal('removing', item)}>remove</button> | |
</div> | |
); | |
const renderModal = ({ modalInfo, hideModal, setItems }) => { | |
if (!modalInfo.type) { | |
return null; | |
} | |
const Component = getModal(modalInfo.type); | |
return <Component modalInfo={modalInfo} setItems={setItems} onHide={hideModal} />; | |
}; | |
const App = () => { | |
const [items, setItems] = useImmer([]); | |
const [modalInfo, setModalInfo] = useState({ type: null, item: null }); | |
const hideModal = () => setModalInfo({ type: null, item: null }); | |
const showModal = (type, item = null) => setModalInfo({ type, item }); | |
return ( | |
<> | |
<div className="mb-3"> | |
<button type="button" onClick={() => showModal('adding')} data-testid="item-add" className="btn btn-secondary">add</button> | |
</div> | |
{items.map((item) => renderItem({ item, showModal }))} | |
{renderModal({ modalInfo, hideModal, setItems })} | |
</> | |
); | |
}; | |
export default App; | |
// END | |
Add.jsx | |
import React, { useEffect, useRef } from 'react'; | |
import _ from 'lodash'; | |
import { useFormik } from 'formik'; | |
import { Modal, FormGroup, FormControl } from 'react-bootstrap'; | |
// BEGIN | |
const generateOnSubmit = ({ setItems, onHide }) => (values) => { | |
const item = { id: _.uniqueId(), body: values.body }; | |
setItems((items) => { | |
items.push(item); | |
}); | |
onHide(); | |
}; | |
const Add = (props) => { | |
const { onHide } = props; | |
const f = useFormik({ onSubmit: generateOnSubmit(props), initialValues: { body: '' } }); | |
const inputRef = useRef(); | |
useEffect(() => { | |
inputRef.current.focus(); | |
}, []); | |
return ( | |
<Modal show> | |
<Modal.Header closeButton onHide={onHide}> | |
<Modal.Title>Add</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form onSubmit={f.handleSubmit}> | |
<FormGroup> | |
<FormControl | |
required | |
ref={inputRef} | |
onChange={f.handleChange} | |
onBlur={f.handleBlur} | |
value={f.values.body} | |
data-testid="input-body" | |
name="body" | |
/> | |
</FormGroup> | |
<input type="submit" className="btn btn-primary mt-2" value="submit" /> | |
</form> | |
</Modal.Body> | |
</Modal> | |
); | |
}; | |
export default Add; | |
// END | |
Remove.jsx | |
import React from 'react'; | |
import { Modal, FormGroup } from 'react-bootstrap'; | |
// BEGIN | |
const generateOnSubmit = ({ modalInfo, setItems, onHide }) => (e) => { | |
e.preventDefault(); | |
setItems((items) => items.filter((i) => i.id !== modalInfo.item.id)); | |
onHide(); | |
}; | |
const Remove = (props) => { | |
const { onHide } = props; | |
const onSubmit = generateOnSubmit(props); | |
return ( | |
<Modal show> | |
<Modal.Header closeButton onHide={onHide}> | |
<Modal.Title>Remove</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form onSubmit={onSubmit}> | |
<FormGroup> | |
<input type="submit" className="btn btn-danger mt-2" value="remove" /> | |
</FormGroup> | |
</form> | |
</Modal.Body> | |
</Modal> | |
); | |
}; | |
export default Remove; | |
// END | |
Rename.jsx | |
import React, { useEffect, useRef } from 'react'; | |
import { useFormik } from 'formik'; | |
import { Modal, FormGroup, FormControl } from 'react-bootstrap'; | |
// BEGIN | |
const generateOnSubmit = ({ modalInfo, setItems, onHide }) => (values) => { | |
setItems((items) => { | |
const item = items.find((i) => i.id === modalInfo.item.id); | |
item.body = values.body; | |
}); | |
onHide(); | |
}; | |
const Rename = (props) => { | |
const { onHide, modalInfo } = props; | |
const { item } = modalInfo; | |
const f = useFormik({ onSubmit: generateOnSubmit(props), initialValues: item }); | |
const inputRef = useRef(); | |
useEffect(() => { | |
inputRef.current.select(); | |
}, []); | |
return ( | |
<Modal show> | |
<Modal.Header closeButton onHide={onHide}> | |
<Modal.Title>Rename</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form onSubmit={f.handleSubmit}> | |
<FormGroup> | |
<FormControl | |
required | |
ref={inputRef} | |
onChange={f.handleChange} | |
onBlur={f.handleBlur} | |
value={f.values.body} | |
data-testid="input-body" | |
name="body" | |
/> | |
</FormGroup> | |
<input type="submit" className="btn btn-primary mt-2" value="submit" /> | |
</form> | |
</Modal.Body> | |
</Modal> | |
); | |
}; | |
export default Rename; | |
// END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
App.jsx | |
Реализуйте и экспортируйте по умолчанию компонент, который реализует приложение "записная книжка". | |
В этом приложении можно добавлять, удалять и редактировать задачи с помощью модальных окон. На любое действие возникает модальное окно, внутри которого можно выполнять разные действия. | |
Для прохождения испытания нужно познакомиться с новыми хуками и использовать готовые компоненты бутстрапа. Рекомендуем проходить это испытание после выполнения предыдущего. | |
Примеры | |
Начальный HTML: | |
<div class="mb-3"> | |
<button type="button" data-testid="item-add" class="btn btn-secondary">add</button> | |
</div> | |
После клика на добавление: | |
<div class="mb-3"> | |
<button type="button" data-testid="item-add" class="btn btn-secondary">add</button> | |
</div> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<div class="modal-title h4">Add</div> | |
<button type="button" class="btn-close" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<form> | |
<div class="form-group"> | |
<input class="form-control" data-testid="input-body" name="body" required="" value="" /> | |
</div> | |
<input class="btn btn-primary" type="submit" value="submit" /> | |
</form> | |
</div> | |
</div> | |
</div> | |
После добавления первой задачи: | |
<div class="mb-3"> | |
<button type="button" data-testid="item-add" class="btn btn-secondary">add</button> | |
</div> | |
<div> | |
<span class="mr-3">first task!</span> | |
<button type="button" class="border-0 btn btn-link mr-3 text-decoration-none" data-testid="item-rename">rename</button> | |
<button type="button" class="border-0 btn btn-link text-decoration-none" data-testid="item-remove">remove</button> | |
</div> | |
Клик по переименованию: | |
<div class="mb-3"> | |
<button type="button" data-testid="item-add" class="btn btn-secondary">add</button> | |
</div> | |
<div> | |
<span class="mr-3">first task!</span> | |
<button type="button" class="border-0 btn btn-link mr-3 text-decoration-none" data-testid="item-rename">rename</button> | |
<button type="button" class="border-0 btn btn-link text-decoration-none" data-testid="item-remove">remove</button> | |
</div> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<div class="modal-title h4">Rename</div> | |
<button type="button" class="btn-close" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<form> | |
<div class="form-group"> | |
<input class="form-control" data-testid="input-body" name="body" required="" value="first task!" /> | |
</div> | |
<input class="btn btn-primary" type="submit" value="submit" /> | |
</form> | |
</div> | |
</div> | |
</div> | |
После переименования все возвращается к предыдущему HTML, но с новым именем. | |
Клик по удалению: | |
<div class="mb-3"> | |
<button type="button" data-testid="item-add" class="btn btn-secondary">add</button> | |
</div> | |
<div> | |
<span class="mr-3">changed name!</span> | |
<button type="button" class="border-0 btn btn-link mr-3 text-decoration-none" data-testid="item-rename">rename</button> | |
<button type="button" class="border-0 btn btn-link text-decoration-none" data-testid="item-remove">remove</button> | |
</div> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<div class="modal-title h4">Remove</div> | |
<button type="button" class="btn-close" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<form> | |
<div class="form-group"> | |
<input class="btn btn-danger" type="submit" value="remove" /> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
После удаления, запись пропадает. | |
modals/Add.jsx | |
Реализуйте модальное окно для добавления задачи. Сделайте так, чтобы при появлении окна происходила фокусировка на поле для ввода. Это важно с точки зрения удобства. | |
modals/Rename.jsx | |
Реализуйте модальное окно для обновления названия задачи. Сделайте так, чтобы при появлении окна происходила фокусировка на поле для ввода и при этом выделялся весь текст. Это важно с точки зрения удобства. | |
modals/Remove.jsx | |
Реализуйте модальное окно для удаления задачи | |
Все модальные окна должны закрываться после отправки формы | |
Подсказки | |
Документация на встроенные хуки: useState, useEffect, useRef https://ru.reactjs.org/docs/hooks-state.html | https://ru.reactjs.org/docs/hooks-effect.html | https://ru.reactjs.org/docs/hooks-reference.html#useref | |
Документация на сторонние хуки: useImmer, useFormik https://github.com/immerjs/use-immer | https://formik.org/docs/api/useFormik | |
Документация React Bootstrap https://react-bootstrap.github.io/ | |
useImmer() используется для точечного обновления в мутабельном стиле, при этом сохраняется преимущество иммутабельных структур данных. Его удобно использовать при необходимости внести изменения в сложные структуры данных, например, когда вам нужно найти объект из списка и изменить у него одно поле. useState() подходит для управления простым состоянием. | |
Вёрстка может немного отличаться |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment