PrimeNG Knob 组件完全指南:从双向绑定到 SVG 拨盘原理
【免费下载链接】primengThe Most Complete Angular UI Component Library项目地址: https://gitcode.com/GitHub_Trending/pr/primeng
Knob 是 PrimeNG 提供的一款表单项组件,用于通过一个可拖拽的圆形拨盘(dial)来定义数字输入。本文以 knob.md 为基础,结合 knob.ts 源码、knob.spec.ts 测试用例及 knobstyle.ts 样式定义,系统讲解 Knob 的模板驱动表单、响应式表单、键盘交互、无障碍支持和主题定制,帮助你在 Angular 应用中快速落地一个可访问、可定制的拨盘式数字输入控件。
组件概览与引入方式
Knob 是标准的 PrimeNG 表单组件,通过p-knob选择器使用,声明式模板完全基于 SVG 渲染(svg > path > text结构,见 knob.ts),无额外 DOM 包装。它实现了ControlValueAccessor,因此既能配合ngModel做模板驱动表单,也能配合formControl/formControlName做响应式表单(见 KNOB_VALUE_ACCESSOR)。
在独立组件(standalone)模式下,只需要导入两个模块:
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobBasicDemo { value!: number; }模块内部实际将Knob组件与SharedModule一并导出(见 knob.ts),因此import { KnobModule } from 'primeng/knob'即可获得完整能力。若仅使用响应式表单,可将FormsModule替换为ReactiveFormsModule。
核心用法:颜色、禁用、边界与步长
自定义颜色
Knob 由三段视觉元素组成:圆环背景(range)、进度弧(value)和中心数值文本(text),分别对应rangeColor、valueColor和textColor三个属性。这些属性在源码中均以@Input()声明,默认值取自设计令牌(design token)CSS 变量,例如valueColor默认为$dt('knob.value.background').variable(见 knob.ts)。
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" valueColor="SlateGray" rangeColor="MediumTurquoise" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobColorDemo { value: number = 50; }颜色值可直接传 CSS 颜色名或任意合法的 CSS 颜色字符串,最终作为stroke和fill属性作用到 SVG 元素上(见 knob.ts)。
禁用与只读
disabled与readonly是两种不同的不可编辑状态:
disabled:整体不可交互,KnobStyle会在根元素追加p-disabled类并施加视觉淡化(见 knobstyle.ts);readonly:值不可编辑,但样式不变,且tabindex被强制置为-1,使控件从键盘 Tab 序列中移除(见 knob.ts)。
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" [disabled]="true" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobDisabledDemo { value: number = 75; }import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" [readonly]="true" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobReadonlyDemo { value: number = 50; }源码层面,onClick、onMouseDown、onKeyDown等所有交互入口都会先判断!this.$disabled() && !this.readonly才继续处理(见 knob.ts),测试也验证了 readonly 状态下点击不会调用updateValue(见 knob.spec.ts)。
设置 min/max 边界
边界值通过min和max配置,默认分别为0和100。拖动或键盘调整后的值会被钳制(clamp)在区间内,updateModelValue会在越界时强制收敛到边界(见 knob.ts)。
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" [min]="-50" [max]="50" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobMinmaxDemo { value: number = 10; }值得注意的是,min、max、size、step、strokeWidth等数值型输入都经过numberAttribute转换,showValue、readonly等布尔型输入则经过booleanAttribute转换(见 knob.ts),因此模板中传字符串"50"与传数字50行为一致,且可以不写方括号。
步长控制
step决定每次拨动的增量,默认1。在拖动更新时,源码通过Math.round((mappedValue - min) / step) * step + min将任意角度映射值对齐到步长的整数倍(见 knob.ts)。
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" [step]="10" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobStepDemo { value!: number; }尺寸与描边:控制拨盘外观
size定义拨盘直径(像素),默认100,最终被应用到 SVG 的width/height内联样式(见 knob.ts);strokeWidth定义圆弧描边宽度,默认14像素。
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" [size]="200" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobSizeDemo { value: number = 60; }import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" [strokeWidth]="5" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobStrokeDemo { value: number = 40; }测试用例验证了size与strokeWidth会正确反映到 DOM:SVG 的style.width/height为150px,两条path的stroke-width均为20(见 knob.spec.ts)。
值文本模板:valueTemplate
中心文本默认显示数值本身(占位符为{value})。通过valueTemplate可自定义文本格式,实现百分比、温度等单位后缀:
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex justify-center"> <p-knob [(ngModel)]="value" valueTemplate="{value}%" /> </div> `, standalone: true, imports: [KnobModule, FormsModule] }) export class KnobTemplateDemo { value: number = 60; }底层实现是简单的字符串替换:valueToDisplay()执行valueTemplate.replace('{value}', this._value.toString())(见 knob.ts)。测试中用'{value}°C'配合 0–40 的温度范围,断言文本渲染为25°C(见 knob.spec.ts)。
外部控制:自定义按钮联动(Reactive 用法)
Knob 也可以“只读展示 + 外部控制器驱动”。将readonly设为true后,通过按钮增减数值即可实现外置式步进控制,同时利用onChange事件或双向绑定同步状态:
import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; import { KnobModule } from 'primeng/knob'; @Component({ template: ` <div class="card flex flex-col items-center gap-2"> <p-knob [(ngModel)]="value" size="150" readonly="true" /> <div class="flex gap-2"> <p-button icon="pi pi-plus" (click)="value = value + 1" [disabled]="value >= 100" /> <p-button icon="pi pi-minus" (click)="value = value - 1" [disabled]="value <= 0" /> </div> </div> `, standalone: true, imports: [ButtonModule, KnobModule, FormsModule] }) export class KnobReactiveDemo { value: number = 0; }响应式表单集成与校验
Knob 通过NG_VALUE_ACCESSOR暴露为 Angular 表单控件(见 knob.ts),可直接挂到FormGroup上,配合invalid属性触发错误态样式,并用p-message展示校验信息:
import { Component, inject } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; import { MessageModule } from 'primeng/message'; import { ToastModule } from 'primeng/toast'; import { ButtonModule } from 'primeng/button'; import { MessageService } from 'primeng/api'; @Component({ template: ` <p-toast /> <div class="card flex justify-center"> <form [formGroup]="exampleForm" (ngSubmit)="onSubmit()" class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-1"> <p-knob formControlName="value" [invalid]="isInvalid('value')" /> @if (isInvalid('value')) { <p-message severity="error" size="small" variant="simple">{{ getErrorMessage('value') }}</p-message> } </div> <button pButton severity="secondary" type="submit"><span pButtonLabel>Submit</span></button> </form> </div> `, standalone: true, imports: [KnobModule, MessageModule, ToastModule, ButtonModule, ReactiveFormsModule] }) export class KnobReactiveformsDemo { messageService = inject(MessageService); items: any[] | undefined; exampleForm: FormGroup | undefined; formSubmitted: boolean = false; constructor() { this.exampleForm = this.fb.group({ value: [15, [Validators.min(25), Validators.max(75)]] }); } onSubmit() { this.formSubmitted = true; if (this.exampleForm.valid) { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Form is submitted', life: 3000 }); this.exampleForm.reset({ value: 15 }); this.formSubmitted = false; } } getControl(controlName: string): AbstractControl | null { return this.exampleForm?.get(controlName) ?? null; } getErrorMessage(controlName: string): string | null { const control = this.getControl(controlName); if (!control || !control.errors) return null; if (control.errors['min']) { return 'Value must be greater than 15.'; } if (control.errors['max']) { return 'Must be less than 75.'; } } isInvalid(controlName: string) { const control = this.getControl(controlName); return control?.invalid && (control.dirty || this.formSubmitted); } }响应式表单的写入路径由writeControlValue完成:将外部值写入内部signal后触发变更检测(见 knob.ts)。测试同时验证了FormControl的min/max校验(如设置 20 时产生errors['min'])以及用户交互反向回写表单值(见 knob.spec.ts)。
模板驱动表单集成与校验
模板驱动场景下,通过#model="ngModel"拿到控件引用判断非法状态,并手动维护提交标记:
import { Component, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KnobModule } from 'primeng/knob'; import { MessageModule } from 'primeng/message'; import { ToastModule } from 'primeng/toast'; import { ButtonModule } from 'primeng/button'; import { MessageService } from 'primeng/api'; @Component({ template: ` <p-toast /> <div class="card flex justify-center"> <form #exampleForm="ngForm" (ngSubmit)="onSubmit(exampleForm)" class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-1"> <p-knob #model="ngModel" [(ngModel)]="value" [invalid]="isInvalid(model)" name="knob" /> @if (isInvalid(model)) { <p-message severity="error" size="small" variant="simple">{{ getErrorMessage(model) }}</p-message> } </div> <button pButton severity="secondary" type="submit"><span pButtonLabel>Submit</span></button> </form> </div> `, standalone: true, imports: [KnobModule, MessageModule, ToastModule, ButtonModule, FormsModule] }) export class KnobTemplatedrivenformsDemo { messageService = inject(MessageService); value: number = 15; formSubmitted: boolean = false; onSubmit(form: NgForm) { this.formSubmitted = true; if (!this.isInvalid(form.controls['knob'])) { this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Form is submitted', life: 3000 }); form.resetForm({ knob: 15 }); this.formSubmitted = false; } } getErrorMessage(control: any): string | null { const value = control?.value; return value < 25 ? 'Value must be greater than 25.' : value > 75 ? 'Must be less than 75.' : null; } isInvalid(control: any): boolean { if (!control) return false; const value = control.value; const hasError = value < 25 || value > 75; return hasError && (this.formSubmitted || control.dirty); } }注意:模板驱动用法中name属性是必填的,它决定表单控件在NgForm.controls中的注册键(此处为knob),源码中该属性作为text元素的name透传到 DOM(见 knob.ts)。
无障碍与键盘操作
屏幕阅读器支持
Knob 的根元素是<svg role="slider">,并携带aria-valuemin、aria-valuemax、aria-valuenow三个关键属性,数值随min、max和当前值动态更新(见 knob.ts)。组件描述文案通过ariaLabelledBy(指向 DOM 中其他元素的 id)或ariaLabel(直接给定字符串)提供,官方示例见 accessibility-doc.ts:
<span id="label_number">Number</span> <p-knob ariaLabelledBy="label_number"/> <p-knob ariaLabel="Number"/>键盘支持
Knob 支持完整的键盘操作(见 knob.ts),键位与行为如下:
| 按键 | 行为 |
|---|---|
tab | 将焦点移动到拨盘(SVG 元素) |
left arrow/down arrow | 值减 1 |
right arrow/up arrow | 值加 1 |
home | 设为最小值(min) |
end | 设为最大值(max) |
page up | 值加 10 |
page down | 值减 10 |
键盘增量是固定值(±1/±10),与step无关;测试对上述每种键位均有断言,例如ArrowUp后值为 51、Home后为 0、PageUp后为 60(见 knob.spec.ts)。键盘调整同样走updateModelValue的越界钳制逻辑,不会超出 min/max。
事件与全部属性参考
Emits
| 名称 | 参数 | 说明 |
|---|---|---|
onChange | value: number | 值变化时触发,与内部EventEmitter<number>对应(见 knob.ts) |
在拖动、点击或键盘调整后,组件会依次调用writeModelValue、onModelChange(表单回写)和onChange.emit(事件通知)(见 knob.ts),测试验证了事件携带正确的数值(见 knob.spec.ts)。
Props 完整清单
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
dt | InputSignal<Object> | undefined | 定义组件作用域的 design tokens |
unstyled | InputSignal<boolean> | undefined | 是否无样式渲染 |
pt | InputSignal<KnobPassThrough> | undefined | 向组件内部 DOM 元素透传属性 |
ptOptions | InputSignal<PassThroughOptions> | undefined | 配置 passthrough 选项 |
required | InputSignalWithTransform<boolean, unknown> | false | 是否必须有值 |
invalid | InputSignalWithTransform<boolean, unknown> | false | 是否呈现非法状态样式 |
disabled | InputSignalWithTransform<boolean, unknown> | false | 是否呈现禁用状态样式 |
name | InputSignal<string> | undefined | 输入控件的 name |
styleClass | string | - | 组件样式类(已废弃,v20.0.0 起建议改用class,见 knob.ts) |
ariaLabel | string | - | 用于无障碍的输入标签 |
ariaLabelledBy | string | - | 指定 DOM 中一个或多个 label 元素的 id |
tabindex | number | 0 | Tab 键导航顺序索引 |
valueColor | string | 设计令牌 | 进度弧(value)背景色 |
rangeColor | string | 设计令牌 | 圆环(range)背景色 |
textColor | string | 设计令牌 | 中心数值文本颜色 |
valueTemplate | string | {value} | 数值文本模板 |
size | number | 100 | 组件直径(像素) |
min | number | 0 | 最小值边界 |
max | number | 100 | 最大值边界 |
step | number | 1 | 拖动时的增减步长 |
strokeWidth | number | 14 | 圆弧描边宽度(像素) |
showValue | boolean | true | 是否显示中心数值 |
readonly | boolean | false | 值是否可编辑 |
默认值均可从源码@Input()声明中逐一核对(见 knob.ts),测试对默认值也有断言(见 knob.spec.ts)。
Pass Through(PT)自定义
Knob 支持通过pt属性向内部各 DOM 层透传 class、style、事件甚至生命周期钩子,可覆盖的层级如下:
| 名称 | 类型 | 说明 |
|---|---|---|
host | PassThroughOption<HTMLElement, I> | 宿主根元素 |
svg | PassThroughOption<SVGElement, I> | SVG 元素 |
range | PassThroughOption<SVGPathElement, I> | 圆环路径 |
value | PassThroughOption<SVGPathElement, I> | 进度路径 |
text | PassThroughOption<SVGTextElement, I> | 文本元素 |
典型用法是在组件上直接传pt,或通过providePrimeNG全局配置:
// 组件级 PT:为 SVG 追加自定义类 <p-knob [pt]="{ host: 'HOST_CLASS', svg: 'SVG_CLASS', range: 'RANGE_CLASS', value: 'VALUE_CLASS', text: 'TEXT_CLASS' }" /> // 全局 PT:所有 Knob 生效 providePrimeNG({ pt: { knob: { host: 'GLOBAL_HOST_CLASS', svg: 'GLOBAL_SVG_CLASS' } } });PT 还支持函数形式,可基于组件实例动态返回属性,例如按当前值决定配色。测试覆盖了字符串 class、对象(class/style/data 属性)、函数回调、事件绑定、行内 PT 与全局 PT 共 8 种场景(见 knob.spec.ts),可作为功能参考。
主题定制:CSS 类与设计令牌
Knob 的结构化 CSS 类定义在 knobstyle.ts,根元素为p-knob p-component,禁用时追加p-disabled:
| 类名 | 对应元素 |
|---|---|
p-knob | 根元素 |
p-knob-range | 圆环(range)路径 |
p-knob-value | 进度(value)路径 |
p-knob-text | 中心文本 |
设计令牌(Design Tokens)将主题值映射为 CSS 变量,供@primeuix/styled的主题系统消费。valueColor、rangeColor、textColor的默认值正是引用自其中三个变量(见 knob.ts):
| Token | CSS 变量 | 说明 |
|---|---|---|
knob.transition.duration | --p-knob-transition-duration | 根元素过渡时长 |
knob.focus.ring.width | --p-knob-focus-ring-width | 焦点环宽度 |
knob.focus.ring.style | --p-knob-focus-ring-style | 焦点环样式 |
knob.focus.ring.color | --p-knob-focus-ring-color | 焦点环颜色 |
knob.focus.ring.offset | --p-knob-focus-ring-offset | 焦点环偏移 |
knob.focus.ring.shadow | --p-knob-focus-ring-shadow | 焦点环阴影 |
knob.value.background | --p-knob-value-background | 进度弧背景色 |
knob.range.background | --p-knob-range-background | 圆环背景色 |
knob.text.color | --p-knob-text-color | 文本颜色 |
底层原理:SVG 弧线与角度映射
从 knob.ts 可以看到 Knob 的几何常量:viewBox为0 0 100 100,圆心midX = midY = 50,半径radius = 40,角度范围从minRadians = 4π/3(240°)扫到maxRadians = -π/3(-60°),即约 300° 的开口圆弧。
拖动交互的核心链路是:
- 鼠标/触摸事件经
onClick/onMouseMove/onTouchMove收集offsetX、offsetY; updateValue以圆心为原点换算dx、dy,用Math.atan2(dy, dx)求出指针角度(见 knob.ts);updateModel用mapRange把角度线性映射到[min, max]数值区间,再按step取整(见 knob.ts);- 数值变更后同步刷新
rangePath()/valuePath()两个 SVG 弧线路径(M … A …指令),完成视觉反馈(见 knob.ts)。
触摸拖动通过window级监听器实现(mousemove/mouseup挂载于 window,拖出组件范围仍可继续操作),并在touchend时统一解绑(见 knob.ts)。测试对mapRange的数学正确性(如mapRange(50, 0, 100, 0, 360) === 180)以及坐标计算均有断言(见 knob.spec.ts)。
更多资料
- 组件源码:packages/primeng/src/knob/knob.ts
- 样式与 CSS 类定义:packages/primeng/src/knob/style/knobstyle.ts
- 单元测试(含键盘、触摸、PT 全覆盖):packages/primeng/src/knob/knob.spec.ts
- 公开导出:packages/primeng/src/knob/public_api.ts
- 展示页文档源码:apps/showcase/doc/knob(含 accessibility-doc、basic-doc、reactiveforms-doc、templatedrivenforms-doc 等)
- 原始指南:apps/showcase/public/llms/components/knob.md
【免费下载链接】primengThe Most Complete Angular UI Component Library项目地址: https://gitcode.com/GitHub_Trending/pr/primeng
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考