创建项目组件
组件提供了一种组织应用程序的方法。本文将引导您创建一个组件来处理列表中的单个项目,并添加检查、编辑和删除功能。本文将介绍 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;
}