Angular Material 双向文本方向(Bidirectionality)指南:用 CDK Bidi 让你的组件自动适配 LTR/RTL
2026/9/12 21:56:18 网站建设 项目流程

Angular Material 双向文本方向(Bidirectionality)指南:用 CDK Bidi 让你的组件自动适配 LTR/RTL

【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components

本文基于 Angular Material 官方指南 guides/bidirectionality.md,系统讲解如何在 Angular 应用中设置文本方向(text-direction),并利用@angular/cdk/bidi提供的Directionality服务与Dir指令,在自己的组件和 Angular Material 组件中感知、响应 LTR/RTL 切换。读完本文,你将掌握dir属性的正确用法、Directionality的注入与订阅方式、auto值的解析规则,以及背后的源码实现与测试验证逻辑。

为你的应用设置文本方向

Web 平台原生提供了dir全局属性 用于声明元素的文本方向。在 Angular 应用中,最常见的做法是把dir设置到页面的<html><body>元素上,从而为整个应用声明方向:

<!-- 全局 RTL(如阿拉伯语、希伯来语页面) --> <html dir="rtl">

也可以把dir用在页面内的任意元素上,只为某个更小的子树指定方向:

<div dir="rtl"> <!-- 这部分内容按从右到左布局 --> </div>

一个关键承诺是:所有 Angular Material 组件都会自动反映其所在容器的 LTR/RTL 方向。也就是说,你不需要为每个组件单独配置,只要在容器上设置好dir,内部的 Material 组件(按钮、对话框、菜单、表单字段等)的布局、对齐与图标方向就会随之适配。

在自己的组件中读取文本方向

如果你的组件也需要感知当前方向(例如调整图标翻转、切换左右对齐),@angular/cdk/bidi提供了可注入的Directionality服务,任何组件都可以直接消费它。使用时需要先从@angular/cdk/bidi导入BidiModule

import {BidiModule} from '@angular/cdk/bidi';

Directionality对外暴露两个核心属性:

属性类型说明
value'ltr' \| 'rtl'当前的文本方向
changeObservable<Direction>文本方向变化时发射事件的可观察流

需要特别注意的是:change只捕获 Angular 应用上下文内部dir属性的变化,即通过Dir指令(见下文)管理的那部分 DOM。它不会因<html><body>上的dir改变而发射事件——因为全局方向被假定为静态的。

官方示例:订阅方向变化

@Component({ /* ... */ }) export class MyCustomComponent { private dir: Direction; constructor(directionality: Directionality) { this.dir = directionality.value; directionality.change.subscribe(() => { this.dir = directionality.value; }); } }

CDK 自带的组件示例中则展示了更完整的写法,包括取消订阅和销毁钩子(见 src/cdk/bidi/bidi.md):

@Component({ ... }) export class MyWidget implements OnDestroy { /** Whether the widget is in RTL mode or not. */ private isRtl: boolean; /** Subscription to the Directionality change EventEmitter. */ private _dirChangeSubscription = Subscription.EMPTY; constructor(dir: Directionality) { this.isRtl = dir.value === 'rtl'; this._dirChangeSubscription = dir.change.subscribe(() => { this.flipDirection(); }); } ngOnDestroy() { this._dirChangeSubscription.unsubscribe(); } }

注意:Directionality.change底层是一个EventEmitter(见 directionality.ts),按惯例在使用后应取消订阅;服务在ngOnDestroy时会调用change.complete()完成该流,测试 directionality.spec.ts 对此有专门覆盖。

源码深挖:Directionality 是如何拿到方向的

理解底层实现有助于你在边界场景下做出正确判断。Directionality的实现位于 src/cdk/bidi/directionality.ts,核心逻辑如下:

  1. 读取文档方向:构造函数通过注入DIR_DOCUMENTtoken 获取文档对象,依次检查document.body.dirdocument.documentElement.dir,取第一个非空值;若都为空则默认'ltr'(directionality.ts)。
  2. 优先级body上的方向优先于html上的方向。这一点在测试中得到验证:即使html设为ltrbody设为rtlvalue也返回'rtl'(directionality.spec.ts)。
  3. 方向解析_resolveDirectionality(rawValue)会把输入值转小写后归一化,任何非法值都回退为'ltr'(directionality.ts)。测试同样覆盖了body.dir = 'not-valid'时默认返回'ltr'的场景(directionality.spec.ts)。

DIR_DOCUMENT是一个独立的 InjectionToken,定义在 dir-document-token.ts。之所以要单独抽象它,是因为测试环境中不能使用真实document——修改真实 DOM 的dir会导致 Safari 中基于几何测量的测试失败;同时单元测试代码自身也要用querySelector,无法整体替换DOCUMENT。注入这个 token 后,测试只需提供一个假的{body: {}, documentElement: {}}对象即可(见 directionality.spec.ts)。

Dir 指令:让任意子树拥有自己的方向上下文

BidiModule还导出与选择器[dir]匹配的Dir指令,其实现位于 src/cdk/bidi/dir.ts。它的设计非常巧妙:

