Angular Portal 与 Overlay 渲染 | ng-hub-ui-portal

Angular portal 库,可在当前 DOM 树之外动态渲染组件、模板和 overlay,并支持定位控制。

最后更新 2026年9月20日

概览

团队为何搜索这个库

这个 Angular portal 库帮助你将内容渲染到 overlay 或自定义容器中,同时保持数据流和 Angular 集成的可预测性。

安装

npm install ng-hub-ui-portal

跳转到

适用于

  • 抽屉
  • 浮动面板
  • overlay 界面
  • 动态容器

关于 portal

ng-hub-ui-portal 在 Angular 界面需要将渲染从当前 DOM 树中分离时非常有用:抽屉、浮动面板、动态容器或基于 overlay 的 UI。它为投影内容和定位渲染提供了更清晰的基础。

功能指南

内容渲染

支持渲染多种内容类型(Component、TemplateRef、String),并具备数据注入和投影能力。

示例:
组件渲染

用组件类打开 portal,并通过返回的引用访问其实例。

代码
导入:
模板:
组件:
TemplateRef 渲染

把 ng-template 渲染为 portal 的内容,close 与 dismiss 会送入模板上下文。

代码
导入:
模板:
组件:
字符串内容

把一段纯文本作为内容传入,适用于只需说一句话的 portal。

代码
导入:
模板:
组件:
数据传递

通过自定义 Injector 把数据交给内容组件,再从关闭值里读回结果。

代码
导入:
模板:
组件:

打开策略

控制 portal 的打开方式:渐进式(堆叠)或独占式(切换)模式。

示例:
渐进式打开(堆叠)

用 open() 逐层堆叠 portal,并向服务查询还有多少个处于打开状态。

Active Portals: 0
代码
导入:
模板:
组件:
切换(互斥)

toggle() 会先消除已打开的 portal,等它隐藏之后,才显示新的那个。

Active Portal: None
代码
导入:
模板:
组件:

目标容器

选择 portal 窗口挂载的位置:默认是文档的 body,也可以是通过 CSS 选择器指定或以 HTMLElement 传入的任意元素。

示例:
自定义容器(旧版)

把窗口送到别的容器,并用 windowClass 为它换装。位置来自本示例自己的 CSS——库只负责挂载,不负责定位。

Configuración de Posicionamiento
Elementos de Referencia
Top LeftEsquina superior izquierda
Top RightEsquina superior derecha
CenterElemento central
Bottom LeftEsquina inferior izquierda
Bottom RightEsquina inferior derecha
Targets de Portal
Target 1
Target 2
Target 3
Configuración Actual
Posición: absolute
Target: body
Offset: X: 0px, Y: 0px
Backdrop: No
代码
导入:
模板:
组件:

遮罩层管理

portal 打开期间,其背后的页面停止滚动,焦点被锁定在最上层窗口内,窗口之外的一切都会被标记为 aria-hidden。

示例:
基础用法(旧版)

服务最日常的用法:打开 portal、等待 result,再带着一个值关闭它。

Portal Controls

Content is rendered dynamically into the DOM (typically body) using the HubPortal service, completely detaching it from this component's DOM hierarchy while preserving Angular context.

代码
导入:
模板:
组件:

交互

可携带结果关闭/消除 portal,并支持键盘操作和自定义触发器选择器。

示例:
内容投影

用 headerSelector 与 footerSelector 选项把内容拆成头部、主体和底部。

代码
导入:
模板:
组件:
组件渲染

用组件类打开 portal,并通过返回的引用访问其实例。

代码
导入:
模板:
组件:

生命周期

用于 portal 状态管理的生命周期钩子和事件订阅。

示例:
服务与注入(旧版)

由服务驱动的 portal:toast、对话框与抽屉,外加 dismissAll、hasOpenPortals 和 activeInstances。

Gestión Programática de Portales
Notificaciones Toast
Modales Dinámicos
Overlays Complejos
Gestión Global
Estado del Sistema de Portales
Portales Activos:0
Último Portal:Ninguno
Z-Index Actual:1000
Auto-increment ID:1
代码
导入:
模板:
组件:
渐进式打开(堆叠)

用 open() 逐层堆叠 portal,并向服务查询还有多少个处于打开状态。

Active Portals: 0
代码
导入:
模板:
组件:

核心特性

最近更改

Version 22.2.2 - 9/20/26, 12:00 AM

changed: The npm keywords declare ng-hub-ui, the family name somebody searching for the ecosystem types. Metadata only: no code, types or styles change.

Version 22.2.1 - 9/16/26, 12:00 AM

