React 交互性:编辑、过滤、条件渲染

随着我们 React 之旅接近尾声(至少目前是这样),我们将为我们的待办事项应用程序中的主要功能区域添加最后的修饰。 这包括允许您编辑现有任务,以及在所有任务、已完成任务和未完成任务之间过滤任务列表。 我们将沿途了解条件 UI 渲染。

先决条件

熟悉核心 HTMLCSSJavaScript 语言,了解 终端/命令行

目标 了解 React 中的条件渲染,以及在我们的应用程序中实现列表过滤和编辑 UI。

编辑任务名称

我们还没有用于编辑任务名称的用户界面。 我们稍后再做。 首先,我们至少可以在 App.jsx 中实现一个 editTask() 函数。 它类似于 deleteTask(),因为它将使用一个 id 来查找其目标对象,但它还将使用一个 newName 属性,其中包含要更新任务的名称。 我们将使用 Array.prototype.map() 而不是 Array.prototype.filter(),因为我们想返回一个包含一些更改的新数组,而不是从数组中删除某些东西。

editTask() 函数添加到您的 <App /> 组件中,与其他函数相同的位置

jsx
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);
}

editTask 作为 prop 传递到我们的 <Todo /> 组件中,与我们对 deleteTask 的操作方式相同

jsx
const taskList = tasks.map((task) => (
  <Todo
    id={task.id}
    name={task.name}
    completed={task.completed}
    key={task.id}
    toggleTaskCompleted={toggleTaskCompleted}
    deleteTask={deleteTask}
    editTask={editTask}
  />
));

现在打开 Todo.jsx。 我们将进行一些重构。

用于编辑的 UI

为了允许用户编辑任务,我们必须为他们提供一个用户界面来执行此操作。 首先,像以前在 <App /> 组件中一样,将 useState 导入 <Todo /> 组件

jsx
import { useState } from "react";

我们将使用它来设置一个 isEditing 状态,其默认值为 false。 将以下行添加到 <Todo /> 组件定义的顶部

jsx
const [isEditing, setEditing] = useState(false);

接下来,我们将重新思考 <Todo /> 组件。 从现在开始,我们希望它显示两个可能的“模板”之一,而不是它迄今为止使用的单个模板

  • “视图”模板,当我们只是查看待办事项时; 这是我们迄今为止在教程中使用的。
  • “编辑”模板,当我们编辑待办事项时。 我们马上就要创建它。

将此代码块复制到 Todo() 函数中,位于 useState() 钩子下方,但在 return 语句上方

jsx
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>
);

现在我们已经有了两个不同的模板结构——“编辑”和“视图”——定义在两个单独的常量中。 这意味着 <Todo />return 语句现在是重复的——它还包含“视图”模板的定义。 我们可以使用**条件渲染**来清理它,以确定组件返回哪个模板,以及它在 UI 中的渲染方式。

条件渲染

在 JSX 中,我们可以使用条件来更改浏览器渲染的内容。 为了在 JSX 中编写条件,我们可以使用 三元运算符

<Todo /> 组件的情况下,我们的条件是“此任务是否正在编辑?” 更改 Todo() 中的 return 语句,使其像这样

jsx
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;

您的浏览器应该像以前一样渲染所有任务。 为了查看编辑模板,您目前必须将代码中的默认 isEditing 状态从 false 更改为 true; 我们将在下一节中看看如何让编辑按钮切换它!

切换 <Todo /> 模板

最后,我们准备使我们的最终核心功能交互。 首先,当用户按下 viewTemplate 中的“编辑”按钮时,我们希望使用 true 的值调用 setEditing(),以便我们可以切换模板。

像这样更新 viewTemplate 中的“编辑”按钮

jsx
<button type="button" className="btn" onClick={() => setEditing(true)}>
  Edit <span className="visually-hidden">{props.name}</span>
</button>

现在我们将相同的 onClick 处理程序添加到 editingTemplate 中的“取消”按钮,但这次我们将 isEditing 设置为 false,以便它将我们切换回视图模板。

