ngx-bootstrap Tooltip 动态内容(Dynamic Content)实现指南:从 Use-Case 规格到源码级绑定原理

原创2026-10-12 18:12:041,354 阅读
文章标签:UI组件前端

ngx-bootstrap Tooltip 动态内容(Dynamic Content)实现指南:从 Use-Case 规格到源码级绑定原理

ngx-bootstrap 是 Angular 生态中基于 Bootstrap 的组件库(支持 Ivy 引擎),其 Tooltip 指令提供了丰富的内容注入方式。本文围绕仓库中 Tooltip 动态内容示例的 Use-Case 规格,完整讲解如何在 ngx-bootstrap 中实现"内容由组件数据驱动"的动态 Tooltip:从 demo 页面的 "Simple binding" 按钮入手,逐步拆解字符串绑定、TemplateRef 模板绑定与动态 HTML 三种方案,并深入 TooltipDirective 与 ComponentLoader 的底层实现,帮助读者掌握动态内容 Tooltip 的完整调用链与实战写法。

一、Use-Case 规格解读:动态内容 Tooltip 的行为契约

仓库 docs/spec/tooltip/tooltip.examples.dynamic-content.use-case.md 以用例(Use-Case)形式定义了 "Tooltip Dynamic content example" 的验收标准,这是理解该功能行为的最权威依据:

  • Primary Actor(主要参与者):User(用户)
  • Scope(适用范围):Ngx-bootstrap DEMO,兼容 Bootstrap 版本 3 与 4
  • Goal(目标):向用户展示如何制作带动态内容的 Tooltip

其 Main success scenario(主成功场景) 逐条规定了交互链路:

  1. 用户打开 Tooltip 演示页面;
  2. 用户点击 "Dynamic content" 子菜单;
  3. 用户看到名为 "Simple binding" 的按钮;
  4. 当用户将鼠标悬停在该按钮上时,Tooltip 容器显示在按钮顶部,内容为来自组件的特定文本;
  5. 当用户将鼠标光标移离按钮时,Tooltip 容器消失;
  6. 组件源码应当使用**内容参数(content params)**编写。

规格同时给出了 Variations(变体):用户也可以直接滚动到 "Dynamic content" 子菜单,而不是点击进入。

这份规格直接对应 demo 源码:组件通过 content 属性持有内容参数,按钮通过 [tooltip]="content" 进行动态绑定——这正是"动态内容"与基础示例(直接在模板里写死字符串)的核心区别。

二、Demo 实现:组件属性驱动的 "Simple binding" 按钮

2.1 模板层:属性绑定替代字面量

在 libs/doc-pages/tooltip/src/lib/demos/dynamic/dynamic.html 中,按钮模板如下:

<button type="button" class="btn btn-info" [tooltip]="content">
  Simple binding
</button>

这里使用方括号属性绑定语法 <a href="https://link.gitcode.com/i/73a74eae0570369bd248275f255cbafd" target="_blank">tooltip]="content" 将组件的 content 属性传给 TooltipDirective 的 tooltip 输入项,而不是像 [基础示例 basic.html 那样直接书写 tooltip="Vivamus sagittis..." 字面量。

2.2 组件层:内容参数承载数据

在 libs/doc-pages/tooltip/src/lib/demos/dynamic/dynamic.ts 中,组件定义简洁明了:

import { Component } from '@angular/core';

@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: 'demo-tooltip-dynamic',
  templateUrl: './dynamic.html',
  standalone: false
})
export class DemoTooltipDynamicComponent {
  content = 'Vivamus sagittis lacus vel augue laoreet rutrum faucibus.';
}

这正是规格第 6 条要求的"组件源码使用内容参数编写":content 属性即"内容参数",它与 Basic demo(空组件、内容写在模板字面量中)形成鲜明对比——动态方案的内容源从模板层移到了组件数据层,意味着它可以随时被计算、被接口返回、被用户输入所替换,无需修改模板。

2.3 路由与菜单挂载

该 demo 组件通过 tooltip-section.list.ts 注册到文档页:

