自定义 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>
我正在尝试创建一个自定义复选框组件来替换旧的 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>