Skip to main content

TP1 - Todolist - partie 2

La suite et fin du premier TP portant sur l'application de gestion de tâches.

Faire fonctionner les checkboxes

Comme vous avez pu le constater, si vous avez suivi à la lettre les exercices de la partie précédente, on ne peut pas compléter les tâches : il ne se passe rien quand on clique dessus. C'est parce que ce sont des éléments contrôlés : on leur donne leur valeur (par la prop checked), mais on n'a pas implémenté de moyen de la modifier.

Regardez l'exemple suivant.

Live Editor
function AlwaysChecked(){
    const [value, setValue] = useState(false)
    const handleChange = evt => {
        setValue(!value)
    }

    return <fieldset>
        <input
            id="my-checkbox"
            type="checkbox"
            checked={value}
            onChange={handleChange}
        />
        <label htmlFor="my-checkbox">Checkme</label>
    </fieldset>
}
Result
Loading...

On pourrait utiliser un state directement à l'intérieur de notre composant TodoItem, comme dans l'exemple ci-dessus. Cependant, l'état (complété ou non) est déjà stocké dans le state du composant parent App.

Sur le même modèle que pour le composant Form et sa prop onAdd, il faudrait que le composant TodoItem prenne par exemple une prop onComplete qui notifie le composant App du changement d'état d'une tâche, afin de modifier son propre state.

Essayez de faire cette modification vous même.

tip

Essayez de modifier un élément de la liste en utilisant Array#map, cela vous permettra de renvoyer une nouvelle liste et ainsi de préserver l'immutabilité.

Correction
src/components/TodoItem.jsx
const TodoItem = props => {
const {
id,
name,
complete,
onComplete,
} = props

return <li className={complete ? 'completed' : ''}>
{/* ... */}
<input
type="checkbox"
checked={completed}
onChange={onComplete}
id={`todo-${id}`}
/>
{/* ... */}
</li>
}
src/components/App.jsx
const App = () => {
// ...
const [todoList, setTodoList] = useState([{
// ...
}])

const handleComplete = todoId => {
setTodoList(
todoList.map((todo) => todoId === todo.id
? {
...todo,
completed: !todo.completed
}
: todo
)
)
}

return <section className="todoapp">
{/* ... */}
{todoList.map(todo => <TodoItem
id={todo.id}
key={todo.id}
name={todo.name}
completed={todo.completed}
onComplete={() => handleComplete(todo.id)}
/>)}
{/* ... */}
</section>
}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 7 : Completing todos" et poussez vers le serveur.

git add .
git commit -m 'Ex 7 : Completing todos'
git push

Filtrage

Notre application comporte trois boutons de filtrage permettant de voir toutes les tâches, seulement les tâches actives ou seulement les tâches complétées.

Les boutons de filtrage

Pour commencer à travailler sur cette fonctionnalité, commencez par extraire un composant FilterButton dans un nouveau fichier filterButton.jsx de manière à pouvoir l'utiliser de la sorte :

src/components/App.jsx
{/* ... */}
<ul className="filters">
<FilterButton
label="Tous"
onClick={() => alert('Filter : none')}
selected={true}
/>
<FilterButton
label="Complétés"
onClick={() => alert('Filter : completed')}
selected={false}
/>
<FilterButton
label="Actifs"
onClick={() => alert('Filter : active')}
selected={false}
/>
</ul>
{/* ... */}

Pour pouvoir changer de filtre, le composant App doit maintenant se souvenir du filtre actuellement sélectionné et préciser au composant FilterButton si il est actuellement sélectionné.

Ajoutez la possibilité de sélectionner un filtre vous même.

Correction
src/components/FilterButton.jsx
import React from 'react'

const FilterButton = props => {
const {
onClick,
label,
selected
} = props

return <li>
<button
className={selected ? 'selected' : ''}
onClick={onClick}
>
{label}
</button>
</li>
}

export default FilterButton
src/components/App.jsx
const App = () => {
const [filter, setFilter] = useState(null)

return <section className="todoapp">
{/* ... */}
<ul className="filters">
<FilterButton
label="Tous"
onClick={() => setFilter(null)}
selected={filter === null}
/>
<FilterButton
label="Complétés"
onClick={() => setFilter('completed')}
selected={filter === 'completed'}
/>
<FilterButton
label="Actifs"
onClick={() => setFilter('active')}
selected={filter === 'active'}
/>
</ul>
{/* ... */}
</section>
}
note

