2
\$\begingroup\$

A junior developer here.

I wrote a simple javascript custom element code here: https://codepen.io/rndbox/pen/mdWxryw as clean as I could with some advice from Junior Vs Senior Code - How To Write Better Code.

If possible, I would appreciate, if any willing Javascript Senior developer could improve / re-write my code in a more "pro" style so I can use his/her version as a reference/guideline on: better naming variables,better comment, use design pattern (if any), how to validate properly, check errors etc..

Here is the JS code:

// Define a custom Element
class Onixjs extends HTMLElement {
  // Set default options
  static defaultOptions = {
    speed: .65,
    pauseDuration: 2000,
    textsGap: '30%', // Margin between the leading and trailing text
    textTag: 'span',
    gradientsWidth: 15, // if > 0 add gradient (width = gradientsWidth px) at the start and the and of the container
  };

  /*== CONSTRUCTOR ==*/
  constructor() {
    super();

    this.defaultInnerHTML = this.innerHTML;

    this.options = { ...Onixjs.defaultOptions};
    this.options.container = this.parentNode;

    this.onixjs = null; // Ref to the slider text outer html
    this.textsGapUnities = ['%','px']; // Possible unity for the textsGap width

    // Animation variables and states
    this.currentTranslation = this.options.gradientsWidth;
    this.rafID = null;
    this.timeoutID = null;
    this.isPaused = false;

    this.startTimeout = 0;
  }

  // Set options
  set opts(opts) {
    this.options = {...this.options, ...opts}
    this.update();
  }

  /*== HELPERS ==*/
  debounce = function(func, ms) {
    // author: javascript.info
    const _ = this;
    return function() {
      clearTimeout(_.timeout);
      _.timeout = setTimeout(() => func.apply(this, arguments), ms);
    };
  }

  toHTML(string){
    const outer = document.createElement('div');
      outer.innerHTML = string;

    let html = string;

    if( outer.children.length == 1){
      html = outer.children[0];
    }
    else if( outer.children.length > 1){
      const el = document.createElement('div');
      el.appendChild(outer);
      html = el;
    }
    return html;
  }

  /*==============*/
  
  /*===== THE STYLE =======*/
  setStyle(){
    this.innerHTML = `
      <style>
        .onixjs *{
          box-sizing: border-box;
        }

        .onixjs{
          width: 100%;
          overflow: hidden;
          position: relative;
        }
        .onix__inner{
          display: flex;
        }

        /* text */
        .onix__text{
          margin: 0;
          display: inline-block;
          white-space: nowrap;
        }

        /* gradient */
        .onix__gradients{
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          pointer-events: none;
        }
        .onix__gradient{
          display: block;
          height: 100%;
          position: absolute;
          top: 0;

          /* to edit */
          /*border: 1px solid blue;*/
        }
        .onix__gradient--is_left{
          left: 0;
          background: linear-gradient(to left, rgba(255, 255, 255, 0), #fff 85%)
        }
        .onix__gradient--is_right{
          right: 0;
          background: linear-gradient(to right, rgba(255, 255, 255, 0), #fff 85%)
        }
      </style>
    `
  }

  // Check if the text overflows its parent
  textIsOverflowing(){

    if(!this.options.container){ return false };

    // Create a "ruler" element
    const ruler = document.createElement('div')
      ruler.innerHTML = this.defaultInnerHTML;

    // Hide the element
    const styles = {
      "visibility": "hidden",
      "position": "absolute"
    }
    Object.assign(ruler.style, styles);

    // Add to container...
    this.options.container.appendChild(ruler);

    // Check its width...
    const rulerWidth = ruler.offsetWidth;
    const containerWidth = this.options.container?
      this.options.container.offsetWidth : rulerWidth;

    // ...Then remove
    ruler.remove()

    return rulerWidth > containerWidth;
  }

  // Convert textsGap to Number
  // '10px' -> 10 
  // '10%' -> 10
  getTextsGapNb(unity){
    const gap = this.options.textsGap;

    const nb = Number(gap.replace(unity,''));
    return isNaN(nb)? 0 : nb;
  }

