自定义 angular 双态复选框组件问题

A custom angular two-state checkbox component issue

我正在尝试创建一个自定义复选框组件来替换旧的 NG Prime 版本,该版本一直在使用,但我想 leaner/cleaner 添加一些 aria 点并利用更多复选框直接属性。尽管我 运行 遇到的问题(我猜)是更新值访问器 / ngmodel 并且可以使用一些指导来解决我显然遗漏的问题...

我认为我所拥有的已经足够简单了,但显然不是。这个想法是 @Input() binary 默认设置为 true,期望 ngModel 有一个布尔值可以使用,但我在初始化时没有从 ngModel 获得值。我需要 ngModel 将它传达给组件,尽管当它像 <app-checkbox [ngModel]="blah"></app-checkbox> 一样独立使用时,但如果它用于反应式 angular 形式等,也有 value_accessor 按预期工作.

在我的实例中,我还收到“NG0303:无法绑定到 'checked',因为它不是 'app-checkbox' 的已知 属性”,但它不在我不明白的 stackblitz。

A Stackblitz showing the implementation / problem

(例子是angular12,我用的是13,但是例子没问题)

以及快速代码参考;

.ts

import {
  Component,
  OnInit,
  Input,
  Output,
  EventEmitter,
  forwardRef,
  ViewChild,
  ElementRef,
  ChangeDetectorRef,
  ChangeDetectionStrategy,
} from '@angular/core';
import {
  NG_VALUE_ACCESSOR,
  ControlValueAccessor,
  FormControl,
} from '@angular/forms';
import { ObjectUtils } from '../objectUtils';

export const CHECKBOX_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CheckboxComponent),
  multi: true,
};

@Component({
  selector: 'app-checkbox',
  templateUrl: './checkbox.component.html',
  styleUrls: ['./checkbox.component.css'],
  providers: [CHECKBOX_VALUE_ACCESSOR],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckboxComponent implements OnInit, ControlValueAccessor {
  @Input() value: any;
  @Input() name: string;
  @Input() disabled: boolean;

  @Input() label: string;
  @Input() ariaLabelledBy: string;
  @Input() ariaLabel: string;
  @Input() tabindex: number;
  @Input() id: string;
  @Input() labelStyleClass: string;
  @Input() formControl: FormControl;
  @Input() required: boolean = false;
  @Input() isValid: boolean = true;
  @Input() invalidMessage: string;

  // This is set true by default now since every instance at the time this replacement component was made requires it for a boolean value bound to ngModel
  // If in the future we start using angular reactive forms more it can be toggled at the instance.
  @Input() binary: boolean = true;

  @ViewChild('cb') inputViewChild: ElementRef;

  @Output() onChange: EventEmitter<any> = new EventEmitter();

  isChecked: boolean = false;

  focused: boolean = false;
  model: any;
  onModelChange: Function = () => {};
  onModelTouched: Function = () => {};

  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit(): void {
    this.id = this.id
      ? this.id
      : `tcl-cb-${Math.random()
          .toString()
          .substr(2, length ? length : 6)}`;
  }

  updateModel(event) {
    let newModelValue;

    if (!this.binary) {
      if (this.isChecked) {
        newModelValue = this.model.filter(
          (val) => !ObjectUtils.equals(val, this.value)
        );
      } else {
        newModelValue = this.model ? [...this.model, this.value] : [this.value];
      }

      this.onModelChange(newModelValue);
      this.model = newModelValue;

      if (this.formControl) {
        this.formControl.setValue(newModelValue);
      }
    } else {
      newModelValue = this.isChecked;
      this.model = newModelValue;
      this.onModelChange(newModelValue);
    }

    this.onChange.emit({ checked: newModelValue, originalEvent: event });
  }

  handleChange(event) {
    this.isChecked = event.srcElement.checked;
    if (!this.disabled) {
      this.updateModel(event);
    }
  }

  onFocus() {
    this.focused = true;
  }

  onBlur() {
    this.focused = false;
    this.onModelTouched();
  }

  focus() {
    this.inputViewChild.nativeElement.focus();
  }

  writeValue(model: any): void {
    this.model = model;
    this.cd.markForCheck();
  }

  registerOnChange(fn: Function): void {
    this.onModelChange = fn;
  }

  registerOnTouched(fn: Function): void {
    this.onModelTouched = fn;
  }

  setDisabledState(val: boolean): void {
    this.disabled = val;
    this.cd.markForCheck();
  }
}

html

<div role="group" class="app-checkbox">
  <input
    #cb
    type="checkbox"
    [attr.id]="id"
    [attr.name]="name"
    [disabled]="disabled"
    [value]="value"
    [checked]="isChecked"
    [attr.tabindex]="tabindex"
    [attr.required]="required"
    [attr.aria-labelledby]="ariaLabelledBy"
    [attr.aria-label]="ariaLabel"
    [attr.aria-checked]="isChecked"
    (focus)="onFocus()"
    (blur)="onBlur()"
    (change)="handleChange($event)"
  />
  <label *ngIf="label" [attr.for]="id" [class]="labelStyleClass">
    {{ label }} <sup *ngIf="required">*</sup>
  </label>
  <div *ngIf="!isValid" class="tcl-checkbox-invalid-msg">
    {{ invalidMessage }}
  </div>
</div>

任何见解都将不胜感激,特别是如果有任何其他遗忘的话!

这会起作用,而且您还可以简化 updateModel 函数。逻辑有点复杂。

model 在组件加载后发生变化,您只需要捕获它并将 isChecked 的值设置为组件外部提供的值即可。

  @Input()
  get model() {
    return this.isChecked;
  }
  set model(v: any) {
    this.isChecked = v;
  }

I need ngModel to communicate that to the component though when it's used standalone as like <app-checkbox [ngModel]="blah"> but also have value_accessor working as expected if it's used in say a reactive angular form etc.

在您的情况下,ngModel 确实将值传达给您的自定义复选框组件,但您没有使用模板中收到的值将其绑定到 input

使用时

<app-checkbox [ngModel]="isChecked"></app-checkbox>

<!-- If isChecked is FormControl name -->
<app-checkbox formControlName="isChecked"></app-checkbox>

writeValue 方法被调用,将表单控件值传递给它。

writeValue 方法中您设置 this.model,但在您的模板中您使用 isChecked 绑定到输入 checked 属性,因此该复选框未选中。您需要将从表单模块收到的值绑定到自定义组件中的正确 属性,在您的情况下它将是:

// within writeValue method
this.isChecked = model;

I also get a "NG0303: Can't bind to 'checked' since it isn't a known property of 'app-checkbox'" in my instance, but it's not on the stackblitz which I don't understand.

如果您在执行以下操作时遇到上述错误,那么这是一个有效错误,您也会在您的 stackblitz 示例中遇到该错误。原因是,CheckboxComponent 没有任何名为 checked.

的输入 属性
<app-checkbox [checked]="isChecked"></app-checkbox>