javascripthtmlnode.jswebsitespark

How can I make this recursive function asynchronous?


function SetText(gg = `textttttt 😀`, cmd = `sudo --info`) {
    window.scrollTo({ top: 0, behavior: 'smooth' });
    if (document.getElementsByClassName('demo').length > 0) {
        var i = 0;
        var speed = 60;

        document.getElementsByClassName('demo')[0].innerHTML = `<code class="shell-session demo hljs nginx"><span class="hljs-attribute">Website</span> <span class="hljs-regexp">~ $</span> ${cmd}`;

        function typeItOut() {
            if (i < gg.length) {
                document.getElementsByClassName('demo')[0].innerHTML += gg.charAt(i);
                i++;
                setTimeout(typeItOut, speed);
            }
        }
        setTimeout(typeItOut, 1800);
    }
}

so that's the code, I want every time I click a button on my website it waits until the recursive finish then starts another one...


Solution

  • Can you use async/await?

    If you can, this will make it much easier to in effect "pause" each iteration through your string by a given timeout duration (see the handleIterateString class function below).

    This async handleIterateString function will "pause" at each await keyword, and wait until the promise returned by the await expression has been resolved. Only then will it continue executing the async function.

    Also, you can "pause" the execution of the async function where you initiate a new complete iteration through your string (see the await demo.handleIterateString call inside the async function SetText below.

    In this way, you can wait for the entire iteration (i.e. typing behaviour) to finish before decrementing your click queue count.

    If you have click events left in your queue, you can at that point call SetText recursively.


    Simply put: using async/await makes it much easier to both control the speed of the typing behaviour, and wait for your typing behaviour to complete before doing anything else.

    Try running the code snippet below.

    class Typer {
      /**
       * @description delays execution for a given amount of time
       * @param {number} ms - time in milliseconds
       * @returns {Promise<void>}
       *
       * @private
       */
      #delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
      /**
       * @description html for for displaying queue information
       * @returns {{hasQueue: string, noQueue: string}}
       * @private
       */
      get #html() {
        return {
          hasQueue: `Click events waiting in queue: <span class="tag is-danger is-light is-large">${this.state.queueCount}</span>`,
          noQueue: 'Queue is clear',
        };
      }
    
      /**
       * @description renders queue count information
       * @returns {void}
       * @private
       */
      #renderCountText = () => {
        const hasQueue = this.state.queueCount > 0;
    
        const fn = hasQueue ? 'add' : 'remove';
        document.getElementById('type-btn').classList[fn]('is-danger');
    
        const htmlContent = this.#html[hasQueue ? 'hasQueue' : 'noQueue'];
        this.render(htmlContent, '.queueCount');
      };
    
      /**
       * @description accepts a html selector string
       * @param {string} selector
       */
      constructor(selector) {
        this.selector = selector;
      }
    
      /**
       * @description state of typer instance
       * @const {{queueCount: number, speed: number}}
       * @public
       */
      state = {
        queueCount: -1,
        speed: 30,
      };
    
      /**
       * @description appends a html string to the instance's html element
       * @param {string} html
       * @returns {void}
       * @public
       */
      append = (html) => {
        document.querySelector(this.selector).innerHTML += html;
      };
    
      /**
       * @description renders given html string inside element with given selector
       * @param {string} html
       * @param {string} [el]
       * @returns {void}
       * @public
       */
      render = (html, el = this.selector) => {
        document.querySelector(el).innerHTML = html;
      };
      /**
       * @description confirms existence of the instance's selector in the DOM
       * @returns {boolean}
       * @public
       */
      exists = () => !!document.querySelector(this.selector);
    
      /**
       * @description
       * - iterates through the passed string and calls
       *   the passed listener on each character in the string
       * - waits for the given time from the state's 'speed' property,
       *   before proceeding to the next iteration
       *
       * @param {string} string
       * @param {string} listener - function to call on each character of string
       * @returns {Promise<void>}
       *
       * @async
       * @public
       */
      handleIterateString = async (string, listener) => {
        for (let i of string) {
          listener(i);
          await this.#delay(this.state.speed);
        }
      };
    
      /**
       * @description increments the queue count in the state by one
       * @public
       * @returns {void}
       */
      incrementQueue = () => {
        this.state.queueCount++;
        this.#renderCountText();
      };
    
      /**
       * decrements the queue count in the state by one
       * @public
       * @returns {void}
       */
      decrementQueue = () => {
        this.state.queueCount--;
        this.#renderCountText();
      };
    }
    
    // instantiate demo
    const demo = new Typer('.demo');
    
    async function SetText(
      gg = `the puppy goes woof 🐶 woof woof woof...`,
      cmd = `sudo --info`
    ) {
      window.scrollTo({
        top: 0,
        behavior: 'smooth',
      });
      if (demo.exists()) {
        const html = `<code class="shell-session demo hljs nginx"><span class="hljs-attribute">Website</span> <span class="hljs-regexp">~ $</span> ${cmd}`;
    
        // render HTML container
        demo.render(html);
    
        // do typing
        await demo.handleIterateString(gg, demo.append);
    
        demo.decrementQueue();
    
        if (demo.state.queueCount >= 0) {
          SetText();
        }
      }
    }
    
    document.getElementById('type-btn').addEventListener('click', async () => {
      if (demo.state.queueCount === -1) {
        SetText();
      }
      demo.incrementQueue();
    });
    .form {
      display: flex;
      justify-content: space-between;
    }
    
    .select-box {
      display: flex;
      align-items: center;
    }
    
    label {
      margin-right: 10px;
      font-size: 0.8em;
    }
    
    .queueCount {
      min-height: 40px;
    }
    
    .demo {
      background: #000;
      color: #fff;
      min-height: 80px;
    }
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">
    
    <div class="container">
      <div class="form mb-3">
        <button class="button is-primary" id="type-btn">
        <span>Click me</span>
        </button>
    
        <div class="select-box">
          <label>speed per character (ms)</label>
          <div class="select">
            <select></select>
          </div>
        </div>
      </div>
    
    
      <div class="content is-normal mb-3">
        <div class="queueCount is-size-5"></div>
      </div>
      <div class="content is-normal">
        <div class="demo is-size-5"></div>
      </div>
    </div>
    
    
    
    <!-- demo select menu -->
    <script>
      const select = document.querySelector('select');
      Array.from({
          length: 50,
        },
        (_, i) => (i + 1) * 30
      ).forEach((num) => {
        select.innerHTML += `<option value="${num}">${num}</option>`;
      });
    
      select.addEventListener('change', (e) => {
        demo.state.speed = parseInt(document.querySelector('select').value, 10);
      });
    </script>