Ember 交互性:事件、类和状态

在这一点上,我们将开始为我们的应用程序添加一些交互性,提供添加和显示新待办事项的功能。在此过程中,我们将看看如何在 Ember 中使用事件,创建组件类来包含控制交互功能的 JavaScript 代码,以及设置服务来跟踪应用程序的数据状态。

先决条件

至少,建议您熟悉核心 HTMLCSSJavaScript 语言,并了解 终端/命令行

更深入地了解现代 JavaScript 特性(如类、模块等)将非常有利,因为 Ember 大量使用它们。

目标 学习如何创建组件类并使用事件来控制交互性,并使用服务来跟踪应用程序状态。

添加交互性

现在我们已经有了我们待办事项应用程序的重构组件化版本,让我们逐步了解如何添加使应用程序正常工作的交互性。

在开始考虑交互性时,最好声明每个组件的目标和职责是什么。在以下部分中,我们将对每个组件执行此操作,然后逐步引导您完成如何实现该功能。

创建待办事项

对于我们的卡片标题/待办事项输入,我们希望能够在按下 Enter 键时提交我们键入的待办事项任务,并使其显示在待办事项列表中。

我们希望能够捕获键入到输入框中的文本。我们这样做是为了让我们的 JavaScript 代码知道我们键入了什么,我们可以保存我们的待办事项并将该文本传递给待办事项列表组件以显示。

我们可以通过 keydown 事件通过 on 修饰符 来捕获该事件,这只是 Ember 对 addEventListenerremoveEventListener 的语法糖(如有必要,请参阅 事件简介)。

将以下显示的新行添加到您的 header.hbs 文件中

hbs
<input
  class='new-todo'
  aria-label='What needs to be done?'
  placeholder='What needs to be done?'
  autofocus
  {{on 'keydown' this.onKeyDown}}
>

此新属性位于双大括号内,这告诉您它是 Ember 动态模板语法的部分。传递给 on 的第一个参数是要响应的事件类型(keydown),最后一个参数是事件处理程序 - 响应 keydown 事件触发而运行的代码。正如您可能期望的那样,从处理 普通 JavaScript 对象 开始,this 关键字指的是组件的“上下文”或“范围”。一个组件的 this 将与另一个组件的 this 不同。

我们可以通过为您的组件生成一个组件类来定义 this 中可用的内容。这是一个普通 JavaScript 类,对 Ember 没有任何特殊意义,除了扩展Component 超类。

要创建与您的标题组件一起使用的标题类,请在您的终端中键入以下内容

bash
ember generate component-class header

这将创建以下空类文件 - todomvc/app/components/header.js

js
import Component from "@glimmer/component";

export default class HeaderComponent extends Component {}

在此文件中,我们将实现事件处理程序代码。将内容更新为以下内容

js
import Component from "@glimmer/component";
import { action } from "@ember/object";

export default class HeaderComponent extends Component {
  @action
  onKeyDown({ target, key }) {
    let text = target.value.trim();
    let hasValue = Boolean(text);

    if (key === "Enter" && hasValue) {
      alert(text);

      target.value = "";
    }
  }
}

@action 装饰器是这里唯一的 Ember 特定代码(除了扩展自 Component 超类,以及我们使用 JavaScript 模块语法 导入的 Ember 特定项目) - 文件的其余部分是普通的 JavaScript,可以在任何应用程序中使用。@action 装饰器声明该函数是一个“操作”,这意味着它是一种从模板中发生的事件调用的函数类型。@action 还将函数的 this 绑定到类实例。

注意:装饰器基本上是一个包装函数,它包装并调用其他函数或属性,在此过程中提供额外的功能。例如,@tracked 装饰器(稍后将看到)运行它应用到的代码,但还会跟踪它并自动更新应用程序的值发生变化时。 阅读 JavaScript 装饰器:它们是什么以及何时使用它们,以获取有关装饰器的更多一般信息。

