创建可重用的 Angular 组件
在本教程中,我将讨论如何在 Angular 应用程序中创建可重用组件。
引言
自上一篇教程以来,我一直难以构思新的教程。很有趣的是,写了这么多年教程,我终于到了没有什么可写的地步——遇到了创作瓶颈。但这不会阻止我。我不得不认真思考,寻找一些值得讨论的内容。
我从未在教程中讨论过的一个概念是在 Angular 应用程序中创建可重用组件。所以,我决定将这个概念作为本教程的主题。问题是我不知道示例应用程序的样子,创作瓶颈。我开始研究 Bootstrap 框架中的组件,希望能找到一些灵感。Bootstrap 框架一直包含一个名为“进度条”的有用组件。据我所知,这个组件一直都是只读的。也就是说,它显示进度/状态(例如百分比值),用户无法与之交互。当我意识到这一点时,我想,为什么这个组件总是只读的?我能做些什么吗?事实证明,有一种方法可以使这个组件具有交互性。而且,当它起作用时,效果非常好!
在本教程中,您将看到我需要解决多个问题。我必须了解可重用组件是如何工作的;子组件如何与父组件通信;以及我如何将静态组件变成用户可交互的东西。所有这些问题都将在本教程中得到解答。
整体架构
本教程的示例应用程序是一个颜色选择器。它使用 Bootstrap 框架中的三个进度条来表示红色、绿色和蓝色的值。用户可以点击进度条上的任意位置来设置颜色新值。还有三个输入字段,显示红色、绿色和蓝色的当前值。用户也可以直接通过这些输入字段更改这些值。更改也会反映在受影响的进度条上。最后,底部的矩形显示由三种原色值表示的颜色。
这是应用程序的截图
此应用程序只有 Angular 代码,没有后端服务。我打算专注于如何创建可重用组件以及如何使进度条具有交互性。三个进度条是相同的可重用组件。将其设计为可重用组件是一个好主意。我将向您展示原因。在下一节中,我将开始创建交互式进度条,然后是如何从中创建一个可重用组件,以及为什么使设计可重用是一个好主意。
制作交互式进度条
对于本教程来说,我必须解决的最大问题是使进度条具有交互性。它的设计初衷是显示进度。据我所知,没有地方将其描述为可交互的。当我意识到这一点时,我便想,为什么没有人尝试将这个炫酷的组件做成交互式的?我确信有人尝试过使其具有交互性。他们可能从未记录下来。如果您想知道如何使此组件具有交互性,那么您应该仔细阅读本节。如果我的解释不清楚,没关系。请下载示例应用程序并进行分析。
这是 Bootstrap 进度条的截图
上面截图对应的 HTML 标记如下所示
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 75%"
aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div>
</div>
从上面的截图可以看出,进度条组件由两个不同的 <div>
组成。带有“progress
”类的外部 <div>
是代表整体进度的灰色条。内部 <div>
显示实际的进度值。它是那条蓝色的条纹,并没有完全覆盖灰色条。
我设想与此类组件的交互方式是,当用户点击进度条上的某个位置时,进度就设置在该位置或其附近。问题是如何找到鼠标光标位置到进度条起点的相对距离,以及进度条的总长度。一旦获得这两个值,我就可以计算进度条的百分比。经过一些研究和侦查工作,我意识到获取这两个值并不难。
事实证明,页面上的每个元素都有基于相对位置的相对位置和尺寸。如果我们知道如何查找,就可以免费获得这些信息。如果我们能获得元素的引用,就可以从引用对象的 offsetWidth
属性中获取其宽度。很简单,不是吗?下一个问题是,如何确定给定元素内鼠标光标的位置?遵循相同的思路,发现通过捕获鼠标单击事件,然后从事件中找到位置,就可以实现鼠标光标相对于元素的位置。事件的 offsetX
和 offsetY
属性是光标相对于光标所在元素的坐标。
既然我们知道如何根据光标位置和整个进度条的长度计算进度百分比,我们就可以将所有这些放入一个函数/方法中,如下所示:
public handleClickProgressBar($event: any) {
let progressBarWidth: number = 0;
let cursorXPos: number = 0;
if ($event) {
cursorXPos = $event.offsetX;
if ($event.currentTarget) {
progressBarWidth = $event.currentTarget.offsetWidth;
this.barWidth = cursorXPos / progressBarWidth;
this.barWidth = Math.ceil(this.barWidth * 100);
this.barUIWidth = "" + this.barWidth + "%";
console.log(this.barWidth);
this.updateProgress.emit({
percentageVal: this.barWidth
});
}
}
}
上述方法中的代码逻辑并不复杂。复杂的是寻找解决方案的过程,我不会在本节中讨论。这会浪费字节,而且对本教程没有好处。该方法接受一个参数,我称之为 $event
。它的匿名类型是 any
。这个参数是鼠标单击事件的引用。工作从 if
块开始。首先,我们需要获取鼠标单击事件发生时光标位置的 x 坐标。这可以通过以下行完成:
...
cursorXPos = $event.offsetX;
...
此行将获取光标位置的 x 坐标。此位置相对于页面上元素的左上角。接下来,我们需要获取进度条的长度。使用 x 坐标值和条形长度,我们应该能够确定进度应显示在新位置。
要弄清楚进度的长度,我们所需要做的就是找到接收鼠标单击事件并调用此方法来处理它的元素。稍作侦查后发现,这非常简单
...
progressBarWidth = $event.currentTarget.offsetWidth;
...
$event.currentTarget
的引用是接收鼠标单击事件并调用上述方法处理事件的元素。该元素的相对宽度存储在名为 offsetWidth
的属性中。获取这两个值很简单,因为现代浏览器已经完成了繁重的工作。
在我解释计算显示新进度值之前,我需要解释进度条的配置。该条被配置为显示从 0 到 100 的进度值(是的,百分比)。另外,正如我之前提到的,进度条组件有两个部分:背景显示灰色,顶部显示彩色进度。这两个部分都是通过 <div>
标签组成的。顶部部分的宽度可以通过百分比值(通过 style
属性)指定。
现在我们知道了进度条是如何配置的,我们可以重新计算进度长度,然后显示它。这是怎么做的
...
this.barWidth = cursorXPos / progressBarWidth;
this.barWidth = Math.ceil(this.barWidth * 100);
this.barUIWidth = "" + this.barWidth + "%";
...
以上代码片段是完成可重用组件的鼠标单击事件处理所需的所有内容。现在是时候向您展示将事件处理方法添加到组件的位置了。这是可重用组件的 HTML 标记
<div class="progress"
(click)="handleClickProgressBar($event)">
<div class="progress-bar"
[ngClass]="barColor"
role="progressbar"
[style.width]="barUIWidth"
[attr.aria-valuenow]="barWidth"
aria-valuemin="0"
aria-valuemax="100">{{barTitle}}</div>
</div>
我用粗体字体高亮了上述标记代码中两个重要的部分。第一行粗体是将事件处理方法附加到元素上。该元素显示了进度条的整个范围。我将事件处理方法附加到此元素,以便方法可以轻松获取进度条的整个长度。另一种方法是将事件处理方法附加到内部 <div>
。这种方法的缺点是,这个内部 <div>
用于显示当前进度。如果值小于 100%,则此内部 <div>
的长度将不是整个条。我必须转到父元素(外部 <div>
)来查找整个条的实际长度。这就是我选择外部 <div>
进行事件处理方法的原因。当有捷径可用时,为什么还要绕远路呢?
粗体显示的第二部分是设置内部 <div>
的宽度值。宽度是在我之前展示的最后一个 TypeScript 代码片段中计算的,在这里进行赋值。语法很特别。它指定对于 HTML 元素属性 style
的 width
属性,将组件对象属性 barUIWidth
绑定到它。
当您运行示例应用程序时,请尝试单击任意位置的进度条,看看进度是如何设置在该位置(或其附近)的。在下一节中,我将讨论如何将本节的工作打包成一个可重用组件。
设计可重用组件
创建可重用组件比设计交互式进度条容易得多。网上有很多教程解释如何做到。在最新的 Angular 框架中创建可重用组件比在 AngularJS 中更容易。父组件和子组件之间的通信也更容易理解。根据我 30 分钟研究的收获,我们只需要理解 @Input()
和 @Output
注解,以及使用属性 setter 作为观察者的概念。一旦我理解了这三个概念,设计一个可重用组件就很容易了。还有其他概念我们需要了解才能正确实现可重用组件。我们将回顾示例代码,并指出所有应该学习的细节。
我在索引页面上实现了进度条。然后,我将 HTML 标记和 TypeScript 代码复制到单独的文件中,使它们成为一个独立的组件。我在上一节中列出的 HTML 标记代码位于一个名为 progressSlider.component.html 的文件中。在同一个文件夹中,您会找到源文件 progressSlider.component.ts。该文件定义了组件的行为。
这是进度条的 TypeScript 代码
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'progress-slider',
templateUrl: './progressSlider.component.html'
})
export class ProgressSliderComponent implements OnInit {
@Input() barColor: string;
private _barWidth: number;
@Input() barTitle: string
public barUIWidth: string;
@Output() updateProgress = new EventEmitter();
constructor() {
}
public get barWidth(): number {
return this._barWidth;
}
@Input()
public set barWidth(val: number) {
this._barWidth = val;
this.barUIWidth = "" + this.barWidth + "%";
}
ngOnInit() {
}
public handleClickProgressBar($event: any) {
let progressBarWidth: number = 0;
let cursorXPos: number = 0;
if ($event) {
cursorXPos = $event.offsetX;
if ($event.currentTarget) {
progressBarWidth = $event.currentTarget.offsetWidth;
this.barWidth = cursorXPos / progressBarWidth;
this.barWidth = Math.ceil(this.barWidth * 100);
this.barUIWidth = "" + this.barWidth + "%";
console.log(this.barWidth);
this.updateProgress.emit({
percentageVal: this.barWidth
});
}
}
}
}
上面的源代码包含已在上一节讨论过的事件处理方法。我还没有解释的唯一部分是方法的最后一部分。稍后我会讲到。让我们先看看我还没有解释过的代码。首先,是组件的声明
...
@Component({
selector: 'progress-slider',
templateUrl: './progressSlider.component.html'
})
export class ProgressSliderComponent implements OnInit {
...
上面的代码片段很容易理解。它创建了一个 component
类,并将该类绑定到名称为 <progress-slider>
的任何元素。templateUrl
指定将替换该元素的 HTML 标记。您猜对了,用于替换的 HTML 标记就是我在上一节中列出的最后一个代码片段。
接下来,我将向您展示属性声明、属性的 getter 和 setter,如下所示:
... @Input() barColor: string; private _barWidth: number; @Input() barTitle: string public barUIWidth: string; @Output() updateProgress = new EventEmitter(); constructor() { } public get barWidth(): number { return this._barWidth; } @Input() public set barWidth(val: number) { this._barWidth = val; this.barUIWidth = "" + this.barWidth + "%"; } ...
在源代码的这一部分,我用 @Input()
装饰了属性。通过用此装饰属性,该属性可以用作 <progress-slider>
元素的属性。这些输入属性将能够将父组件的属性绑定到此子组件。此外,无论何时父组件中的属性更改值,更改都将传播到此子组件。这就是父组件与子组件通信的方式——通过值传播。如果您还记得,我之前说过不需要观察者(在 AngularJS 中很有用)。这是因为我们有属性访问器(getter 和 setter)。在此类中,我为 _barWidth
定义了一个 setter。此 setter 用 @Input()
装饰。它将能够与父组件的属性绑定。当父组件的属性值发生变化时,将调用此 setter。在其中,我们可以设置此 setter 背后的私有属性,并更改另一个属性的值。这就是 setter 的用途,几乎等同于 AngularJS 中的观察者。
还有一个用 @Output()
注解装饰的属性。此属性的类型为 EventEmitter
。任何类型为 EventEmitter
的属性都必须用 @Output()
注解。此属性也可以是 <progress-slider>
元素的属性。它可以绑定到父组件中的回调方法。您可以看到这个属性被事件处理方法使用,如下所示:
public handleClickProgressBar($event: any) { ... if ($event) { ... if ($event.currentTarget) { ... this.updateProgress.emit({ percentageVal: this.barWidth }); } } }
如您所见,输出属性可用于调用 .emit(<要传回的对象>)
。发生的情况是,绑定到此属性的回调方法将被调用,并处理从这里传回的对象。在事件处理方法中,一旦处理了鼠标单击事件,就会更新实际进度的长度,然后我们将新值(0 到 100 之间的百分比值)传递给父组件。
本节揭示了这个组件的所有秘密。很有趣,不是吗?我惊叹于创建可重用组件竟然如此简单。在下一节中,我将讨论如何将此可重用组件集成到索引页面。
集成可重用组件
在本节中,我将讨论如何使用这个可重用组件。此应用程序只有一个页面。上面有三个交互式进度条。它们都是相同的可重用组件。每个都代表一种原色(红、绿、蓝)的值。进度条使用 0 到 100 的刻度。原色的值范围是 0 到 255。因此,当我在交互中使用进度条时,必须进行某种值转换。这是我们都需要注意的事情。到时候我会指出来。首先,我想向您展示如何将组件添加到 HTML 页面。在 index.component.html 文件中,您将看到
<div class="row mb-2">
<div class="col">
<progress-slider [barColor]="barColorRed"
[barWidth]="barWidthRed" [barTitle]="barTitleRed"
(updateProgress)="handleRedBarValuaUpdate($event)" />
</div>
</div>
<div class="row mb-2">
<div class="col">
<progress-slider [barColor]="barColorBlue"
[barWidth]="barWidthBlue" [barTitle]="barTitleBlue"
(updateProgress)="handleBlueBarValuaUpdate($event)" />
</div>
</div>
<div class="row mb-2">
<div class="col">
<progress-slider [barColor]="barColorGreen"
[barWidth]="barWidthGreen" [barTitle]="barTitleGreen"
(updateProgress)="handleGreenBarValuaUpdate($event)" />
</div>
</div>
在这个标记代码片段中,我高亮了使用可重用组件的三个地方。页面上的元素称为 <progress-slider>
。对于此元素,有四个属性
barColor
:这是可重用组件的输入属性。它绑定到当前组件的一个名为barColorRed
(第一个)的属性。不仅barColorRed
引用的值会传递给可重用组件,而且barColorRed
引用的值更改也会反映在可重用组件中。barWidth
,barWidth
,barTitle
:它们都与barColor
的行为相同。updateProgress
:这个属性将当前组件的回调方法绑定到可重用组件。当可重用组件决定与父组件联系时,它将使用此回调方法进行联系。
这些名称看起来熟悉吗?它们应该是。这些是在 ProgressSliderComponent
组件中用 @Input()
或 @Output()
注解装饰的组件属性。这就是组件之间的属性绑定工作方式。
在 index.component.ts 文件中,有三个回调方法。它们都执行相同的工作。我们将检查其中一个——handleRedBarValuaUpdate()
public handleRedBarValuaUpdate(evt: any): void {
// XXX, in case you wonder, this._maxColorVal = 255
console.log(evt);
if (evt) {
this.redColorVal = Math.ceil(evt.percentageVal * this._maxColorVal / 100);
}
}
在上一节中,我列出了带有组件属性 updateProgress
(已用 @Output
注解的那个)的源代码。它可以通过 emit()
方法调用。调用此方法后,在父组件中,将调用绑定的回调方法来处理由子组件中 emit()
方法调用返回的值。这听起来很混乱,不是吗?让我来总结一下
- 当子组件在页面加载期间渲染时,进度条的初始值由输入属性使用父组件的值设置。
- 用户使用鼠标光标与进度条交互。子组件有一个鼠标单击事件处理器。在其中,将计算新的进度值。进度条显示将更新。
- 在事件处理器中,进度条更新后。它将使用输出属性
updateProgress
(类型为EventEmitter
)调用emit()
方法。这将把更新后的百分比值传递回父组件。 - 父组件具有回调方法
handleXXXBarValuaUpdate()
(XXX 是进度条的颜色),该方法将被调用并更新目标进度条的值。
这看起来非常复杂。但实际上不是。您只需要运行示例应用程序,并在我描述的几个位置设置几个断点。然后,您可以单击进度条,观察执行流程如何运行。
当新的进度条值传递给父组件时,底部的颜色显示应该会更新。这是通过父组件中颜色值的 setter 来完成的。这是一个更新颜色显示 <div>
背景颜色的示例
public set redColorVal(val: number) {
this._redColorVal = val;
this.barWidthRed = Math.ceil(this._redColorVal / this._maxColorVal * 100);
this.colorToDisplay = "rgb(" + this._redColorVal + ", " +
this._greenColorVal + ", " + this._blueColorVal + ")"
console.log(this.colorToDisplay);
}
在示例应用程序中,我还添加了三个文本框,供用户手动设置颜色值。值可以从 0 到 255。每次用户在文本框中输入新值时。将调用与绑定到文本框的属性关联的上述 setter。您可以自己尝试一下。在“Red
”的文本框中,您会看到颜色进度条更新,底部的颜色显示也会相应更改。
这就是这个示例应用程序的全部内容。在下一节中,我将解释如何运行此示例应用程序。它比我以前的教程要容易得多。
如何运行示例应用程序
我有一个好消息——运行这个示例应用程序非常容易。涉及几个步骤。首先,请下载示例应用程序并将其解压缩到某个位置。然后,运行以下命令来安装所有必需的 node.js 库
npm install
在成功安装库后,您可以运行以下命令来在本地 Web 服务器上启动 Web 应用程序
ng serve
Web 服务器成功启动后,就可以测试 Web 应用程序了。请使用浏览器导航到
https://:4200/
Web 应用程序的外观如下
尝试单击任意一个进度条,看看相应文本框中的颜色值是否会发生变化,以及底部的组合颜色显示。或者,您可以更改文本框中的原色值。进度条会随着值的变化而拉伸或收缩。这表明父子组件之间的双向通信正在工作。您应该使用调试断点来更好地理解机制的工作原理。
摘要
好了,这是一篇有趣的教程。刚开始时,我被卡住了,不知道该写什么。经过一些研究,我能够构思出这个。在开发过程中,我学到了很多。我学到的第一件事是如何将静态的 Bootstrap 组件变成可交互的东西。在此基础上,我能够从中设计一个可重用组件。这对您读者和我来说都是双赢。
这是我写的第 50 篇教程。我再次为它的成果感到激动。教程本身质量不如我预期的那么高。但示例应用程序是顶级的。在示例应用程序中,您将看到 Bootstrap 的进度条如何增强为一个交互式组件。我甚至向您展示了如何查找鼠标光标位置和进度条的长度。通过这些,计算进度条显示的百分比值将很容易。创建可重用组件并不难。这都归功于我学会了如何创建可重用组件。这使我能够快速学习新概念。正如我多次说过的,几乎所有新知识都可以建立在旧知识之上。这是最好的学习方式。
完成这篇教程是一段精彩的旅程。我希望它能在某些方面帮助您。即使不能,它至少看起来很酷。祝您学习顺利!
历史
- 2023年9月20日 - 初稿