创建项目组件

组件提供了一种组织应用程序的方法。本文将引导您创建一个组件来处理列表中的单个项目,并添加检查、编辑和删除功能。本文将介绍 Angular 事件模型。

先决条件 熟悉核心 HTMLCSSJavaScript 语言,了解 终端/命令行
目标 了解有关组件的更多信息,包括事件如何处理更新。添加检查、编辑和删除功能。

创建新组件

在命令行中,使用以下 CLI 命令创建一个名为 item 的组件

bash
ng generate component item

ng generate component 命令会创建一个组件和文件夹,其名称由您指定。在此处,文件夹和组件名称为 item。您可以在 app 文件夹中找到 item 目录

src/app/item
├── item.component.css
├── item.component.html
├── item.component.spec.ts
└── item.component.ts

AppComponent 一样,ItemComponent 由以下文件组成

  • item.component.html 用于 HTML
  • item.component.ts 用于逻辑
  • item.component.css 用于样式
  • item.component.spec.ts 用于测试组件

您可以在 item.component.ts 中的 @Component() 装饰器元数据中看到对 HTML 和 CSS 文件的引用。

js
@Component({
  selector: 'app-item',
  standalone: true,
  imports: [],
  templateUrl: './item.component.html',
  styleUrl: './item.component.css'
})

为 ItemComponent 添加 HTML

ItemComponent 可以接管为用户提供一种方法来将项目标记为已完成、编辑或删除项目的任务。

通过用以下内容替换 item.component.html 中的占位符内容,添加用于管理项目的标记

html
<div class="item">
  <input
    [id]="item.description"
    type="checkbox"
    (change)="item.done = !item.done"
    [checked]="item.done" />
  <label [for]="item.description">{{item.description}}</label>

  <div class="btn-wrapper" *ngIf="!editable">
    <button class="btn" (click)="editable = !editable">Edit</button>
    <button class="btn btn-warn" (click)="remove.emit()">Delete</button>
  </div>

  <!-- This section shows only if user clicks Edit button -->
  <div *ngIf="editable">
    <input
      class="sm-text-input"
      placeholder="edit item"
      [value]="item.description"
      #editedItem
      (keyup.enter)="saveItem(editedItem.value)" />

    <div class="btn-wrapper">
      <button class="btn" (click)="editable = !editable">Cancel</button>
      <button class="btn btn-save" (click)="saveItem(editedItem.value)">
        Save
      </button>
    </div>
  </div>
</div>

第一个输入是一个复选框,以便用户可以在项目完成时将项目勾选。复选框 <label> 中的双大括号 {{}} 表示 Angular 的插值。Angular 使用 {{item.description}}items 数组中检索当前 item 的描述。下一节将详细介绍组件如何共享数据。

用于编辑和删除当前项目的另外两个按钮位于 <div> 中。在这个 <div> 上有一个 *ngIf,这是一个内置的 Angular 指令,您可以使用它来动态更改 DOM 的结构。此 *ngIf 表示如果 editablefalse,则此 <div> 位于 DOM 中。如果 editabletrue,Angular 将从 DOM 中删除此 <div>

html
<div class="btn-wrapper" *ngIf="!editable">
  <button class="btn" (click)="editable = !editable">Edit</button>
  <button class="btn btn-warn" (click)="remove.emit()">Delete</button>
</div>

当用户单击编辑按钮时,editable 变为 true,这将从 DOM 中删除此 <div> 及其子级。如果用户没有单击编辑,而是单击删除,则 ItemComponent 会引发一个事件,通知 AppComponent 删除操作。

下一个 <div> 上也有一个 *ngIf,但设置为 editable 值为 true。在这种情况下,如果 editabletrue,Angular 将把 <div> 及其子级 <input><button> 元素置于 DOM 中。

html
<!-- This section shows only if user clicks Edit button -->
<div *ngIf="editable">
  <input
    class="sm-text-input"
    placeholder="edit item"
    [value]="item.description"
    #editedItem
    (keyup.enter)="saveItem(editedItem.value)" />

  <div class="btn-wrapper">
    <button class="btn" (click)="editable = !editable">Cancel</button>
    <button class="btn btn-save" (click)="saveItem(editedItem.value)">
      Save
    </button>
  </div>
</div>

使用 [value]="item.description"<input> 的值绑定到当前项目的 description。此绑定使项目的 description 成为 <input> 的值。因此,如果 descriptioneat,则 description 已经在 <input> 中。这样,当用户编辑项目时,<input> 的值已经是 eat

<input> 上的模板变量 #editedItem 表示 Angular 将用户在此 <input> 中键入的任何内容存储在一个名为 editedItem 的变量中。如果用户选择按 Enter 键而不是单击保存,则 keyup 事件将调用 saveItem() 方法并传入 editedItem 值。