  // Convert textsGap value to absolute px value number
  // '10px' -> 10 
  // ex : '10%' -> 108.5
  getTextsGapPx(containerWidth){
    let unity = '%';
    const gap = this.options.textsGap;
    let pxValue = 0;

    this.textsGapUnities.forEach((unit)=>{
      if(gap.includes(unit)){
        unity = unit
      }
    })

    if(unity == '%'){
      pxValue = this.getTextsGapNb('%') / 100 * containerWidth;
    }else if(unity == 'px'){
      pxValue = this.getTextsGapNb('px')
    }
    return pxValue;
  }

  buildLeadingText(){
    /* creates leading/head text */
    const leadingText = document.createElement(this.options.textTag);
    leadingText.classList = 'onix__text onix__text--is_heading';
    leadingText.textContent = this.defaultInnerHTML;

    return leadingText;
  }

  buildTrailingText(){
    const trailingText = document.createElement( this.options.textTag );
      trailingText.className = 'onix__text onix__text--is_trailing';
      trailingText.textContent = this.defaultInnerHTML;

    // Add gap between heading and trailing text
    // The gap width must be higher than the gradients width
    const gradsWidth = this.options.gradientsWidth;
    const _gap = this.getTextsGapPx(this.options.container.offsetWidth);
    const gap =  _gap > gradsWidth ? _gap : gradsWidth ;

    trailingText.style.marginLeft = `${ gap }px`;

    return trailingText;
  }

  buildGradients(){
    if( this.options.gradientsWidth < 0){ return }

    /* creates & adds gradients */
    const gradsWidth = this.options.gradientsWidth;

    const gradients = document.createElement('div');
      gradients.className = 'onix__gradients';

    const gradientLeft = document.createElement('span');
      gradientLeft.className = 'onix__gradient onix__gradient--is_left';

    const gradientRight = document.createElement('span');
      gradientRight.className = 'onix__gradient onix__gradient--is_right';

    gradientLeft.style.width = `${gradsWidth}px`;
    gradientRight.style.width = `${gradsWidth}px`;

    gradients.appendChild( gradientLeft );
    gradients.appendChild( gradientRight );

    return gradients;    
  }

  buildConcreate(){
    if(this.toHTML(this.defaultInnerHTML) instanceof HTMLElement){
      throw new Error('onix-js takes a simple string, not an HTMLElement')
    }

    const docFrag =  document.createDocumentFragment();

    /* creates container and save reference*/
    this.onixjs = document.createElement('div');
    const onixjs = this.onixjs;
    onixjs.classList.add('onixjs');

    /* creates inner container */
    const onixInner = document.createElement('div');
    onixInner.classList.add('onix__inner');

    /* appending html */
    onixInner.appendChild(this.buildLeadingText());
    onixInner.appendChild(this.buildTrailingText());

    // Outer doesnt exist append it to container
    onixjs.appendChild(onixInner);
    onixjs.appendChild(this.buildGradients());

    /* append html to page */
    docFrag.appendChild(onixjs);

    try {
      // Clean then append new content
      this.setStyle()
      this.appendChild(docFrag);
    } catch (error) {
      console.error(error);
    }
  }

  buildDefaultContent(){
    this.innerHTML = this.defaultInnerHTML;
  }

  // Back to default text or init scroll text
  update(){
    const scrollWasInitialized = this.querySelector('.onixjs') !== null;
    const scrollShouldInitialize = this.textIsOverflowing();

    if(!scrollWasInitialized && scrollShouldInitialize){
      this.init();

    }else if(scrollWasInitialized && !scrollShouldInitialize){
      this.stopAnimation();
      this.onixjs.remove();
      this.buildDefaultContent();
    
    }else if(scrollWasInitialized && scrollShouldInitialize){
      this.stopAnimation();
      this.onixjs.remove();
      this.init();

    }else {
      this.stopAnimation();
    }
  }

  debouncedUpdate(){
    this.debounce(this.update.bind(this), 1500)();
  }

   /*== Animation ==*/
  setPos(position=0){
    const onixInner = this.onixjs.querySelector('.onix__inner');
    onixInner.style.cssText = `transform: translateX(${position}px)`;
  }