@Directive({ selector: '[dir]', providers: [{provide: Directionality, useExisting: Dir}], host: {'[attr.dir]': '_rawDir'}, exportAs: 'dir', }) export class Dir implements Directionality, AfterContentInit, OnDestroy { // ... @Output('dirChange') readonly change = new EventEmitter<Direction>(); // ... }

几个值得注意的实现细节:

  • 自我提供为 DirectionalityDir通过useExisting把自己注册为Directionality。因此任何注入Directionality的后代组件,拿到的一定是最近的祖先方向上下文,而不是全局的<html>/<body>方向。测试验证了这一点:在带dir的元素内部注入Directionality,得到的value就是该元素的dir值(directionality.spec.ts)。
  • API 与 Directionality 一致Dir同样暴露valuechange,并额外提供dirChange输出事件(@Output('dirChange')),模板中可用(dirChange)监听局部方向变化。
  • 保留原始属性值_rawDir保存用户传入的原始值并作为宿主属性回写到 DOM,因此dir="auto"之类的值会被保留在元素上,而内部归一化后的value则是'ltr'/'rtl'(dir.ts)。测试断言了dir="auto"元素同时满足"DOM 上是autovalue'ltr'"(directionality.spec.ts)。
  • 大小写不敏感_resolveDirectionality会先做toLowerCase(),所以[dir]="'RTL'"也能被正确解析为'rtl'(directionality.spec.ts)。
  • 首次赋值不触发事件DirngAfterContentInit时才把_isInitialized置为true,避免初始值设置阶段就误发change事件(dir.ts)。测试中把dirrtl切到ltr恰好只触发一次dirChange(directionality.spec.ts)。

BidiModule本身非常简单,只是导入并再导出Dir(bidi-module.ts)。使用时可按需在组件的imports中引入BidiModule或直接引入Dir指令。

解读 auto 值:CDK 与浏览器行为的差异

原生dir支持auto值——由浏览器根据元素文本内容自动判定方向。CDK 也支持auto,但出于性能考量采用了不同的解析方式(见 bidi.md 的 "Interpreting the auto value" 一节):

  • CDK 的做法:读取浏览器语言navigator.language,与一组已知的 RTL 语言环境正则匹配,命中则视为rtl,否则为ltr
  • 浏览器的做法:基于元素的实际文本内容判定,成本较高。

这个差异在 directionality.ts 中体现得很清楚:RTL_LOCALE_PATTERN是一个借鉴自goog.i18n.bidi.isRtlLanguage的正则,覆盖阿拉伯语系(ar)、希伯来语(he/iw)、波斯语(fa)、乌尔都语(ur)等,以及AdlmArabHebrNkooRohgThaa等书写系统;同时排除了带Latn/Cyrl的复合语言标签:

const RTL_LOCALE_PATTERN = /^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*-_)(?!.*-_($|-|_))($|-|_)/i;

之所以必须解析auto,是因为 CDK 中像 overlay 浮动层、键盘导航这类功能需要明确知道元素处于 RTL 还是 LTR 布局才能正确工作,而不能依赖浏览器的内容推断。如果你的业务代码同样需要确定性行为,就应理解:dir="auto"在 CDK 语境下解析为"按浏览器语言判定",而不是"按内容判定"。

在 Material 组件中的实际应用

Angular Material 组件确实在广泛消费Directionality。例如:

  • src/material/form-field/form-field.ts 中const dir = inject(Directionality);用于感知方向以调整前缀/后缀、错误提示等元素的排布;
  • src/material/tooltip/tooltip.ts 中protected _dir = inject(Directionality);用于决定 tooltip 的弹出方向与对齐。

这印证了指南中的声明:组件通过注入Directionality,即可自动获得最近的祖先方向上下文。无论全局是 LTR 还是局部某个容器是 RTL,Material 组件都会正确响应。你同样可以在自己的组件里采用这一模式,并在模板中使用Dir指令的dirChange事件或直接订阅Directionality.change来响应方向切换。

最佳实践小结

  1. 全局方向:在<html><body>上设置dir="rtl",Material 组件会自动适配。
  2. 局部方向:在任意元素上使用dir(模板中推荐[dir]="'rtl'"[dir]="direction()"),后代组件会拿到最近的局部方向。
  3. 自定义组件:从@angular/cdk/bidi导入BidiModule,注入Directionality,读取value、订阅change,并在ngOnDestroy中取消订阅。
  4. 理解事件边界change只响应 Angular 上下文内部的dir变化,不监听<html>/<body>的静态方向改动。
  5. 谨慎使用auto:CDK 以浏览器语言而非元素内容解析auto,需要确定性方向时应显式写ltr/rtl
  6. 了解回退规则body方向优先于html;非法值一律回退为ltrDirvalue与 DOM 上的原始dir属性值可不同(如auto)。

如需查看完整测试用例以加深理解,可阅读 src/cdk/bidi/directionality.spec.ts;模块与指令的公开 API 汇总见 src/cdk/bidi/public-api.ts。

【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询