Home > Net >  How can I emulate $() from JQuery when cycling an array of elements with foreach()?
How can I emulate $() from JQuery when cycling an array of elements with foreach()?

Time:11-15

I am playing with my JS code and trying to shorten it, because I always liked JQuery's extremely short syntax and I'd like to have a little less clutter in my code.

My main goal is to make document.querySelectorAll calls shorter, much like JQuery did with $(). So far so good, this is what I've got (CodePen here):

function getEle(e) {
  if(document.querySelectorAll(e).length > 0) {
    e = document.querySelectorAll(e);
    if(e.length == 1)
      e = e[0];
    return e;
  }
}

// -------------------------------
// Example: grab multiple elements
// -------------------------------
getEle('p').forEach(e => {
  e.style.color = 'blue';
});

// ----------------------------
// Example: grab single element
// ----------------------------
getEle('h1').style.color = 'red';
getEle('#product-221').style.color = 'orange';

Using function $(e){} instead of function getEle(e){} would make it shorter but I don't want to mess with stuff that looks like JQuery at first glance.

Now here's my question: the function returns a single element if there is one only, else it returns the entire array of elements. In which case I need to cycle them with a foreach() loop, as you can see in my example. But what if I don't know if there are multiple elements or just one?

In JQuery if you run $(p).css('color','red') it will apply the red color regardless if you have one or more p elements (or none at all). You don't need to run a foreach() loop because it does that for you.

Can I do something similar?

I'd like this piece of code to be able to automatically check if there are >1 elements and cycle them accordingly, applying style.color = 'red' to each element:

getEle('p').style.color = 'red';

instead of being forced to do this:

getEle('p').forEach(e => {
  e.style.color = 'red';
});

Basically, I'd like to incorporate foreach() inside the function and apply whatever I need from outside (example: style.color = 'red').

I hope I was detailed enough, let me know if you need more details!

CodePudding user response:

Indeed, jQuery's set-based nature is (IMHO) its killer feature.

I'd definitely look to have the function's return value be consistent. You can always have a different function for situations where you know there will be only one element, if you want.

To get the jQuery-ness when using it, you'll probably want to have the function return an object containing the elements to operate on and methods for operating on them, like

You have a couple of options, but you can basically do what jQuery does: Have a set of functions that do things (css, html, etc.) that contain the loop, and have an object that contains the elements that the functions should operate on.

Implementing it, you have a basic choice:

  • Make each method implement the loop, or
  • Have a function that does the loop and calls implementation functions that do the work on an individual element

I'm sure there are pros and cons each way, but I'd probably lean toward the latter. You're probably going to want a generic "just loop through them" method anyway (like jQuery's each), so you can reuse that.

Aside from its set-based nature, another handy feature of jQuery's API is that most methods return the object you called them on, which is handy for chaining.

Here's a sketch of an implementation (a sketch, but it does work), see the comments for how things work:

"use strict";

// ==== "q.js" module

// import { qAddMethod } from "./q/core.js";

// The function that selects/wraps elements (vaguely like jQuery's `$`)
/*export */function q(selectorOrElement) {
    // Get or create the instance (if called with `new`, we already have an instance;
    // if not, create it with our prototype).
    const instance = new.target ? this : Object.create(q.prototype);

    // Get our `elements` array
    if (typeof selectorOrElement === "string") {
        // Assume it's a selector (note this doesn't support the way jQuery
        // hyper-overloads the `$` function; make creating new elements a
        // different function
        instance.elements = [...document.querySelectorAll(selectorOrElement)];
    } else if (selectorOrElement) {
        // Assume it's a DOM element
        instance.elements = [selectorOrElement];
    } else {
        // Start off empty
        instance.elements = [];
    }

    // Return the instance
    return instance;
}

// A method to loop through each element calling the given callback function.
// Calls the callback with the element and its index in the set followed by
// any additional arguments passed to `each`.
qAddMethod(function each(fn, ...args) {
    const { elements } = this;
    let index;
    const length = elements.length;
    for (index = 0; index < length;   index) {
        const element = elements[index];
        fn.call(this, element, index, ...args);
    }
    return this;
});

