You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何以Angular风格实现带复选框的自定义下拉表单控件?

Angular 带多选复选框的下拉表单组件实现(符合规范)

带多选复选框的下拉组件效果

一、核心实现:正确对接 Angular 表单系统

要让组件能作为通用表单控件复用,必须严格实现ControlValueAccessor接口,并注册NG_VALUE_ACCESSOR令牌,让 Angular 表单系统识别并管理它。

组件类完整代码(修正并完善你提供的基础实现)

import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-custom-dropdown',
  templateUrl: './custom-dropdown.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomDropdownComponent),
      multi: true
    }
  ]
})
export class CustomDropdownComponent implements ControlValueAccessor {
  @Input() label: string = '';
  @Input() options: { id: any; label: string }[] = []; // 可根据实际业务调整选项结构

  selectedValues: any[] = [];
  isDisabled = false;
  isDropdownOpen = false;

  private onChange!: (value: any[]) => void;
  private onTouched!: () => void;

  // 写入表单值到组件(表单控件值变更时触发)
  writeValue(value: any[]): void {
    // 避免空值或非数组导致的异常
    if (value && Array.isArray(value)) {
      this.selectedValues = [...value];
    } else {
      this.selectedValues = [];
    }
  }

  // 注册表单值变化回调(组件值变更时通知表单)
  registerOnChange(fn: (value: any[]) => void): void {
    this.onChange = fn;
  }

  // 注册触摸状态回调(标记组件已被用户交互)
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  // 设置组件禁用状态(表单控件禁用时同步)
  setDisabledState(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

  // 复选框选中/取消逻辑
  toggleOption(optionId: any): void {
    if (this.isDisabled) return;

    const index = this.selectedValues.indexOf(optionId);
    index > -1 ? this.selectedValues.splice(index, 1) : this.selectedValues.push(optionId);

    // 通知表单更新值(传递数组副本避免引用类型问题)
    this.onChange([...this.selectedValues]);
    // 标记组件已触摸
    this.onTouched();
  }

  // 切换下拉面板展开/收起
  toggleDropdown(): void {
    if (this.isDisabled) return;
    this.isDropdownOpen = !this.isDropdownOpen;
    this.onTouched();
  }

  // 获取选中项的标签文本,用于组件顶部展示
  get selectedLabels(): string {
    return this.options
      .filter(opt => this.selectedValues.includes(opt.id))
      .map(opt => opt.label)
      .join(', ');
  }
}

关键修正与补充

  • 新增setDisabledState方法:同步表单控件的禁用状态,是ControlValueAccessor接口的必填方法
  • 修复writeValue逻辑:不再直接push值,而是正确初始化/覆盖选中数组,避免重复累加
  • 注册NG_VALUE_ACCESSOR提供者:让 Angular 能将组件纳入表单体系
  • 完善值变更通知:传递数组副本,避免引用类型导致的变更检测异常

二、组件模板实现(custom-dropdown.component.html)

实现下拉触发区、复选框列表和选中项展示:

<div class="custom-dropdown-container" [class.disabled]="isDisabled">
  <!-- 下拉触发区域 -->
  <div class="dropdown-trigger" (click)="toggleDropdown()" [class.open]="isDropdownOpen">
    <span class="label-text">{{ label }}:</span>
    <span class="selected-content">{{ selectedLabels || '请选择' }}</span>
    <span class="dropdown-icon">▼</span>
  </div>

  <!-- 下拉面板 -->
  <div class="dropdown-panel" *ngIf="isDropdownOpen">
    <div class="option-item" *ngFor="let opt of options">
      <input
        type="checkbox"
        [id]="'opt-' + opt.id"
        [checked]="selectedValues.includes(opt.id)"
        (change)="toggleOption(opt.id)"
        [disabled]="isDisabled"
      />
      <label [for]="'opt-' + opt.id">{{ opt.label }}</label>
    </div>
  </div>
</div>

基础样式(可选)

添加 CSS 让组件呈现下拉框样式:

.custom-dropdown-container {
  position: relative;
  width: 280px;
  margin: 12px 0;
}

.dropdown-trigger {
  padding: 8px 14px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.dropdown-trigger.disabled {
  background-color: #f5f5f5;
  cursor: not-allowed;
  color: #999;
}

.dropdown-trigger.open {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: none;
}

.label-text {
  font-weight: 500;
  margin-right: 8px;
}

.selected-content {
  flex: 1;
  text-align: left;
}

.dropdown-icon {
  font-size: 12px;
}

.dropdown-panel {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  border: 1px solid #e0e0e0;
  border-top: none;
  border-bottom-left-radius: 4px;
  border-bottom-right-radius: 4px;
  background-color: #fff;
  max-height: 220px;
  overflow-y: auto;
  z-index: 100;
}

.option-item {
  padding: 7px 14px;
  display: flex;
  align-items: center;
  cursor: pointer;
}

.option-item:hover {
  background-color: #fafafa;
}

.option-item input {
  margin-right: 8px;
}

三、组件使用示例

完全匹配你提供的用法,支持响应式表单和模板驱动表单:

响应式表单用法

// 父组件TS
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-parent',
  template: `
    <form [formGroup]="demoForm">
      <app-custom-dropdown 
        label="Group List" 
        [options]="groupOptions" 
        formControlName="groupsList"
      ></app-custom-dropdown>
    </form>
    <p>当前选中值: {{ demoForm.get('groupsList')?.value | json }}</p>
  `
})
export class ParentComponent {
  demoForm: FormGroup;
  groupOptions = [
    { id: 1, label: 'Group A' },
    { id: 2, label: 'Group B' },
    { id: 3, label: 'Group C' }
  ];

  constructor(private fb: FormBuilder) {
    this.demoForm = this.fb.group({
      groupsList: [[]] // 初始值设为空数组
    });
  }
}

模板驱动表单用法

<form #demoForm="ngForm">
  <app-custom-dropdown 
    label="Group List" 
    [options]="groupOptions" 
    name="groupsList"
    [(ngModel)]="selectedGroups"
  ></app-custom-dropdown>
</form>
<p>当前选中值: {{ selectedGroups | json }}</p>

四、符合 Angular 规范的核心要点

  • 严格实现ControlValueAccessor接口的所有方法,确保表单控件的双向同步
  • 注册NG_VALUE_ACCESSOR令牌,让 Angular 能识别自定义表单控件
  • 使用不可变值传递(数组副本),避免引用类型导致的变更检测异常
  • 同步表单控件的禁用、触摸等状态,保证组件与表单系统的一致性
  • 组件输入输出清晰,通过@Input接收配置项,内部逻辑封装,便于多处复用

内容的提问来源于stack exchange,提问作者user9081655

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 13:05:06