changed: Repository, issue and README links follow the move to the hub-env organization. Issues for every Hub UI package are now gathered in hub-env/hub-ui, and the repository and bugs fields of the manifest point at the new addresses. No code, types or styles change.

Version 22.2.0 - 9/8/26, 12:00 AM

fixed: HubPortal.open() with a plain string opened an empty dialog. Every other kind of content is split into the three slots the window destructures, header, body and footer, but the string path returned a single slot: the text landed in the header and the body arrived undefined, which Angular projects as nothing, so the reader saw a blank box. The string now goes into the body slot like everything else, and four specs pin it. Worth stating plainly, because the same defect behaved worse in ng-hub-ui-modal, where it also took the keyboard down with it: here Escape never stopped working, because this window arms its listeners in ngOnInit rather than at the tail of the entry transition.

changed: The body mark is hub-portal-open. The old portal-open claimed a name in the application namespace rather than in the library one, the same defect ng-hub-ui-utils retired from the bare [tooltip] attribute in 22.14.0, and nothing warns a host whose own .portal-open rule is silently joined by ours. Both classes are written for now, so a stylesheet matching the old name keeps working; portal-open is removed in 23.0.0. See BREAKING_CHANGES.md.

changed: scrollable is delivered by the dialog rather than by a class on the content component host. That host is only a query root: its children are handed to the window and the host itself never enters the document, so component-host-scrollable, and the only rule this library shipped for it, matched nothing and asking for scrollable did nothing at all. The option now reaches the dialog through portal-dialog-scrollable, which the window already set, and the stylesheet dresses that class: the content box is pinned and the body scrolls inside it, or, where the content brings no .portal-body of its own, the content box is what scrolls.

changed: The slot split is one documented function instead of three inline expressions. Both declared slots are taken out of the container before the body is captured, and the body is captured as a static array. The old code read the body between the two extractions and got away with it only because what it captured was the live childNodes list, which Angular snapshots later, once both markers are already gone: correct by accident, disagreeing with the Node[][] ContentRef declares, and going empty the moment the nodes are projected out. No behaviour changes, and a spec now guards the contract.

added: Both READMEs state where this library stands on server-side rendering. The honest answer is not verified: a portal window only exists after a gesture, so the site prerender, which is the running proof for the libraries that render markup on the page, never draws one.

Version 22.1.0 - 9/6/26, 12:00 AM

changed: Dropped the angular16 keyword from the package manifest. The peer range has required Angular 18 or newer since 22.0.1, so the keyword was advertising the package to exactly the searchers whose install it would refuse.

changed: HubPortalRef is generic, so opening a portal no longer costs you the types. HubPortal.open<C, R>() and toggle<C, R>() infer C from the class handed to them, which is what makes componentInstance the component instead of any, while R types the value that travels through close(), result and closed. Until now the README answered this with a hand-written cast that claims a type nobody checks. Both parameters default to any, but componentInstance narrows from any to C | void, so a call site that reached straight through it stops compiling: see BREAKING_CHANGES.md. content is now typed Type<C> | TemplateRef<any> | string, which is what the stack already accepted, and the whole shape mirrors what ng-hub-ui-modal settled on in 22.5.0. Dismiss reasons stay untyped on purpose, because they carry either an internal PortalDismissReasons value or whatever the consumer passed.

removed: Removed the unused BACKDROP_ATTRIBUTES constant, a commented-out import in portal-config.ts and two commented-out lines of an older toggle() implementation. None of it was reachable, and backdropClass, the option that constant named, is not part of HubPortalOptions, so leaving it in suggested a backdrop API the library does not have. No behaviour changes.

fixed: Escape now dismisses the portal, as the keyboard option has always promised. The option was declared, defaulted to true in HubPortalConfig and documented in the README and on the docs site, but no key listener existed anywhere in the library: the close button was the only way out of a focus-trapped role=dialog, which left keyboard and screen-reader users stuck and forced every consumer to wire their own listener inside the projected component. The window now rejects its result promise with PortalDismissReasons.ESC, the reason the library exported without ever emitting it, honours keyboard: false per portal, steps aside when another handler has already consumed the key, and reacts only in the window holding focus, so a stack dismisses one dialog at a time from the top.

fixed: scrollable was declared a string on the portal window while HubPortalOptions declares it a boolean. Nothing misbehaved, because the template only tests the input for truthiness and the option reaches it through the name-based setInput, which no compiler ever checks, so the two had been free to disagree since the input was written. They now agree, which is what stops the next reader from believing the window and passing a string.

