swimlane/ngx-ui

View on GitHub
projects/swimlane/ngx-ui/src/lib/components/button-toggle/button-toggle-group.component.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { coerceBooleanProperty, BooleanInput } from '@angular/cdk/coercion';
import {
  Component,
  ChangeDetectionStrategy,
  forwardRef,
  Input,
  ViewEncapsulation,
  Output,
  EventEmitter,
  ContentChildren,
  QueryList,
  AfterViewInit,
  ChangeDetectorRef,
  OnDestroy
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ButtonToggleComponent } from './button-toggle.component';

const BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => ButtonToggleGroupComponent),
  multi: true
};

let nextId = 0;

@Component({
  selector: 'ngx-button-toggle-group',
  templateUrl: './button-toggle-group.component.html',
  styleUrls: ['./button-toggle-group.component.scss'],
  providers: [BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR],
  host: {
    role: 'group',
    class: 'ngx-button-toggle-group'
  },
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ButtonToggleGroupComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
  readonly UNIQUE_ID = `ngx-button-toggle-group-${++nextId}`;
  private _value = false;
  private _disabled;

  @Input() id: string = this.UNIQUE_ID;
  @Input() label;

  @Input()
  get value(): any {
    return this._value;
  }
  set value(newValue: any) {
    this._value = newValue;
    this.selectButtonToggle(newValue);

    this.animationHolderLeft = 0;
    this.animationHolderWidth = 0;

    this.cdr.markForCheck();
  }

  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: BooleanInput) {
    this._disabled = coerceBooleanProperty(value);

    this.checkChildren(this._disabled);
  }

  @Output() readonly valueChange = new EventEmitter();

  @ContentChildren(forwardRef(() => ButtonToggleComponent), {
    descendants: true
  })
  buttonToggles: QueryList<ButtonToggleComponent>;

  // eslint-disable-next-line @typescript-eslint/no-empty-function
  onChangeCallback: (value: any) => void = () => {};
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  onTouchedCallback: (value: any) => void = () => {};

  writeValue(obj: any): void {
    this.value = obj;
  }

  registerOnChange(fn: any): void {
    this.onChangeCallback = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouchedCallback = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  animationHolderLeft: number | undefined;
  animationHolderWidth = 0;

  get itemCount(): number {
    return this.buttonToggles?.length || 0;
  }

  readonly destroy$ = new Subject<void>();

  constructor(private readonly cdr: ChangeDetectorRef) {}

  ngOnDestroy(): void {
    this.destroy$.next();
  }

  ngAfterViewInit(): void {
    if (this.value) {
      this.selectButtonToggle(this.value);
    }

    this.checkChildren(this.disabled);

    this.listenChildren();
  }

  listenChildren() {
    this.buttonToggles?.forEach(toggle => {
      toggle.valueChange.pipe(takeUntil(this.destroy$)).subscribe(selectedToggleValue => {
        this.notifyChange(selectedToggleValue);
      });
    });
  }

  notifyChange(selectedToggleValue: any) {
    this.selectButtonToggle(selectedToggleValue);

    this.valueChange.emit(selectedToggleValue);
    this.onChangeCallback(selectedToggleValue);

    this.cdr.detectChanges();
  }

  selectButtonToggle(incomingValue: any): void {
    if (!this.buttonToggles) {
      return;
    }

    //before changing the active selection, calculate animation dimensions
    this.calcAnimationDimensions(incomingValue);

    this.clearButtonToggles();
    this.buttonToggles.forEach(toggle => {
      if (toggle.value !== undefined && toggle.value === incomingValue) {
        toggle.checked = true;
        toggle.markForCheck();
      }
    });
  }

  clearButtonToggles() {
    this.buttonToggles?.forEach(toggle => {
      toggle.checked = false;
    });
  }

  checkChildren(disabled: boolean) {
    this.buttonToggles?.forEach(toggle => {
      toggle.disabled = disabled;
      toggle.markForCheck();
    });
  }

  private calcAnimationDimensions(selectedToggleValue: any) {
    const newIncomingIndex = this.getToggleIndex(selectedToggleValue);

    let leftPosition = 0;
    this.buttonToggles.toArray().forEach((toggle, index) => {
      if (index < newIncomingIndex) {
        leftPosition += toggle.element?.nativeElement?.clientWidth || 0;
      }

      if (index === newIncomingIndex) {
        this.animationHolderWidth = toggle.element?.nativeElement.clientWidth - 4;
      }
    });

    this.animationHolderLeft = leftPosition + (newIncomingIndex + 0.5) * 2;
  }

  private getToggleIndex(incomingValue) {
    return this.buttonToggles?.toArray().findIndex(toggle => {
      return toggle.value === incomingValue;
    });
  }
}