{
  title: 'Dynamic Content',
  anchor: 'dynamic-content',
  component: require('!!raw-loader!./demos/dynamic/dynamic.ts'),
  html: require('!!raw-loader!./demos/dynamic/dynamic.html'),
  description: `<p>Pass a string as tooltip content</p>`,
  outlet: DemoTooltipDynamicComponent
},

它同时出现在 Overview(含源码展示)与 Examples 两个 Tab 中,这就是 Use-Case 中 "Dynamic content sub-menu" 的来源。文档页模块 tooltip.module.ts 将 DemoTooltipDynamicComponent 声明并导入 TooltipModule,保证 demo 可以实际运行。

三、源码级原理:动态内容在 Tooltip 指令中的流转

3.1 tooltip 输入:支持字符串与模板引用的 model

TooltipDirective 是动态内容的核心载体,其选择器为 [tooltip], [tooltipHtml],导出名 bs-tooltip,并声明为 standalone: true:

@Directive({
  selector: '[tooltip], [tooltipHtml]',
  exportAs: 'bs-tooltip',
  standalone: true,
  providers: [ComponentLoaderFactory, PositioningService]
})
export class TooltipDirective implements OnInit, OnDestroy {
  /**
   * Content to be displayed as tooltip.
   */
  readonly tooltip = model<string | TemplateRef<unknown> | undefined>();

注意 tooltip 是 Angular 的 model() 信号,类型为 string | TemplateRef<unknown> | undefined。这意味着动态内容不仅可以是字符串,还可以是 ng-template 模板引用——这正是后续"自定义内容模板"与"动态 HTML"两个扩展示例的技术基础。指令同时提供 tooltipHtml 等已废弃输入(仅用于兼容旧写法,运行时通过 warnOnce 提示改用 tooltip)。

3.2 show():内容如何进入 Tooltip 容器

当用户触发显示(默认 hover/focus)时,show() 方法读取当前 tooltip() 值并交给 ComponentLoader 展示:

show(): void {
  this._positionService.setOptions({ ... });

  const tooltipValue = this.tooltip();
  const isDisabledValue = this.isDisabled() || ...;
  ...
  if (this.isOpen || isDisabledValue || this._delayTimeoutId || !tooltipValue) {
    return;
  }

  const showTooltip = () => {
    this._tooltip
      .attach(TooltipContainerComponent)
      .to(containerValue)
      .position({ attachment: placementValue })
      .show({
        content: tooltipValue,
        placement: placementValue,
        containerClass: this.containerClass(),
        id: `tooltip-${this.tooltipId}`
      });
  };
  ...
}

几个值得注意的源码细节:

