创建项目组件
组件提供了一种组织应用程序的方法。本文将引导您创建一个组件来处理列表中的单个项目,并添加检查、编辑和删除功能。本文将介绍 Angular 事件模型。
| 先决条件 | 熟悉核心 HTML、CSS 和 JavaScript 语言,了解 终端/命令行。 |
|---|---|
| 目标 | 了解有关组件的更多信息,包括事件如何处理更新。添加检查、编辑和删除功能。 |
创建新组件
在命令行中,使用以下 CLI 命令创建一个名为 item 的组件
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用于 HTMLitem.component.ts用于逻辑item.component.css用于样式item.component.spec.ts用于测试组件
您可以在 item.component.ts 中的 @Component() 装饰器元数据中看到对 HTML 和 CSS 文件的引用。
@Component({
selector: 'app-item',
standalone: true,
imports: [],
templateUrl: './item.component.html',
styleUrl: './item.component.css'
})
为 ItemComponent 添加 HTML
ItemComponent 可以接管为用户提供一种方法来将项目标记为已完成、编辑或删除项目的任务。
通过用以下内容替换 item.component.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 表示如果 editable 为 false,则此 <div> 位于 DOM 中。如果 editable 为 true,Angular 将从 DOM 中删除此 <div>。
<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。在这种情况下,如果 editable 为 true,Angular 将把 <div> 及其子级 <input> 和 <button> 元素置于 DOM 中。
<!-- 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> 的值。因此,如果 description 是 eat,则 description 已经在 <input> 中。这样,当用户编辑项目时,<input> 的值已经是 eat。
<input> 上的模板变量 #editedItem 表示 Angular 将用户在此 <input> 中键入的任何内容存储在一个名为 editedItem 的变量中。如果用户选择按 Enter 键而不是单击保存,则 keyup 事件将调用 saveItem() 方法并传入 editedItem 值。
当用户单击取消按钮时,editable 切换为 false,这将从 DOM 中删除用于编辑的输入和按钮。当 editable 为 false 时,Angular 将带有编辑和删除按钮的 <div> 放回 DOM 中。
单击保存按钮将调用 saveItem() 方法。saveItem() 方法将从 #editedItem 元素中获取值,并将项目的 description 更改为 editedItem.value 字符串。
准备 AppComponent
在下一节中,您将添加依赖于 AppComponent 和 ItemComponent 之间通信的代码。在 app.component.ts 文件的顶部附近添加以下行,以导入 Item
import { Item } from "./item";
import { ItemComponent } from "./item/item.component";
然后,通过将以下内容添加到同一文件的类中来配置 AppComponent
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 导入
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { CommonModule } from "@angular/common";
import { Item } from "../item";
添加 Input、Output 和 EventEmitter 允许 ItemComponent 与 AppComponent 共享数据。通过导入 Item,ItemComponent 可以理解 item 是什么。您可以更新 @Component 以在 app/item/item.component.ts 中使用 CommonModule,以便我们可以使用 ngIf 指令
@Component({
selector: 'app-item',
standalone: true,
imports: [CommonModule],
templateUrl: './item.component.html',
styleUrl: './item.component.css',
})
在 item.component.ts 中,用以下内容替换生成的 ItemComponent 类
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
@Component({
selector: 'app-item',
// ...
要在 AppComponent 中使用 ItemComponent 选择器,您需要添加元素 <app-item>,该元素对应于您为组件类定义的选择器,添加到 app.component.html。用以下更新版本替换 app.component.html 中的当前无序列表 <ul>
<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
@Component({
standalone: true,
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
imports: [CommonModule, ItemComponent],
})
<h2> 中的双大括号语法 {{}} 将插值 items 数组的长度,并显示数字。
<h2> 中的 <span> 使用 *ngIf 和 else 来确定 <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 之间的值绑定到 AppComponent 和 ItemComponent 之间。
现在,您应该能够从列表中编辑和删除项目。当您添加或删除项目时,项目的数量也应该改变。为了使列表更易于使用,请向 ItemComponent 添加一些样式。
向 ItemComponent 添加样式
您可以使用组件的样式表来添加特定于该组件的样式。以下 CSS 添加了基本样式,用于按钮的 flexbox 以及自定义复选框。
将以下样式粘贴到 item.component.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.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;
}