Trello 사이트를 클론 코딩 해보자
selector
의get
을 이용해 Minutes를 입력하면 Hours가 자동으로 바뀌게 해보자.
function App() {
return <div></div>;
}
export default App;
import { atom, selector } from 'recoil';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { RecoilRoot } from 'recoil';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
import { darkTheme } from './theme';
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400&display=swap');
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu, ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
*{
boxsizing: border-box;
}
body{
font-family: 'Source Sans Pro', sans-serif;
background-color: ${(props) => props.theme.bgColor};
color: ${(props) => props.theme.textColor}
}
a{
text-decoration: none;
color: inherit;
}
`;
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<ThemeProvider theme={darkTheme}>
<GlobalStyle />
<App />
</ThemeProvider>
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);
App.tsx 에 input을 추가해주자
function App() {
return (
<div>
<input type='number' placeholder='Minutes' />
<input type='number' placeholder='Hours' />
</div>
);
}
export default App;
atoms.tsx 에 minuteState atom을 만들어주자
import { atom, selector } from 'recoil';
export const minuteState = atom({
key: 'minutes',
default: 0
});
App.tsx 에 input과 minuteState를 연결해주자
import { useRecoilState } from 'recoil';
import { minuteState } from './atom';
function App() {
const [minutes, setMinutes] = useRecoilState(minuteState);
const onMinutesChange = (event: React.FormEvent<HTMLInputElement>) => {
setMinutes(parseInt(event.currentTarget.value));
};
return (
<div>
<input value={minutes} onChange={onMinutesChange} type='number' placeholder='Minutes' />
<input type='number' placeholder='Hours' />
</div>
);
}
export default App;
💡 useRecoilState : atom의 값과 그 atom을 수정할 수 있는 함수를 돌려줌
⛔ 나만 오류나나?
setMinutes(+event.currentTarget.value);
로 했을 때 최종 결과에서 0이 무조건 붙음.
atom.tsx 에 selector을 사용하자
export const hourSelector = selector({
key: 'hours',
get: ({ get }) => {
const minutes = get(minuteState);
return minutes / 60;
}
});
App.tsx에 hourSelector의 값을 가져오자
import { useRecoilState, useRecoilValue } from 'recoil';
import { hourSelector, minuteState } from './atom';
function App() {
const [minutes, setMinutes] = useRecoilState(minuteState);
const hours = useRecoilValue(hourSelector);
const onMinutesChange = (event: React.FormEvent<HTMLInputElement>) => {
setMinutes(+event.currentTarget.value);
};
return (
<div>
<input value={minutes} onChange={onMinutesChange} type='number' placeholder='Minutes' />
<input value={hours} type='number' placeholder='Hours' />
</div>
);
}
export default App;
selector
의set
을 이용해 반대로 Hours를 입력해도 Minutes가 나오도록 만들어보자.
atom.tsx에서 set함수에 접근해보자
export const hourSelector = selector<number>({
key: 'hours',
get: ({ get }) => {
const minutes = get(minuteState);
return minutes / 60;
},
set: ({ set }, newValue) => {
console.log(newValue);
}
});
App.tsx에서 hour가 바뀔 수 있도록 수정하자
import { useRecoilState, useRecoilValue } from 'recoil';
import { hourSelector, minuteState } from './atom';
function App() {
const [minutes, setMinutes] = useRecoilState(minuteState);
const [hours, setHours] = useRecoilState(hourSelector); // selector set 함수 접근을 위해 useRecoilState로 변경
const onMinutesChange = (event: React.FormEvent<HTMLInputElement>) => {
setMinutes(parseInt(event.currentTarget.value));
};
const onHoursChange = (event: React.FormEvent<HTMLInputElement>) => {
setHours(parseInt(event.currentTarget.value));
};
return (
<div>
<input value={minutes} onChange={onMinutesChange} type='number' placeholder='Minutes' />
<input value={hours} onChange={onHoursChange} type='number' placeholder='Hours' />
</div>
);
}
export default App;
💡 setHours 함수는 selector로부터 오는 함수로, selector의 set에 있는 함수를 실행시킨다.
⇒ 값들이 콘솔에 잘 들어가고 있다.
atom.tsx 에 hourSelector에서 minuteState를 수정하자
💡 set(변경하길 원하는 아톰명, 변경 값);
export const hourSelector = selector<number>({
key: 'hours',
get: ({ get }) => {
const minutes = get(minuteState);
return minutes / 60;
},
set: ({ set }, newValue) => {
const minutes = Number(newValue) * 60;
set(minuteState, minutes);
}
});
react-beautiful-dnd
라이브러리를 사용해 드래그앤 드롭을 배워보자.
drag-and-grop context
: 기본적으로 드래그 앤 드롭을 가능하게 하고 싶은 앱의 부분droppable
: 어떤 것을 드롭할 수 있는 영역draggable
: 드래그할 수 있는 영역
react beautiful dnd 라이브러리를 다운로드하자.
⇒ npm i react-beautiful-dnd
⇒ npm i --save-dev @types/react-beautiful-dnd
App.tsx에서 DragDropContext
를 만들자
⇒ DragDropContext에는 onDragEnd함수, children이 필요하다.
💡 onDragEnd : 유저가 드래그를 끝낸 시점에 불려지는 함수
import { DragDropContext } from 'react-beautiful-dnd';
function App() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<div></div>
</DragDropContext>
);
}
export default App;
Droppable
을 만들자
⇒ Droppable은 droppableId, children가 필요하다
⛔ Droppable의 children은 react 요소이면 안되고, **함수**여야 한다!import { DragDropContext, Droppable } from 'react-beautiful-dnd';
function App() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<div>
<Droppable droppableId='one'>{() => <ul></ul>}</Droppable>
</div>
</DragDropContext>
);
}
export default App;
Draggable
을 만들자
⇒ Draggable은 draggableId, index, children이 필요하다.
⇒ Draggable 또한 Droppable과 같이 children에 함수가 들어가야 한다.
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
function App() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<div>
<Droppable droppableId='one'>
{() => (
<ul>
<Draggable draggableId='first' index={0}>
{() => <li>One</li>}
</Draggable>
<Draggable draggableId='second' index={1}>
{() => <li>Two</li>}
</Draggable>
</ul>
)}
</Droppable>
</div>
</DragDropContext>
);
}
export default App;
beautiful-dnd가 제공하는 특별한 prop들을 받아보자
Droppable에 prop들을 넣어보자.
⇒ magic 이름은 아무상관이 없다. 아무렇게나 해도 됨.
<Droppable droppableId='one'>
{(magic) => (
<ul ref={magic.innerRef} {...magic.droppableProps}>
<Draggable draggableId='first' index={0}>
{() => <li>One</li>}
</Draggable>
<Draggable draggableId='second' index={1}>
{() => <li>Two</li>}
</Draggable>
</ul>
)}
</Droppable>
Draggable에 prop들을 넣어보자
💡 dragHandleProps : 사방에서 드래그가 보통 가능한데, 코너 등 특정 부분에서만 드래그가 가능하도록 처리하는 것
<ul ref={magic.innerRef} {...magic.droppableProps}>
<Draggable draggableId='first' index={0}>
{(magic) => (
<li ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
One
</li>
)}
</Draggable>
<Draggable draggableId='second' index={1}>
{(magic) => (
<li ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
Two
</li>
)}
</Draggable>
</ul>
<Draggable draggableId='first' index={0}>
{(magic) => (
<li ref={magic.innerRef} {...magic.draggableProps}>
<span {...magic.dragHandleProps}>🔥</span>
One
</li>
)}
</Draggable>
<Draggable draggableId='second' index={1}>
{(magic) => (
<li ref={magic.innerRef} {...magic.draggableProps}>
<span {...magic.dragHandleProps}>🔥</span>
Two
</li>
)}
</Draggable>
테마 스타일을 변경하자
Theme.ts 에서 색상을 변경하자
import { DefaultTheme } from 'styled-components';
export const darkTheme: DefaultTheme = {
bgColor: '#3F8CF2',
boardColor:"#DADFE9",
cardColor: "white",
};
styled.d.ts 에 변경되었음을 알려주자
// import original module declarations
import 'styled-components';
// and extend them!
declare module 'styled-components' {
export interface DefaultTheme {
bgColor: string;
boardColor: string;
cardColor: string;
}
}
index.tsx의 GlobalStyle의 body도 변화에 맞게 변경하자
body{
font-family: 'Source Sans Pro', sans-serif;
background-color: ${(props) => props.theme.bgColor};
color: black;
}
App.tsx에 Board, Card의 styled component를 만들어주자
import styled from 'styled-components';
const Board = styled.div`
background-color : ${(props) => props.theme.boardColor}}`;
const Card = styled.div`
background-color : ${(props) => props.theme.cardColor}}`;
Board, Card로 ul, li를 변경하자
function App() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<div>
<Droppable droppableId='one'>
{(magic) => (
<Board ref={magic.innerRef} {...magic.droppableProps}>
<Draggable draggableId='first' index={0}>
{(magic) => (
<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
One
</Card>
)}
</Draggable>
</Board>
)}
</Droppable>
</div>
</DragDropContext>
);
Style 추가
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
max-width: 480px;
width: 100%;
margin: 0 auto;
justify-content: center;
align-items: center;
height: 100vh;
`;
const Boards = styled.div`
display: grid;
width: 100%;
grid-template-columns: repeat(1, 1fr);
`;
const Board = styled.div`
padding: 20px 10px;
background-color : ${(props) => props.theme.boardColor}}
padding-top: 30px;
border-radius: 5px;
min-height: 200px;
`;
const Card = styled.div`
border-radius: 5px;
padding: 5px 10px;
margin-bottom: 5px;
background-color : ${(props) => props.theme.cardColor}}`;
function App() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Wrapper>
<Boards>
<Droppable droppableId='one'>
{(magic) => (
<Board ref={magic.innerRef} {...magic.droppableProps}>
<Draggable draggableId='first' index={0}>
{(magic) => (
<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
One
</Card>
)}
</Draggable>
</Board>
)}
</Droppable>
</Boards>
</Wrapper>
</DragDropContext>
);
}
export default App;
toDo를 추가해 toDo에 맞게 card가 나오도록 해보자
const toDos = ['a', 'b', 'c', 'd', 'e', 'f'];
function App() {
const onDragEnd = () => {};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Wrapper>
<Boards>
<Droppable droppableId='one'>
{(magic) => (
<Board ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<Draggable draggableId={toDo} index={index}>
{(<magic) => (
<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
{toDo}
</Card>
)}
</Draggable>
))}
{magic.placeholder}
</Board>
)}
</Droppable>
</Boards>
</Wrapper>
</DragDropContext>
);
}
💡 Board 밑에 placeholder
를 붙여주면 사이즈가 변화하지 않는다.
아이템을 드롭했을 때 재정렬하는 기능을 구현해보자.
toDo State를 위한 atom을 만들자
import { atom, selector } from 'recoil';
export const toDoState = atom({
key: 'toDo',
default: ['a', 'b', 'c', 'd', 'e', 'f']
});
App.tsx에 toDoState 값을 가져오자
const [toDos, setToDos] = useRecoilState(toDoState);
App.tsx에 onDragEnd 함수를 구현하자 - 1
⇒ onDragEnd
: 드래그가 끝났을 때 실행되는 함수
const onDragEnd = () => {
console.log('draggin finished');
};
<aside>
💡 onDragEnd는 드래그가 끝났을 때 많은 것을 알려준다!
const onDragEnd = (args: any) => {
console.log(args);
};
⇒ 무엇을 드래그 한 것인지, 드래그해서 어디로 가는지 등…
4. App.tsx에 onDragEnd 함수를 구현하자 - 2splice
사용! : splice(시작 인덱스, 없앨 개수, 대체할 문자);App.tsx의 setToDos() 함수를 구현하자
const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
if (!destination) return;
setToDos((oldToDos) => {
const copyToDos = [...oldToDos];
// 1) Delete item on source.index
copyToDos.splice(source.index, 1);
// 2) Put back the item on the destination.index
copyToDos.splice(destination?.index, 0, draggableId);
return copyToDos;
});
};
Draggable의 key를 toDo로 변경하자
<Board ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<Draggable key={toDo} draggableId={toDo} index={index}>
{(magic) => (
<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
{toDo}
</Card>
)}
</Draggable>
))}
{magic.placeholder}
</Board>
💡 Draggable에서 key와 draggableId는 같아야 한다!
→ 다음 시간에 해결해보자!
움직일 때 글자가 가끔 떨리는 문제를 해결해보자
코드를 정리해보자
a. Components 폴더 생성 후, 안에 DraggableCard.tsx 생성
```jsx
import { Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
const Card = styled.div`
border-radius: 5px;
padding: 5px 10px;
margin-bottom: 5px;
background-color : ${(props) => props.theme.cardColor}}`;
interface IDraggableCardProps {
toDo: string;
index: number;
}
function DraggableCard({ toDo, index }: IDraggableCardProps) {
return (
<Draggable key={toDo} draggableId={toDo} index={index}>
{(magic) => (
<Card ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
{toDo}
</Card>
)}
</Draggable>
);
}
export default DraggableCard;
```
b. App.tsx에 DraggableCard 추가
```jsx
return (
<DragDropContext onDragEnd={onDragEnd}>
<Wrapper>
<Boards>
<Droppable droppableId='one'>
{(magic) => (
<Board ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Board>
)}
</Droppable>
</Boards>
</Wrapper>
</DragDropContext>
);
```
react memo
를 사용해 계속 렌더링되는 것을 방지한다.
💡 react memo는 react.js한테 prop이 바뀌지 않는다면 컴포넌트를 렌더링 하지 말라고 한다!
import React from 'react';
export default React.memo(DraggableCard);
여러 개의 보드를 만들어보자
state를 object로 만들어주자
import { atom, selector } from 'recoil';
export const toDoState = atom({
key: 'toDo',
default: {
to_do: ['a', 'b'],
doing: ['c', 'd'],
done: ['e', 'f']
}
});
Components 폴더 안에 Board.tsx 파일을 생성하자
import { Droppable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import DraggableCard from './DraggableCard';
const Wrapper = styled.div`
padding: 20px 10px;
background-color : ${(props) => props.theme.boardColor}};
border-radius: 5px;
min-height: 200px;
`;
interface IBoardProps {
toDos: string[];
boardId: string;
}
function Board({ toDos, boardId }: IBoardProps) {
return (
<Droppable droppableId={boardId}>
{(magic) => (
<Wrapper ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Wrapper>
)}
</Droppable>
);
}
export default Board;
Atoms.tsx 변경
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
import { useRecoilState } from 'recoil';
import styled from 'styled-components';
import { toDoState } from './atom';
import DraggableCard from './Components/DraggableCard';
import Board from './Components/Board';
const Wrapper = styled.div`
display: flex;
width: 100%;
max-width: 680px;
margin: 0 auto;
justify-content: center;
align-items: center;
height: 100vh;
`;
const Boards = styled.div`
display: grid;
width: 100%;
gap: 10px;
grid-template-columns: repeat(3, 1fr);
`;
function App() {
const [toDos, setToDos] = useRecoilState(toDoState);
const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
if (!destination) return;
// setToDos((oldToDos) => {
// const copyToDos = [...oldToDos];
// // 1) Delete item on source.index
// copyToDos.splice(source.index, 1);
// // 2) Put back the item on the destination.index
// copyToDos.splice(destination?.index, 0, draggableId);
// return copyToDos;
// });
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Wrapper>
<Boards>
{Object.keys(toDos).map((boardId) => (
<Board boardId={boardId} key={boardId} toDos={toDos[boardId]} />
))}
</Boards>
</Wrapper>
</DragDropContext>
);
}
export default App;
atom.tsx 에 인터페이스를 추가하자
import { atom, selector } from 'recoil';
interface IToDoState {
[key: string]: string[];
}
export const toDoState = atom<IToDoState>({
key: 'toDo',
default: {
to_do: ['a', 'b'],
doing: ['c', 'd'],
done: ['e', 'f']
}
});
Board 안에서 카드 재배열을 다시 구현하자!
import { Droppable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import DraggableCard from './DraggableCard';
const Wrapper = styled.div`
width: 300px;
padding: 20px 10px;
padding-top: 10px;
background-color : ${(props) => props.theme.boardColor}};
border-radius: 5px;
min-height: 300px;
`;
const Title = styled.h2`
text-align: center;
font-weight: 600;
maring-bottom: 10px;
font-size: 18px;
`;
interface IBoardProps {
toDos: string[];
boardId: string;
}
function Board({ toDos, boardId }: IBoardProps) {
return (
<Wrapper>
<Title>{boardId}</Title>
<Droppable droppableId={boardId}>
{(magic) => (
<div ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</div>
)}
</Droppable>
</Wrapper>
);
}
export default Board;
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
import { useRecoilState } from 'recoil';
import styled from 'styled-components';
import { toDoState } from './atom';
import DraggableCard from './Components/DraggableCard';
import Board from './Components/Board';
const Wrapper = styled.div`
display: flex;
width: 100vw;
max-width: 680px;
margin: 0 auto;
justify-content: center;
align-items: center;
height: 100vh;
`;
const Boards = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
gap: 10px;
`;
function App() {
const [toDos, setToDos] = useRecoilState(toDoState);
const onDragEnd = ({ draggableId, destination, source }: DropResult) => {
if (!destination) return;
// setToDos((oldToDos) => {
// const copyToDos = [...oldToDos];
// // 1) Delete item on source.index
// copyToDos.splice(source.index, 1);
// // 2) Put back the item on the destination.index
// copyToDos.splice(destination?.index, 0, draggableId);
// return copyToDos;
// });
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Wrapper>
<Boards>
{Object.keys(toDos).map((boardId) => (
<Board boardId={boardId} key={boardId} toDos={toDos[boardId]} />
))}
</Boards>
</Wrapper>
</DragDropContext>
);
}
export default App;
import { atom, selector } from 'recoil';
interface IToDoState {
[key: string]: string[];
}
export const toDoState = atom<IToDoState>({
key: 'toDo',
default: {
'To Do': ['a', 'b'],
Doing: ['c', 'd'],
Done: ['e', 'f']
}
});
App.tsx의 onDragEnd를 수정하자 - 1
const onDragEnd = (info: DropResult) => {
console.log(info);
};
App.tsx의 onDragEnd를 수정하자 - 2
const onDragEnd = (info: DropResult) => {
console.log(info);
const { destination, draggableId, source } = info;
if (destination?.droppableId === source.droppableId) {
setToDos((oldToDos) => {
const boardCopy = [...oldToDos[source.droppableId]];
boardCopy.splice(source.index, 1);
boardCopy.splice(destination?.index, 0, draggableId);
return {
...oldToDos,
[source.droppableId]: boardCopy
};
});
}
};
보드를 넘나드는 이동을 만들어보자
App.tsx에 구현하자
if (destination?.droppableId !== source.droppableId) {
// cross board movement
setToDos((allBoards) => {
const sourceBoard = [...allBoards[source.droppableId]];
const destinationBoard = [...allBoards[destination.droppableId]];
sourceBoard.splice(source.index, 1);
destinationBoard.splice(destination?.index, 0, draggableId);
return {
...allBoards,
[source.droppableId]: sourceBoard,
[destination.droppableId]: destinationBoard
};
});
}
→ 다음 시간에 해결해보자!
보드를 떠날 때 색상을 바꿔야할 타이밍과 목적지 보드에 도착했을 때 색상을 바꿔야 할 타이밍을 정해보자
Board.tsx에 Area 컴포넌트를 추가하자
const Area = styled.div`
background-color: blue;
`;
function Board({ toDos, boardId }: IBoardProps) {
return (
<Wrapper>
<Title>{boardId}</Title>
<Droppable droppableId={boardId}>
{(magic) => (
<Area ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Area>
)}
</Droppable>
</Wrapper>
);
}
Area 영역을 늘려 아무데나 이동할 수 있게 해보자.
const Wrapper = styled.div`
width: 300px;
padding: 20px 10px;
padding-top: 10px;
background-color : ${(props) => props.theme.boardColor}};
border-radius: 5px;
min-height: 300px;
display: flex;
flex-direction: column;
`;
const Area = styled.div`
background-color: blue;
flex-grow: 1;
`;
💡 flex-grow : 자동으로 플렉스박스 아이템의 크기를 늘리게 해준다.
snapshot
을 사용해 이동할 보드의 Area의 색상을 변경해주자
💡 snapshot : isDraggingOver에 대한 boolean 값을 넘겨 board로 들어왔는지 알려주고,
draggingFromThisWith로 DraggableID를 넘겨 드래그를 시작한 아이디를 알려줌.
⇒ snapshot의 이름은 info로 대체했다.
const Area = styled.div<{ isDraggingOver: boolean }>`
background-color: ${(props) => (props.isDraggingOver ? 'pink' : 'blue')};
flex-grow: 1;
`;
function Board({ toDos, boardId }: IBoardProps) {
return (
<Wrapper>
<Title>{boardId}</Title>
<Droppable droppableId={boardId}>
{(magic, info) => (
<Area isDraggingOver={info.isDraggingOver} ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Area>
)}
</Droppable>
</Wrapper>
);
}
snapshot을 사용해 떠난 보드의 Area 색상을 변경해주자
interface IAreaProps {
isDraggingOver: boolean;
isDraggingFromThis: boolean;
}
const Area = styled.div<IAreaProps>`
background-color: ${(props) => (props.isDraggingOver ? 'pink' : props.isDraggingFromThis ? 'red' : 'blue')};
flex-grow: 1;
`;
<Droppable droppableId={boardId}>
{(magic, info) => (
<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Area>
)}
</Droppable>
Area에 transition을 넣어주자
const Area = styled.div<IAreaProps>`
background-color: ${(props) => (props.isDraggingOver ? 'pink' : props.isDraggingFromThis ? 'red' : 'blue')};
flex-grow: 1;
transition: background-color 0.3s ease-in-out;
`;
드래그하고 있는 카드의 스타일을 변경해보자
DraggableCard.tsx에서 snapshot을 사용해 스타일을 변경해보자
const Card = styled.div<{ isDragging: boolean }>`
border-radius: 5px;
padding: 10px 10px;
margin-bottom: 5px;
background-color: ${(props) => (props.isDragging ? '#74b9ff' : props.theme.cardColor)};
box-shadow: ${(props) => (props.isDragging ? '0px 2px 5px rgba(0,0,0,0.5)' : 'none')};
`;
function DraggableCard({ toDo, index }: IDraggableCardProps) {
return (
<Draggable key={toDo} draggableId={toDo} index={index}>
{(magic, snapshot) => (
<Card isDragging={snapshot.isDragging} ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
{toDo}
</Card>
)}
</Draggable>
);
}
Board.tsx의 스타일도 변경해주자
const Wrapper = styled.div`
width: 300px;
padding-top: 10px;
background-color : ${(props) => props.theme.boardColor}};
border-radius: 5px;
min-height: 300px;
display: flex;
flex-direction: column;
`;
const Area = styled.div<IAreaProps>`
background-color: ${(props) => (props.isDraggingOver ? '#dfe6e9' : props.isDraggingFromThis ? '#b2bec3' : 'transparent')};
flex-grow: 1;
transition: background-color 0.3s ease-in-out;
padding: 20px;
`;
ref 사용법을 알아보자
💡 reference : react 코드를 이용해 HTML 요소를 요소를 지정하고, 가져올 수 있는 방법.
useRef 사용
import { useRef } from 'react';
function Board({ toDos, boardId }: IBoardProps) {
const inputRef = useRef<HTMLInputElement>(null);
const onClick = () => {
inputRef.current?.focus();
setTimeout(() => {
inputRef.current?.blur();
}, 5000);
};
return (
<Wrapper>
<Title>{boardId}</Title>
<input ref={inputRef} placeholder='grab me' />
<button onClick={onClick}>Click me</button>
<Droppable droppableId={boardId}>
{(magic, info) => (
<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Area>
)}
</Droppable>
</Wrapper>
);
}
react-hook-form의 useForm를 사용해 폼을 만들어보자
react-hook-form을 설치하자
⇒ npm i react-hook-form
useForm을 불러오고 인터페이스를 생성하자
import { useForm } from 'react-hook-form';
interface IForm {
toDo: string;
}
form을 구현하자
const Form = styled.form`
width: 100%;
input {
width: 100%;
}
`;
function Board({ toDos, boardId }: IBoardProps) {
const { register, setValue, handleSubmit } = useForm<IForm>();
return (
<Wrapper>
<Title>{boardId}</Title>
<Form>
<input {...register('toDo', { required: true })} placeholder={`Add task on ${boardId}`} />
</Form>
<Droppable droppableId={boardId}>
{(magic, info) => (
<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo} index={index} toDo={toDo} />
))}
{magic.placeholder}
</Area>
)}
</Droppable>
</Wrapper>
);
}
onValid를 설정해 IForm 타입의 데이터를 받자
const onValid = (data: IForm) => {
};
return (
<Wrapper>
<Title>{boardId}</Title>
<Form onSubmit={handleSubmit(onValid)}>
atoms.tsx에서 state를 변경하자
import { atom, selector } from 'recoil';
export interface IToDo {
id: number;
text: string;
}
interface IToDoState {
[key: string]: IToDo[];
}
export const toDoState = atom<IToDoState>({
key: 'toDo',
default: {
'To Do': [],
Doing: [],
Done: []
}
});
prop들을 모두 업데이트하자
interface IBoardProps {
toDos: ITodo[];
boardId: string;
}
function Board({ toDos, boardId }: IBoardProps) {
const { register, setValue, handleSubmit } = useForm<IForm>();
const onValid = (data: IForm) => {};
return (
<Wrapper>
<Title>{boardId}</Title>
<Form onSubmit={handleSubmit(onValid)}>
<input {...register('toDo', { required: true })} placeholder={`Add task on ${boardId}`} />
</Form>
<Droppable droppableId={boardId}>
{(magic, info) => (
<Area isDraggingOver={info.isDraggingOver} isDraggingFromThis={Boolean(info.draggingFromThisWith)} ref={magic.innerRef} {...magic.droppableProps}>
{toDos.map((toDo, index) => (
<DraggableCard key={toDo.id} index={index} toDoId={toDo.id} toDoText={toDo.text} />
))}
{magic.placeholder}
</Area>
)}
</Droppable>
</Wrapper>
);
}
interface IDraggableCardProps {
toDoId: number;
toDoText: string;
index: number;
}
function DraggableCard({ toDoId, toDoText, index }: IDraggableCardProps) {
return (
<Draggable key={toDoId + ''} draggableId={toDoId + ''} index={index}>
{(magic, snapshot) => (
<Card isDragging={snapshot.isDragging} ref={magic.innerRef} {...magic.draggableProps} {...magic.dragHandleProps}>
{toDoText}
</Card>
)}
</Draggable>
);
}
App.tsx에서 onDragEnd 함수를 obj로 변환해 옮겨주자
const onDragEnd = (info: DropResult) => {
console.log(info);
const { destination, draggableId, source } = info;
if (!destination) return;
if (destination?.droppableId === source.droppableId) {
// same board movement
setToDos((oldToDos) => {
const boardCopy = [...oldToDos[source.droppableId]];
const taskObj = boardCopy[source.index];
boardCopy.splice(source.index, 1);
boardCopy.splice(destination?.index, 0, taskObj);
return {
...oldToDos,
[source.droppableId]: boardCopy
};
});
}
if (destination?.droppableId !== source.droppableId) {
// cross board movement
setToDos((allBoards) => {
const sourceBoard = [...allBoards[source.droppableId]];
const taskObj = sourceBoard[source.index];
const destinationBoard = [...allBoards[destination.droppableId]];
sourceBoard.splice(source.index, 1);
destinationBoard.splice(destination?.index, 0, taskObj);
return {
...allBoards,
[source.droppableId]: sourceBoard,
[destination.droppableId]: destinationBoard
};
});
}
};
Board.tsx에서 Task를 추가해보자
const onValid = ({ toDo }: IForm) => {
const newToDo = {
id: Date.now(),
text: toDo
};
setToDos((allBoards) => {
return {
...allBoards,
[boardId]: [...allBoards[boardId], newToDo]
};
});
setValue('toDo', '');
};
💪 Challenge