1
\$\begingroup\$

Assume $ is not the browser. Now have to implement $, it will take a string, which is a query, it will use the querySelector to select the element. (Reference: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)

$('text')

Now implement jquery like functions addClass and removeClass. (Reference: https://developer.mozilla.org/en-US/docs/Web/API/Element/classList)

$('#test').removeClass('blue').addClass('red');

Now implement jquery like functions delay. (Reference: https://api.jquery.com/delay/)

$('#test').removeClass('blue').delay(2000).delay(1000).addClass('red');

Code Sample

function $(selector) {
    let element = document.querySelector(selector)
    element.queue = []
    element.active = false
    return element
}

Element.prototype.next = function () {
    if (this.queue.length) this.runTask(this.queue.shift())
}

Element.prototype.runTask = function (callBack) {
    this.active = true
    callBack().then(() => {
        this.active = false
        this.next()
    })
}

Element.prototype.register = function(callBack){
    if (this.active) {
        this.queue.push(callBack)
    } else {
        this.runTask(callBack)
    }
}

Element.prototype.addClass = function (className) {
    that = this
    let callBack = () => new Promise(resolve => setTimeout(function () {
        that.classList.add(className)
        resolve()
    }, 0))
    this.register(callBack)
    return this
}

Element.prototype.removeClass = function (className) {
    that = this
    let callBack = () => new Promise(resolve => setTimeout(function () {
        that.classList.remove(className)
        resolve()
    }, 0))
    this.register(callBack)
    return this
}

Element.prototype.delay = function (ms) {
    that = this
    let callBack = () => new Promise(resolve => setTimeout(function () {
        resolve()
    }, ms))

    this.register(callBack)
    return this
}

$('#test')
.removeClass("red").delay(500)
.addClass("blue").delay(500).delay(500).removeClass("blue")
.delay(500).addClass("red").delay(500).removeClass("red")
.delay(500).addClass("blue").delay(500).removeClass("blue")
.delay(500).addClass("red").delay(500).removeClass("red")
<!DOCTYPE html>

<html>
    <head>
        <style>
            .blue{
                background-color: blue;
            }
            .red{
                background-color: red;
            }
        </style>
    </head>
    <body>
        <div id="test" style="width: 150px; height: 150px; margin:10px;" class="red"></div>
    </body>
</html>

Can anyone please review the Code?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Welcome to Code Review. Does the code work successfully? Can you tell us more about what the code is supposed to do? References are good for bonus context, but the problem you solved should be clearly stated in the question itself. Thank you. \$\endgroup\$
    – Mast
    Apr 29, 2020 at 11:30
  • 1
    \$\begingroup\$ Does it boil down to writing an alternative for addClass, removeClass and delay? Any limitations on this? What prompted you to do this? How does this differ from something like text.classList.add("foo")? \$\endgroup\$
    – Mast
    Apr 29, 2020 at 11:32

1 Answer 1

4
\$\begingroup\$

Mutating built-in prototypes is not a good idea. It's not only inelegant, it can also lead to conflicts when other scripts on the page expect the prototypes to be unmutated. (It can even damage future attempts to integrate functionality officially, if enough sites use the bad code.)

Instead of using Element.prototype, create your own class, and put methods onto its prototype.

If you want to emulate jQuery, you should probably select all elements that match the selector, not just the first one. (Using a collection of elements will also avoid errors if the collection is empty - jQuery doesn't error when methods are called on an empty collection)

The active flag would be more informative if it was named more precisely, perhaps call it methodInProgress.

that = this is an antipattern in modern Javascript. If you need to use the calling context from the outer scope, use arrow functions instead. (You're already using ES2015 in multiple places)

callback is a single word, not two, so the proper way to camelCase it would be to keep it as-is. Calling the arguments callBack instead could result in bugs later when other readers/writers of the code expect it to be formatted conventionally.

Promises should be reserved for asynchronous actions. If you're going to run something that's completely synchronous, using a Promise adds unnecessary and confusing noise. Consider having your functions return Promises only if they're asynchronous, and while processing the queue, if a callback returns a Promise, wait for the Promise to resolve before moving onto the next callback. If the callback doesn't return a Promise, you can still await it without errors, and the next queue callback will run immediately.

Since you have multiple methods which add callback to the queue and return this, consider passing those methods through a helper function to make the code a bit more DRY.

Refactored:

const $ = selector => new PsuedoJquery(selector);
class PsuedoJquery {
  constructor(selector) {
    this.elements = [...document.querySelectorAll(selector)];
    this.queue = [];
  }
  async runNext() {
    await this.queue.shift()();
    if (this.queue.length) {
      await this.runNext();
    } else {
      this.methodInProgress = false;
    }
  }
  register(callback) {
    this.queue.push(callback);
    // If errors are a possibility, catch them here
    if (!this.methodInProgress) {
      this.methodInProgress = true;
      this.runNext();
    }
  }
  makeChainable(callback) {
    this.register(callback);
    return this;
  }
  addClass(className) {
    return this.makeChainable(() => {
      for (const elm of this.elements) {
        elm.classList.add(className);
      }
    });
  }
  removeClass(className) {
    return this.makeChainable(() => {
      for (const elm of this.elements) {
        elm.classList.remove(className);
      }
    });
  }
  delay(ms) {
    return this.makeChainable(() => new Promise((resolve) => {
      setTimeout(resolve, ms);
    }));
  }
}

$('#test')
  .removeClass("red").delay(500)
  .addClass("blue").delay(500).delay(500).removeClass("blue")
  .delay(500).addClass("red").delay(500).removeClass("red")
  .delay(500).addClass("blue").delay(500).removeClass("blue")
  .delay(500).addClass("red").delay(500).removeClass("red")
.blue {
  background-color: blue;
}

.red {
  background-color: red;
}
<div id="test" style="width: 150px; height: 150px; margin:10px;" class="red"></div>

\$\endgroup\$

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.

Not the answer you're looking for? Browse other questions tagged or ask your own question.