0
\$\begingroup\$

Button component that I really would like to be perfect, but I'm not confident about it.

Should I keep working on it or start over?

import Emitter from '../../modules/emitter';

export default class Button {
  emitter = new Emitter(this);

  #iconElementLoading;
  #iconElementLeft;
  #iconElementRight;
  #textElement;
  #options = {};
  #rendered = false;
  #validator = {
    element: value => value instanceof HTMLElement,
    text: value => typeof value === 'string' && value.length > 0,
    color: value => typeof value === 'string' && value.length > 2,
    iconLeft: value => typeof value === 'string' && value.length > 1,
    iconRight: value => typeof value === 'string' && value.length > 1,
    uppercase: value => typeof value === 'boolean',
    disabled: value => typeof value === 'boolean',
    loading: value => typeof value === 'boolean',
    onClick: value => typeof value === 'function'
  };

  #optionsHandler = {
    set: (target, key, value) => {
      if (this.#validator[key] === undefined || this.#validator[key](value) === false) {
        console.warn(`Button ${key} value is not valid`);
        return true;
      }

      this.#options[key] = value;
      return true;
    }
  };

  constructor(options) {
    this.options = new Proxy({}, this.#optionsHandler);

    for (const key in this.#validator) {
      if (options[key] !== undefined && this.#validator[key](options[key]) === true) {
        this.#options[key] = options[key];
      }
    }

    this.render();
    return this;
  }

  render() {
    this.empty();

    this.element = document.createElement('button');
    this.element.classList.add('m-button');

    if (this.#options.color) {
      if (this.#validator.color(this.#options.color) === true) {
        this.element.classList.add(`m-button--color-${this.#options.color}`);
      } else {
        console.warn('Button color value must be a string');
      }
    }

    this.#textElement = document.createElement('span');
    this.#textElement.classList.add('m-button__label');
    this.element.appendChild(this.#textElement);
    if (this.#validator.text(this.#options.text) === true) {
      this.#textElement.appendChild(document.createTextNode(this.#options.text));
    } else {
      console.error('Button text value must be a string and not empty');
      return;
    }

    if (this.#options.iconLeft) {
      if (this.#validator.iconLeft(this.#options.iconLeft) === true) {
        this.#iconElementLeft = this.#renderIconElement(this.#options.iconLeft);
        this.element.insertBefore(this.#iconElementLeft, this.element.firstChild);
      } else {
        console.warn('Button iconLeft value must be a string');
      }
    }
    if (this.#options.iconRight) {
      if (this.#validator.iconRight(this.#options.iconRight) === true) {
        this.#iconElementRight = this.#renderIconElement(this.#options.iconRight);
        this.element.appendChild(this.#iconElementRight);
      } else {
        console.warn('Button iconRight value must be a string');
      }
    }

    if (this.#options.uppercase) {
      if (this.#validator.uppercase(this.#options.uppercase) === true) {
        this.element.style.textTransform = 'uppercase';
      } else {
        console.warn('Button uppercase value must be a boolean');
      }
    }

    if (this.#options.disabled) {
      if (this.#validator.disabled(this.#options.disabled) === true) {
        this.element.disabled = true;
      } else {
        console.warn('Button disabled value must be a boolean');
      }
    }

    this.element.addEventListener('click', ev => {
      this.emit('click', ev);
      if (this.#options.onClick) {
        if (this.#validator.onClick(this.#options.onClick) === true) {
          this.#options.onClick.call(this, ev);
        } else {
          console.warn('Button onClick value must be a function');
        }
      }
    });

    if (this.#options.element) {
      this.#options.element.appendChild(this.element);
    }

    this.#rendered = true;

    return this.element;
  }

  empty() {
    if (this.element) {
      this.element.remove();
      this.element = undefined;
      this.#iconElementLoading = undefined;
      this.#iconElementLeft = undefined;
      this.#iconElementRight = undefined;
      this.#textElement = undefined;
      this.#rendered = false;
    }
  }

  #renderIconElement(icon) {
    const iconElement = document.createElement('i');
    iconElement.classList.add('fa');
    iconElement.classList.add(`fa-${icon}`);

    return iconElement;
  }

  get color() {
    return this.#options.color;
  }

  set color(color) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (color === undefined) {
      this.#removeColor();
      return;
    }

    if (this.#validator.color(color) === true) {
      this.#removeColor();
      this.element.classList.add(`m-button--color-${color}`);
      this.#options.color = color;
    } else {
      console.warn('Button color value must be a string or undefined if you want to remove the color');
    }
  }

  #removeColor() {
    if (this.#options.color) {
      this.element.classList.remove(`m-button--color-${this.#options.color}`);
      this.#options.color = undefined;
    }
  }

  get iconLeft() {
    return this.#options.iconLeft;
  }

  set iconLeft(icon) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (icon === undefined) {
      this.#removeIconLeft();
      return;
    }

    if (this.#validator.iconLeft(icon) === true) {
      this.#removeIconLeft();

      if (!this.#iconElementLeft) {
        this.#iconElementLeft = this.#renderIconElement(icon);
        this.element.insertBefore(this.#iconElementLeft, this.#textElement);
      }

      this.#iconElementLeft.classList.add(`fa-${icon}`);
      this.#options.iconLeft = icon;
    } else {
      console.warn('Button iconLeft value must be a string or undefined');
    }
  }

  #removeIconLeft() {
    if (this.#iconElementLeft) {
      this.#iconElementLeft.remove();
      this.#iconElementLeft = undefined;
    }
    if (this.#options.iconLeft) {
      this.#iconElementLeft.classList.remove(`fa-${this.#options.iconLeft}`);
      this.#options.iconLeft = undefined;
    }
  }

  get iconRight() {
    return this.#options.iconRight;
  }

  set iconRight(icon) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (icon === undefined) {
      this.#removeIconRight();
      return;
    }

    if (this.#validator.iconRight(icon) === true) {
      this.#removeIconRight();

      if (!this.#iconElementRight) {
        this.#iconElementRight = this.#renderIconElement(icon);
        this.element.appendChild(this.#iconElementRight);
      }

      this.#iconElementRight.classList.add(`fa-${icon}`);
      this.#options.iconRight = icon;
    } else {
      console.warn('Button iconRight value must be a string or undefined');
    }
  }

  #removeIconRight() {
    if (this.#iconElementRight) {
      this.#iconElementRight.remove();
      this.#iconElementRight = undefined;
    }

    if (this.#options.iconRight) {
      this.#iconElementRight.classList.remove(`fa-${this.#options.iconRight}`);
      this.#options.iconRight = undefined;
    }
  }

  get text() {
    return this.#options.text;
  }

  set text(text) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (this.#validator.text(text) === false) {
      console.warn('Button text value must be a string with a length of 1 or more or undefined if you want to remove the text');
      return;
    }

    if (!text) {
      this.#textElement.textContent = '';
      this.#options.text = undefined;
    } else if (typeof text === 'string') {
      this.#textElement.textContent = text;
      this.#options.text = text;
    }
  }

  get loading() {
    return this.#options.loading;
  }

  set loading(value) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (this.#validator.loading(value) === false) {
      console.warn('Button loading value must be a boolean');
      return;
    }

    if (value === false) {
      if (this.#iconElementLoading) {
        this.#iconElementLoading.classList.remove('m-button--loading');
        this.#iconElementLoading.remove();
        this.#iconElementLoading = undefined;
      }

      this.#options.loading = undefined;
      this.disabled = false;
    } else if (value === true) {
      if (!this.#iconElementLoading) {
        this.#iconElementLoading = this.#renderIconElement('spinner-third');
        this.#iconElementLoading.classList.add('m-button--loading');
        this.#iconElementLoading.classList.add('fa-duotone');
        this.#iconElementLoading.classList.add('fa-spin');
        this.element.prepend(this.#iconElementLoading);
      }

      this.#options.loading = true;
      this.disabled = true;
    }
  }

  get uppercase() {
    return this.#options.uppercase;
  }

  set uppercase(value) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (this.#validator.uppercase(value) === false) {
      console.warn('Button uppercase value must be a boolean');
      return;
    }

    this.#options.uppercase = value;

    if (value === true) {
      this.element.style.textTransform = 'uppercase';
    } else if (value === false) {
      this.element.style.textTransform = 'initial';
    }
  }

  get disabled() {
    return this.#options.disabled;
  }

  set disabled(value) {
    if (this.#rendered === false) {
      console.error('Button element is not rendered please call render() first');
      return;
    }

    if (this.#validator.disabled(value) === false) {
      console.warn('Button disabled value must be a boolean');
      return;
    }

    this.#options.disabled = value;
    this.element.disabled = value;
  }

  get rendered() {
    return this.#rendered;
  }
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Seems complex. What do you mean by "perfect"? Is there a specification? Could you share an example of this being used? Thanks. \$\endgroup\$
    – ggorlen
    Mar 24 at 18:04

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.