Jason Charney

Web Dev Geek from St. Louis

Query Select All The Things!

Almost all the Codepens I've written this year have had some form of a JavaScript function that I call
qs()()
. That's not a typo! There are two sets of parenthesis following it, and it is good practice as this function uses closures. In just one line, I create a Swiss Army knife of several functions.
Here's what it looks like.
var qs = parent => (query,all) => (typeof(parent) === "string") ? qs(qs()(parent))(query,all) : (parent||document)[`querySelector${(all||false)?"All":""}`](query);
That's a lot going on in just one line of code, let's break it down.
/* @func: qs
 * @desc: querySelector[All] in one function
 * @param parent : (@default is document) A string or Element to find the query element(s)
 * @param query  : (required) A CSS string matching an id, class, or attribute of elements inside the parent.
 * @param all    : (@default is false) A Boolean which will look for one element (false) or more than one element (true).
 * @returns:
 *  null if nothing is found and all is false.
 *  Node if query is found and all is false.
 *  NodeList if all is true even if no Nodes are found.
 */
var qs = function(parent){
  return function(query){
    if(typeof(parent) === "string"){
      // If parent is a string, do a queryString on the parent.
      let parent_query = qs()(parent);
      return qs(parent_query)(query,all);	
    } else {
      // If parent is NOT a string, do the following
      // If parent is undefined, use document instead. (This is common)
      var parent_element = (parent||document);
      // If select_all is not defined, use false instead.
      var select_all = (all||false);
      if(select_all){
        // Return the NodeList of all Nodes that match the query inside the parent element.
        // Returns an empty NodeList if there are no matches.
        return parent_element.querySelectorAll(query);
      } else {
        // Return the Node that matches the query inside the parent element
        // Return null if no match
        return parent_element.querySelector(query);
      }
    }
  } // inner function
} // outer function
You may have noticed there is a little bit of a recursion if
parent
is a string. This is so that if you choose to modify elements inside of an element, you can do so without having to call another
qs()()
command.
In older version of this function, much of what goes in in
qs()()
was in separate functions. While it may also be faster to use other functions such as
element
.getElementById()
or
element
.getElementsByClassName()
(which often
element
will be
document
), which return a
HTMLCollection
, I find using
element
.querySelector()
and
element
.querySelectorAll()
to be the most ideal functions to use primarily because query searches are CSS Selectors. If the
parent
argument is a string it can also be a CSS selector, acting as a document-wide filter.
If there is no
parent
argument, the default value
document
will be used. If using an extra pair of parenthesis doesn't work well for you, you could define a new function.
var dqs = qs(document);
"Wait? Where's the second set of parenthesis?" Closures are rather interesting. I used to do a lot of programming in C++ back in college, so you could probably say that our parent function
qs(
parent
)
is a template for what the child function can do. Templates in C++ could be though of as closures that used data types to define what kind of argument you could use before using a function. Maybe the ECMA committee will eventually move toward using data types in ECMAScript eventually, especially since TypeScript uses data types.
So what can we use our
dqs()
function for, and how would it work differently if we used
qs()()
? Basically it work the same way.
// This
var dqsDiv = dqs("#div1");
// does the same thing as this
var qsdDiv  = qs(document)("#div1");
// And so does this because `document` is the default `parent` value.
var qsDiv = qs()("#div1");
All three of those statements will be assigned the same element that has the id
div1
.
The other feature of
qs()()
is the last argument in the second tuple, the
all
variable. This variable is and optional Boolean value (meaning it can be either
true
or
false
). The default value for
all
is
false
unless
all
is defined to be
true
. When it is
true
,
element
.querySelectorAll()
is used instead of
element
.querySelector()
.
Let's break down the latter part of
qs()()
:
var select_all = (all||false);
var callback = (select_all) ? "querySelectorAll" : "querySelector";
var parent_callback = parent[callback];
parent_callback(query);
Our first line we already stated means that
select_all
is the value of
all
unless is not defined then it will be set to
false
. The second line defines the string of the name of callback function which will be used. The callback name must be the name of a function that is part of the class or interface of the data type that
parent
is. We've already stated that the
parent
argument is either an
Element
or
Document
type.
Because
Object
members, which are key-value pairs, can be called using the dot method (
object.member
) or the array method (
object["member"]
), we can take advantage of the array method, because the key
"member"
is a
String
, which will return the value which can be any type including the
Function
type which we are looking for. So on the third line, the variable
parent_callback
represents a function. We just need to add the arguments outside the variable to the right, which is what happens on the last line. So with some substitutions, we can contract all all this information.
var select_all = (all||false);
// Let's rephrase callback
var callback = "querySelector" + ((select_all) ? "All" : "");
// combine the third and fourth lines
parent[callback](query);
Now let's use a template string on the second line.
var select_all = (all||false);
// Template string instead of concatenation
var callback = `querySelector${(select_all)?"All":""}`;
// combine the third and fourth lines
parent[callback](query);
Finally some variable substitution. Goodbye,
select_all
and
callback
.
// combine the all our lines
parent[`querySelector${(all||false)?"All":""}`](query);
Look how much is done. Now remember
parent
is optional, so we need to make sure that
document
can be used when
parent
is not.
(parent||document)[`querySelector${(all||false)?"All":""}`](query);
That's the false half of
qs()()
.
So why use this instead of some long named command?
If you've used jQuery, which you should really stop doing because more than likely you don't need it and is a terrible excuse not to learn "vanilla" JavaScript, and by "vanilla" I mean no-framework JavaScript not the framework Vanilla JS, you may be familiar with the
jQuery()
or
$()
function. The problem with using that, is that it returns another
jQuery
object.
qs()()
will return ether a
Node
or
NodeList
because it uses
querySelector()
and
querySelectorAll()
.
querySelector()
replaces the need to use
getElementById()
.
querySelectorAll()
replaces the need to use
getElementsByTagName()
,
getElementsByClassName()
, and
getElementsByName()
.
But that's with the simple stuff. The
querySelector()
and
querySelectorAll()
functions have more control than the classic
getElement[s]By*()
functions, which includes the CSS Selectors. By combining
querySelector()
and
querySelectorAll()
into a single function we can simplify object calls to just one function instead of four or two.
While researching this information, I do recognize that some improvements should still be made. Like, it might just be more logical only to use
querySelectorAll()
, test the quantity of items returned, and if there is only one item, just return that item instead of needing to tack on a
[0]
like jQuery's
$()
is known for, which would eliminate the
all
variable from
qs()()
. However, I think what I have right now is fine enough as it demonstrates several important features of JavaScript.
It's also important to remember, that when you select more than one item and want to map a function to each item, any
querySelectorAll()
will return a
NodeList
NOT an
Array
. To map, you must convert the
NodeList
to an
Array
, which can be done using
Array.from()
. And although
Array.from()
does have another argument to execute a
.map()
function, I recommend doing that separately, especially if you want to use other function like
.reduce()
or
.filter()
.
I hope you've enjoyed this article as much as I've enjoyed writing it. If you'd like me describe some of the other functions I've been using in my projects, such as one ones I've been using to reduce and in some cases eliminate the use of HTML in my projects, let me know in the community.

Tags

Comments

August 20th, 2019

There was a paragraph that I didn’t finish regarding why using qs()() should be used instead of jQuery(). Hopefully the editor will be a little more careful next time to let the writers know about these kinds of things such that guys like me don’t forget to finish a thought before its published. :wink:

August 27th, 2019

Whoops! How I described the closures in the long version of my qs()() function was incorrect. Every time you use an arrow function => that creates another scope layer. And if you do it at the beginning, you are setting up a nesting structure. I hope this new code will provide some better clarity.
I’m also going to use JSDoc from now on to describe my functions and arguments (parameters).

More by Jason Charney

Topics of interest