Notez ici que je n'ai pas créé d'event handler que j'aurais pu nommer handleFilterChange par exemple. La fonction étant très simple, j'ai préféré l'écrire directement dans l'attribut, mais ce n'est qu'une question de préférences.

Le filtrage des tâches

Maintenant que nous stockons le filtre actuellement sélectionné, nous sommes en mesure d'effectuer le filtrage des tâches affichées.

Modifiez l'affichage de la liste de tâches de manière à filtrer celles qui ne sont pas concernées par le filtre en cours.

tip

Comme vous le savez, il existe en Javascript une méthode Array#filter qui retourne une nouvelle liste dont on a retiré certains éléments...

Correction
src/components/App.jsx
const App = () => {
const filterItem = todo => filter === 'completed'
? todo.completed
: filter === 'active'
? !todo.completed
: true

// ...

return <section className="todoapp">
{/* ... */}
<ul className="todo-list">
{todoList
.filter(filterItem)
.map(todo => <TodoItem
key={todo.id}
{/* ... */}
/>)
}
</ul>
{/* ... */}
</section>
}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 8 : Filtering todos" et poussez vers le serveur.

git add .
git commit -m 'Ex 8 : Filtering todos'
git push

Compteur de tâches et pluralisation

Vous avez peut-être remarqué la partie "compteur de taches" en bas à gauche de l'application. Il est sensé afficher le nombre de tâches non cochées.

Commencez par enregistrer le nombre de tâches restantes dans une variable nommée par exemple leftTodos dans le composant App.

Correction
src/components/App.jsx
const leftTodos = todoList.filter(todo => !todo.completed).length

Maintenant, il ne nous reste plus qu'à afficher ce nombre à l'endroit adéquat :

<span className="todo-count">
<strong>
{leftTodos}
</strong> tâches restantes
</span>

Résultat :

Ah...

Ah... Ça ne convient pas vraiment (tâches et restantes devraient être au singulier), il faut modifier la phrase en fonction du nombre restant.

Essayez donc de le faire vous même.

tip

C'est peut-être une nouvelle bonne occasion pour utiliser l'opérateur ternaire pour faire de l'affichage conditionnel...

Correction
src/components/App.jsx
{/* ... */}
<span className="todo-count">
<strong>
{leftTodos}
</strong>
{ leftTodos > 1
? ' tâches restantes'
: ' tâche restante'
}
</span>
{/* ... */}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 9 : Counting tasks" et poussez vers le serveur.

git add .
git commit -m 'Ex 9 : Counting tasks'
git push

Enlever une tâche

Comme vous pouvez le voir, il y a dans le composant TodoItem une croix prévue pour enlever une tâche de la liste, nous allons donc ajouter cette fonctionnalité.

Pour notifier le composant App de la suppression d'une tâche, il faut passer au composant TodoItem une prop onDestroy qui sera appelée au clic sur la croix. Cette prop devra appeler une fonction handleDestroy. Cette fonction doit recevoir l'id de la tâche en argument, puis appeler setTodoList avec une nouvelle liste des tâches amputée de la tâche supprimée (cf. Immutabilité).

tip

Vous connaissez déjà la méthode de l'objet Array qui retourne un nouvel Array amputé de certains éléments...

Essayez de faire tout ça vous même à l'aide des sections précédentes.

Correction App

src/components/TodoItem.jsx
const TodoIem = props {

const {
name,
completed,
onComplete,
onDestroy
} = props

return <li className={completed ? 'completed' : ''}>
{/* ... */}
<button
className="destroy"
onClick={onDestroy}
/>
{/* ... */}
</li>
}
src/components/App.jsx
const App = () => {
// ...
const [todoList, setTodoList] = useState([{
// ...
}])

// ...

const handleDestroy = id => {
setTodoList(todoList.filter(todo => todo.id !== id))
}

return <section className="todoapp">
{/* ... */}
{todoList.map(todo => <TodoItem
{/* ... */}
onDestroy={() => handleDestroy(todo.id)}
/>)}
{/* ... */}
</section>
}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 10 : Removing tasks" et poussez vers le serveur.