当用户单击取消按钮时,editable 切换为 false,这将从 DOM 中删除用于编辑的输入和按钮。当 editablefalse 时,Angular 将带有编辑删除按钮的 <div> 放回 DOM 中。

单击保存按钮将调用 saveItem() 方法。saveItem() 方法将从 #editedItem 元素中获取值,并将项目的 description 更改为 editedItem.value 字符串。

准备 AppComponent

在下一节中,您将添加依赖于 AppComponentItemComponent 之间通信的代码。在 app.component.ts 文件的顶部附近添加以下行,以导入 Item

ts
import { Item } from "./item";
import { ItemComponent } from "./item/item.component";

然后,通过将以下内容添加到同一文件的类中来配置 AppComponent

js
remove(item: Item) {
  this.allItems.splice(this.allItems.indexOf(item), 1);
}

remove() 方法使用 JavaScript Array.splice() 方法从相关项目的 indexOf 中删除一个项目。通俗地说,这意味着 splice() 方法从数组中删除该项目。有关 splice() 方法的更多信息,请参阅 Array.prototype.splice() 文档

向 ItemComponent 添加逻辑

要使用 ItemComponent UI,您必须向组件添加逻辑,例如函数以及数据进出的方式。在 item.component.ts 中,按如下方式编辑 JavaScript 导入

js
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { CommonModule } from "@angular/common";
import { Item } from "../item";

添加 InputOutputEventEmitter 允许 ItemComponentAppComponent 共享数据。通过导入 ItemItemComponent 可以理解 item 是什么。您可以更新 @Component 以在 app/item/item.component.ts 中使用 CommonModule,以便我们可以使用 ngIf 指令

js
@Component({
  selector: 'app-item',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './item.component.html',
  styleUrl: './item.component.css',
})

item.component.ts 中,用以下内容替换生成的 ItemComponent

js
export class ItemComponent {

  editable = false;

  @Input() item!: Item;
  @Output() remove = new EventEmitter<Item>();

  saveItem(description: string) {
    if (!description) return;

    this.editable = false;
    this.item.description = description;
  }
}

editable 属性有助于切换用户可以在其中编辑项目的模板部分。editable 是模板中与 *ngIf 语句中相同的属性,*ngIf="editable"。当您在模板中使用属性时,还必须在类中声明它。

@Input()@Output()EventEmitter 促进了两个组件之间的通信。@Input() 作为数据进入组件的门户,@Output() 作为数据离开组件的门户。@Output() 必须是 EventEmitter 类型,以便组件在有数据准备与其他组件共享时可以引发事件。

注意: 类属性声明中的 ! 称为 明确赋值断言。此运算符告诉 TypeScript item 字段始终已初始化,而不是 undefined,即使 TypeScript 无法从构造函数的定义中判断出来也是如此。如果您的代码中没有包含此运算符,并且您具有严格的 TypeScript 编译设置,则应用程序将无法编译。

使用 @Input() 指定属性的值可以来自组件外部。使用 @Output()EventEmitter 结合使用,指定属性的值可以离开组件,以便其他组件可以接收该数据。

saveItem() 方法以 description(类型为 string)作为参数。description 是用户在编辑列表中的项目时输入到 HTML <input> 中的文本。此 description 是与具有 #editedItem 模板变量的 <input> 相同的字符串。

如果用户没有输入值,而是单击保存saveItem() 不会返回任何内容,也不会更新 description。如果您没有此 if 语句,用户可以在 HTML <input> 中什么都不输入就单击保存,并且 description 将变为空字符串。

如果用户输入文本并单击保存,saveItem()editable 设置为 false,这会导致模板中的 *ngIf 删除编辑功能并再次呈现编辑删除按钮。

尽管应用程序应该在此时编译,但您需要在 AppComponent 中使用 ItemComponent,以便您可以在浏览器中看到新功能。

在 AppComponent 中使用 ItemComponent

在父子关系的上下文中将一个组件包含在另一个组件中,可以让您灵活地在需要的地方使用组件。

AppComponent 作为应用程序的外壳,您可以在其中包含其他组件。

要在 AppComponent 中使用 ItemComponent,请将 ItemComponent 选择器置于 AppComponent 模板中。Angular 在 @Component() 装饰器的元数据中指定组件的选择器。在此示例中,我们已将选择器定义为 app-item