回到我们的应用程序正在运行的浏览器选项卡,我们可以键入任何我们想要的内容,当我们按下 Enter 时,我们会收到一个警报消息,告诉我们我们键入了什么。

the initial placeholder state of the add function, showing the text entered into the input elements being alerted back to you.

在标题输入的交互性完成后,我们需要一个地方来存储待办事项,以便其他组件可以访问它们。

使用服务存储待办事项

Ember 具有内置的应用程序级状态管理,我们可以使用它来管理待办事项的存储并允许我们的每个组件访问来自该应用程序级状态的数据。Ember 将这些构造称为 服务,并且它们在页面的整个生命周期中都存在(页面刷新将清除它们;将数据持久保存更长的时间超出了本教程的范围)。

运行以下终端命令为我们生成一个服务,以将我们的待办事项列表数据存储在其中

bash
ember generate service todo-data

这应该给你一个类似这样的终端输出

installing service
  create app/services/todo-data.js
installing service-test
  create tests/unit/services/todo-data-test.js

这会在 todomvc/app/services 目录中创建一个 todo-data.js 文件来包含我们的服务,该服务最初包含一个导入语句和一个空类

js
import Service from "@ember/service";

export default class TodoDataService extends Service {}

首先,我们要定义什么是待办事项。我们知道我们想要跟踪待办事项的文本以及它是否已完成。

在现有导入语句下方添加以下导入语句

js
import { tracked } from "@glimmer/tracking";

现在在您添加的上一行下方添加以下类

js
class Todo {
  @tracked text = "";
  @tracked isCompleted = false;

  constructor(text) {
    this.text = text;
  }
}

此类表示一个待办事项 - 它包含一个@tracked text 属性,该属性包含待办事项的文本,以及一个@tracked isCompleted 属性,该属性指定待办事项是否已完成。实例化时,Todo 对象的初始text 值将等于创建时赋予它的文本(见下文),而isCompleted 值将为false。此类中唯一与 Ember 相关的部分是@tracked 装饰器 - 这与响应式系统挂钩,并允许 Ember 自动更新您在应用程序中看到的内容,如果跟踪的属性发生变化。 此处可以找到有关跟踪的更多信息

现在是时候添加服务的正文了。

首先在前面的导入语句下方添加另一个import 语句,以使操作在服务内部可用

js
import { action } from "@ember/object";

将现有的export default class TodoDataService extends Service { } 块更新为以下内容

js
export default class TodoDataService extends Service {
  @tracked todos = [];

  @action
  add(text) {
    let newTodo = new Todo(text);

    this.todos.pushObject(newTodo);
  }
}

在这里,服务上的todos 属性将维护我们的待办事项列表,这些待办事项包含在数组中,我们将在其上标记@tracked,因为当todos 的值更新时,我们希望 UI 也更新。

就像之前一样,将从模板调用的add() 函数用@action 装饰器进行注释,以将其绑定到类实例。虽然这看起来可能像熟悉的 JavaScript,但您可能会注意到我们正在对我们的todos 数组调用方法 pushObject()。这是因为 Ember 默认情况下扩展了 JavaScript 的 Array 原型,为我们提供了方便的方法来确保 Ember 的跟踪系统了解这些更改。有数十种这样的方法,包括pushObjects()insertAt()popObject(),这些方法可以与任何类型一起使用(不仅仅是对象)。Ember 的 ArrayProxy 还为我们提供了更多方便的方法,例如isAny()findBy()filterBy(),以使生活更轻松。

从我们的标题组件中使用服务

现在我们已经定义了一种添加待办事项的方法,我们可以从header.js 输入组件与该服务进行交互,以实际开始添加它们。

首先,需要通过@inject 装饰器将服务注入模板,我们将将其重命名为@service 以获得语义清晰度。为此,将以下import 行添加到header.js,位于两个现有的import 行下方

js
import { inject as service } from "@ember/service";

