Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 13, 2022 02:30 am GMT

Drag and Drop Kanban Board with React TypeScript

Demo (TLDR)

This is the source code and the Kanban board that we will be building.

HTML Drag and Drop API

HTML Drag and Drop API is required to implement the drag and drop function on any DOM element.

State Management

Coming out with the right design pattern for state management is important for an interactive web application.

I am using useReducer as the state is complex.

This is the initial state. isDragOver is required to update the style of the item that is being drag over. For simplicity, Date.now() is used as our unique item id.

type Category = "todo" | "doing" | "done";type Item = { id: number; content: string; isDragOver: boolean };type State = { [key in Category]: Item[] };const initialState: State = {  todo: [{ id: Date.now(), content: "Task 4", isDragOver: false }],  doing: [{ id: Date.now() + 1, content: "Task 3", isDragOver: false }],  done: [    { id: Date.now() + 2, content: "Task 2", isDragOver: false },    { id: Date.now() + 3, content: "Task 1", isDragOver: false },  ],};

These are the actions that are performed by the reducer.

type Action =  | { type: "CREATE"; content: string }  | {      type: "UPDATE_CATEGORY";      newCategory: Category;      oldCategory: Category;      position: number;      id: number;    }  | {      type: "UPDATE_DRAG_OVER";      id: number;      category: Category;      isDragOver: boolean;    }  | { type: "DELETE"; id: number; category: Category };

Action: CREATE

The create action creates an item in the todo column of the Kanban board.

case "CREATE": {    if (action.content.trim().length === 0) return state;    return {      ...state,      todo: [        { id: Date.now(), content: action.content, isDragOver: false },        ...state.todo      ]    };}

Action: UPDATE_CATEGORY

The UPDATE_CATEGORY action updates the position and category of the item.

First, we will find the old position and the item by using the id given in the action object. To avoid using mutation, Immediately Invoked Function Expression (IIFE) is used on this function to return both of the values.

const { oldPosition, found } = (() => {  const oldPosition = state[oldCategory].findIndex(    (item) => item.id === action.id  );  return { oldPosition, found: state[oldCategory][oldPosition] };})();

The original state is return if the item is not found or when the category and position did not change.

if (oldPosition === -1) return state;if (newCategory === oldCategory && position === oldPosition) return state;

The item is removed from the old category list. The new category list is determined by whether the original category has been changed.

const filtered = state[oldCategory].filter((item) => item.id !== action.id);const newCategoryList = newCategory === oldCategory ? filtered : [...state[newCategory]];

The lists are updated according the new item's position.

if (position === 0) {  return {    ...state,    [oldCategory]: filtered,    [newCategory]: [found, ...newCategoryList],  };}return {  ...state,  [oldCategory]: filtered,  [newCategory]: [    ...newCategoryList.slice(0, position),    found,    ...newCategoryList.slice(position),  ],};

The complete code.

case "UPDATE_CATEGORY": {    const { position, newCategory, oldCategory } = action;    const { oldPosition, found } = (() => {      const oldPosition = state[oldCategory].findIndex(        (item) => item.id === action.id      );      return { oldPosition, found: state[oldCategory][oldPosition] };    })();    if (oldPosition === -1) return state;    if (newCategory === oldCategory && position === oldPosition) return state;    const filtered = state[oldCategory].filter(      (item) => item.id !== action.id    );    const newCategoryList =      newCategory === oldCategory ? filtered : [...state[newCategory]];    if (position === 0) {      return {        ...state,        [oldCategory]: filtered,        [newCategory]: [found, ...newCategoryList]      };    }    return {      ...state,      [oldCategory]: filtered,      [newCategory]: [        ...newCategoryList.slice(0, position),        found,        ...newCategoryList.slice(position)      ]    };}

Action: UPDATE_DRAG_OVER

This action will update the item that has another item that drags over or out of it.

case "UPDATE_DRAG_OVER": {    const updated = state[action.category].map((item) => {      if (item.id === action.id) {        return { ...item, isDragOver: action.isDragOver };      }      return item;    });    return {      ...state,      [action.category]: updated    };}

Action: DELETE

Lastly, this action will delete the item in the Kanban board.

case "DELETE": {    const filtered = state[action.category].filter(      (item) => item.id !== action.id    );    return {      ...state,      [action.category]: filtered    };}

Add Item Form State

There are two other states that are used to manage the add item to todo column of the Kanban board.

The add state determines to hide or show the add item form while the addInput state will store the title of the new item.

const [state, dispatch] = useReducer(reducer, initialState); // our reducerconst [add, setAdd] = useState(false);const [addInput, setAddInput] = useState("");

User Interface (UI)

We have now covered everything about the state management of the Kanban board. I will go through some of the core UI components of the Kanban board.

Add Item Form

The TSX of the add item form.

{  add && (    <div className="addItem">      <input        type="text"        onKeyUp={(e) => {          if (e.code === "Enter") {            e.preventDefault();            e.stopPropagation();            dispatch({ type: "CREATE", content: addInput });            setAddInput("");            setAdd(false);          }        }}        onChange={onAddInputChange}        value={addInput}      />      <div>        <button          onClick={() => {            dispatch({ type: "CREATE", content: addInput });            setAddInput("");            setAdd(false);          }}        >          Add        </button>        <button onClick={() => setAdd(false)}>Cancel</button>      </div>    </div>  );}

The input change event listener function.

const onAddInputChange = (event: ChangeEvent<HTMLInputElement>) => {  const value = event.currentTarget.value;  setAddInput(value);};

Kanban Board Columns

