Introduction
Si vous n'avez pas suivi la partie 1, veuillez vous référer à React
- Trello - Part 1.
Si vous n'avez pas suivi la partie 2, veuillez vous référer à React
- Trello - Part 2.
Si vous n'avez pas suivi la partie 3, veuillez vous référer à React
- Trello - Part 3.
Jusqu'à présent, nous avons créé un tableau Trello qui permet de permuter les colonnes, d'ajouter de nouvelles colonnes et des tâches. Maintenant, nous allons nous concentrer sur la mise en place de la permutation des tâches entre elles.
Drag colonne
Pour commencer, nous allons ajouter un type à notre DragItem :
export type CardDragItem = {
index: number
id: string
columnId: string
text: string
type: "CARD"
}
export type ColumnDragItem = {
index: number
id: string
text: string
type: "COLUMN"
}
export type DragItem = CardDragItem | ColumnDragItem
Maintenant, notre type peut être soit CardDragItem, soit ColumnDragItem. Nous avons défini ce type, mais il est important de revoir notre modèle de carte et d'ajouter les propriétés suivantes :
- id : pour différencier les cartes de la même manière que nous l'avons fait pour les colonnes
- isPreview : pour savoir si la carte est actuellement en mode de prévisualisation ou non, comme pour les colonnes
- columnId : pour savoir dans quelle colonne se trouve la carte.
interface CardProps {
text: string
index: number
id: string
columnId: string
isPreview?: boolean
}
Pas de panique, vous allez rencontrer plusieurs erreurs ! Avant de les corriger, je vous propose d'implémenter notre reducer qui sera situé dans le composant Card. Nous allons ajouter l'action MOVE_TASK :
{
type: "MOVE_TASK"
payload: {
dragIndex: number
hoverIndex: number
sourceColumn: string
targetColumn: string
}
}
Nous pouvons remarquer que nous avons deux attributs supplémentaires pour l'action MOVE_TASK par rapport à l'action pour les colonnes :
- sourceColumn : l'ID de la colonne source
- targetColumn : l'ID de la colonne cible.
Nous devons maintenant déclarer cette action dans notre reducer :
case "MOVE_TASK": {
const {
dragIndex,
hoverIndex,
sourceColumn,
targetColumn
} =
action.payload;
return {};
}
Nous avons déstructuré notre payload pour fournir les éléments suivants :
- sourceColumnIndex : qui nous permettra de récupérer l'index de l'élément dans la colonne source grâce à sourceColumn
- targetColumnIndex : qui nous permettra de récupérer l'index de l'élément dans la colonne cible grâce à targetColumn
- updatedSourceColumn : la colonne source mise à jour en utilisant l'indice dragIndex
- stateWithUpdatedSourceColumn : la mise à jour de l'état avec la colonne mise à jour.
case "MOVE_TASK": {
const {
dragIndex,
hoverIndex,
sourceColumn,
targetColumn
} =
action.payload;
const sourceColumnIndex = findItemIndexById(state.lists, sourceColumn);
const targetColumnIndex = findItemIndexById(state.lists, targetColumn);
const sourceList = state.lists[sourceColumnIndex];
const task = sourceList.tasks[targetColumnIndex];
const updatedSourceList = {
...sourceList,
tasks: removeItemAtIndex(sourceList.tasks, dragIndex),
};
const stateWithUpdatedSourceList = {
...state,
lists: overrideItemAtIndex(
state.lists,
updatedSourceList,
sourceColumnIndex
),
};
return {};
}
Voici quelques explications :
- sourceColumnIndex et targetColumnIndex contiennent l'index de la tâche présente dans la colonne
- sourceList représente la colonne (= liste de tâches) obtenue grâce à sourceColumnIndex
- task est obtenue grâce à targetColumnIndex et représente la tâche
updatedSourceList met à jour la liste source en retirant la tâche déplacée
- stateWithUpdatedSourceList met à jour l'état
Nous devons maintenant faire la même chose pour la cible, puis retourner l'état mis à jour avec les actions effectuées :
const targetList = stateWithUpdatedSourceList.lists[targetListIndex];
const updatedTargetList = {
…
targetList,
tasks: insertItemAtIndex(targetList.tasks, task, hoverIndex),
};
return {
…
stateWithUpdatedSourceList,
lists: overrideItemAtIndex(
stateWithUpdatedSourceList.lists,
updatedTargetList,
targetListIndex
),
};
Notre reducer est prêt ! Nous allons simplement refactoriser notre dispatcher useItemDrop pour qu'il puisse accepter des cartes ainsi que des colonnes. Comme vous le savez, ce dispatcher peut accepter le type DragItem qui peut être une colonne ou une carte. Pour différencier les types, nous avons la chance de posséder une propriété nommée type. Cela risque de générer beaucoup de types codés en dur (ne vous inquiétez pas, nous améliorerons le code dans les futurs chapitres). Je vous propose donc de créer une énumération. Créons un dossier enums et ajoutons-y l'énumération type.ts :
export enum TypeEnum {
Column = "COLUMN",
Card = "CARD",
}
Je vais effectuer la mise à jour nécessaire pour remplacer toutes les valeurs codées en dur par l'énumération. Ensuite, nous allons mettre à jour notre composant pour qu'il accepte les cartes et les colonnes. Tout d'abord, nous pouvons séparer le code pour les colonnes comme suit :
function dispatchForColumn(
item: DragItem,
hoverIndex: number,
dispatch: Function
) {
const dragIndex = item.index;
if (dragIndex === hoverIndex) {
return;
}
dispatch({
type: "MOVE_LIST",
payload: {
dragIndex,
hoverIndex
}
});
}
export const useItemDrop = (index: number) => {
const {
dispatch
} = useAppState();
const [, drop] = useDrop({
accept: [TypeEnum.Column, TypeEnum.Card],
hover(item: DragItem) {
const hoverIndex = index;
console.log(typeof item);
if (item.type === TypeEnum.Column) {
dispatchForColumn(item, hoverIndex, dispatch);
} else {}
item.index = hoverIndex;
},
});
return {
drop
};
};
Pour les cartes, nous devons suivre un comportement différent :
- Nous devons vérifier que l'ID de la carte sur laquelle on survole n'est pas l'élément que l'on déplace
- Nous devons récupérer les index de la colonne source et de la colonne cible
- Nous devons utiliser le dispatcher pour appeler l'action MOVE_TASK
- Nous devons ajouter l'ID de la colonne.
function dispatchForColumn(
item: DragItem,
hoverIndex: number,
dispatch: Function
) {
const dragIndex = item.index;
if (dragIndex === hoverIndex) {
return;
}
dispatch({ type: "MOVE_LIST", payload: { dragIndex, hoverIndex } });
}
function dispatchForCard(
item: CardDragItem,
hoverIndex: number,
targetColumn: string,
dispatch: Function
) {
const dragIndex = item.index;
const sourceColumn = item.columnId;
dispatch({
type: "MOVE_TASK",
payload: { dragIndex, hoverIndex, sourceColumn, targetColumn },
});
item.index = hoverIndex;
item.columnId = targetColumn;
}
export const useItemDrop = (index: number, columnId: string) => {
const { dispatch } = useAppState();
const [, drop] = useDrop({
accept: [TypeEnum.Column, TypeEnum.Card],
hover(item: DragItem) {
const hoverIndex = index.toString() === columnId ? 0 : index;
if (item.type === TypeEnum.Column) {
dispatchForColumn(item, hoverIndex, dispatch);
} else {
dispatchForCard(item, hoverIndex, columnId, dispatch);
}
item.index = hoverIndex;
},
});
return { drop };
};
Rien de bien sorcier si vous avez suivi les étapes précédentes. La principale différence avec la colonne est que nous prenons l'index de l'élément en cours de déplacement (dragIndex) et l'ID de la colonne source (sourceColumn), tout comme les informations relatives à la survol. Nous avons donc mis en place la fonctionnalité de dépôt pour les cartes. La bonne nouvelle est que nous n'avons rien à changer pour notre fonctionnalité de drag. Nous devons maintenant importer nos deux dispatchers dans notre Card.tsx :
interface CardProps {
text: string;
index: number;
id: string;
columnId: string;
isPreview?: boolean;
}
export const Card = ({ text, id, index, columnId, isPreview }: CardProps) => {
const { state } = useAppState();
const ref = useRef<HTMLDivElement>(null);
const { drag } = useItemDrag({
type: TypeEnum.Card,
id,
index,
text,
columnId,
});
const { drop } = useItemDrop(index, columnId);
drag(drop(ref));
return (
<CardContainer
isHidden={isHidden(isPreview, state.draggedItem, TypeEnum.Card, id)}
isPreview={isPreview}
ref={ref}
>
{text}
</CardContainer>
);
};
Cela ressemble à la mise en place pour les colonnes, mais avec quelques modifications pour adapter notre composant. Maintenant, nous pouvons adapter notre CardContainer qui doit implémenter DragPreviewContainer :
export const CardContainer = styled(DragPreviewContainer)`
background-color: #fff;
cursor: pointer;
margin-bottom: 0.5rem;
padding: 0.5rem 1rem;
max-width: 300px;
border-radius: 3px;
box-shadow: #091e4240 0px 1px 0px 0px;
`;
Avant de pouvoir tester notre code, nous devons corriger une erreur. Cette erreur concerne le composant Card défini dans le composant Column. Nous devons en effet passer les éléments manquants à notre type :
export const Column = ({ text, index, id, isPreview }: ColumnProps) => {
const { state, dispatch } = useAppState();
const ref = useRef<HTMLDivElement>(null);
const { drop } = useItemDrop(index, id);
const { drag } = useItemDrag({ type: TypeEnum.Column, id, index, text });
drag(drop(ref));
return (
<ColumnContainer
isPreview={isPreview}
ref={ref}
isHidden={isHidden(isPreview, state.draggedItem, TypeEnum.Column, id)}
>
<ColumnTitle>{text}</ColumnTitle>
{state.lists[index].tasks.map((task, i) => (
<Card
id={task.id}
columnId={id}
text={task.text}
key={task.id}
index={i}
/>
))}
<AddNewItem
toggleButtonText="+ Add another card"
onAdd={(text) =>
dispatch({ type: "ADD_TASK", payload: { text, listId: id } })
}
dark
/>
</ColumnContainer>
);
};
Nous devons aussi mettre à jour notre CustomDragLayer afin qu'il accepte les cards :
const CustomDragLayer: React.FC = () => {
const { isDragging, item, currentOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<CustomDragLayerContainer>
<div style={getItemStyles(currentOffset)}>
{item.type === "COLUMN" ? (
<Column
id={item.id}
text={item.text}
index={item.index}
isPreview={true}
/>
) : (
<Card
columnId={item.columnId}
isPreview={true}
index={0}
id={item.id}
text={item.text}
/>
)}
</div>
</CustomDragLayerContainer>
);
};
export default CustomDragLayer;
Il y a un problème : lorsque nous essayons de déplacer les colonnes, une erreur se produit. Cela vient du fait que notre survol ne détecte pas le bon élément ! Pour y remédier, nous allons supprimer notre dispatcher useItemDrop et laisser le composant gérer cela :
export const Column = ({ text, index, id, isPreview }: ColumnProps) => {
const { state, dispatch } = useAppState();
const ref = useRef<HTMLDivElement>(null);
const [, drop] = useDrop({
accept: [TypeEnum.Column, TypeEnum.Card],
hover(item: DragItem) {
if (item.type === TypeEnum.Column) {
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
dispatch({ type: "MOVE_LIST", payload: { dragIndex, hoverIndex } });
item.index = hoverIndex;
} else {
const dragIndex = item.index;
const hoverIndex = 0;
const sourceColumn = item.columnId;
const targetColumn = id;
if (sourceColumn === targetColumn) {
return;
}
dispatch({
type: "MOVE_TASK",
payload: { dragIndex, hoverIndex, sourceColumn, targetColumn },
});
item.index = hoverIndex;
item.columnId = targetColumn;
}
},
});
const { drag } = useItemDrag({ type: TypeEnum.Column, id, index, text });
drag(drop(ref));
return (
<ColumnContainer
isPreview={isPreview}
ref={ref}
isHidden={isHidden(isPreview, state.draggedItem, TypeEnum.Column, id)}
>
<ColumnTitle>{text}</ColumnTitle>
{state.lists[index].tasks.map((task, i) => (
<Card
id={task.id}
columnId={id}
text={task.text}
key={task.id}
index={i}
/>
))}
<AddNewItem
toggleButtonText="+ Add another card"
onAdd={(text) =>
dispatch({ type: "ADD_TASK", payload: { text, listId: id } })
}
dark
/>
</ColumnContainer>
);
};
Vous devriez avoir le résultat suivant :
Conclusion
Pour résumer, dans toute formation, il est important de présenter les concepts les plus importants. Cependant, il est également important de tenir compte des améliorations possibles au niveau du code, telles que :
- Diviser correctement le code en regroupant les types dispersés dans un dossier "models".
- Utiliser des énumérations pour les textes. Si vous devez faire des choix dans les textes, il est conseillé de les regrouper dans des énumérations.
- Créer des composants réutilisables afin de gagner du temps dans le futur.
- Nous avons créé nos dispatchers, mais il est possible d'utiliser Redux à la place (nous verrons cela plus tard