git add .
git commit -m 'Ex 10 : Removing todos'
git push

Modifier une tâche

Nous pouvons maintenant ajouter et enlever des tâches, mais il serait quand même très pratique de pouvoir modifier les tâches existantes.

Pour pouvoir modifier une tâche, il nous faut :

  • gérer dans le composant App quelle tâche est en cours d'édition
  • ajouter un "mode édition" au composant TodoItem
  • récupérer la nouvelle valeur de la tâche et la passer au composant App
  • modifier le nom de la tâche dans le state todoList du composant App
info

Ces différentes étapes sont détaillées ci-dessous, mais peuvent être réalisées dans un autre ordre car elles sont toutes dépendantes les unes des autres.

Essayez de faire toutes ces étapes d'une traite pour faire fonctionner l'édition.

Préparation du composant App

Le composant App va devoir se souvenir de quelle tâche est en cours d'édition. Il lui faut donc un state supplémentaire :

const [editing, setEditing] = useState(null)

Ce state devra contenir l'id de la tâche en cours d'édition.

Pour être conscient de son état, le composant TodoItem doit maintenant recevoir par ses props à la fois le fait qu'il soit en mode édition ou non (un booléen editing par exemple), et un moyen pour qu'il puisse signaler au composant App qu'il passe en mode édition (une fonction onEdit par exemple). Essayez de faire ces modifications.

Mode édition de TodoItem

Le composant TodoItem contient un formulaire caché par défaut en CSS. Pour l'afficher, il suffit de donner un nom de class différent à l'élément li parent : editing.

Faites en sorte que le nom de classe de cet élément change en fonction du booléen passé dans une prop editing, et que la fonction passée dans la prop onEdit soit appelée lorsque l'utilisateur effectue un double-clic sur un élément.

tip

Il existe un event doubleClick sur les éléments React...

Le formulaire d'édition de TodoItem

A la manière du composant Form sur lequel nous avons travaillé dans la partie 1 du TP, ajoutez dans le composant TodoItem un state pour contenir l'état de l'input, et captez ses modifications dans ce state.

Il faut ensuite notifier le composant App que la tâche a été modifiée au travers d'une fonction contenue dans une prop nommée par exemple onChange. Faites en sorte que cette fonction soit appelée dans les cas suivants :

  • l'utilisateur appuie sur entrée (évènement submit du form)
  • l'utilisateur clique ailleurs (évènement blur de l'input)

Modification de la liste des tâches

Préparez la méthode handleChange qui sera passée en prop onChange dans le composant App. Elle doit prendre pour argument l'id de la tâche à modifier ainsi que son nouveau contenu. Cette méthode doit modifier le contenu de la tâche dans todoList. Essayez de l'écrire par vous même sur le même modèle que la fonction handleComplete.

Maintenant que notre méthode handleEdit est prête, il nous faut faire en sorte de l'utiliser quand une tâche est modifiée.

Correction

Correction
src/components/App.jsx
const App = () => {

const [editing, setEditing] = useState(null)

const handleChange = (id, name) => {
setTodoList(
todoList.map(todo => todo.id === id
? {
...todo,
name
}
: todo
)
)
setEditing(null)
}

return <section className="todoapp">
{/* ... */}

</section>
}
const handleChange = (id, name) => {
// On modifie la liste des tâches en itérant dessus
setTodoList(todoList.map(todo => {
if(todo.id === id){
// Si la tâche a le même id, on modifie son nom
return {
...todo,
name
}
}
// Sinon, on retourne la tâche inchangée
return todo
}))
}

// OU en version plus courte
const handleChange = (id, name) => setTodoList(
todoList.map(todo => todo.id === id
? {
...todo,
name
}
: todo
)
)
src/components/TodoItem.jsx
const TodoItem = props => {

const {
name,
completed,
editing,
onEdit,
onChange,
onComplete,
onDestroy
} = props


const [value, setValue] = useState(name)

const handleChange = evt => {
setValue(evt.target.value)
}

const handleSubmit = evt => {
evt.preventDefault()
onChange(value)
}

return <li className={editing
? 'editing'
: completed
? 'completed'
: ''
}>
{/* ... */}
{editing && <form onSubmit={handleSubmit}>
<input
type="text"
className="edit"
autoFocus
value={value}
onChange={handleChange}
onBlur={handleSubmit}
/>
<input type="submit" className="hidden" value="Valider" />
</form>}
</li>
}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 11 : Editing todos" et poussez vers le serveur.