The TSX of the columns in the Kanban Board.

<div  className="items"  onDragOver={(e) => e.preventDefault()}  onDrop={(e) => onItemsDrop(e, "doing")} // "todo" | "doing" | "done">  {Items(state.doing, "doing")}  {/* "todo" | "doing" | "done" */}</div>

The onDrop listener function for the columns is to detect whether a draggable element has been dropped on the column. The e.dataTransfer can get, store or clear data from the draggable element. The data needs to be JSON parsed as dataTransfer only accepts string.

const onItemsDrop = (  e: React.DragEvent<HTMLDivElement>,  newCategory: Category) => {  const item = e.dataTransfer.getData("text/plain");  const parsedItem = JSON.parse(item);  const decodedItem = ItemDecoder.verify(parsedItem);  dispatch({    type: "UPDATE_CATEGORY",    id: decodedItem.id,    newCategory,    oldCategory: decodedItem.category,    position: state[newCategory].length,  });};

Decoders

Decoders is my go-to data validation library for JavaScript and NodeJS. It's lightweight , has good TypeScript support and is extendable. The parsed item is validated by this library.

const decodedItem = ItemDecoder.verify(parsedItem);

Action is dispatched to the reducer to update the columns in the Kanban board.

Items in Kanban Board

The TSX function to render the items in the Kanban Board.

const Items = (items: Item[], category: Category) => {  return items.map(({ id, content, isDragOver }) => (    <div      key={id}      draggable={true}      onDragStart={(e: React.DragEvent<HTMLDivElement>) => {        e.dataTransfer.setData(          "text/plain",          JSON.stringify({ id, content, category, isDragOver })        );      }}      onDragOver={(e: React.DragEvent<HTMLDivElement>) => {        e.preventDefault();        dispatch({          type: "UPDATE_DRAG_OVER",          category,          id,          isDragOver: true,        });      }}      onDragLeave={(e: React.DragEvent<HTMLDivElement>) => {        e.preventDefault();        dispatch({          type: "UPDATE_DRAG_OVER",          category,          id,          isDragOver: false,        });      }}      onDrop={(e: React.DragEvent<HTMLDivElement>) => {        e.stopPropagation();        const item = e.dataTransfer.getData("text/plain");        const parsedItem = JSON.parse(item);        const decodedItem = ItemDecoder.verify(parsedItem);        const position = state[category].findIndex((i) => i.id === id);        dispatch({          type: "UPDATE_CATEGORY",          id: decodedItem.id,          newCategory: category,          oldCategory: decodedItem.category,          position,        });        dispatch({          type: "UPDATE_DRAG_OVER",          category,          id,          isDragOver: false,        });      }}    >      <div className={"itemContent" + (isDragOver ? " dashed" : "")}>        <h2>{content}</h2>        <button onClick={() => dispatch({ type: "DELETE", category, id })}>          <DeleteIcon height={13} width={13} />        </button>      </div>    </div>  ));};

Draggable

To make the div draggable. draggable={true} is added to the properties of the div DOM.

OnDragStart

OnDragStart listener is triggered when an item is drag. The required data are stored as string into the dataTransfer Drag and Drop API.

onDragStart={(e: React.DragEvent<HTMLDivElement>) => {    e.dataTransfer.setData(      "text/plain",      JSON.stringify({ id, content, category, isDragOver })    );}}

onDragOver and onDragLeave

These two listeners are triggered when an item is drag over or leave another item in the Kanban board.

onDragOver={(e: React.DragEvent<HTMLDivElement>) => {    e.preventDefault();    dispatch({      type: "UPDATE_DRAG_OVER",      category,      id,      isDragOver: true    });  }}onDragLeave={(e: React.DragEvent<HTMLDivElement>) => {    e.preventDefault();    dispatch({      type: "UPDATE_DRAG_OVER",      category,      id,      isDragOver: false    });}}

onDrop

Lastly, we have our onDrop listener. This is similar to the onItemsDrop listener for the Kanban board columns. e.stopPropagation() is to prevent this listener from bubbling up to the parent elements and triggering the same listener again. Check out this article to find out how does this works.

onDrop={(e: React.DragEvent<HTMLDivElement>) => {    e.stopPropagation();    const item = e.dataTransfer.getData("text/plain");    const parsedItem = JSON.parse(item);    const decodedItem = ItemDecoder.verify(parsedItem);    const position = state[category].findIndex((i) => i.id === id);    dispatch({      type: "UPDATE_CATEGORY",      id: decodedItem.id,      newCategory: category,      oldCategory: decodedItem.category,      position    });    dispatch({      type: "UPDATE_DRAG_OVER",      category,      id,      isDragOver: false    });}}

Kanban Board Item

The isDragOver variable of each item is used to update the style of the item when another item drags over it. Item can be removed from the Kanban board as well.

<div className={"itemContent" + (isDragOver ? " dashed" : "")}>  <h2>{content}</h2>  <button onClick={() => dispatch({ type: "DELETE", category, id })}>    <DeleteIcon height={13} width={13} />  </button></div>;

Conclusion

We have come to the end of this article. There are still features that can be enhanced or added to our Kanban board. Here's a non-exhaustive list of it.

  1. Updating of item's title
  2. Body content for Kanban item
  3. Saving Kanban item data to a database/storage.
  4. Person assignment of Kanban item.

The goal of this article is to kickstart how to create a Kanban Board without any external libraries and I hope I did. Thank you for reading!


Original Link: https://dev.to/luazhizhan/drag-and-drop-kanban-board-with-react-typescript-2b34

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To