deprecated: HubPortalModule is deprecated and will be removed in 23.0.0. Its whole body is providers: [HubPortal], and HubPortal is providedIn: root, so importing the module never enabled the service; it only added a redundant second instance in whichever injector declared the import, delegating to the same root HubPortalStack and HubPortalConfig. Inject HubPortal and drop the import. The class carried no @deprecated tag until now, so neither an editor nor the build could warn anyone it was on its way out. See BREAKING_CHANGES.md.

Version 22.0.5 - 9/1/26, 12:00 AM

changed: The homepage in the manifest points at this library's own documentation page rather than at the site root. It is the link a registry shows beside the package and the one a reader clicks from it, and landing on a front page they then have to search is a worse answer than landing on the reference for the package they were already looking at. Metadata only — no code, no types, no styles change, and nothing a consumer imports is affected.

Version 22.0.4 - 8/8/26, 12:00 AM

fixed: Documentation links now point at the canonical localized URLs. The README linked to https://hubui.dev/<path> with no locale prefix and no trailing slash, and both forms are 301-redirected, so every reader arriving from npm or GitHub landed on a redirect instead of the canonical page.

Version 22.0.3 - 7/28/26, 12:00 AM

fixed: Removed the invalid aria-portal attribute from the portal window host (not a real ARIA attribute); role=dialog and the aria-labelledby/describedby wiring stay.

Version 22.0.2 - 7/26/26, 12:00 AM

fixed: Declared the real ng-hub-ui-utils peer range (>=22.0.0); the previous >=1.0.0 floor allowed resolving an incompatible utils major.

Version 22.0.1 - 6/26/26, 12:00 AM

fixed: Corrected the Angular peer dependency range to >=18.0.0. The library uses APIs introduced in Angular 17 (signal input()/output(), the @if control flow and signal queries), so the previous >=16.0.0 range let it install on versions it cannot run on.

fixed: Corrected the ng-hub-ui-utils peer range. The previous caret range resolved to >=1 <2, which excluded the current utils (22.x) and made the peer impossible to satisfy.

Version 22.0.0 - 6/17/26, 12:00 AM

changed: Aligned with Angular 22.

changed: README documentation standardized.

Version 0.3.4 - 6/14/26, 12:00 AM

fixed: Portal window options (animation, windowClass, portalDialogClass, …) are applied through ComponentRef.setInput instead of being assigned onto the instance. Since HubPortalWindow declares them as signal inputs, the direct assignment replaced the read-only signal function and threw TypeError: ctx.animation is not a function on every open().

fixed: Guarded parentNode when removing the window element during teardown, which threw when the element had already been detached.

常见问题

ng-hub-ui-portal 是做什么用的?

用代码把一个组件、一个 TemplateRef 或一段普通字符串挂到 DOM 的别处。注入 HubPortal,调用 open(),你会拿到一个 HubPortalRef,它的 result 是一个 Promise:close() 传入的值让它 resolve,dismiss() 传入的原因让它 reject,而 closed、dismissed、shown 和 hidden 是对应的 observable。在内容内部注入 HubActivePortal,就能从里面自己关闭,不必持有打开方的引用。

怎么指定内容挂载到哪里?

container 选项接受一个 CSS 选择器字符串或一个 HTMLElement,默认是 document.body。它只在你调用 open() 的那一刻解析一次,所以目标必须已经在文档里:选择器什么都匹配不到时,调用会直接抛错,而不是等下去。内容会追加到那个容器的末尾,而 windowClass、portalDialogClass 和 portalContentClass 就是留给你挂自己样式的地方。

portal 和 ng-hub-ui-modal 有什么区别?

ng-hub-ui-modal 是对话框,遮罩、层叠、尺寸和位置都已经替你定好。Portal 是不带这些约定的挂载机制:它不画遮罩,也不做任何定位,长什么样由你自己写。它确实会把内容包进自己的 window 里并带上 role=dialog,而且仍然负责通过 keyboard 选项处理 Escape、打开时设置焦点、关闭时还原焦点、用 beforeDismiss 做拦截,以及对辅助技术隐藏页面的其余部分。

能像 Angular CDK 那样在模板里声明式地用吗?

不能。既没有指令也没有出口:公开接口就是 HubPortal 服务、它的 ref 和那个选项对象。有两个后果值得掂量。输入是通过给 componentInstance 赋值来设置的,而不是走 setInput,所以基于 signal 输入构建的组件不会按你预期的方式拿到值。另外 open() 会立即访问 document,因此它属于浏览器:服务端渲染未经验证,这个调用需要你自己加一层判断。