  // Manage scroll and check pause
  translateX(){
    const gapWidth = this.getTextsGapPx(this.options.container.offsetWidth);
    const gradientsWidth = this.options.gradientsWidth;

    const leading = this.onixjs.querySelector('.onix__text--is_heading');
    const maxTranslation = leading.offsetWidth + gapWidth;

    const maxTranslationWithOffset = maxTranslation - gradientsWidth;
    const step = this.options.speed;

    let newTranslation = this.currentTranslation > 0 ? 
      (this.currentTranslation - step) :
      - Math.abs((this.currentTranslation - step));

    // Should Pause the animation
    if(Math.abs(newTranslation) > (maxTranslationWithOffset)){

      this.isPaused = true
      this.currentTranslation = gradientsWidth
      this.setPos(gradientsWidth);

      // Continue after pause
      this.timeoutID = setTimeout(this.continue.bind(this), this.options.pauseDuration)
      
      return;
    }

    this.currentTranslation = newTranslation
    this.setPos(this.currentTranslation);
  }

  // Loop animation and pause animation
  loopTranslationAnimation(){
    if(!this.isPaused){
      this.translateX();
      this.rafID = window.requestAnimationFrame(this.loopTranslationAnimation.bind(this));
    }else{
      this.pause();
    }
  }

  initAnimation(){
    clearTimeout(this.startTimeout);
    this.startTimeout = window.setTimeout(this.loopTranslationAnimation.bind(this), this.options.pauseDuration);
  }

  // Loop animation with start position and delay
  beginAnimation(){
    this.isPaused = false;
    this.currentTranslation = this.options.gradientsWidth;
    this.setPos(this.currentTranslation);
    this.initAnimation();
  }

  continue(){
    this.isPaused = false;
    this.loopTranslationAnimation();
  }

  pause(){
    window.cancelAnimationFrame(this.rafID);
  }

  stopAnimation(){
    this.isPaused = true;
    clearTimeout(this.timeoutID);
    this.pause();
  }

  bindImmediateEvents(){
    window.addEventListener('resize', this.debouncedUpdate.bind(this));
  }
  unbindImmediateEvents(){
    window.removeEventListener('resize', this.debouncedUpdate.bind(this));
  }

  bindEvents(){
    // Pause
    this.onixjs.addEventListener('mouseover',this.stopAnimation.bind(this))

    // Continue
    this.onixjs.addEventListener('mouseleave',this.continue.bind(this))

  }

  init(){
    this.bindImmediateEvents();
    if(!this.textIsOverflowing()){return}
    this.buildConcreate();
    this.bindEvents();
    this.beginAnimation();

  }

  // Custom element cyclelife hooks
  connectedCallback(){
    this.init();
  }

  disconnectedCallback(){
    this.stopAnimation();
  }

}
customElements.define("onix-js", Onixjs);

/*================= Add Options ==================*/
const onixjs = document.querySelector("onix-js");
onixjs.opts = {
  speed: .8,
  textsGap: '40%',
  textTag: 'h3',
}

The HTML :

 <div class="player__sound_title_container">
          
   <!-- Use custom element -->
   <onix-js>
     If You Cant Ride Two Horses At Once...You Should Get Out Of The Circus
   </onix-js>
          
 </div>

Thanks very much!

Ps: the code is in english although french is my native language.

\$\endgroup\$
4
  • \$\begingroup\$ The video shows what "clean code" may look like but doesn't show how to get there. I would highly advise that you check out Wealcome and Michaël Azerhad (the founder) on his talks on what real TDD is. He happens to be french as well. Doing proper TDD will force you to write your code better. \$\endgroup\$ Jun 5 at 19:23
  • \$\begingroup\$ That being said, the things that sticks out to me as a big no no is that you've essentially written one class that handles everything: the framework-like logic, helpers, the styles. You're essentially mixing a lot of different concepts together. You should probably separate the different parts, which will make things a lot easier to test. \$\endgroup\$ Jun 5 at 19:27
  • 1
    \$\begingroup\$ Regarding comments, they're not really needed as your tests should logically explain how your code works. Clean code should be understandable just from looking at the code. Comments are useful for documentation purposes on how to use your library (js-doc for example). If you had to implement something complex without having a simpler solution, then sure a comment or two would help. \$\endgroup\$ Jun 5 at 19:31
  • \$\begingroup\$ Validation and error checking will come naturally with TDD. \$\endgroup\$ Jun 5 at 19:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.