Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular ControlValueAccessor checkbox always true when using SVG

I have a custom checkbox component that currently uses some Material Icons icons to display. I'm using the component in a few places, but I'm having trouble with one specific instance.

I have a grid with checkboxes along the left side to select multiple rows at once. When <i> is used to display the icon from the font, these checkboxes work properly. When I switch to using an SVG instead, these checkboxes break. I can only select one at a time, and I can't deselect one that's selected. If I check a box, it checks like you'd expect. If I click it again, nothing happens--the box stays checked. If I check a different box, the first one is unchecked.

This is the original checkbox component template:

<label class="my-checkbox" [class.pointer]="!disabled">
  <input
      type="checkbox"
      class="form-control norowclick"
      [class.indeterminateinput]="indeterminate"
      [checked]="checked"
      [(ngModel)]="value"
      (blur)="onBlur()"
      [disabled]="disabled"
  >
  <i class="material-icons md-18 indeterminate norowclick">indeterminate_check_box</i>
  <i class="material-icons md-18 checked norowclick">check_box</i>
  <i class="material-icons md-18 unchecked norowclick">check_box_outline_blank</i>
</label>

This is the new template, using angular-svg-icon. When rendered, there's an <svg-icon> element with the <svg> element as its only child. This is the only change made to any of the code, this is what causes it to break.

<label class="my-checkbox" [class.pointer]="!disabled">
  <input
      type="checkbox"
      class="form-control norowclick"
      [class.indeterminateinput]="indeterminate"
      [checked]="checked"
      [(ngModel)]="value"
      (blur)="onBlur()"
      [disabled]="disabled"
  >
  <svg-icon name="indeterminate_check_box" class="icon-18 indeterminate norowclick"></svg-icon>
  <svg-icon name="check_box" class="icon-18 checked norowclick"></svg-icon>
  <svg-icon name="check_box_outline_blank" class="icon-18 unchecked norowclick"></svg-icon>
</label>

This is the checkbox component code:

@Component({
  selector: 'my-checkbox',
  templateUrl: './my-checkbox.component.html',
  styleUrls: ['./my-checkbox.component.less'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyCheckboxComponent),
      multi: true
    }
  ]
})
export class MyCheckboxComponent implements OnInit, ControlValueAccessor {
  @Input() formControlName: string;
  @Input() checked: boolean = false;
  @Input() indeterminate: boolean = false;
  @Input() disabled: boolean = false;
  public control: AbstractControl;

  private innerValue: any = '';
  private controlContainer: ControlContainer;

  private onTouchedCallback: () => void = noop;
  private onChangeCallback: (_: any) => void = noop;

  constructor(
    @Optional()
    @Host()
    @SkipSelf()
    private _controlContainer: ControlContainer
  ) {
    this.controlContainer = _controlContainer;
  }

  ngOnInit() {
    this._getFormControl();
  }

  private _getFormControl() {
    if (this.controlContainer) {
      if (this.formControlName) {
        this.control = this.controlContainer.control.get(this.formControlName);
      }
    }
  }

  get value(): any {
    return this.innerValue;
  }

  set value(v: any) {
    if (v !== this.innerValue) {
      this.indeterminate = false;
      this.innerValue = v;
      this.onChangeCallback(v);
    }
  }

  onBlur() {
    this.onTouchedCallback();
  }

  writeValue(value: any) {
    if (value !== this.innerValue) {
      this.innerValue = value;
    }
  }

  registerOnChange(fn: any) {
    this.onChangeCallback = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouchedCallback = fn;
  }
}

The grid column is rendered as a template like this. When passed to the checkSecret() function, $event.target.checked is always true in the new version.

<ng-template #checkBoxTemplate let-value="value" let-item="item">
  <div class="rowCheck">
    <span *ngIf="!item['isFolder']" [class.rowHoverInline]="!hasSelections()">
      <my-checkbox class="norowclick" [checked]="item['isSelected']" (change)="checkSecret(item, $event)"></my-checkbox>
    </span>
  </div>
</ng-template>
like image 869
vaindil Avatar asked Apr 24 '19 17:04

vaindil


1 Answers

I am not sure but I suspect something weird with the input state and events management. Here is an attempt to quick-fix that I have absolutely not tested.

<label class="my-checkbox" [class.pointer]="!disabled">
  <input
      type="checkbox"
      class="form-control norowclick"
      [class.indeterminateinput]="indeterminate"
      [checked]="checked"
      (change)="onInputChange($event)" <-- CHANGE HERE ->
      (blur)="onBlur()"
      [disabled]="disabled"
  >
  <i class="material-icons md-18 indeterminate norowclick">indeterminate_check_box</i>
  <i class="material-icons md-18 checked norowclick">check_box</i>
  <i class="material-icons md-18 unchecked norowclick">check_box_outline_blank</i>
</label>

@Component({
  selector: 'my-checkbox',
  templateUrl: './my-checkbox.component.html',
  styleUrls: ['./my-checkbox.component.less'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyCheckboxComponent),
      multi: true
    }
  ]
})
export class MyCheckboxComponent implements OnInit, ControlValueAccessor {
  @Input() formControlName: string;
  @Input() checked: boolean = false;
  @Input() indeterminate: boolean = false;
  @Input() disabled: boolean = false;
  public control: AbstractControl;

  private innerValue: any = '';
  private controlContainer: ControlContainer;

  private onTouchedCallback: () => void = noop;
  private onChangeCallback: (_: any) => void = noop;

  constructor(
    @Optional()
    @Host()
    @SkipSelf()
    private _controlContainer: ControlContainer
  ) {
    this.controlContainer = _controlContainer;
  }

  ngOnInit() {
    this._getFormControl();
  }

  private _getFormControl() {
    if (this.controlContainer) {
      if (this.formControlName) {
        this.control = this.controlContainer.control.get(this.formControlName);
      }
    }
  }

 // CHANGE HERE 
 onInputChange(event) {
    const newValue: boolean = event.target.checked;
    if (newValue !== this.innerValue) {
      this.indeterminate = false;
      this.innerValue = v;
      this.onChangeCallback(v);
    }
  }

  onBlur() {
    this.onTouchedCallback();
  }

  writeValue(value: any) {
    if (value !== this.innerValue) {
      this.innerValue = value;
    }
  }

  registerOnChange(fn: any) {
    this.onChangeCallback = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouchedCallback = fn;
  }
}

Beside that, I want to tell you that (from my comprehension) you are supposed to use [(ngModel)] when calling your custom control value accessor. like :

<my-checkbox class="norowclick" [(ngModel)]="item['isSelected']"></my-checkbox>

or, if you really want to catch the ngModel change to do something else than updating the value :

<my-checkbox class="norowclick" [ngModel]="item['isSelected']" (ngModelChange)="checkSecret(item, $event)"></my-checkbox>

Then the overriden writeValue can be used to update your inner model (innerValue) catching change events from the parent component.

To send model changes from your ControlValueAccessor back to your parent component, you use the callback registered in registerOnChange, I think you did it right.

Also I have the feeling that variables checked and innerValue might be kind of redundant.

like image 159
Julien Avatar answered Nov 12 '22 10:11

Julien