ThingsBoard HTML Container 组件实战:用 Angular 模板与 Tab 导航构建复合仪表盘
ThingsBoard HTML Container 组件实战:用 Angular 模板与 Tab 导航构建复合仪表盘
本文围绕 ThingsBoard 官方 HTML Container 组件库中"带 Tab 导航的复合仪表盘"示例展开,该示例由三部分组成——HTML 模板(angular_tabs_html.md)、JavaScript 容器函数(angular_tabs_js.md)与 CSS(angular_tabs_css.md)。读完本文,你将掌握如何在 HTML Container 组件(Angular 模式)中编写自定义 Angular 模板、通过
tb-dashboard-state嵌入其他仪表盘状态,并借助ctx.stateController实现跨状态切换,最终在单块仪表盘内拼装出多页签的"复合仪表盘"实战方案。
HTML Container 组件:Angular 模式与 Plain 模式
HTML Container 是 ThingsBoard 仪表盘组件库中用于承载"自定义 HTML/CSS/JavaScript"的容器型组件。从源码看,html-container-widget.component.ts 在初始化时会根据设置中的 type 字段分派两条渲染路径:
- PLAIN 模式:将设置中的 HTML 直接写入容器,CSS 通过
cssjs解析并注入带命名空间的<style>元素,JavaScript 函数以function (ctx, container)形式执行,container为实际 DOM 根元素(对应 container_plain_fn.md)。 - ANGULAR 模式:将设置中的 HTML 当作 Angular 模板动态编译,JavaScript 函数以
function (ctx)形式执行,并通过this向模板暴露数据与事件处理器(对应 container_angular_fn.md)。
组件设置面板(html-container-settings.component.html)顶部提供了 PLAIN / ANGULAR 切换开关,下方依次是 Resources、CSS、HTML、JavaScript 四个编辑区。选择 Angular 模式后,HTML 标签页标题会变为 "Angular HTML template",且 JavaScript 函数的参数自动收敛为单个 ctx(见 html-container-settings.component.html)。
我们讨论的 Tab 导航示例正是基于 Angular 模式构建,这也是它能够直接使用
*ngFor、[class.is-active]等 Angular 指令的前提。
示例全景:Tab 导航复合仪表盘的三块拼图
该示例在帮助中心以三个可独立复制的代码片段呈现(仓库中位于 ui-ngx/src/assets/help/en_US/widget/lib/html_container/examples/ 目录):
| 文件 | 内容 | 作用 |
|---|---|---|
| angular_tabs_html.md | HTML 模板 | 定义页签按钮、Toast 容器与内嵌仪表盘状态 |
| angular_tabs_js.md | JavaScript 函数 | 定义 tab 数据、维护 activeTab / stateId、响应状态变更 |
| angular_tabs_css.md | CSS | 页签栏、激活态与内嵌区域的样式 |
三者通过 this 绑定相互衔接:JS 函数把 tabs、activeTab、selectTab 挂到 this 上,HTML 模板直接以无前缀形式引用(container_angular_fn.md 明确说明:"Reference them in the template without the this prefix")。CSS 则负责让这套结构呈现为视觉上的页签组件。
HTML 模板逐段解析(示例核心)
示例 HTML 模板完整代码如下(原样来自 angular_tabs_html.md):
<div class="composite">
<nav class="tabs">
<button *ngFor="let tab of tabs"
class="tab"
[class.is-active]="tab.id === activeTab"
(click)="selectTab(tab.id)">
{{ tab.label }}
</button>
</nav>
<div tb-toast toastTarget="layout" class="container">
<tb-dashboard-state class="state" [ctx]="ctx" [stateId]="stateId" [syncParentStateParams]="true"></tb-dashboard-state>
</div>
</div>
页签导航栏
<nav class="tabs"> 中通过 *ngFor="let tab of tabs" 遍历 JS 侧定义的 tab 数组,为每个 tab 渲染一个按钮:
[class.is-active]="tab.id === activeTab":当 tab 的id等于当前激活 tab 的id时,为按钮追加is-active类,配合 CSS 实现激活态高亮;(click)="selectTab(tab.id)":点击时调用 JS 侧暴露的selectTab处理器,传入该 tab 的id;{{ tab.label }}:显示页签文案。
tb-toast:把通知消息锁定在组件内部
<div tb-toast toastTarget="layout" class="container"> 使用了 ThingsBoard 的 tb-toast 指令。从 toast.directive.ts 的实现看,该指令接收 toastTarget 输入(默认值为 'root'),并订阅 ToastNotificationService 的消息流:
shouldDisplayMessage()(toast.directive.ts)会比较通知消息的target与指令的toastTarget,只有两者一致时才在本容器内展示;- 在宽屏(
gt-sm)且toastTarget !== 'root'时,指令会以内嵌 Toast 面板形式将消息渲染到当前组件内部(showToastPanel),否则退化为全局 SnackBar。
因此这里将 toastTarget 设为 'layout',意味着"复合仪表盘"内部产生的通知只显示在它自己的布局区域中,不会弹到整个页面顶部——这对嵌在大型仪表盘中的复合组件非常实用。
tb-dashboard-state:把另一个仪表盘状态嵌进来
<tb-dashboard-state <a href="https://link.gitcode.com/i/5ab174da193dea43da0a91aae62e0c1c" target="_blank">ctx]="ctx" [stateId]="stateId" [syncParentStateParams]="true"> 是整套方案的核心。它由 ThingsBoard 提供,用于将仪表盘配置中的某个状态(state)嵌入当前组件内部渲染。对应实现位于 [dashboard-state.component.ts,支持的输入参数如下:
| 输入 | 含义 | 默认值 |
|---|---|---|
ctx |
当前组件的 WidgetContext,用于读取仪表盘配置与状态控制器 |
必填 |
stateId |
要嵌入的仪表盘状态 ID(须存在于当前仪表盘的 configuration.states) |
— |
syncParentStateParams |
是否同步父仪表盘状态参数并监听状态变更 | false |
defaultAutofillLayout |
是否将嵌入布局的 autoFillHeight / mobileAutoFillHeight 强制设为 true |
true |
defaultMargin / defaultOuterMargin / defaultBackgroundColor |
覆盖嵌入布局的边距与背景色 | 不覆盖 |
从 dashboard-state.component.ts 的 ngOnInit 可以看到它的工作方式:
- 通过
ctx.stateController.dashboardCtrl.dashboardCtx.getDashboard()深拷贝当前仪表盘配置; - 取出
configuration.states[stateId]对应的状态,若存在则遍历其所有 layout,应用autoFillHeight、margin、outerMargin、backgroundColor等覆盖项; - 调用
updateCurrentState()(dashboard-state.component.ts),把当前状态参数(含可选的entityId)编码为currentState,供内嵌的仪表盘页使用; - 若
syncParentStateParams为true,则订阅stateChanged事件,父级状态一旦变化就同步刷新; - 若指定
stateId不存在,stateExists置为false,模板会渲染"仪表盘状态不存在"的错误提示(见 dashboard-state.component.html)。
内嵌渲染本身由 dashboard-state.component.html 中的 <tb-dashboard-page [embedded]="true" [hideToolbar]="true" [currentState]="currentState" ...> 完成——它把目标状态作为一个"无工具栏的嵌入式仪表盘页"呈现在组件内部,这正是"复合仪表盘"能实现嵌套的根本机制。
JavaScript 容器函数:状态驱动的页签逻辑
与模板配套的容器函数(angular_tabs_js.md)负责全部数据与交互逻辑:
this.ctx = ctx;
this.tabs = [
{ id: 'assets', label: 'Assets', stateId: 'assetsState' },
{ id: 'devices', label: 'Devices', stateId: 'devicesState' },
{ id: 'customers', label: 'Customers', stateId: 'customersState' },
];
const apply = (subState) => {
const tab = this.tabs.find((t) => t.id === subState) || this.tabs[0];
this.activeTab = tab.id;
this.stateId = tab.stateId;
};
apply(ctx.stateController.getStateParams().subState);
this.selectTab = (subState) => ctx.stateController.updateState(null, { subState });
const stateSub = ctx.stateController.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => {
apply(ctx.stateController.getStateParams().subState);
ctx.detectChanges();
});
ctx.registerDestroyCallback(() => {
stateSub.unsubscribe();
});
其设计思路可拆解为四点:
1. 页签与仪表盘状态的映射。 tabs 数组中每个元素携带三要素:id(页签标识,同时作为 URL/状态参数中的 subState 值)、label(页签文案)、stateId(指向仪表盘配置中实际存在的状态)。模板中的 <tb-dashboard-state [stateId]="stateId"> 正是消费这里的 stateId。
2. 用状态参数驱动激活页签。 apply(subState) 根据传入的 subState 查找对应 tab,同时更新 this.activeTab 与 this.stateId。组件初始化时直接读取 ctx.stateController.getStateParams().subState 完成首屏定位——这意味着 当前激活的是哪个页签,本身就是一个可随仪表盘状态持久化的参数,刷新或跳转后可以恢复。
3. 点击页签即更新仪表盘状态。 selectTab 调用 ctx.stateController.updateState(null, { subState })。对照 IStateController 接口定义(widget-api.models.ts),updateState(id?: string, params?: StateParams, openRightLayout?: boolean) 的第一个参数为空表示停留在当前状态、仅更新状态参数 subState。这正是 HTML Container 组件能"操控"外层仪表盘状态的关键能力。
4. 响应式刷新与资源清理。 组件订阅了 ctx.stateController.dashboardCtrl.dashboardCtx.stateChanged,任何状态变化(包括由页签点击、地址栏或外部控件触发的)都会重新调用 apply 并执行 ctx.detectChanges() 触发 Angular 变更检测,让模板中的 activeTab / stateId 与最新状态保持一致。同时,该订阅通过 ctx.registerDestroyCallback(() => stateSub.unsubscribe()) 注册销毁回调,确保组件被移除时不会产生内存泄漏——这是 container_angular_fn.md 中反复强调的规范。
注意:Angular 模式下 HTML Container 不会自动绑定组件数据源。JS 侧并没有使用
ctx.data,因为这类容器组件不会依据 widget 配置自动填充数据(参见 container_angular_fn.md 的 Notes)。如果需要实时数据,应通过ctx.subscriptionApi.createSubscription(...)显式创建订阅,并在ctx.registerDestroyCallback(...)中释放。
CSS:页签视觉与布局
配套样式(angular_tabs_css.md)定义了三层结构:
.composite:display: flex; flex-direction: column,撑满容器高度,上下依次排列页签栏与内容区;.tabs/.tab:页签栏采用inline-flex胶囊形态(圆角背景#eef1f6);普通态为灰色文字、hover 加深、激活态(.tab.is-active)切换为白色背景 +#2f6bff蓝色文字,营造"当前页签"的视觉反馈;.container/.state:内容区flex-grow: 1,带边框圆角并overflow: hidden,内嵌的.state同样撑满,保证<tb-dashboard-state>渲染出的子仪表盘填满剩余空间。
运行原理:Angular 模式下模板如何被编译执行
了解底层机制有助于排查问题。在 html-container-widget.component.ts 中,Angular 模式初始化分为三步:
loadAngularModules():加载 Resources 中标记为isModule("资源为扩展模块")的外部 Angular 模块,用于在模板中使用第三方组件;compileAngularFunction():通过parseTbFunction(this.ctx.http, this.settings.js, <a href="https://link.gitcode.com/i/4e07270688c0f0437a11ee587422c249" target="_blank">'ctx'])编译 JS 函数,参数列表只有ctx([html-container-widget.component.ts);initAngularComponent():调用DynamicComponentFactoryService.createDynamicComponent(...)将设置中的 HTML 作为模板、CSS 作为组件样式,动态创建一个容器组件实例,并把 ThingsBoard 的sharedModule、widgetComponentsModule、homeComponentsModule(以及外部扩展模块)并入其编译环境(html-container-widget.component.ts)。
动态组件的 ngOnInit 会执行 containerFunction.apply(this, [instance.ctx])——这正是 JS 侧 this 绑定得以成立、模板能够无前缀引用 tabs / selectTab 等标识符的原因。组件还通过 ngDoCheck 周期性执行 detectChanges,因此 JS 侧在状态变更后调用 ctx.detectChanges() 即可让模板立刻重渲染。
在 Widget 编辑器中配置与验证
要在自己的仪表盘中复现该示例,可按以下步骤操作:
- 在仪表盘编辑页添加 HTML Container 组件,进入组件设置;
- 在 Type 切换开关处选择 Angular;
- 将示例 HTML 模板、CSS、JavaScript 函数分别粘贴到 HTML / CSS / JavaScript 编辑区;
- 关键前提:示例中的
assetsState、devicesState、customersState必须是你仪表盘States配置里真实存在的状态 ID(在仪表盘编辑器的 States 面板中创建并命名);从源码看,tb-dashboard-state会校验configuration.states[stateId],若缺失将显示"non-existent-dashboard-state"错误; - 保存并预览:点击页签应能切换到对应的仪表盘状态,且该页面产生的 Toast 消息会局限在组件布局区域内显示。
最佳实践与注意事项
- 辅助函数用箭头函数:
container_angular_fn.md明确要求"Define helper functions as arrow functions to preservethis",示例中的apply与selectTab均为此写法; - 手动清理订阅:任何
subscribe都必须在ctx.registerDestroyCallback(...)中unsubscribe,否则组件销毁后会遗留状态监听; - 变更检测:修改模板绑定的属性后调用
ctx.detectChanges(),否则界面不会刷新; - 数据订阅需显式创建:HTML Container 不会自动填充
ctx.data,实时数据要用ctx.subscriptionApi.createSubscription(...); - 无
container参数:Angular 模式没有 Plain 模式那样的container参数,需要访问 DOM 根元素时用ctx.$container[0]或从事件对象经event.currentTarget.closest(...)定位; stateId与状态参数协同:让"当前页签"可被外部恢复的最优雅做法,就是像示例一样把subState放进状态参数,并让模板的stateId完全由它推导。
相关仓库资源
- 示例 HTML 模板:angular_tabs_html.md
- 示例 JavaScript 函数:angular_tabs_js.md
- 示例 CSS:angular_tabs_css.md
- Angular 模式容器函数说明:container_angular_fn.md
- HTML Container 组件实现:html-container-widget.component.ts
- 嵌入状态组件实现与模板:dashboard-state.component.ts / dashboard-state.component.html
- Toast 指令实现:toast.directive.ts
- 状态控制器接口
IStateController:widget-api.models.ts - 组件设置面板:html-container-settings.component.html
同目录下还有 angular_split_html.md(可调整分割的主从布局)、angular_drawer_html.md(侧滑设备详情 + 双向 RPC)等姊妹示例,它们是进一步掌握 HTML Container 组件能力的绝佳范本。