  • 空内容直接拦截:!tooltipValue 时直接 return,即 [tooltip]="''" 或未绑定时不会弹出空 Tooltip;
  • 延迟显示:若配置了 delay,会通过 rxjs timer(delayValue) 延后 showTooltip(),并监听关闭类触发器取消未执行的延迟(见 tooltip.directive.ts);
  • 定位参数:position({ attachment: placementValue }) 决定 Tooltip 出现在按钮的哪个方位(默认 top,即规格中"shown on the top of button")。

3.3 ComponentLoader:字符串内容的渲染分支

内容真正被渲染是在 component-loader.class.ts 的 _getContentRef() 中。该方法按内容类型分派三种路径:

private _getContentRef(content, context?, initialState?): ContentRef {
  if (!content) return new ContentRef([]);

  if (content instanceof TemplateRef) {
    // 路径一:TemplateRef —— 创建内嵌视图(EmbeddedView),支持上下文绑定
    ...
  }

  if (typeof content === 'function') {
    // 路径二:组件类 —— 动态创建组件实例
    ...
  }

  const nodes = this._renderer
    ? [this._renderer.createText(`${content}`)]
    : [];
  return new ContentRef([nodes]);
}

对于本文主角——字符串动态内容——走的是最后一条分支:Renderer2.createText() 创建一个纯文本节点。也就是说,动态字符串内容不会被当作 HTML 解析,天然规避了 XSS 风险;如果确有渲染 HTML 的需求,必须显式走 TemplateRef + [innerHtml] 路径(见下文第五节)。

创建文本节点后,ComponentLoader.show() 会通过 createComponent() 实例化 TooltipContainerComponent,并把内容节点作为 projectableNodes 投影进容器模板的 <ng-content> 中:

this._componentRef = createComponent(this._compType, {
  environmentInjector: this._environmentInjector,
  elementInjector,
  projectableNodes: this._contentRef.nodes
});

容器组件模板定义于 tooltip-container.component.ts:

<div class="tooltip-arrow arrow"></div>
<div class="tooltip-inner"><ng-content></ng-content></div>

即:动态文本最终被放置在 .tooltip-inner 内,配以箭头元素;宿主类(host class)会组合 show tooltip in tooltip-top bs-tooltip-top top 等定位与样式类,并根据 Bootstrap 版本(getBsVer() 检测,兼容 BS3/4/5)做 PlacementForBs5 映射——这正好呼应 Use-Case Scope 中"BS version 3&4"的兼容性说明。

3.4 显示与消失:默认触发器 hover focus

规格第 4、5 条要求"悬停显示、移开消失"。这一行为由 TooltipConfig 的默认值保证:

@Injectable({ providedIn: 'root' })
export class TooltipConfig {
  /** sets disable adaptive position */
  adaptivePosition = true;
  /** tooltip placement, supported positions: 'top', 'bottom', 'left', 'right' */
  placement = 'top';
  /** array of event names which triggers tooltip opening */
  triggers = 'hover focus';
  /** a selector specifying the element the tooltip should be appended to. */
  container?: string;
  /** delay before showing the tooltip */
  delay = 0;
}

triggers = 'hover focus' 表示同时监听鼠标悬停与键盘聚焦;placement = 'top' 即规格中"shown on the top of button"。在 ngOnInit() 中,指令将触发器注册到 ComponentLoader:

this._tooltip.listen({
  triggers: this.triggers(),
  show: () => this.show()
});

ComponentLoader.listen() 内部通过 listenToTriggersV2 绑定事件:hover 事件触发 show,对应的 mouseleave 触发 hide,从而完整实现"悬停出现、移开消失"的交互闭环。hide() 中还会先移除 in 类(配合淡出动画),并在 tooltipFadeDuration(默认 150ms)后真正销毁组件。

3.5 无障碍支持

show() 被调用后,onShown 事件会触发 setAriaDescribedBy(),为宿主元素动态设置 aria-describedby="tooltip-{id}"(隐藏时移除),见 tooltip.directive.ts。动态内容 Tooltip 因此天然支持屏幕阅读器,符合 WCAG 语义。

四、测试佐证:动态内容场景的行为验证

仓库单元测试 src/tooltip/testing/tooltip.directive.spec.ts 验证了与动态内容强相关的行为契约:

  • 初始不显示:tooltip should not be displayed until user does not any actions——断言页面加载时 .tooltip-inner 为 null(对应规格第 3 条:先看到按钮,而非 Tooltip);
  • focus 触发显示:tooltip should be displayed by focus event after 0 ms by default——focus() 后立即出现 .tooltip-inner;
  • mouseenter 触发显示:tooltip should be displayed by mouseenter event——模拟 mouseenter 后出现内容;
  • 延迟显示:设置 delay = 1000 后 tick(1100) 才出现 Tooltip;
  • 编程式 show/hide:通过 tooltip.show() / tooltip.hide() 暴露的 bs-tooltip 引用手动控制。

测试模板中 tooltip="See? Now click away..." 与 [delay]="delay"、triggers="focus mouseenter" 等写法,展示了动态内容与触发器、延迟等配置组合使用的典型形态,可作为读者编写自己场景的参考模板。

五、动态内容的三种进阶形态

Use-Case 文档聚焦字符串动态内容,但 TooltipDirective 的 tooltip 输入类型决定了它同时支持模板引用。结合 demo 仓库,可在同一篇文章中厘清三种形态的适用边界:

5.1 字符串绑定(本文主体)

<button type="button" class="btn btn-info" [tooltip]="content">Simple binding</button>

内容为纯文本,走 createText 分支,安全、轻量,适合提示文案、接口返回的普通字符串。

5.2 TemplateRef 模板绑定(自定义内容模板)

见 custom-content demo:

<ng-template #tolTemplate>Just another: {{content}}</ng-template>
<button type="button" class="btn btn-warning" [tooltip]="tolTemplate">
  TemplateRef binding
</button>

模板引用走 _getContentRef 的 TemplateRef 分支,通过 createEmbeddedView() 创建内嵌视图,支持插值表达式 {{content}}、结构指令等完整 Angular 模板能力,且随组件数据自动更新。

5.3 动态 HTML(需显式 innerHtml)

见 dynamic-html demo:

<ng-template #popTemplate>Here we go: <div [innerHtml]="html"></div></ng-template>
<button type="button" class="btn btn-success" [tooltip]="popTemplate">
  Show me tooltip with html
</button>
html = `<span class="btn-block btn-danger well-sm">Never trust not sanitized HTML!!!</span>`;

动态 HTML 必须通过 ng-template + [innerHtml] 组合实现(Angular 的 innerHtml 绑定自带安全净化,并依赖 DomSanitizer 策略)。demo 中刻意使用"Never trust not sanitized HTML!!!"警示文案,提醒开发者不要直接拼接未净化的 HTML 字符串——因为纯字符串走 createText 分支不会解析 HTML,而 innerHtml 的净化规则仍需由使用者负责。三种方式的选型总结:

形态 绑定写法 底层分支 支持 HTML 典型场景
字符串 [tooltip]="content" createText 否(纯文本) 提示文案、接口字符串
模板引用 [tooltip]="myTemplateRef" createEmbeddedView 是(Angular 模板能力) 结构化富内容、插值
动态 HTML [tooltip]="tpl" + [innerHtml] 模板分支 + innerHtml 是(受净化策略约束) 服务端返回的 HTML 片段

六、快速接入与运行

6.1 安装与模块引入

动态内容 Tooltip 是 TooltipModule 的常规能力。模块方式引入(见 docs/usage.md):

import { TooltipModule } from 'ngx-bootstrap/tooltip';

@NgModule({
  imports: [TooltipModule, ...]
})
export class AppModule() {}

由于 TooltipDirective 本身是 standalone: true,也支持 Standalone 组件直接 imports: <a href="https://link.gitcode.com/i/5cd1065d6fffeebdef963fc25ac49f8f" target="_blank">TooltipModule]。TooltipModule 内部实现([tooltip.module.ts)仅导入并导出 CommonModule、TooltipDirective、TooltipContainerComponent,公共 API 由 public_api.ts 统一导出 TooltipContainerComponent、TooltipDirective、TooltipModule、TooltipConfig。

6.2 在组件中落地动态内容

将 Use-Case 规格落地到自有组件,只需三步:

  1. 在组件类中声明内容参数:content = '...';
  2. 模板中使用属性绑定:<button [tooltip]="content">Simple binding</button>;
  3. 根据内容形态选择字符串 / TemplateRef / ng-template + [innerHtml] 三种方案之一。

如需调整默认行为,可通过全局 TooltipConfig(providedIn: 'root')或指令输入覆盖 placement(top/bottom/left/right)、triggers(如 'hover focus')、delay、container、adaptivePosition 等配置项。

七、小结

从 动态内容 Use-Case 规格 出发,本文完整还原了 ngx-bootstrap 动态内容 Tooltip 的"契约 → Demo → 源码 → 测试"全链路:[tooltip]="content" 属性绑定把内容参数注入 TooltipDirective 的 tooltip 信号,经 ComponentLoader 的 _getContentRef() 按类型分派渲染,最终由 TooltipContainerComponent 投影到 .tooltip-inner 中;默认 hover focus 触发器与 top 定位保证了"悬停顶部显示、移开消失"的交互闭环,单元测试与 aria-describedby 逻辑则分别验证了行为与无障碍语义。对于需要展示富内容或动态 HTML 的场景,可在此基础上无缝切换为 TemplateRef 与 [innerHtml] 组合,实现从纯文本到结构化内容的渐进式升级。

登录后查看全文
ngx-bootstrap