What is this post about ?
In this article we will explore how to build a Kanban style todo app. Each todo task will have 3 possible states : Not started, In Progress, Done. As soon as a new todo is added the default state would be Not started and the user will be able to drag between states. This app is not focused on styling rather on the bare minimum functional implementation.
Check out the app here : Kanban style todo App
Content
- Design
- Add new todo
- Changing todo state
- Deleting todo
Lets go deep dive into each one and explore how it was implemented.
Design
Add new todo
Todo will be added using Input area and the default state will be Not Started.
// Component
const TodoInput = ({ onTodoAdd }) => {
const [todoInput, setTodoInput] = useState("");
const onAdd = (e) => {
e.preventDefault();
onTodoAdd(todoInput);
setTodoInput("");
};
return (
<form className="todo-input">
<input
placeholder="Add Todo entry"
value={todoInput}
onChange={(e) => setTodoInput(e.target.value)}
/>
<button onClick={onAdd} type="submit">
Add
</button>
</form>
);
};
// onTodoAdd Prop implementation
const onTodoAdd = (todoText) => {
setTodos((_t) => {
return [
..._t,
{
id: uuidv4(), // Generate unique id
value: todoText,
state: TODO_STATE.NOT_STARTED,
},
];
});
};
Changing todo state
Depending on the states for the todo we will need that many state containers to hold the todo. So in this case there will be 3 containers [Not started, In Progress, Done].
Each container will act as drop zone for the items to be dropped on.
Each task item will act as draggable item and can be dropped in any of the drop zones available.
Drop zones : Not started , In Progress, Done, Delete.
For implementing drag and drop functionality we will be using React DnD
export const TODO_STATE = {
NOT_STARTED: "Not started",
IN_PROGRESS: "In progress",
DONE: "Done",
};
const TodoContent = ({ todos, onTodoDrag, onTodoDelete }) => {
const notStartedTodos = getTodosBasedOnState(todos, TODO_STATE.NOT_STARTED);
const inProgressTodos = getTodosBasedOnState(todos, TODO_STATE.IN_PROGRESS);
const doneTodos = getTodosBasedOnState(todos, TODO_STATE.DONE);
const [isDragActive, setIsDragActive] = useState(false);
const onDragActive = (dragActive) => {
setIsDragActive(dragActive);
};
return (
<DndProvider backend={HTML5Backend}>
<div className="todo-content">
<DraggableItemContainer
title="Not Started"
todos={notStartedTodos}
onTodoDrag={onTodoDrag}
state={TODO_STATE.NOT_STARTED}
onDragActive={onDragActive}
onTodoDelete={onTodoDelete}
/>
<DraggableItemContainer
title="In Progress"
todos={inProgressTodos}
onTodoDrag={onTodoDrag}
state={TODO_STATE.IN_PROGRESS}
onDragActive={onDragActive}
onTodoDelete={onTodoDelete}
/>
<DraggableItemContainer
title="Done"
todos={doneTodos}
onTodoDrag={onTodoDrag}
state={TODO_STATE.DONE}
onDragActive={onDragActive}
onTodoDelete={onTodoDelete}
/>
</div>
{isDragActive && (
<div className="delete-drag-container">
<DeleteDragItemBox />
</div>
)}
</DndProvider>
);
};
Draggable Container
const DraggableItemContainer = ({
title = "",
todos = [],
onTodoDrag,
state,
onDragActive,
onTodoDelete,
}) => {
const [{ canDrop, isOver }, drop] = useDrop(() => ({
accept: ITEM_TYPES.TODO,
drop: () => ({ state }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}));
const isActive = canDrop && isOver;
const style = {
border: isActive ? "3px dashed black" : "1px solid black",
};
return (
<div className="draggable-item-container" ref={drop} style={style}>
<h4 className="title">
{title} - [{todos.length}]
</h4>
<div className="content">
{todos.map((t) => {
return (
<DraggableItem
key={t.id}
todo={t}
onTodoDrag={onTodoDrag}
onDragActive={onDragActive}
onTodoDelete={onTodoDelete}
/>
);
})}
</div>
</div>
);
};
Draggable Item a.k.a Todo item
const DraggableItem = ({ todo, onTodoDrag, onDragActive, onTodoDelete }) => {
const [{ isDragging }, drag] = useDrag(() => ({
type: ITEM_TYPES.TODO,
item: { todo },
end: (item, monitor) => {
const dropResult = monitor.getDropResult();
if (item && dropResult) {
if (!dropResult.delete) {
onTodoDrag(item.todo.id, dropResult.state);
} else {
onTodoDelete(item.todo.id);
}
onDragActive(false);
}
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
handlerId: monitor.getHandlerId(),
}),
isDragging: (monitor) => {
if (todo.id === monitor.getItem().todo.id) {
onDragActive(true);
}
},
}));
const opacity = isDragging ? 0.4 : 1;
const textDecoration =
todo.state === TODO_STATE.DONE ? "line-through" : "none";
let backgroundColor = "";
switch (todo.state) {
case TODO_STATE.NOT_STARTED: {
backgroundColor = "lightcoral";
break;
}
case TODO_STATE.IN_PROGRESS: {
backgroundColor = "lightyellow";
break;
}
case TODO_STATE.DONE: {
backgroundColor = "lightgreen";
break;
}
default: {
backgroundColor = "white";
break;
}
}
return (
<div
className="draggable-item"
ref={drag}
style={{ opacity, textDecoration, backgroundColor }}
>
{todo.value}
</div>
);
};
Event : onTodoDrag
const onTodoDrag = (id, state) => {
setTodos((_t) =>
_t.map((t) => {
if (t.id === id) {
return { ...t, state };
}
return t;
})
);
};
Deleting todo
Delete Drop zone
const DeleteDragItemBox = () => {
const [{ canDrop, isOver }, drop] = useDrop(() => ({
accept: ITEM_TYPES.TODO,
drop: () => ({ delete: true }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}));
const isActive = canDrop && isOver;
const style = {
border: isActive ? "3px dashed black" : "none",
};
return (
<div className="delete-drag-box" style={style} ref={drop}>
<DeleteIcon width={'4rem'}/>
</div>
);
};
const onTodoDelete = (id) => {
setTodos((_t) => _t.filter((t) => t.id !== id));
};
Conclusion
Implementing this game will allow you to learn about using state and side effects in React to implement the desired logic. This app was made as part of learning new components which are used in real life applications.
Stay safe and lend a hand to another :)
Top comments (1)
Always interesting to have be able to go DIY Style !
We're fans of this subject and what gravitates around it, in our team. We wrote an article on the subject and we'd love your opinion: zenkit.com/en/blog/kanban-explaine...