ngx-bootstrap Modal 动画选项(Animation option)完全指南:从 use-case 规格到源码实现
ngx-bootstrap Modal 动画选项(Animation option)完全指南:从 use-case 规格到源码实现
本篇技术指南围绕 ngx-bootstrap 中 Modal 服务的动画选项(animated)展开,完整解析其官方 use-case 规格中描述的交互流程(启用/禁用动画、带动画打开弹窗、关闭弹窗),并深入 BsModalService、ModalOptions 与 ModalContainerComponent 的源码实现,帮助你理解动画开关在打开、关闭、backdrop 过渡三个阶段的底层时序,掌握在真实业务中配置 animated: true/false 的完整实战方案。
功能定位:Modal 的动画开关是什么
在 ngx-bootstrap 的 Modal 服务中,animated 是一个布尔型配置项,用于控制弹窗及其背景遮罩(backdrop)在打开与关闭时是否播放淡入淡出(fade)过渡动画。它对应官方规格文档 modals.service-examples.animation-option.use-case.md 所定义的 "Animation option" 演示场景,作用于 Ngx-bootstrap DEMO / Bootstrap 3 & 4 两种版本。
从源码看,该选项在 ModalOptions 中声明为 animated?: boolean,并在默认配置 modalConfigDefaults 中取值为 animated: true(见 modal-options.class.ts),即默认开启动画。这意味着:除非你显式传入 animated: false,否则弹窗打开/关闭都会经历标准的 CSS 过渡动画。
官方 use-case 规格:完整交互流程
规格文档描述了用户在 DEMO 页上的主成功场景(Main success scenario),共 7 个步骤:
- 用户打开 "Modals" 演示页面;
- 用户点击 "Animation option" 子菜单;
- 用户点击 "Enable animation" 按钮后,按钮标题应变为 "Disable animation";
- 随后点击 "Open modal with custom css class" 按钮,弹窗带动画效果打开;
- 用户点击弹窗右上角的叉号关闭按钮,弹窗关闭;
- 用户点击 "Disable animation" 按钮后,按钮标题变为 "Enable animation";
- 随后点击 "Open modal" 按钮,弹窗不带动画效果打开。
规格还记录了唯一变体(Variations):2*. 用户滚动(scrolls)到 "Animation option",即用户可以直接滚动定位到该小节,而不仅限于点击子菜单。
这段规格的验证价值在于:它明确了动画开关是一个可动态切换的状态——DEMO 中通过同一个按钮在 "Enable/Disable animation" 之间来回切换,并且每一次打开弹窗时都会读取当前的开关状态来决定是否播放动画。
动手实践:在 DEMO 中复现动画开关
对应 Demo 的真实源码
"Animation option" 小节在文档站点的组件清单 modal-section.list.ts 中被注册为:
title: 'Animation option'anchor: 'service-disable-animation'outlet: DemoModalServiceDisableAnimationComponent
其组件实现位于 disable-animation.ts,核心逻辑非常精简:
import { Component, TemplateRef } from '@angular/core';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
@Component({
selector: 'demo-modal-service-disable-animation',
templateUrl: './disable-animation.html',
standalone: false
})
export class DemoModalServiceDisableAnimationComponent {
modalRef?: BsModalRef;
config = {
animated: true
};
constructor(private modalService: BsModalService) {}
openModal(template: TemplateRef<void>) {
this.modalRef = this.modalService.show(template, this.config);
}
}
对应的模板 disable-animation.html 实现了规格中的按钮交互:
<button type="button" class="btn btn-primary" (click)="openModal(template)">Open modal</button>
<br>
<br>
<button type="button" class="btn btn-primary btn-sm" (click)="config.animated = !config.animated">
{{config.animated ? 'Disable' : 'Enable'}} animation
</button>
<ng-template #template>
<div class="modal-header">
<h4 class="modal-title pull-left">Modal</h4>
<button type="button" class="close btn-close pull-right" aria-label="Close" (click)="modalRef?.hide()">
<span aria-hidden="true" class="visually-hidden">×</span>
</button>
</div>
<div class="modal-body">
Just a modal with a bunch of words inside, nothing serious.
</div>
</ng-template>
对应关系一目了然:
| 规格中的动作 | 模板中的实现 |
|---|---|
| 点击 "Enable animation"/"Disable animation" 切换按钮标题 | (click)="config.animated = !config.animated",标题由 {{config.animated ? 'Disable' : 'Enable'}} animation 插值决定 |
| 点击 "Open modal" 打开弹窗 | (click)="openModal(template)",调用 modalService.show(template, this.config) |
| 点击叉号关闭弹窗 | (click)="modalRef?.hide()",调用 BsModalRef.hide() |
注意 config = { animated: true } 作为 BsModalService.show() 的第二个参数传入,每次打开都会把当前 config.animated 的值带给服务——这就是"先切换开关、再打开弹窗"能影响动画效果的机制。
关于 "Open modal with custom css class"
规格步骤 4 中提到的 "Open modal with custom css class" 按钮对应的是文档站点的另一个独立小节 "Custom css class"(见 modal-section.list.ts),其实现位于 custom-css-class.ts:
openModalWithClass(template: TemplateRef<void>) {
this.modalRef = this.modalService.show(
template,
Object.assign({}, { class: 'gray modal-lg' })
);
}
它通过 class 配置项为弹窗对话框追加自定义 CSS 类,与 animated 属于正交的两个配置维度:动画开关控制过渡效果,class 控制弹窗样式。规格文档将两者放在同一流程中,是为了演示"开启动画 + 自定义类名的弹窗"这一组合形态;实际使用时二者可独立配置。
animated 配置项详解:参数、默认值与相关配置
在 modal-options.class.ts 中,ModalOptions 是 BsModalService.show() 的配置载体。与动画直接相关的参数如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
animated |
boolean |
true |
切换动画:true 时弹窗与 backdrop 播放 fade 过渡;false 时立即显示/隐藏 |
class |
string |
'' |
追加到 .modal-dialog 上的自定义 CSS 类(如 'gray modal-lg') |
围绕弹窗行为的其他默认配置(modalConfigDefaults,见 modal-options.class.ts)也值得了解,它们共同决定弹窗体验:
| 参数 | 默认值 | 说明 |
|---|---|---|
backdrop |
true |
是否渲染遮罩层,可传 'static' 表示点击遮罩不关闭 |
keyboard |
true |
按 Esc 是否关闭弹窗 |
focus |
true |
打开后是否聚焦到弹窗元素 |
show |
false |
初始化时是否立即显示 |
ignoreBackdropClick |
false |
是否忽略遮罩点击 |
initialState |
{} |
传入模态内容组件的初始数据 |
closeInterceptor |
void 0 |
关闭拦截回调(返回 Promise) |
配置合并规则:在 bs-modal.service.ts 的 show() 方法中,配置按 modalConfigDefaults → MODAL_CONFIG_DEFAULT_OVERRIDE(可选)→ 调用方传入的 config 的顺序合并,因此你传入的 { animated: false } 会覆盖默认的 animated: true。若想全局关闭动画,可通过 MODAL_CONFIG_DEFAULT_OVERRIDE 注入令牌覆盖默认配置。
源码级原理:animated 如何驱动三个阶段的过渡
动画开关的影响并非只作用在弹窗主体上,而是贯穿"打开 → backdrop 出现 → 关闭"的完整生命周期。以下按源码调用链逐段拆解。
1. 打开阶段:服务层创建 backdrop 与容器
BsModalService.show()(bs-modal.service.ts)内部依次调用 _showBackdrop() 和 _showModal():
_showBackdrop()(bs-modal.service.ts)在第一个弹窗出现时,通过_backdropLoader.attach(ModalBackdropComponent).show({ isAnimated: this.config.animated })把animated值传给 backdrop 组件;_showModal()(bs-modal.service.ts)为容器提供ModalOptions注入后,调用modalContainerRef.show({ content, isAnimated: this.config.animated, ... }),把动画开关注入ModalContainerComponent。
2. backdrop 的动画时序
ModalBackdropComponent 暴露 isAnimated 属性并在 ngOnInit() 中处理:
ngOnInit(): void {
if (this.isAnimated) {
this.renderer.addClass(this.element.nativeElement, `${CLASS_NAME.FADE}`);
Utils.reflow(this.element.nativeElement);
}
this.isShown = true;
}
animated: true:先给遮罩元素加上fade类,并调用Utils.reflow()强制浏览器回流,使 CSS 过渡能正确触发;随后isShown = true再加上show类,遮罩即从透明淡入;animated: false:跳过fade类,遮罩直接显示,无过渡。
3. 弹窗容器的动画时序
ModalContainerComponent 的 ngOnInit() 是动画差异最集中的地方:
ngOnInit(): void {
this._focusEl = document.activeElement;
if (this.isAnimated) {
this._renderer.addClass(this._element.nativeElement, CLASS_NAME.FADE);
}
this._renderer.setStyle(this._element.nativeElement, 'display', 'block');
setTimeout(() => {
this.isShown = true;
this._renderer.addClass(this._element.nativeElement, CLASS_NAME.SHOW);
}, this.isAnimated ? TRANSITION_DURATIONS.BACKDROP : 0);
...
}
- 动画开启时:弹窗根元素先加
fade类,显示为block,然后**延迟TRANSITION_DURATIONS.BACKDROP(150ms)**再加show类,让淡入效果与 backdrop 的 150ms 淡入同步; - 动画关闭时:
setTimeout延迟为0,show类立即加上,弹窗瞬时出现。
容器模板(modal-container.component.ts)同时把 config.class 拼接到对话框类名上:
<div [class]="'modal-dialog' + (config.class ? ' ' + config.class : '')" role="document" focusTrap>
4. 关闭阶段的延迟移除
BsModalService.hide()(bs-modal.service.ts)在关闭时同样以 animated 决定延迟:
setTimeout(() => {
this._hideModal(id);
this.removeLoaders(id);
}, this.config.animated ? TRANSITION_DURATIONS.BACKDROP : 0);
即:动画开启时,等待 150ms(backdrop 淡出时长)后再真正移除弹窗 DOM;关闭动画期间,_hideBackdrop()(bs-modal.service.ts)先移除遮罩的 show 类触发淡出,再在 TRANSITION_DURATIONS.BACKDROP 后从 DOM 移除遮罩。
而容器自身的 _hide()(modal-container.component.ts)在移除 show 类后,同样按 isAnimated ? TRANSITION_DURATIONS.MODAL : 0 决定延迟,其中 TRANSITION_DURATIONS.MODAL = 300(见 modal-options.class.ts),保证弹窗本体 300ms 淡出完成后再触发 hide() 清理。两个时长常量定义如下:
| 常量 | 值 | 作用对象 |
|---|---|---|
TRANSITION_DURATIONS.MODAL |
300 |
弹窗主体 .modal 的过渡时长 |
TRANSITION_DURATIONS.BACKDROP |
150 |
遮罩 .modal-backdrop 的过渡时长 |
5. 指令方式的动画处理(ModalDirective)
除了服务方式,<a href="https://link.gitcode.com/i/edd4471bf76303d8a89b081e2d953b82" target="_blank">bsModal] 指令([modal.directive.ts)也支持 animated 配置,默认值同样来自 modalConfigDefaults(见 modal.directive.ts)。其处理逻辑与服务方式完全一致:
_hide()(modal.directive.ts):animated: true时用setTimeout(..., TRANSITION_DURATION)(300ms)延迟执行hideModal(),否则立即执行;showElement()(modal.directive.ts):动画开启时先Utils.reflow()再添加show类,并通过setTimeout(transitionComplete, TRANSITION_DURATION)等待过渡完成后发出onShown事件;关闭动画时transitionComplete()同步执行;showBackdrop()(modal.directive.ts):把isAnimated: this._config.animated传给 backdrop 组件,动画开启时在BACKDROP_TRANSITION_DURATION(150ms)后执行回调。
无论走 BsModalService 还是 ModalDirective,动画的"时长节奏"都由这两个常量统一控制,行为保持一致。
测试验证:isAnimated 的读写行为
动画开关的可测试性在 modal-backdrop.component.spec.ts 中有直接体现:
it('should get and set isAnimated correctly', () => {
component.isAnimated = true;
expect(component.isAnimated).toBeTruthy();
});
该测试验证了 ModalBackdropComponent.isAnimated 属性的 getter/setter 行为——服务层正是通过 show({ isAnimated: this.config.animated }) 这条路径把配置写进组件实例的。它同时佐证了 animated 配置最终会以组件属性的形式贯穿到 backdrop 渲染逻辑(modal-backdrop.component.ts)。
实践要点与注意事项
- 默认开启动画:
animated默认值为true。若你的业务要求弹窗"瞬开瞬关"(例如高频提示、批量弹窗),务必在每次show()时显式传入{ animated: false }。 - 开关是动态读取的:DEMO 之所以能"先切换再打开",是因为
config.animated在每次show()调用时都会被重新合并进配置。不要试图在弹窗已经打开后修改开关——它只影响下一次打开/关闭的过渡行为。 - 与
class配置正交:动画开关不改变弹窗的样式类拼接逻辑,class(如'gray modal-lg')在任何动画状态下都会追加到.modal-dialog上,二者可安全组合。 - 关闭方式的等价性:无论通过点击叉号(
modalRef?.hide())、点击 backdrop、按 Esc 还是BsModalService.hide()关闭,animated都会决定关闭过渡是否播放,时序统一由TRANSITION_DURATIONS(MODAL 300ms / BACKDROP 150ms)控制。 - Bootstrap 3 与 4 均适用:规格明确该场景作用于 Bootstrap 3 与 4 两种版本,
fade/show类是两者的通用机制;使用 Bootstrap 4 及以上时还需自行引入配套的模态样式表。 - 目录速查:动画配置定义在 src/modal/modal-options.class.ts,服务层实现在 src/modal/bs-modal.service.ts,容器动画时序在 src/modal/modal-container.component.ts,DEMO 源码在 libs/doc-pages/modal/src/lib/demos/service-options/disable-animation,规格文档见 docs/spec/modals/modals.service-examples.animation-option.use-case.md。