git add .
git commit -m 'Ex 11 : Editing todos'
git push

Tout compléter

Vous avez remarqué la checkbox à gauche du formulaire d'ajout ? C'est la checkbox générale, du type "tout cocher". C'est un élément d'interface que vous connaissez bien, mais en voici le principe pour rappel :

  • Elle doit être cochée quand toutes les taches sont cochées
  • La cocher doit cocher toutes les tâches non cochées
  • La décocher doit décocher toutes les tâches
tip

Si vous n'êtes pas certains, vous pouvez vérifier son comportement sur l'appli terminée.

Essayez de faire fonctionner cette partie par vous même.

Correction
src/components/App.jsx
const App = () => {
// ...
const handleToggleAll = () => {
setTodoList(todoList.map(todo => ({
...todo,
completed: leftTodos > 0
})))
// ...
return <section className="todoapp">
{/* ... */}
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
checked={leftTodos === 0}
onChange={handleToggleAll}
/>
<label htmlFor="toggle-all">
Tout compléter
</label>
{/* ... */}
</section>
}
}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 12 : Completing all todos" et poussez vers le serveur.

git add .
git commit -m 'Ex 12 : Completing all todos'
git push

Enlever les tâches complétées

Il nous reste une dernière fonctionnalité à ajouter à notre application : le bouton "Effacer les complétés", qui permet d'enlever de la liste toutes les tâches complétées.

Au clic sur ce bouton, il faut modifier le state contenant la liste des tâches pour en supprimer les complétées. Essayez de réaliser cette fonctionnalité par vous même à l'aide des sections précédentes.

tip

Pensez à réutiliser la variable que nous avions créée à l'étape du compteur de tâches...

Correction
src/components/App.jsx
const App = () => {
// ...

const handleClearCompleted = () => {
setTodoList(todoList.filter(todo => !todo.completed))
}

// ...
return <section className="todoapp">
<button
className={todoList.length > leftTodos ? 'clear-completed' : ''}
onClick={handleClearCompleted}
>
Effacer les complétés
</button>
</section>
}

iuh

Touches finales

Vous avez certainement remarqué les commentaires dans le code vous proposant de cacher certaines parties de l'application sous certaines conditions, comme vous pouvez l'observer sur l'appli complète.

Faites ces dernières modifications si vous ne les aviez pas déjà faites.

tip

C'est l'occasion de se servir en autres de l'opérateur logique AND...

Correction
const App = () => {
return <section className="todoapp">
{/* ... */}
{todoList.length > 0 && <section className="main">
{/* ... */}
</section>}
{/* ... */}
{todoList.length > 0 && <footer className="footer">
{/* ... */}
<button
className={todoList.length > leftTodos ? 'clear-completed' : 'hidden'}
onClick={handleClearCompleted}
>
Effacer les complétés
</button>
</footer>}
</section>
}

Une fois ceci terminé, faites un commit avec le commentaire "Ex 13 : Final details" et poussez vers le serveur.

git add .
git commit -m 'Ex 13 : Final details'
git push

Conclusion et piste d'améliorations

Si tout s'est correctement déroulé, vous devriez maintenant disposer de la même application que celle ci.

Si ce n'est pas le cas, vous pouvez accéder à la correction complète ici.

Félicitations, vous venez de réaliser votre première application avec React. Les principes enseignés lors de cette pratique sont les fondamentaux de l'utilisation de cette bibliothèque, et sont utilisés par chaque application faite avec React.

Vous souhaitez allez plus loin ?

Il est tout à fait possible d'étendre les fonctionnalités de cette application. Votre imagination est la limite, mais voici quelques suggestions par ordre de difficulté :

  • Ajouter des options de tri par date, nom ou état
  • Ajouter un bouton (ou un raccourci clavier) "Annuler" pour effacer la dernière action
  • Ajouter de la persistence pour conserver les données d'une fois sur l'autre
  • Ajouter un système de priorité