使用 Angular 指令来扩展您不拥有的组件。
在 Twitter 上关注我:@tim_deschryver | 订阅新闻简报| 原文发表于timdeschryver.dev。
请访问 timdeschryver.dev 阅读 TLDR 版本
Angular 指令的使用率很低,我认为这是因为我们并不了解它们的全部功能。
如果你正在使用 Angular,你可能熟悉常用的结构化指令*ngIf,*ngFor但你的代码库中是否包含自定义指令呢?答案很可能是否定的,而你可能因为更熟悉组件而选择了使用指令。
在这篇博文中,我想向大家展示一种使用指令以统一方式配置第三方组件的实用技巧。与创建包装组件相比,我认为这是一种更优雅的解决方案。
我们来看一个例子。
默认指令
在我目前的项目中,我们正在使用 PrimeNG 的组件库,我反复看到以下代码,每个日期选择器都是如此。
<p-calendar
[(ngModel)]="date"
required
id="date"
name="date"
dateFormat="dd/mm/yy"
[showIcon]="true"
[showButtonBar]="true"
[monthNavigator]="true"
[yearNavigator]="true"
yearRange="1900:2050"
[firstDayOfWeek]="1"
>
</p-calendar>
这是配置组件以实现我们预期行为所需的标记。
依我看,这代码量很大,不仅污染了模板,还会让我们误以为事情比实际更复杂。
我可能还会忘记(或者根本不知道)给新的日期选择器添加属性,这会给用户带来不同的体验。最后,当组件移除、更改或添加属性时,我可能需要修改p-datepicker代码库中的所有元素。
简而言之,这会对开发人员和用户都造成影响。
当我们使用指令重构代码时,模板又会变得简单,而且我们确信始终能为用户提供相同的体验。
重构后的模板如下所示。
<p-calendar [(ngModel)]="date" required id="date" name="date"></p-calendar>
但是,如何将 14 行 HTML 代码简化为一行(这是 Prettier 的格式化方式)?
答案是使用指令。
该指令使用p-calender组件选择器应用于所有日历元素。
实例Calendar被注入到指令中,并可根据需要进行配置。
import { Directive } from '@angular/core';
import { Calendar } from 'primeng/calendar';
@Directive({
selector: 'p-calendar',
})
export class CalenderDirective {
constructor(private calendar: Calendar) {
this.calendar.dateFormat = 'dd/mm/yy';
this.calendar.showIcon = true;
this.calendar.showButtonBar = true;
this.calendar.monthNavigator = true;
this.calendar.yearNavigator = true;
this.calendar.yearRange = '1900:2050';
this.calendar.firstDayOfWeek = 1;
}
}
覆盖默认实现
该指令为所有日期选择器元素提供了一个坚实的基础。
但对于一些特殊情况,可以覆盖指令的预定义值,以便对需要不同配置的元素进行自定义设置。
在下面的示例中,导航器选项通过显式地将其值设置为 来禁用false。
<p-calendar [monthNavigator]="false" [yearNavigator]="false"></p-calendar>
选择加入指令
我们并非使用指令来改变所有元素的行为,而是修改选择器,使其能够针对具有特定用途的元素。例如,对于具有通用约定的下拉元素,我们可以配置这些名为“codes-dropdown”的元素的下拉行为。请注意,选择器
新增的属性仅针对“codes-dropdown”元素。[codes]
import { Directive, OnInit } from '@angular/core';
import { Dropdown } from 'primeng/dropdown';
import { sortByLabel } from '@core';
@Directive({
selector: 'p-dropdown[codes]',
})
export class CodesDropdownDirective implements OnInit {
constructor(private dropdown: Dropdown) {
this.dropdown.optionLabel = 'label';
this.dropdown.optionValue = 'key';
this.dropdown.showClear = true;
}
public ngOnInit(): void {
this.dropdown.options = [...this.dropdown.options].sort(sortByLabel);
if(this.dropdown.options.length > 10) {
this.dropdown.filter = true;
this.dropdown.filterBy = 'label';
this.dropdown.filterMatchMode = 'startsWith';
}
}
}
这样,只有p-dropdown带有该codes属性的元素才会受到上述指令的配置。
要在 HTML 模板中使用上述指令,我们需要将该codes属性添加到p-dropdown元素中。
<p-dropdown [(ngModel)]="favoriteSport" codes required id="sport" name="sport"></p-dropdown>
选择退出指令
:not()另一种方法是对大多数情况下需要相同配置,但在少数情况下需要特殊配置的元素使用选择器。例如,假设我们应用程序中 90% 的下拉列表元素的数据源包含“代码”。在这种情况下,我们不希望强制要求codes为这些指令添加属性,而是希望定义何时不希望对剩余的 10% 的下拉列表元素使用该指令。
我们不使用该codes属性来标记代码下拉列表,而是假设这是默认行为,但使用该resetDropdown属性可以选择退出该行为。
import { Directive, OnInit } from '@angular/core';
import { Dropdown } from 'primeng/dropdown';
import { sortByLabel } from '@core';
@Directive({
selector: 'p-dropdown:not(resetDropdown)',
})
export class CodesDropdownDirective implements OnInit {
constructor(private dropdown: Dropdown) {
this.dropdown.optionLabel = 'label';
this.dropdown.optionValue = 'key';
this.dropdown.showClear = true;
}
public ngOnInit(): void {
this.dropdown.options = [...this.dropdown.options].sort(sortByLabel);
if(this.dropdown.options.length > 10) {
this.dropdown.filter = true;
this.dropdown.filterBy = 'label';
this.dropdown.filterMatchMode = 'startsWith';
}
}
}
在 HTML 模板中,这翻译成如下内容。
<!-- uses the codes dropdown -->
<p-dropdown [(ngModel)]="favoriteSport" required id="sport" name="sport"></p-dropdown>
<!-- opt-out of the codes dropdown and use the default p-dropdown behavior -->
<p-dropdown
[(ngModel)]="preference"
resetDropdown
required
id="preference"
name="preference"
></p-dropdown>
加载数据的指令
我们可以在指令的实现中做更多的事情。
这里我们看到一个指令,它使用数据填充下拉列表,这对于常用数据源非常有用。
这个示例的一个变体是使数据源可配置。
在下面的示例中,我们添加了一个[countries]属性,以便将指令绑定到特定的下拉列表,从而使用国家/地区列表作为数据源。此指令可以与其他下拉列表指令一起使用。
此外,该指令还包含一个@Output发射器,用于在国家/地区列表加载完毕后触发事件。
import { Directive, EventEmitter, OnInit, Output } from '@angular/core';
import { Dropdown } from 'primeng/dropdown';
import { GeoService, sortByLabel } from '@core';
@Directive({
selector: 'p-dropdown[countries]',
})
export class CountriesDropdownDirective implements OnInit {
@Output() loaded = new EventEmitter<ReadonlyArray<Countries>>();
constructor(private dropdown: Dropdown, private geoService: GeoService) {}
public ngOnInit(): void {
this.geoService.getCountries().subscribe((result) => {
this.dropdown.options = result.map((c) => ({ label: c.label, key: c.id })).sort(sortByValue);
this.loaded.emit(this.dropdown.options);
});
}
}
<p-dropdown
[(ngModel)]="country"
countries
required
id="country"
name="country"
(loaded)="countriesLoaded($event)"
></p-dropdown>
结论
Angular 指令非常棒,但可惜的是,它并没有得到充分利用。
指令正是开闭原则的核心所在。组件本身是封闭的,不允许修改,但指令允许你在不改变组件内部结构的情况下对其进行扩展。
例如,通过指令,我们可以在不访问组件代码的情况下改变第三方库或内部组件库的行为。
我们可以使用包装组件和具有丰富配置选项的组件来实现同样的效果,但这需要更多的代码,而且更难维护。
为了针对需要不同配置的元素,我们可以使用选择器来定位特定元素。由于指令可以堆叠,我们可以限制指令的职责,使其只执行一项操作。
在 Twitter 上关注我:@tim_deschryver | 订阅新闻简报| 原文发表于timdeschryver.dev。
文章来源:https://dev.to/this-is-angular/use-angular-directives-to-extend-components-that-you-dont-own-1jio