React Interaktivität: Bearbeiten, Filtern, bedingtes Rendering
Da wir uns dem Ende unserer React-Reise nähern (zumindest vorerst), werden wir die letzten Feinheiten an den Hauptfunktionen unserer To-Do-Listen-App hinzufügen. Dies beinhaltet die Möglichkeit, vorhandene Aufgaben zu bearbeiten und die Liste der Aufgaben zwischen allen, abgeschlossenen und unvollständigen Aufgaben zu filtern. Unterwegs werden wir uns das bedingte Rendering der Benutzeroberfläche ansehen.
Voraussetzungen: |
Vertrautheit mit den Kernsprachen HTML, CSS und JavaScript, Kenntnisse über das Terminal/Kommandozeile. |
---|---|
Zielsetzung: | Erlernen des bedingten Renderings in React und Implementierung von Listenfiltern und einer Bearbeitungsoberfläche in unserer App. |
Den Namen einer Aufgabe bearbeiten
Wir haben noch keine Benutzeroberfläche zum Bearbeiten des Namens einer Aufgabe. Das werden wir gleich angehen. Zunächst können wir zumindest eine Funktion editTask()
in App.jsx
implementieren. Sie wird ähnlich wie deleteTask()
sein, da sie eine id
benötigt, um das Zielobjekt zu finden, sie wird jedoch auch eine newName
Eigenschaft enthalten, die den neuen Namen darstellt, auf den die Aufgabe aktualisiert werden soll. Wir verwenden Array.prototype.map()
anstelle von Array.prototype.filter()
, weil wir ein neues Array mit einigen Änderungen zurückgeben möchten, anstatt etwas aus dem Array zu löschen.
Fügen Sie die Funktion editTask()
in Ihre <App />
Komponente ein, an der gleichen Stelle wie die anderen Funktionen:
function editTask(id, newName) {
const editedTaskList = tasks.map((task) => {
// if this task has the same ID as the edited task
if (id === task.id) {
// Copy the task and update its name
return { ...task, name: newName };
}
// Return the original task if it's not the edited task
return task;
});
setTasks(editedTaskList);
}
Übergeben Sie editTask
in unsere <Todo />
Komponenten als Prop auf die gleiche Weise, wie wir es mit deleteTask
gemacht haben:
const taskList = tasks.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
Öffnen Sie nun Todo.jsx
. Wir werden etwas umstrukturieren.
Eine Benutzeroberfläche zum Bearbeiten
Um den Nutzern das Bearbeiten einer Aufgabe zu ermöglichen, müssen wir eine Benutzeroberfläche bereitstellen. Importieren Sie zuerst useState
in die <Todo />
Komponente, ähnlich wie wir es zuvor mit der <App />
Komponente getan haben:
import { useState } from "react";
Wir werden dies verwenden, um einen isEditing
Zustand mit einem Standardwert von false
festzulegen. Fügen Sie die folgende Zeile direkt am Anfang Ihrer <Todo />
Komponentendefinition hinzu:
const [isEditing, setEditing] = useState(false);
Als Nächstes denken wir die <Todo />
Komponente neu. Von nun an möchten wir, dass sie eines von zwei möglichen "Templates" anzeigt, anstatt des einzigen Templates, das sie bisher verwendet hat:
- Das "Ansicht"-Template, wenn wir nur eine Aufgabe ansehen; dies haben wir im Tutorial bisher verwendet.
- Das "Bearbeitungs"-Template, wenn wir eine Aufgabe bearbeiten. Wir werden dieses gleich erstellen.
Kopieren Sie diesen Codeblock in die Todo()
Funktion, unterhalb Ihres useState()
Hooks, aber oberhalb der return
Anweisung:
const editingTemplate = (
<form className="stack-small">
<div className="form-group">
<label className="todo-label" htmlFor={props.id}>
New name for {props.name}
</label>
<input id={props.id} className="todo-text" type="text" />
</div>
<div className="btn-group">
<button type="button" className="btn todo-cancel">
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
<button type="submit" className="btn btn__primary todo-edit">
Save
<span className="visually-hidden">new name for {props.name}</span>
</button>
</div>
</form>
);
const viewTemplate = (
<div className="stack-small">
<div className="c-cb">
<input
id={props.id}
type="checkbox"
defaultChecked={props.completed}
onChange={() => props.toggleTaskCompleted(props.id)}
/>
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
<div className="btn-group">
<button type="button" className="btn">
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button
type="button"
className="btn btn__danger"
onClick={() => props.deleteTask(props.id)}>
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</div>
);
Wir haben nun die beiden unterschiedlichen Template-Strukturen — "Bearbeitungs" und "Ansicht" — in zwei separaten Konstanten definiert. Dies bedeutet, dass die return
Anweisung von <Todo />
jetzt repetitiv ist — sie enthält auch eine Definition des "Ansichts"-Templates. Wir können dies sauberer gestalten, indem wir bedingtes Rendering verwenden, um zu bestimmen, welches Template die Komponente zurückgibt und somit in der Benutzeroberfläche gerendert wird.
Bedingtes Rendering
In JSX können wir eine Bedingung verwenden, um zu ändern, was vom Browser gerendert wird. Um eine Bedingung in JSX zu schreiben, können wir einen ternären Operator verwenden.
Im Falle unserer <Todo />
Komponente ist unsere Bedingung "Wird diese Aufgabe bearbeitet?" Ändern Sie die return
Anweisung innerhalb von Todo()
, damit sie so aussieht:
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
Ihr Browser sollte alle Aufgaben wie zuvor rendern. Um das Bearbeitungstemplate zu sehen, müssen Sie den Standard isEditing
-Zustand in Ihrem Code vorläufig von false
auf true
ändern; wir werden im nächsten Abschnitt sehen, wie der Bearbeiten-Button dies umschalten kann!
Umschalten der <Todo />
Templates
Endlich sind wir bereit, unsere letzte Kernfunktion interaktiv zu machen. Zuerst möchten wir setEditing()
mit einem Wert von true
aufrufen, wenn ein Benutzer den "Bearbeiten"-Button in unserem viewTemplate
drückt, damit wir die Templates umschalten können.
Aktualisieren Sie den "Bearbeiten"-Button im viewTemplate
wie folgt:
<button type="button" className="btn" onClick={() => setEditing(true)}>
Edit <span className="visually-hidden">{props.name}</span>
</button>
Nun fügen wir den gleichen onClick
-Handler auch dem "Abbrechen"-Button im editingTemplate
hinzu, allerdings setzen wir isEditing
auf false
, um wieder zum Ansichtstemplate zurückzukehren.
Aktualisieren Sie den "Abbrechen"-Button im editingTemplate
wie folgt:
<button
type="button"
className="btn todo-cancel"
onClick={() => setEditing(false)}>
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
Mit diesem Code sollten Sie in der Lage sein, die "Bearbeiten" und "Abbrechen" Buttons in Ihren Aufgaben zu drücken, um zwischen den Templates zu wechseln.
Der nächste Schritt ist, die Bearbeitungsfunktionalität tatsächlich funktionieren zu lassen.
Bearbeiten über die Benutzeroberfläche
Ein Großteil dessen, was wir jetzt tun werden, wird die Arbeiten widerspiegeln, die wir in Form.jsx
durchgeführt haben: während der Nutzer in unser neues Eingabefeld tippt, müssen wir den eingegebenen Text verfolgen; sobald sie das Formular abschicken, müssen wir einen Callback-Prop nutzen, um unseren Zustand mit dem neuen Namen der Aufgabe zu aktualisieren.
Wir beginnen mit einem neuen Hook zum Speichern und Festlegen des neuen Namens. Bleiben Sie in Todo.jsx
, und fügen Sie das Folgende unterhalb des bestehenden Hooks hinzu:
const [newName, setNewName] = useState("");
Als Nächstes erstellen Sie eine handleChange()
Funktion, die den neuen Namen festlegt; fügen Sie diese unterhalb der Hooks aber vor den Templates hinzu:
function handleChange(e) {
setNewName(e.target.value);
}
Nun aktualisieren wir das <input />
Feld unseres editingTemplate
, indem wir ihm ein value
Attribut mit newName
zuweisen und unsere handleChange()
Funktion an sein onChange
Ereignis binden. Aktualisieren Sie es folgendermaßen:
<input
id={props.id}
className="todo-text"
type="text"
value={newName}
onChange={handleChange}
/>
Schließlich müssen wir eine Funktion erstellen, die das onSubmit
Ereignis des Bearbeitungsformulars behandelt. Fügen Sie Folgendes direkt unterhalb von handleChange()
hinzu:
function handleSubmit(e) {
e.preventDefault();
props.editTask(props.id, newName);
setNewName("");
setEditing(false);
}
Denken Sie daran, dass unser editTask()
Callback-Prop sowohl die ID der Aufgabe, die wir bearbeiten, als auch deren neuen Namen benötigt.
Binden Sie diese Funktion an das submit
Ereignis des Formulars, indem Sie den folgenden onSubmit
Handler zum <form>
im editingTemplate
hinzufügen:
<form className="stack-small" onSubmit={handleSubmit}>
Sie sollten jetzt in der Lage sein, eine Aufgabe in Ihrem Browser zu bearbeiten. An diesem Punkt sollte Ihre Todo.jsx
Datei wie folgt aussehen:
function Todo(props) {
const [isEditing, setEditing] = useState(false);
const [newName, setNewName] = useState("");
function handleChange(e) {
setNewName(e.target.value);
}
function handleSubmit(e) {
e.preventDefault();
props.editTask(props.id, newName);
setNewName("");
setEditing(false);
}
const editingTemplate = (
<form className="stack-small" onSubmit={handleSubmit}>
<div className="form-group">
<label className="todo-label" htmlFor={props.id}>
New name for {props.name}
</label>
<input
id={props.id}
className="todo-text"
type="text"
value={newName}
onChange={handleChange}
/>
</div>
<div className="btn-group">
<button
type="button"
className="btn todo-cancel"
onClick={() => setEditing(false)}>
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
<button type="submit" className="btn btn__primary todo-edit">
Save
<span className="visually-hidden">new name for {props.name}</span>
</button>
</div>
</form>
);
const viewTemplate = (
<div className="stack-small">
<div className="c-cb">
<input
id={props.id}
type="checkbox"
defaultChecked={props.completed}
onChange={() => props.toggleTaskCompleted(props.id)}
/>
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
<div className="btn-group">
<button
type="button"
className="btn"
onClick={() => {
setEditing(true);
}}>
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button
type="button"
className="btn btn__danger"
onClick={() => props.deleteTask(props.id)}>
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</div>
);
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
}
export default Todo;
Zurück zu den Filterbuttons
Da unsere Hauptfunktionen nun vollständig sind, können wir an unsere Filterbuttons denken. Derzeit wiederholen sie das Label "All", und sie haben keine Funktionalität! Wir werden einige der Fähigkeiten, die wir mit unserer <Todo />
Komponente angewendet haben, erneut anwenden, um:
- Einen Hook zum Speichern des aktiven Filters zu erstellen.
- Ein Array von
<FilterButton />
Elementen zu rendern, das es den Nutzern ermöglicht, den aktiven Filter zwischen allen, abgeschlossenen und unvollständigen Aufgaben zu wechseln.
Hinzufügen eines Filter-Hooks
Fügen Sie Ihrer App()
Funktion einen neuen Hook hinzu, der einen Filter liest und setzt. Wir möchten, dass der Standardfilter All
ist, da anfänglich alle unsere Aufgaben angezeigt werden sollten:
const [filter, setFilter] = useState("All");
Definieren unserer Filter
Unser Ziel ist jetzt zweifach:
- Jeder Filter sollte einen einzigartigen Namen haben.
- Jeder Filter sollte ein einzigartiges Verhalten haben.
Ein JavaScript-Objekt wäre eine großartige Möglichkeit, Namen mit Verhaltensweisen zu verknüpfen: jeder Schlüssel ist der Name eines Filters; jede Eigenschaft ist das Verhalten, das mit diesem Namen verknüpft ist.
Am Anfang von App.jsx
, unterhalb unserer Importe, aber oberhalb der App()
Funktion, fügen wir ein Objekt namens FILTER_MAP
hinzu:
const FILTER_MAP = {
All: () => true,
Active: (task) => !task.completed,
Completed: (task) => task.completed,
};
Die Werte von FILTER_MAP
sind Funktionen, die wir verwenden werden, um das tasks
Datenarray zu filtern:
- Der
All
Filter zeigt alle Aufgaben, also geben wirtrue
für alle Aufgaben zurück. - Der
Active
Filter zeigt Aufgaben, derencompleted
Eigenschaftfalse
ist. - Der
Completed
Filter zeigt Aufgaben, derencompleted
Eigenschafttrue
ist.
Fügen Sie unter unserer vorherigen Ergänzung Folgendes hinzu — hier verwenden wir die Methode Object.keys()
, um ein Array von FILTER_NAMES
zu sammeln:
const FILTER_NAMES = Object.keys(FILTER_MAP);
Hinweis: Wir definieren diese Konstanten außerhalb unserer App()
Funktion, weil sie, wenn sie innerhalb definiert wären, jedes Mal neu berechnet würden, wenn die <App />
Komponente neu gerendert wird, und das wollen wir nicht. Diese Informationen werden sich nie ändern, egal was unsere Anwendung macht.
Rendern der Filter
Da wir jetzt das FILTER_NAMES
Array haben, können wir es verwenden, um alle drei Filter zu rendern. Innerhalb der App()
Funktion können wir eine Konstante namens filterList
erstellen, die wir verwenden, um unser Array von Namen zu durchlaufen und eine <FilterButton />
Komponente zurückzugeben. Denken Sie daran, hier brauchen wir auch Schlüssel.
Fügen Sie das Folgende unter Ihrer taskList
Konstantendeklaration hinzu:
const filterList = FILTER_NAMES.map((name) => (
<FilterButton key={name} name={name} />
));
Nun werden wir die drei wiederholten <FilterButton />
s in App.jsx
durch diesen filterList
ersetzen. Ersetzen Sie das Folgende:
<FilterButton />
<FilterButton />
<FilterButton />
Mit diesem:
{filterList}
Das wird noch nicht funktionieren. Wir haben vorher noch ein wenig Arbeit zu erledigen.
Interaktive Filter
Um unsere Filterbuttons interaktiv zu machen, sollten wir überlegen, welche Props sie verwenden müssen.
- Wir wissen, dass der
<FilterButton />
melden sollte, ob er derzeit gedrückt ist, und er sollte gedrückt werden, wenn sein Name mit dem aktuellen Wert unseres Filterzustands übereinstimmt. - Wir wissen, dass der
<FilterButton />
einen Callback benötigt, um den aktiven Filter festzulegen. Wir können unserensetFilter
Hook direkt nutzen.
Aktualisieren Sie Ihre filterList
Konstante wie folgt:
const filterList = FILTER_NAMES.map((name) => (
<FilterButton
key={name}
name={name}
isPressed={name === filter}
setFilter={setFilter}
/>
));
Ähnlich wie wir es zuvor mit unserer <Todo />
Komponente gemacht haben, müssen wir jetzt FilterButton.jsx
aktualisieren, um die Props zu verwenden, die wir ihm gegeben haben. Machen Sie dies für jeden der folgenden Punkte und denken Sie daran, geschweifte Klammern zu verwenden, um diese Variablen zu lesen!
- Ersetzen Sie
all
durch{props.name}
. - Setzen Sie den Wert von
aria-pressed
auf{props.isPressed}
. - Fügen Sie einen
onClick
Handler hinzu, derprops.setFilter()
mit dem Namen des Filters aufruft.
Wenn alles erledigt ist, sollte Ihre FilterButton.jsx
Datei so aussehen:
function FilterButton(props) {
return (
<button
type="button"
className="btn toggle-btn"
aria-pressed={props.isPressed}
onClick={() => props.setFilter(props.name)}>
<span className="visually-hidden">Show </span>
<span>{props.name}</span>
<span className="visually-hidden"> tasks</span>
</button>
);
}
export default FilterButton;
Besuchen Sie erneut Ihren Browser. Sie sollten sehen, dass die verschiedenen Buttons ihre entsprechenden Namen erhalten haben. Wenn Sie auf einen Filterbutton drücken, sollten Sie sehen, dass sein Text einen neuen Umriss annimmt — dies zeigt Ihnen an, dass er ausgewählt wurde. Und wenn Sie sich den Page Inspector Ihres DevTools ansehen, während Sie die Buttons klicken, werden Sie sehen, dass sich die Werte des aria-pressed
Attributs entsprechend ändern.
Unsere Buttons filtern die Aufgaben in der Benutzeroberfläche jedoch noch nicht! Machen wir das fertig.
Aufgaben in der Benutzeroberfläche filtern
Derzeit durchläuft unsere taskList
Konstante in App()
den Aufgabenstatus und gibt eine neue <Todo />
Komponente für alle Aufgaben zurück. Das ist nicht das, was wir wollen! Eine Aufgabe sollte nur gerendert werden, wenn sie in den Ergebnissen des angewendeten Filters enthalten ist. Bevor wir über den Aufgabenstatus mappen, sollten wir ihn filtern (mit Array.prototype.filter()
), um Objekte zu eliminieren, die wir nicht rendern möchten.
Aktualisieren Sie Ihre taskList
wie folgt:
const taskList = tasks
.filter(FILTER_MAP[filter])
.map((task) => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
Um zu entscheiden, welche Callback-Funktion in Array.prototype.filter()
verwendet werden soll, greifen wir auf den Wert in FILTER_MAP
zu, der dem Schlüssel unseres Filterzustands entspricht. Wenn der Filter zum Beispiel All
ist, wird FILTER_MAP[filter]
zu () => true
evaluiert.
Die Auswahl eines Filters in Ihrem Browser entfernt nun die Aufgaben, die nicht seinen Kriterien entsprechen. Auch die Anzahl im Überschrift über der Liste wird sich ändern, um die Liste zu reflektieren!
Zusammenfassung
Das war's also — unsere App ist nun funktional komplett. Allerdings können wir, jetzt, wo wir alle unsere Funktionen implementiert haben, einige Verbesserungen vornehmen, um sicherzustellen, dass eine breitere Benutzergruppe unsere App nutzen kann. Unser nächster Artikel rundet unsere React-Tutorials ab, indem er die Einbeziehung von Fokusmanagement in React betrachtet, was die Benutzerfreundlichkeit verbessern und Verwirrung für sowohl Tastaturnutzer als auch Screenreader-Nutzer reduzieren kann.