mseemann/angular2-mdl

View on GitHub
projects/core/src/lib/textfield/mdl-textfield.component.ts

Summary

Maintainability
C
1 day
Test Coverage
A
98%
import {
  Component,
  DoCheck,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostBinding,
  Inject,
  InjectionToken,
  Input,
  OnChanges,
  Optional,
  Output,
  Renderer2,
  ViewChild,
  ViewEncapsulation,
} from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";

import { toBoolean } from "../common/boolean-property";
import { toNumber } from "../common/number.property";
import { noop } from "../common/noop";

export const DISABLE_NATIVE_VALIDITY_CHECKING = new InjectionToken<boolean>(
  "disableNativeValidityChecking"
);

let nextId = 0;

const IS_FOCUSED = "is-focused";
const IS_DISABLED = "is-disabled";
const IS_INVALID = "is-invalid";
const IS_DIRTY = "is-dirty";

@Component({
  selector: "mdl-textfield",
  template: `
    <div *ngIf="!icon">
      <textarea
        *ngIf="rows"
        #input
        [rows]="rows"
        class="mdl-textfield__input"
        type="text"
        [attr.name]="name"
        [id]="id"
        [placeholder]="placeholder ? placeholder : ''"
        (focus)="onFocus($event)"
        (blur)="onBlur($event)"
        (keydown)="keydownTextarea($event)"
        (keyup)="onKeyup($event)"
        [(ngModel)]="value"
        [disabled]="disabled"
        [required]="required"
        [autofocus]="autofocus"
        [readonly]="readonly"
        [maxlength]="maxlength"
      ></textarea>
      <input
        *ngIf="!rows"
        #input
        class="mdl-textfield__input"
        [type]="type"
        [attr.name]="name"
        [id]="id"
        [pattern]="pattern ? pattern : '.*'"
        [attr.min]="min"
        [attr.max]="max"
        [attr.step]="step"
        [placeholder]="placeholder ? placeholder : ''"
        [autocomplete]="autocomplete ? autocomplete : ''"
        (focus)="onFocus($event)"
        (blur)="onBlur($event)"
        (keyup)="onKeyup($event)"
        [(ngModel)]="value"
        [disabled]="disabled"
        [required]="required"
        [autofocus]="autofocus"
        [readonly]="readonly"
        [attr.tabindex]="tabindex"
        [maxlength]="maxlength"
      />
      <label class="mdl-textfield__label" [attr.for]="id">{{ label }}</label>
      <span class="mdl-textfield__error">{{ errorMessage }}</span>
    </div>
    <div *ngIf="icon">
      <button mdl-button mdl-button-type="icon" (click)="setFocus()">
        <mdl-icon>{{ icon }}</mdl-icon>
      </button>
      <div class="mdl-textfield__expandable-holder">
        <input
          #input
          class="mdl-textfield__input"
          [type]="type"
          [attr.name]="name"
          [id]="id"
          [pattern]="pattern ? pattern : '.*'"
          [attr.min]="min"
          [attr.max]="max"
          [attr.step]="step"
          [placeholder]="placeholder ? placeholder : ''"
          [autocomplete]="autocomplete ? autocomplete : ''"
          (focus)="onFocus($event)"
          (blur)="onBlur($event)"
          (keyup)="onKeyup($event)"
          [(ngModel)]="value"
          [disabled]="disabled"
          [required]="required"
          [autofocus]="autofocus"
          [readonly]="readonly"
          [attr.tabindex]="tabindex"
          [maxlength]="maxlength"
        />
        <label class="mdl-textfield__label" [attr.for]="id">{{ label }}</label>
        <span class="mdl-textfield__error">{{ errorMessage }}</span>
      </div>
    </div>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MdlTextFieldComponent),
      multi: true,
    },
  ],
  encapsulation: ViewEncapsulation.None,
})
/* eslint-disable  @angular-eslint/no-conflicting-lifecycle */
export class MdlTextFieldComponent
  implements ControlValueAccessor, OnChanges, DoCheck
{
  // eslint-disable-next-line
  @Output("blur")
  blurEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
  // eslint-disable-next-line
  @Output("focus")
  focusEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
  // eslint-disable-next-line
  @Output("keyup")
  keyupEmitter: EventEmitter<KeyboardEvent> = new EventEmitter<KeyboardEvent>();
  @ViewChild("input")
  inputEl: ElementRef | undefined;
  @Input()
  type = "text";
  @Input()
  label: string | undefined;
  @Input()
  pattern: string | undefined;
  @Input()
  min: number | string | undefined;
  @Input()
  max: number | string | undefined;
  @Input()
  step: number | string | undefined;
  @Input()
  name: string | undefined;
  @Input()
  id = `mdl-textfield-${nextId++}`;
  // eslint-disable-next-line
  @Input("error-msg")
  errorMessage: string | undefined;
  @HostBinding("class.has-placeholder")
  @Input()
  placeholder: string | undefined;
  @Input()
  autocomplete: string | undefined;
  @HostBinding("class.mdl-textfield--expandable")
  @Input()
  icon: string | undefined;
  @Input()
  tabindex: number | string | null = null;
  @Input()
  maxlength: number | string | null = null;
  @HostBinding("class.mdl-textfield")
  isTextfield = true;
  @HostBinding("class.is-upgraded")
  isUpgraded = true;
  private valueIntern: string | number | null = null;

  private readonly el: HTMLElement;
  private onTouchedCallback: () => void = noop;
  private onChangeCallback: (_: unknown) => void = noop;
  private disabledIntern = false;
  private readonlyIntern = false;
  private requiredIntern = false;
  private autofocusIntern = false;
  private isFloatingLabelIntern = false;
  private rowsIntern: number | undefined | null = null;
  private maxrowsIntern = -1;
  // @experimental
  private disableNativeValidityCheckingIntern = false;

  constructor(
    private renderer: Renderer2,
    private elmRef: ElementRef,
    @Optional()
    @Inject(DISABLE_NATIVE_VALIDITY_CHECKING)
    private nativeCheckGlobalDisabled: boolean
  ) {
    this.el = elmRef.nativeElement;
  }

  get value(): string | number | null {
    return this.valueIntern;
  }

  @Input() set value(v: string | number | null) {
    this.valueIntern =
      this.type === "number" ? (v === "" ? null : parseFloat(v as string)) : v;
    this.onChangeCallback(this.value);
  }

  @Input()
  get disabled(): boolean {
    return this.disabledIntern;
  }

  set disabled(value: boolean | string) {
    this.disabledIntern = toBoolean(value);
  }

  @Input()
  get readonly(): boolean {
    return this.readonlyIntern;
  }

  set readonly(value: boolean) {
    this.readonlyIntern = toBoolean(value);
  }

  @Input()
  get required(): boolean {
    return this.requiredIntern;
  }

  set required(value: boolean | string) {
    this.requiredIntern = toBoolean(value);
  }

  @Input()
  get autofocus(): boolean {
    return this.autofocusIntern;
  }

  set autofocus(value: boolean | string) {
    this.autofocusIntern = toBoolean(value);
  }

  @HostBinding("class.mdl-textfield--floating-label")
  @Input("floating-label")
  get isFloatingLabel(): boolean {
    return this.isFloatingLabelIntern;
  }

  set isFloatingLabel(value: boolean | string) {
    this.isFloatingLabelIntern = toBoolean(value);
  }

  @Input()
  get rows(): number | string | null | undefined {
    return this.rowsIntern;
  }

  set rows(value: number | string | null | undefined) {
    this.rowsIntern = toNumber(value);
  }

  @Input()
  get maxrows(): number {
    return this.maxrowsIntern;
  }

  set maxrows(value: number | string | null) {
    this.maxrowsIntern = toNumber(value) ?? -1;
  }

  @Input()
  get disableNativeValidityChecking(): boolean | string {
    return this.disableNativeValidityCheckingIntern;
  }

  set disableNativeValidityChecking(value: boolean | string) {
    this.disableNativeValidityCheckingIntern = toBoolean(value);
  }

  public writeValue(value: string | number): void {
    this.valueIntern = value;
    this.checkDirty();
  }

  public registerOnChange(fn: () => unknown): void {
    this.onChangeCallback = fn;
  }

  public registerOnTouched(fn: () => unknown): void {
    this.onTouchedCallback = fn;
  }

  public setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle
  ngOnChanges(): void {
    this.checkDisabled();
  }

  // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle
  ngDoCheck(): void {
    this.checkValidity();
    this.checkDirty();
  }

  setFocus(): void {
    if (!this.inputEl) {
      return;
    }
    (this.inputEl.nativeElement as HTMLInputElement).dispatchEvent(
      new Event("focus")
    );
  }

  keydownTextarea($event: KeyboardEvent): void {
    const currentRowCount =
      this.inputEl?.nativeElement.value.split("\n").length;
    // eslint-disable-next-line
    if ($event.keyCode === 13) {
      if (currentRowCount >= this.maxrows && this.maxrows !== -1) {
        $event.preventDefault();
      }
    }
  }

  // model value.
  triggerChange(event: Event): void {
    this.value = (event.target as HTMLInputElement).value;
    this.onTouchedCallback();
  }

  onFocus(event: FocusEvent): void {
    this.renderer.addClass(this.el, IS_FOCUSED);
    this.focusEmitter.emit(event);
  }

  onBlur(event: FocusEvent): void {
    this.renderer.removeClass(this.el, IS_FOCUSED);
    this.onTouchedCallback();
    this.blurEmitter.emit(event);
  }

  onKeyup(event: KeyboardEvent): void {
    this.keyupEmitter.emit(event);
  }

  private checkDisabled() {
    if (this.disabled) {
      this.renderer.addClass(this.el, IS_DISABLED);
    } else {
      this.renderer.removeClass(this.el, IS_DISABLED);
    }
  }

  private checkValidity() {
    // check the global setting - if globally disabled do no check
    if (this.nativeCheckGlobalDisabled === true) {
      return;
    }
    // check local setting - if locally disabled do no check
    if (this.disableNativeValidityChecking) {
      return;
    }
    if (this.inputEl && this.inputEl.nativeElement.validity) {
      if (!this.inputEl.nativeElement.validity.valid) {
        this.renderer.addClass(this.el, IS_INVALID);
      } else {
        this.renderer.removeClass(this.el, IS_INVALID);
      }
    }
  }

  private checkDirty() {
    const dirty =
      this.inputEl &&
      this.inputEl.nativeElement.value &&
      this.inputEl.nativeElement.value.length > 0;
    if (dirty) {
      this.renderer.addClass(this.el, IS_DIRTY);
    } else {
      this.renderer.removeClass(this.el, IS_DIRTY);
    }
  }
}