Source

Validatable.ts

import merge from 'lodash/merge'

import {isSet} from 'src/util/isSet'

import {VPOptions} from 'src/interfaces/VPOptions'
import {ValidationStrategies} from 'src/interfaces/validation/ValidationStrategy'
import {ValidationLifecycle, ValidationCB} from 'src/interfaces/validation/ValidationLifecycle'

import {DOMMessaging} from 'src/lib/DOMMessaging'
import {EventEmitter} from 'src/mixins/EventEmitter'

import {ValidatableOptions} from 'src/models/VPOptions/ValidatableOptions'
import {getAttributeIfSet} from 'src/util/getAttributeIfSet'

const EEMessaging = EventEmitter(DOMMessaging);

/**
 * VPValidatable Generic
 * @description
 * Generic instance all Validatable instances inherit from. Defines default shared logic and interfaces.
 * @augments module:EventEmitter
 * @augments DOMMessaging
 */
export class Validatable<T extends ValidatableOptions<T>> extends EEMessaging {
  $options: T
  $element: HTMLElement
  $lifecycleElements: HTMLElement[]
  $strategies: ValidationStrategies
  $valid: boolean | null

  constructor (element: HTMLElement, options: (VPOptions<T> | ValidatableOptions<T>)) {
    super()

    this.$lifecycleElements = [];
    this.$element = element
    this.$lifecycleElements.push(element);
    this.$valid = null

    // This is a generic. If options aren't derived from ValidatableOptions, we throw
    if (!(options instanceof ValidatableOptions)) throw new Error('Options were unset');
    else {
      this.$options = merge(options, {
        ErrorClassName: getAttributeIfSet(element, 'vp-error-class', '-isError'),
        ValidClassName: getAttributeIfSet(element, 'vp-valid-class', '-isValid')
      }) as T;
    }

    this.setLifecycle(this.$options.Lifecycle)
    this.$strategies = {
      all: (fieldstatus: boolean[]) => fieldstatus.every((f: boolean) => f),
      some: (fieldstatus: boolean[]) => fieldstatus.some((f: boolean) => f),
      none: (fieldstatus: boolean[]) => fieldstatus.every((f: boolean) => !f),
      one: (fieldstatus: boolean[]) => fieldstatus.filter((f: boolean) => f).length === 1
    }

    // DOMMessaging
    this.$MessageClassName = this.$options.MessageClassName
    this.$MessageContainerClassName = this.$options.MessageContainerClassName

    // Allow for manually calling the messageNodeBuilder if it cannot be accomplished right away
    // Used in Vue Bindings
    if (this.$options.MessageAnchor instanceof HTMLElement) {
      this.generateMessageNode(this.$options.MessageAnchor, this.$options.MessagePOS)
    }
    // END DOMMessaging
  }

  get $isValid (): boolean | null {
    return this.$valid
  }

  set $isValid (isValid: boolean | null) {
    this.$valid = isValid

    if (isValid) {
      this.$lifecycleElements.forEach((element) => {
        element.classList.remove(this.$options.ErrorClassName)
        element.classList.add(this.$options.ValidClassName)
      })

      if (Array.isArray(this.$options.Lifecycle.Valid.CB)) {
        this.$options.Lifecycle.Valid.CB
          .forEach((CB: ValidationCB<T>) => CB(this))
      }

      const ValidMessage: (string | undefined) = this.$options.Lifecycle.Valid.Message
      if (typeof ValidMessage === 'string' && ValidMessage.length > 0) {
        this.addMessage(
          this.$options.Lifecycle.Valid.Message as string,
          this.$options.ValidClassName
        )
      }
    } else {
      this.$lifecycleElements.forEach((element) => {
        element.classList.remove(this.$options.ValidClassName)
        element.classList.add(this.$options.ErrorClassName)
      })

      if (Array.isArray(this.$options.Lifecycle.Invalid.CB)) {
        this.$options.Lifecycle.Invalid.CB
          .forEach((CB: ValidationCB<T>) => CB(this));
      }

      const InvalidMessage: (string | undefined) = this.$options.Lifecycle.Invalid.Message
      if (typeof InvalidMessage === 'string' && InvalidMessage.length > 0) {
        this.addMessage(
          this.$options.Lifecycle.Invalid.Message as string,
          this.$options.ErrorClassName
        )
      }
    }
  }

  /**
   * Scroll to the tracked element
   */
  scrollTo (): void {
    // While always true in a modern browser, we check due to limitations with JSDOM
    if (this.$options.ScrollAnchor instanceof Element
      && typeof this.$options.ScrollAnchor.scrollIntoView === 'function') {
      this.$options.ScrollAnchor.scrollIntoView(this.$options.ScrollOptions);
    }
    else {
      console.debug('[VP] Element Scrolling is unavailable.')
    }
  }

  setLifecycle (lifecycle: ValidationLifecycle<T>): void {
    const isValidationLifecycle = function (lifecycle: ValidationLifecycle<T>) {
      return isSet(lifecycle) &&
        ('Valid' in lifecycle || 'Invalid' in lifecycle)
    }

    const valid = this.$options.Lifecycle.Valid || {}
    const invalid = this.$options.Lifecycle.Invalid || {}
    this.$options.Lifecycle = {
      Valid: {
        Message: valid.Message,
        CB: valid.CB
      },
      Invalid: {
        Message: invalid.Message,
        CB: invalid.CB
      }
    }

    if (isValidationLifecycle(lifecycle)) {
      if (lifecycle.Valid) {
        if (typeof lifecycle.Valid.Message === 'string') {
          this.$options.Lifecycle.Valid.Message = lifecycle.Valid.Message
        }

        if (Array.isArray(lifecycle.Valid.CB)) {
          this.$options.Lifecycle.Valid.CB = lifecycle.Valid.CB
        } else if (typeof lifecycle.Valid.CB === 'function') {
          if (!Array.isArray(this.$options.Lifecycle.Valid.CB)) {
            this.$options.Lifecycle.Valid.CB = []
          }

          this.$options.Lifecycle.Valid.CB.push(lifecycle.Valid.CB)
        }
      }
      if (lifecycle.Invalid) {
        if (typeof lifecycle.Invalid.Message === 'string') {
          this.$options.Lifecycle.Invalid.Message = lifecycle.Invalid.Message
        }
        if (Array.isArray(lifecycle.Invalid.CB)) {
          this.$options.Lifecycle.Invalid.CB = lifecycle.Invalid.CB
        } else if (typeof lifecycle.Invalid.CB === 'function') {
          if (!Array.isArray(this.$options.Lifecycle.Invalid.CB)) {
            this.$options.Lifecycle.Invalid.CB = []
          }

          this.$options.Lifecycle.Invalid.CB.push(lifecycle.Invalid.CB)
        }
      }
    }
  }

  /**
   * Helper method to determine if the element is visible within the DOM
   * @param {HTMLElement} element - Element to test
   * @returns boolean
   */
  isElementVisible (element: HTMLElement): boolean {
    if (element instanceof HTMLElement) {
      return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
    }

    return false
  }
}