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' | 当前的文本方向 |
change | Observable<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,核心逻辑如下:
- 读取文档方向:构造函数通过注入
DIR_DOCUMENTtoken 获取文档对象,依次检查document.body.dir与document.documentElement.dir,取第一个非空值;若都为空则默认'ltr'(directionality.ts)。 - 优先级:
body上的方向优先于html上的方向。这一点在测试中得到验证:即使html设为ltr、body设为rtl,value也返回'rtl'(directionality.spec.ts)。 - 方向解析:
_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>(); // ... }几个值得注意的实现细节:
- 自我提供为 Directionality:
Dir通过useExisting把自己注册为Directionality。因此任何注入Directionality的后代组件,拿到的一定是最近的祖先方向上下文,而不是全局的<html>/<body>方向。测试验证了这一点:在带dir的元素内部注入Directionality,得到的value就是该元素的dir值(directionality.spec.ts)。 - API 与 Directionality 一致:
Dir同样暴露value与change,并额外提供dirChange输出事件(@Output('dirChange')),模板中可用(dirChange)监听局部方向变化。 - 保留原始属性值:
_rawDir保存用户传入的原始值并作为宿主属性回写到 DOM,因此dir="auto"之类的值会被保留在元素上,而内部归一化后的value则是'ltr'/'rtl'(dir.ts)。测试断言了dir="auto"元素同时满足"DOM 上是auto、value是'ltr'"(directionality.spec.ts)。 - 大小写不敏感:
_resolveDirectionality会先做toLowerCase(),所以[dir]="'RTL'"也能被正确解析为'rtl'(directionality.spec.ts)。 - 首次赋值不触发事件:
Dir在ngAfterContentInit时才把_isInitialized置为true,避免初始值设置阶段就误发change事件(dir.ts)。测试中把dir从rtl切到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)等,以及Adlm、Arab、Hebr、Nkoo、Rohg、Thaa等书写系统;同时排除了带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来响应方向切换。
最佳实践小结
- 全局方向:在
<html>或<body>上设置dir="rtl",Material 组件会自动适配。 - 局部方向:在任意元素上使用
dir(模板中推荐[dir]="'rtl'"或[dir]="direction()"),后代组件会拿到最近的局部方向。 - 自定义组件:从
@angular/cdk/bidi导入BidiModule,注入Directionality,读取value、订阅change,并在ngOnDestroy中取消订阅。 - 理解事件边界:
change只响应 Angular 上下文内部的dir变化,不监听<html>/<body>的静态方向改动。 - 谨慎使用
auto:CDK 以浏览器语言而非元素内容解析auto,需要确定性方向时应显式写ltr/rtl。 - 了解回退规则:
body方向优先于html;非法值一律回退为ltr;Dir的value与 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),仅供参考