
不久前我们团队发布了Nebular的稳定版本。 Nebular 是一个 Angular 库,可简化复杂的丰富 UI 应用程序的开发。它由以下模块组成:主题、身份验证和安全。
在多个候选版本期间,我们经历了一些挑战,我想告诉您最有趣的挑战。
如您所知,使用组件库构建用户界面要容易得多。这些库的重要组成部分之一是浮动组件 – 在其他组件上渲染并可能与它们重叠的组件,例如对话框、上下文菜单、Toast、工具提示等。 Nebular 库并不排除并且也需要此类组件。
第一个决定构建的是 Popover 组件。该组件的想法是在宿主元素周围的区域中呈现内容。这是一张图片只是为了说明:
在此处查看完整的 Popover 文档
我们决定将其创建为 Angular 指令,该指令接受@Input
形式的内容,如下所示:
// nebular_meets_angular_cdk_popover_showcase.html
<button [nbPopover]="hello">Open Popover</button>
<ng-template #hello>
<span>Hello, Popover!</span>
</ng-template>
在文档中了解如何使用 Popover
结果相当成功,从我们的实验中,我们还发现了将其用作其他组件基础的潜力,例如上下文菜单、工具提示等。为了给您提供一些技术细节,我们使用原生 JavaScript API 实现了定位,或者换句话说 – element.getBoundingClientRect()
。这使我们能够根据宿主元素的位置计算 Popover 的位置。我们添加的另一个很酷的功能是AdjustmentStrategy
。该策略的关键点是提供根据主机元素周围的可用空间重新定位 Popover 位置的功能。例如,如果我们将其放置在顶部位置,并且视口中没有足够的空间来渲染它, AdjustmentStrategy
将尝试将其渲染在主机下方。
我们对结果感到满意,并开始根据 Popover 实现构建其余组件。在最初的步骤中一切都很好,但进一步我们发现它有一些我们最初没有想到的局限性。它非常适合渲染主机元素周围的浮动面板,例如工具提示和上下文菜单,但不幸的是,它在渲染全局面板(例如 Toast 和对话框)方面并不那么成功。此时,我们了解到所需的解决方案非常广泛,并且其实施将非常耗时。这就是为什么我们开始寻找第三方替代品而不是从头开始构建它。
我们知道 Angular 提供了一个组件开发工具包 (CDK),因此显然我们决定先看看那里。事实证明它有很多基本概念对我们的案例非常有用。让我解释一下其中哪些对我们帮助最大。
您是否曾经遇到过这样的情况:您的下拉菜单被某些具有overflow: hidden
父元素切断?嗯,这是一个非常常见的问题,我们只能通过将组件呈现在文档树顶部的某个位置来解决,例如在<body>
开始标记之后,并将元素定位为绝对位置。
这是我们试图在 Nebular 中解决的类似问题。首先,我们实施了定制解决方案。 Nebular 有一个根组件,包裹着整个应用程序。该根组件能够使用 Angular 核心模块提供的ComponentFactoryResolver
在其余内容之前动态渲染组件。
但是,通过使用 CDK,我们能够利用门户和覆盖概念更优雅地解决这个问题。我们来看看吧。
Portal
是一种Component
类型或TemplateRef
,可以在页面上称为PortalOutlets
预定义插槽之一中动态呈现。这是一个简单的例子:
// nebular_meets_angular_cdk_portals.ts
this.loginPortal = new ComponentPortal(LoginComponent);
<ng-template [cdkPortalOutlet]="loginPortal"></ng-template>
Overlay
又是PortalOutlet
的实现。关键点是它将提供的Portal
呈现为页面上某处的浮动内容。另外,它支持位置策略,以便我们可以描述内容在文档中的放置方式。
// nebular_meets_angular_cdk_overlays.ts
constructor(protected overlay: Overlay) {}
const overlayRef = overlay.create();
const userProfilePortal = new ComponentPortal(UserProfile);
overlayRef.attach(userProfilePortal);
使用这个概念我们可以非常简单地创建一个组件。这是工具提示的一个简单示例:
// nebular_meets_angular_cdk_tooltip_showcase.ts
@Directive({ selector: '[nbTooltip]' })
export class NbTooltipDirective implements OnInit {
@Input() content: string = '';
// inject Angular CDK overlay service
constructor(protected overlay: Overlay) {}
ngOnInit() {
// create overlay
const overlayRef = this.overlay.create({ … });
}
show() {
// create tooltip Portal
const tooltipPortal = new ComponentPortal(NbTooltipComponent);
// attach Portal to overlay
const tooltipRef = overlayRef.attach(tooltipPortal);
// render provided content
tooltipRef.instance.content = this.content;
}
}
真实世界的实现可以在Nebular GitHub上找到
实现看起来很简单。我们只是注入 CDK Overlay,实例化它,使用自定义组件创建门户并将其附加到之前创建的覆盖引用。最后,我们通过将字符串传递给创建的组件来设置工具提示实例内容。工具提示指令的用法如下:
// nebular_meets_angular_cdk_tooltip_usage.html
<span nbTooltip="This is a tooltip!">Hover on me</span>
查看文档中如何使用 Tooltip
准备好工具提示的最后一件事是添加一些代码,这些代码将显示/隐藏工具提示作为对用户交互的反应。这就是叠加触发器发挥作用的地方。
显示和隐藏浮动元素的任务看起来并不那么复杂。即便如此,我们仍然需要创建一个抽象,以确保能够轻松地使其适应所有组件。 CDK 没有这样的功能,但是,我们可以使用我们自己的TriggerStrategy
概念轻松扩展它。 TriggerStrategy
的想法是能够订阅显示和隐藏事件,但是根据策略的实现,事件将被不同地触发 – hover
、 click
、 focus
等。界面非常简单:
// nebular_meets_angular_cdk_trigger_strategy.ts
export interface TriggerStrategy {
readonly show: Observable<Event>;
readonly hide: Observable<Event>;
}
对于我们的用例,我们开发了以下实现: ClickTriggerStrategy
、 HintTriggerStrategy
和FocusTriggerStrategy
。
我们来看看HintTriggerStrategy
实现:
// nebular_meets_angular_cdk_hint_trigger_strategy.ts
export class NbHintTriggerStrategy extends NbTriggerStrategy {
show: Observable<Event> = observableFromEvent<Event>(this.host, 'mouseenter');
hide: Observable<Event> = observableFromEvent(this.host, 'mouseleave');
}
如果用户开始将鼠标移到宿主元素上,则将触发 Show 事件,而当将鼠标移出宿主元素时,将触发 hide 事件。就这么简单。其余的触发策略以类似的方式实现。浮动组件只需注册所需的触发策略并显示/隐藏作为对触发事件的反应。
辅助功能是任何现代网络应用程序的重要组成部分。星云也不例外。在开发对话框组件期间,我们面临以下可访问性挑战:当用户通过单击某个按钮打开对话框时,浏览器焦点仍然在该按钮上。这意味着如果您多次按空格键,您将创建更多对话框。这绝对是不好的。当您的操作系统显示确认警报时,您只需按 Enter 键即可确认。您无需移动鼠标或使用 Tab 来聚焦“确定”按钮。相同的行为必须应用于对话框。焦点必须移动到创建的对话框中的第一个可聚焦元素。
这里 Angular CDK 辅助功能模块派上了用场。它提供了FocusTrap
帮助器,它提供了将焦点捕获在元素内的能力。这正是我们所需要的对话框组件。它的用法非常简单:
// nebular_meets_angular_cdk_dialog_component.ts
export class DialogComponent implements OnInit {
protected focusTrap: FocusTrap;
constructor(protected focusTrapFactory: FocusTrapFactory,
protected elementRef: ElementRef) {
}
ngOnInit() {
// create new focus trap
this.focusTrap = this.focusTrapFactory.create(this.elementRef.nativeElement);
// focus first focusable element inside dialog component
this.focusTrap.focusInitialElement();
}
}
就是这样!渲染DialogComponent
后,其中的第一个可聚焦元素将获得焦点。但这里我们有另一个问题——我们必须在关闭对话框后恢复焦点元素。此功能不是 Angular CDK 的一部分,这就是我们扩展它并自己实现焦点恢复功能的原因:
// nebular_meets_angular_cdk_focus_trap.ts
export class NbFocusTrap extends FocusTrap {
protected previouslyFocusedElement: HTMLElement;
constructor() {
// …
this.saveFocusedElement();
}
restoreFocus() {
this.previouslyFocusedElement.focus();
}
protected saveFocusedElement() {
this.previouslyFocusedElement = this.document.activeElement;
}
}
通过阅读代码了解如何使用焦点陷阱
它的用法变成如下:
// nebular_meets_angular_cdk_final_dialog.ts h
export class DialogComponent implements OnInit, OnDestroy {
protected focusTrap: NbFocusTrap;
constructor(protected focusTrapFactory: FocusTrapFactory,
protected elementRef: ElementRef) {
}
ngOnInit() {
// create new focus trap
this.focusTrap = this.focusTrapFactory.create(this.elementRef.nativeElement);
// focus first focusable element inside dialog component
this.focusTrap.focusInitialElement();
}
ngOnDestroy() {
this.focusTrap.restoreFocus();
}
}
正如您所看到的,现在将焦点捕获在组件内部并在组件被销毁时释放它变得非常容易。对于可能出现在页面上然后在用户交互后消失的交互式组件,此行为是必需的。
Nebular 的功能之一是能够作为嵌入式应用程序在另一个应用程序中呈现。例如,作为其他 Angular 甚至非 Angular 应用程序的一部分。这就是为什么滚动处理和应用程序测量必须在应用程序容器级别而不是窗口级别完成。不幸的是, ScrollDispatcher
和ViewportRuler
等 Angular CDK 服务在窗口级别测量所有内容。幸运的是,提供自定义实现没有问题,这对我们的案例非常有帮助。
例如, ScrollDispatcher
适配器如下所示:
// nebular_meets_angular_cdk_scroll_dispatcher_adapter.ts
@Injectable()
export class NbScrollDispatcherAdapter extends ScrollDispatcher {
scrolled(auditTimeInMs?: number): Observable<CdkScrollable | void> {
return this.nebularScrollService.onScroll();
}
}
然后提供如下:
// nebular_meets_angular_cdk_adapter_module.ts
@NgModule({
providers: [
{ provide: ScrollDispatcher, useClass: NbScrollDispatcherAdapter },
],
})
export class NbCdkAdapterModule {}
检查所有打包服务的完整列表
正如你所看到的,由于依赖注入,Angular CDK 具有相当好的可扩展性。
回顾一下,我不得不说 Angular CDK 的集成是一项非常好的投资。它为我们节省了大量的时间,使我们能够停止重新发明轮子,并完全专注于组件和 Nebular 特定功能的开发。此外,Angular CDK 很容易扩展,这意味着我们可以根据我们的需求进行调整。
有用的链接
文章来源地址:https://blog.angular.dev/nebular-meets-angular-cdk-b83fc921d6b2