Angular 工具库 | ng-hub-ui-utils

Angular 工具库,包含 pipes、焦点管理、overlay、过渡和翻译助手,供可复用 UI 组件共享使用。

交互式示例

探索实时示例,了解该库在常见 Angular 用例中的表现。

工具 pipes

GetPipe - Access nested properties

user | get:'profile.name'→ John Doe
user | get:'profile.address.city'→ New York

IsStringPipe - Type checking

'Hello' | isString→ true
123 | isString→ false

IsObjectPipe - Object detection

{ a: 1 } | isObject→ true
'string' | isObject→ false

UcfirstPipe - Capitalize first letter

'hello world' | ucfirst→ Hello world

UnwrapAsyncPipe - Observable or plain value, same template

status$ | unwrapAsync→ nothing emitted yet
'not an observable' | unwrapAsync→ not an observable

翻译服务与 pipe

TranslatePipe - Basic usage

'welcome' | translate→ Welcome to ng-hub-ui

TranslatePipe - With parameters

'greeting' | translate:{ name: 'Carlos' }→ Hello, Carlos!

TranslatePipe - Nested keys

'buttons.save' | translate→ Save
'buttons.cancel' | translate→ Cancel

Programmatic access

translationService.getTranslation('buttons.delete')→ Delete

外部 i18n 适配器

在 app.config.ts 中配置一个响应式翻译桥接器,供所有 Hub UI 库使用。

工具函数

Type Guards

isString('hello')→ true
isNumber(42)→ true
isDefined(null)→ false
isDefined('value')→ true

Deep Equality

equals({a:1}, {a:1})→ true
equals([1,2], [1,2])→ true

Object Access

getValue(user, 'profile.name')→ John Doe

String Interpolation

interpolateString('Hello {{name}}', {name: 'World'})→ Hello World

String Utilities

removeAccents('Ñoño café')→ Nono cafe
padNumber(5)→ 05

焦点捕获

Focus Trap Demo

Click "Enable Focus Trap" and try to Tab outside the blue box. Focus will stay trapped inside.

Focus Trap Area

Status: 🔓 Free

Focusable Elements

The selector FOCUSABLE_ELEMENTS_SELECTOR finds all focusable elements.

getFocusableBoundaryElements(container)→ [first, last] elements

Scrollbar 工具

ScrollBar Service

The ScrollBar service helps manage scrollbar visibility and compensate for layout shifts when hiding scrollbars (e.g., when opening modals).

Use Case: Modal Body Lock

When opening a modal, you typically hide the body scrollbar. The ScrollBar.hide() method handles this automatically and returns a reverter function.

Status: 🔓 Normal scrolling

Code Example

import { inject } from '@angular/core';
import { ScrollBar } from 'ng-hub-ui-utils';

export class ModalService {
  private scrollBar = inject(ScrollBar);
  private revertScrollbar: (() => void) | null = null;

  openModal() {
    // Hide scrollbar and get reverter function
    this.revertScrollbar = this.scrollBar.hide();
  }

  closeModal() {
    // Restore scrollbar
    if (this.revertScrollbar) {
      this.revertScrollbar();
      this.revertScrollbar = null;
    }
  }
}

覆盖层系统

A connected overlay positioned below the trigger, with a backdrop that closes it on outside click.

提示指令

Placements (hover the buttons)
Themed with --hub-tooltip-* variables

在窗口边缘翻转

Every hint below asks for the default top placement, and none of them carries hubTooltipPlacement. Scroll until one of them reaches the top of your window and hover it: with no room above, it opens downwards. The middle one, which has room on every side, keeps top. Keep scrolling with a label open and it re-decides on the way.

The placement is a preference, not an instruction: it gives way only when the requested side genuinely has no room and the opposite side has more. An overflow on the cross axis is answered by sliding the label back inside, because flipping a centred label from top to bottom leaves it exactly as far over the edge.

从 TypeScript 打开弹窗

Nothing open.

等待 CSS 过渡结束

transition: height 600ms · measured at 0 ms
  1. Run a transition to see when the observable completes.

DOM 与 RxJS 辅助函数

closest()

Click anywhere inside; the helper walks up to the nearest panel.

outer
inner

closest(target, '[data-panel]') → —

reflow()

Replaying an animation means removing the class and adding it back. Without a forced reflow the browser coalesces both into no change at all.

replay me
getActiveElement()

Focus either control. The second one lives inside a shadow root.

document.activeElement → —

getActiveElement() → —

runInZone()

Both streams are created with runOutsideAngular; only one is piped through the operator.

plain subscriberisInAngularZone() → —
piped through runInZone(zone)isInAngularZone() → —
ticks0

解析任意 CSS 颜色

Try one:

#0b6eff
parsed
parseColor(){ r: 11, g: 110, b: 255, a: 1 }
toHex()#0b6eff
rgbToOklch(){ l: 0.58, c: 0.23, h: 260 }
isValidColor()true
toRgb()already parsed — same object back
HUB_NAMED_COLORSnot a bareword — 148 in the table

选择可读的墨色

readableOn(accent, metric)
Agrees with the design-system token on all nine
primary
#0d6efd

ink#ffffff
WCAG4.5:1
APCA-75.8
secondary
#6c757d

ink#ffffff
WCAG4.69:1
APCA-78
success
#198754

ink#ffffff
WCAG4.53:1
APCA-76.5
danger
#dc3545

ink#ffffff
WCAG4.53:1
APCA-75.4
warning
#ffc107

ink#000000
WCAG12.88:1
APCA76
info
#0dcaf0

ink#000000
WCAG10.72:1
APCA66.9
neutral
#6c757d

ink#ffffff
WCAG4.69:1
APCA-78
light
#f8f9fa

ink#000000
WCAG19.92:1
APCA102.4
dark
#212529

ink#ffffff
WCAG15.43:1
APCA-105

A highlighted border marks a chip whose ink differs from what --hub-sys-color-*-on paints in CSS.


Translucent ink has to be composited before it is measured

relativeLuminance() ignores alpha, because a translucent colour has no luminance of its own until something is behind it. Measure the ink as written and you are scoring solid black; compositeOver() blends it onto the surface first, which is what the eye is reading.

Sample text
relativeLuminance(surface)0.5942
contrastRatio(ink, surface)12.88:1
…with compositeOver(ink, surface)6.73:1

sRGB 色域并非圆柱体

red 21°
0.232
amber 85°
0.118
green 157°
0.138
cyan 218°
0.104
blue 260°
0.232
purple 320°
0.277

clampToSrgbGamut()

Asking for chroma 0.35 on the amber hue at the current lightness.

clipped per channel#d24900
chroma reduced#9a7300
in gamut as asked: false
chroma kept: 0.118

从一个品牌色推导整套调色板

Primary
oklch(0.55 0.17 260)
success #00866bwas #198754
warning #e7cd16was #ffc107
danger #d8336bwas #dc3545
info #44c4ffwas #0dcaf0
100
200
300
400
500
600
700
800
900

Hue rotation capped at 15°; neutral chroma capped at 0.015. Success and danger stay 173° apart.