js
@Component({
  selector: 'app-item',
  // ...

要在 AppComponent 中使用 ItemComponent 选择器,您需要添加元素 <app-item>,该元素对应于您为组件类定义的选择器,添加到 app.component.html。用以下更新版本替换 app.component.html 中的当前无序列表 <ul>

html
<h2>
  {{items.length}}
  <span *ngIf="items.length === 1; else elseBlock">item</span>
  <ng-template #elseBlock>items</ng-template>
</h2>

<ul>
  <li *ngFor="let i of items">
    <app-item (remove)="remove(i)" [item]="i"></app-item>
  </li>
</ul>

更改 app.component.ts 中的 imports 以包括 ItemComponent 以及 CommonModule

js
@Component({
  standalone: true,
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  imports: [CommonModule, ItemComponent],
})

<h2> 中的双大括号语法 {{}} 将插值 items 数组的长度,并显示数字。

<h2> 中的 <span> 使用 *ngIfelse 来确定 <h2> 应该显示“item”还是“items”。如果列表中只有一个项目,则显示包含“item”的 <span>。否则,如果 items 数组的长度不是 1,则 <ng-template>(我们已将其命名为 elseBlock,语法为 #elseBlock)将显示,而不是 <span>。当您不希望默认情况下呈现内容时,可以使用 Angular 的 <ng-template>。在这种情况下,当 items 数组的长度不是 1 时,*ngIf 将显示 elseBlock,而不是 <span>

<li> 使用 Angular 的重复器指令 *ngFor 来遍历 items 数组中的所有项目。Angular 的 *ngFor*ngIf 一样,是另一个帮助您在编写更少代码的同时更改 DOM 结构的指令。对于每个 item,Angular 会重复 <li> 及其内部的所有内容,其中包括 <app-item>。这意味着对于数组中的每个项目,Angular 都会创建另一个 <app-item> 实例。对于数组中的任意数量的项目,Angular 将创建与之数量相等的 <li> 元素。

您也可以在其他元素上使用 *ngFor,例如 <div><span><p>,仅举几例。

AppComponent 具有用于删除项目的 remove() 方法,该方法绑定到 ItemComponent 中的 remove 属性。方括号 [] 中的 item 属性将 i 之间的值绑定到 AppComponentItemComponent 之间。

现在,您应该能够从列表中编辑和删除项目。当您添加或删除项目时,项目的数量也应该改变。为了使列表更易于使用,请向 ItemComponent 添加一些样式。

向 ItemComponent 添加样式

您可以使用组件的样式表来添加特定于该组件的样式。以下 CSS 添加了基本样式,用于按钮的 flexbox 以及自定义复选框。

将以下样式粘贴到 item.component.css 中。

css
.item {
  padding: 0.5rem 0 0.75rem 0;
  text-align: left;
  font-size: 1.2rem;
}

.btn-wrapper {
  margin-top: 1rem;
  margin-bottom: 0.5rem;
}

.btn {
  /* menu buttons flexbox styles */
  flex-basis: 49%;
}

.btn-save {
  background-color: #000;
  color: #fff;
  border-color: #000;
}

.btn-save:hover {
  background-color: #444242;
}

.btn-save:focus {
  background-color: #fff;
  color: #000;
}

.checkbox-wrapper {
  margin: 0.5rem 0;
}

.btn-warn {
  background-color: #b90000;
  color: #fff;
  border-color: #9a0000;
}

.btn-warn:hover {
  background-color: #9a0000;
}

.btn-warn:active {
  background-color: #e30000;
  border-color: #000;
}

.sm-text-input {
  width: 100%;
  padding: 0.5rem;
  border: 2px solid #555;
  display: block;
  box-sizing: border-box;
  font-size: 1rem;
  margin: 1rem 0;
}

/* Custom checkboxes
Adapted from https://css-tricks.org.cn/the-checkbox-hack/#custom-designed-radio-buttons-and-checkboxes */

/* Base for label styling */
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
  position: absolute;
  left: -9999px;
}
[type="checkbox"]:not(:checked) + label,
[type="checkbox"]:checked + label {
  position: relative;
  padding-left: 1.95em;
  cursor: pointer;
}

/* checkbox aspect */
[type="checkbox"]:not(:checked) + label:before,
[type="checkbox"]:checked + label:before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 1.25em;
  height: 1.25em;
  border: 2px solid #ccc;
  background: #fff;
}

/* checked mark aspect */
[type="checkbox"]:not(:checked) + label:after,
[type="checkbox"]:checked + label:after {
  content: "\2713\0020";
  position: absolute;
  top: 0.15em;
  left: 0.22em;
  font-size: 1.3em;
  line-height: 0.8;
  color: #0d8dee;
  transition: all 0.2s;
  font-family: "Lucida Sans Unicode", "Arial Unicode MS", Arial;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked) + label:after {
  opacity: 0;
  transform: scale(0);
}
[type="checkbox"]:checked + label:after {
  opacity: 1;
  transform: scale(1);
}

/* accessibility */
[type="checkbox"]:checked:focus + label:before,
[type="checkbox"]:not(:checked):focus + label:before {
  border: 2px dotted blue;
}

总结

现在,您应该拥有一个经过样式化的 Angular 待办事项列表应用程序,可以添加、编辑和删除项目。下一步是添加过滤,以便您可以查看满足特定条件的项目。