使用此导入,我们现在可以通过todos 对象(使用@service 装饰器)在HeaderComponent 类中使用todo-data 服务。在打开的export… 行下方添加以下行

js
@service('todo-data') todos;

现在可以将占位符alert(text); 行替换为对我们新的add() 函数的调用。用以下内容替换它

js
this.todos.add(text);

如果我们在浏览器(npm start,转到localhost:4200)中尝试使用待办事项应用程序,在按下 Enter 键后,看起来什么也没发生(虽然应用程序在没有错误的情况下构建是一个好兆头)。但是,使用 Ember Inspector,我们可以看到我们的待办事项已添加

The app being shown in the Ember inspector, to prove that added todos are being stored by the service, even if they are not being displayed in the UI yet

显示我们的待办事项

现在我们知道我们可以创建待办事项,需要一种方法来用我们实际创建的待办事项替换我们静态的“购买电影票”待办事项。在TodoList 组件中,我们希望从服务中获取待办事项,并为每个待办事项渲染一个Todo 组件。

为了从服务中检索待办事项,我们的TodoList 组件首先需要一个支持组件类来包含此功能。按 Ctrl + C 停止开发服务器,然后输入以下终端命令

bash
ember generate component-class todo-list

这将生成新的组件类todomvc/app/components/todo-list.js

使用以下代码填充此文件,该代码通过todos 属性将todo-data 服务公开给我们的模板。这使得它在类和模板内部都可以通过this.todos 访问

js
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";

export default class TodoListComponent extends Component {
  @service("todo-data") todos;
}

这里的一个问题是我们的服务名为todos,但待办事项列表也名为todos,因此目前我们将使用this.todos.todos 访问数据。这并不直观,因此我们将为todos 服务添加一个 getter,名为all,它将表示所有待办事项。

为此,请返回您的todo-data.js 文件,并在@tracked todos = []; 行下方添加以下内容

js
get all() {
  return this.todos;
}

现在我们可以使用this.todos.all 访问数据,这更加直观。为了使其生效,请转到您的todo-list.hbs 组件,并将静态组件调用替换为以下内容

hbs
<Todo />
<Todo />

使用动态的 #each 块(本质上是对 JavaScript 的 forEach() 的语法糖),为服务 all() 获取器返回的待办事项列表中的每个待办事项创建一个 <Todo /> 组件。

hbs
{{#each this.todos.all as |todo|}}
<Todo @todo={{todo}} />
{{/each}}

另一种看待方式

  • this - 渲染上下文 / 组件实例。
  • todos - this 上的属性,我们在 todo-list.js 组件中使用 @service('todo-data') todos; 定义。这是一个对 todo-data 服务的引用,允许我们直接与服务实例交互。
  • all - todo-data 服务上的获取器,返回所有待办事项。

尝试再次启动服务器并导航到我们的应用程序,你会发现它有效!好吧,有点。每当你输入一个新的待办事项时,在文本输入框下方会出现一个新的列表项,但不幸的是,它总是显示“购买电影票”。

这是因为每个列表项中的文本标签是硬编码的,如 todo.hbs 中所示。

hbs
<label>Buy Movie Tickets</label>

更新这一行以使用参数 @todo - 它将代表我们在 todo-list.hbs 中调用此组件时传入的待办事项,在行 <Todo @todo={{todo}} /> 中。

hbs
<label>{{@todo.text}}</label>

好的,再试一次。你应该发现现在在 <input> 中提交的文本在 UI 中正确地反映出来。

The app being shown in its final state of this article, with entered todo items being shown in the UI

总结

好的,所以现在进展很大。我们现在可以将待办事项添加到我们的应用程序中,数据的状态使用我们的服务跟踪。接下来,我们将继续让我们的页脚功能起作用,包括待办事项计数器,并查看条件渲染,包括在待办事项被选中时正确地对其进行样式化。我们还将连接我们的“清除已完成”按钮。