像这样更新 editingTemplate 中的“取消”按钮

jsx
<button
  type="button"
  className="btn todo-cancel"
  onClick={() => setEditing(false)}>
  Cancel
  <span className="visually-hidden">renaming {props.name}</span>
</button>

有了这段代码,您应该能够按下待办事项项目中的“编辑”和“取消”按钮来切换模板。

The eat todo item showing the view template, with edit and delete buttons available

The eat todo item showing the edit template, with an input field to enter a new name, and cancel and save buttons available

下一步是实际使编辑功能起作用。

从 UI 编辑

我们即将做的大部分工作将反映我们在 Form.jsx 中所做的工作:当用户在我们的新输入字段中键入时,我们需要跟踪他们输入的文本; 他们提交表单后,我们需要使用回调 prop 来更新我们的状态,使用任务的新名称。

我们将从创建一个用于存储和设置新名称的新钩子开始。 仍在 Todo.jsx 中,将以下内容放在现有钩子下方

jsx
const [newName, setNewName] = useState("");

接下来,创建一个 handleChange() 函数,它将设置新名称; 将其放在钩子下方,但在模板之前

jsx
function handleChange(e) {
  setNewName(e.target.value);
}

现在我们将更新 editingTemplate<input /> 字段,设置 newNamevalue 属性,并将 handleChange() 函数绑定到其 onChange 事件。 像这样更新它

jsx
<input
  id={props.id}
  className="todo-text"
  type="text"
  value={newName}
  onChange={handleChange}
/>

最后,我们需要创建一个函数来处理编辑表单的 onSubmit 事件。 将以下内容添加到 handleChange() 之后

jsx
function handleSubmit(e) {
  e.preventDefault();
  props.editTask(props.id, newName);
  setNewName("");
  setEditing(false);
}

请记住,我们的 editTask() 回调 prop 需要我们正在编辑的任务的 ID 以及其新名称。

通过将以下 onSubmit 处理程序添加到 editingTemplate<form>,将此函数绑定到表单的 submit 事件

jsx
<form className="stack-small" onSubmit={handleSubmit}>

您现在应该能够在浏览器中编辑任务。 此时,您的 Todo.jsx 文件应如下所示

jsx
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;

返回过滤按钮

现在我们的主要功能已经完成,我们可以考虑我们的过滤器按钮。 目前,它们重复了“全部”标签,并且没有任何功能! 我们将重新应用我们在 <Todo /> 组件中使用的一些技能,以

  • 创建一个用于存储活动过滤器的钩子。
  • 渲染一个 <FilterButton /> 元素数组,允许用户在所有、已完成和未完成之间更改活动过滤器。

添加过滤器钩子

向您的 App() 函数添加一个新的钩子,它读取并设置过滤器。 我们希望默认过滤器为 All,因为最初应该显示所有任务

jsx
const [filter, setFilter] = useState("All");

定义我们的过滤器

我们现在的目标是双重的

  • 每个过滤器应该有唯一的名称。
  • 每个过滤器应该有唯一的行为。

JavaScript 对象将是将名称与行为相关联的好方法:每个键都是过滤器的名称; 每个属性都是与该名称关联的行为。

App.jsx 的顶部,在我们的导入下方,但在 App() 函数上方,让我们添加一个名为 FILTER_MAP 的对象

jsx
const FILTER_MAP = {
  All: () => true,
  Active: (task) => !task.completed,
  Completed: (task) => task.completed,
};

FILTER_MAP 的值是我们用来过滤 tasks 数据数组的函数

  • All 过滤器显示所有任务,因此我们对所有任务都返回 true
  • Active 过滤器显示其 completed prop 为 false 的任务。
  • Completed 过滤器显示其 completed prop 为 true 的任务。

在我们的先前添加下方,添加以下内容——这里我们使用 Object.keys() 方法来收集 FILTER_NAMES 的数组