// ==== "q/core.js" module

// import { q } from "./q.js";

// Add a method to the `q` prototype
/*export */function qAddMethod(fn) {
    Object.defineProperty(q.prototype, fn.name, {
        value: fn,
        writable: true,
        configurable: true,
    });
}

// ==== "q/css.js" module:

// import { qAddMethod } from "q/core.js";

// Implementation of the `css` method when given a style name and value
const cssImplOne = (element, index, styleName, value) => {
    element.style[styleName] = value;
};

// Implementation of the `css` method when given an object
const cssImplMulti = (element, index, styles) => {
    Object.assign(element.style, styles);
};

// Define the `css` method
qAddMethod(function css(styles, value) {
    if (typeof styles === "string") {
        return this.each(cssImplOne, styles, value);
    }
    return this.each(cssImplMulti, styles);
});

// ==== "q/html.js" module:

// import { qAddMethod } from "q/core.js";

// Implementation of the `html` method
const htmlImpl = (element, index, str) => {
    element.innerHTML = str;
};

// Define it
qAddMethod(function html(str) {
    return this.each(htmlImpl, str);
});

// ==== "qall.js" module:
// This is a rollup module that imports everything, fully populating the `q` prototype.
// You wouldn't use this if you wanted tree-shaking.

// import { q } from "./q.js";
// import "./q/css.js";
// import "./q/html.js";
//
// export q;

// ==== Using it

// Using the rollup:
// import { q } from "./qall.js";
// Or using only individual parts:
// import { q } from "./q.js";
// import "./q/css.js";
// import "./q/html.js";

q(".a").css("color", "green");
q(".b").css({
    fontWeight: "bold",
    border: "1px solid black",
});
q(".c")
    .html("hi")
    .each((element, index) => {
        element.textContent = `Testing ${index}`;
    });
<div class="a">class a</div>
<div class="b">class b</div>
<div class="c">class c</div>
<div class="c">class c</div>
<div class="c">class c</div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

Notice that I implemented css and html in their own modules, and they only add themselves to q.prototype if the module is loaded. That allows you to build bundles where only the relevant methods are included, not ones you never use. But again, it's a sketch, I'm sure there is a lot that is suboptimal above.

CodePudding user response:

From the above comments ...

"1/2 ... Of cause it can be done. For the desired syntax of e.g. getEle('p').style.color = 'red'; the OP needs to re-implement the entire DOM specification for an own super capable collection(wrapper) object. There are many reasons why John Resig 15 years ago did choose chained methods and not something the OP does request. The latter has to be implemented as ineterchained/interconnected class/type abstractions and getters/setters for every DOM API interface and every element property."

// - test implementation of a
//   node list `style` bulk accessor.
class ListStyle {
  #nodeList;
  constructor(nodeList) {
    this.#nodeList = nodeList;
  }
  set color(value) {
    this
      .#nodeList
      .forEach(node =>
        node.style.color = value
      );
    return value;
  }
  get color() {
    return [
      ...this.#nodeList
    ].map(
      node => [node, node.style.color]
    );
  }
}

// - old school module syntax.
const $$ = (function () {

  class NodeListWrapper {
    constructor(selector) {

      const nodeList = document.querySelectorAll(selector)
  
      Object.defineProperty(this, 'style', {
        value: new ListStyle(nodeList),
        // enumerable: true,
      });
    }
  }

  function $$(selector) {
    return new NodeListWrapper(selector)
  }
  // default export.
  return $$;

}());


// The OP's desired node list accessor/setter syntax.
$$('p').style.color = 'blue';


console.log(
  "$$('div, p') ...", $$('div, p')
);
console.log(
  "$$('div, p').style ...", $$('div, p').style
);
console.log(
  "$$('div, p').style.color ...", $$('div, p').style.color
);
* { margin: 0; padding: 0; }
body { margin: -2px 0 0 0; zoom: .88; }
.test p { color: #fc0; }
.test div { color: #c0f; }
<div class="test">
  <p>Foo bar ... paragraph</p>

  <div>Baz biz ... div</div>

  <p>Buzz booz ... paragraph</p>

  <div>Foo bar ... div</div>
</div>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related