jsx
const FILTER_NAMES = Object.keys(FILTER_MAP);

注意: 我们在 App() 函数外部定义这些常量,因为如果它们在函数内部定义,它们将在每次 <App /> 组件重新渲染时重新计算,而我们不希望这样。 无论我们的应用程序做什么,这些信息都不会改变。

渲染过滤器

现在我们有了 FILTER_NAMES 数组,我们可以使用它来渲染所有三个过滤器。 在 App() 函数中,我们可以创建一个名为 filterList 的常量,我们将使用它来映射我们的名称数组,并返回一个 <FilterButton /> 组件。 请记住,我们也需要密钥。

将以下内容添加到您的 taskList 常量声明下方

jsx
const filterList = FILTER_NAMES.map((name) => (
  <FilterButton key={name} name={name} />
));

现在我们将 App.jsx 中三个重复的 <FilterButton /> 替换为此 filterList。 替换以下内容

jsx
<FilterButton />
<FilterButton />
<FilterButton />

用这个

jsx
{filterList}

这还不能用。 我们还有更多工作要做。

交互式过滤器

为了使我们的过滤器按钮具有交互性,我们应该考虑它们需要使用哪些道具。

  • 我们知道 <FilterButton /> 应该报告它当前是否被按下,并且如果它的名称与我们过滤器状态的当前值匹配,它应该被按下。
  • 我们知道 <FilterButton /> 需要一个回调来设置活动过滤器。 我们可以直接使用我们的 setFilter 钩子。

像这样更新您的 filterList 常量

jsx
const filterList = FILTER_NAMES.map((name) => (
  <FilterButton
    key={name}
    name={name}
    isPressed={name === filter}
    setFilter={setFilter}
  />
));

与我们之前在 <Todo /> 组件中所做的一样,我们现在必须更新 FilterButton.jsx,以利用我们赋予它的道具。 执行以下每个操作,并记住使用花括号来读取这些变量!

  • all 替换为 {props.name}
  • aria-pressed 的值设置为 {props.isPressed}
  • 添加一个 onClick 处理程序,它使用过滤器的名称调用 props.setFilter()

完成所有操作后,您的 FilterButton.jsx 文件应如下所示

jsx
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;

再次访问您的浏览器。 您应该会看到不同的按钮被赋予了各自的名称。 当您按下过滤器按钮时,您应该会看到它的文本获得新的轮廓——这告诉您它已被选中。 并且如果您在单击按钮时查看 DevTool 的页面检查器,您会看到 aria-pressed 属性值相应地更改。

The three filter buttons of the app - all, active, and completed - with a focus highlight around completed

但是,我们的按钮仍然没有真正过滤 UI 中的待办事项! 让我们完成它。

在 UI 中过滤任务

现在,App() 中的 taskList 常量映射到任务状态,并为它们返回一个新的 <Todo /> 组件。 这不是我们想要的! 任务只应在包含在应用选定过滤器结果中的情况下才能渲染。 在我们映射任务状态之前,我们应该使用 Array.prototype.filter() 过滤它,以消除我们不想渲染的对象。

像这样更新您的 taskList

jsx
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}
    />
  ));

为了决定在 Array.prototype.filter() 中使用哪个回调函数,我们访问 FILTER_MAP 中与我们过滤器状态的键相对应的值。 例如,当过滤器为 All 时,FILTER_MAP[filter] 将计算为 () => true

在您的浏览器中选择过滤器现在将删除不满足其条件的任务。 标题中列表上方的计数也将更改以反映列表!

The app with the filter buttons in place. Active is highlighted, so only the active todo items are being shown.

总结

就是这样——我们的应用程序现在在功能上已经完整了。但是,现在我们已经实现了所有功能,我们可以做一些改进,以确保更广泛的用户可以使用我们的应用程序。我们的下一篇文章将总结我们的 React 教程,介绍如何在 React 中包含焦点管理,这可以提高可用性,并减少键盘用户和屏幕阅读器用户的困惑。