Jason Charney

Web Dev Geek from St. Louis

Re-Query Select All the Things!

Remember last week when I spoke about how to use
querySelect()
and
querySelectAll()
and how it could all be done in one command I called
qs()()
? Remember reading the part about how after some research, I've had though about redesigning the function?
Here's what the old version looks like.
var qs = parent => (query,all) => (typeof(parent) === "string") ? qs(qs()(parent))(query,all) : (parent||document)[`querySelector${(all||false)?"All":""}`](query);
And here's the new version of
qs()()
.
var qs = parent => query => {let q; return (typeof(parent) === "string") ? qs(qs()(parent))(query) : (q = (parent||document).querySelectAll(query)).length > 1 ? q : (q[0]||null);};
Try as I might, I really hoped that that
let q
(or more appropriately
var q
) would have slyly stuck itself into the
q = (parent||document).querySelectorAll(query)
part where assigning
q
there allows us to have
q.length > 1
without having to put our assignment operation (
=
) on a separate line with the
let q
statement, but I cannot do that, and here's why.
Remember that when you assign a value to a variable, the assignment operator returns the value assigned to that variable. You can try it out in the debugging console of your browser or in a Node.js console right now. If you type in
five = 5;
the console will return
5
. That
5
is stored in the variable
five
until your refresh your browser or exit the Node.js console.
I could go on about operator precedence in JavaScript, but I don't want to.
I could have gone without the
let q
statement and not used the curly braces (
{...}
) and
return
statement as JavaScript variable hoisting would have allowed me to assign a value to
q
(with
q = (parent||document).querySelectorAll(query)
) without initially declaring
q
with the
let q
statement but the scope of using the
let
keyword inside a set of parenthesis would not allow me to get
q.length
because if you use
let
,
var
, or
const
before a variable, it will return
undefined
, and
undefined.length
throws a
TypeError
. And if you are using strict mode, either by putting
"use strict";
at the top of your file or at the beginning of your function, declaring
q
without
let
,
var
, or
const
throws a
ReferenceError
.
// Let's review (assuming strict mode is not initially set in this figure)
five = 5; // => 5 (because the assignment operator returns the value that is assigned to the variable; because strict mode is not enabled, five is hoisted, and can be initialized without declaration first)
var six; // => undefined (declaration keywords (var, let, and const) return undefined even though the variable. If six was previously used, it would have been hoisted.)
var seven = 7; // => undefined (although 7 is assigned to seven; seven is declared and assigned so technically it's hoisted)
var num = x => {
 "use strict";
 let w;  // => undefined (although w is defined)
 y = x;  // => ReferenceError (because y was not defined)
 w = (var z=[x,y,3]).length; // SyntaxError (because unexpected token "var" next to z) (also, if it had worked the code inside the parenthesis would have returned undefined, of which undefined.length is a TypeError)
 return w;
};
At any rate, because our
let q
couldn't be integrated into our one-liner, we have to bring back the curly braces (
{...}
) and
return
statement, since we can't return our
q
value implicitly.
//Remember that this form, known as FUNCTIONAL FORM,...
var f = x => x;
// Means the same as this form, known as FUNCTIONAL-IMPERATIVE FORM,...
var f = x => { return x; }
// Means the same as this form known as IMPERATIVE FORM
var f = function(x){ return x; }
So here is the new
qs()()
in detail.
/* @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.
 * @returns:
 *  null if nothing is found.
 *  Node if query matches one result.
 *  NodeList query matches more than one result.
 */
var qs = function(parent){
  return function(query){
    let q;	// q is declared in the inner function
    if(typeof(parent) === "string"){
      let parent_query = qs()(parent);
      return qs(parent_query)(query);
    } else {
      // If parent is undefined, use document.
      var parent_element = parent || document;
      q = parent_element.querySelectorAll(query);
      if(q.length > 1){
        // if q match more than one instance return all matches as a NodeList.
        return q;	  
      } else {
        // Otherwise, return the first element only
        // Note: If [] is returned, and [][0] is undefined (meaning no matches), return null.
        return (q[0]||null);
      }
    }
  } // inner function
} // outer function
Before I talk about what makes this different from the old version, I want to talk about two functions in the
Array
class. The
.find()
function matches the first instance of a query. The
.filter()
matches all instances of a query and returns an array. What if you use
.filter()
and it only returns one instance? You still have to tack on
[0]
to get that one item out of the returned array. The
.find()
function, is basically the
.filter()
function where that one item is returned without needed to append
[0]
if it finds something.
There is one other thing to note. If
.filter()
finds no results, it returns an empty array,
[]
, but
.find()
returns
undefined
. Furthermore, the zeroth item in an empty array,
[][0]
, is
undefined
.
The
.querySelector()
function is basically the
.find()
function for a
NodeList
, which makes
.querySelectorAll()
the
.filter()
function. But there is one problem:
.querySelector()
doesn't return
undefined
, it returns
null
, and
null
is not the same as
undefined
.
Just about all
.getElementsBy*()
function returns an empty array, like
.querySelectorAll()
. But
.getElementById()
returns
null
like
.querySelector()
. We still need
null
any way because
typeof([]) === "object"
and
typeof(null) === "object"
too. So if
q[0]
is
undefined
,
null
will be returned.
With that, we eliminate the
all
variable that was in the old version of
qs()()
. While this function doesn't take advantage of template strings like the old version did, it finally eliminates
.querySelector()
, meaning
.querySelectorAll()
is the ultimate query function and the only one we need.
So that's the new
qs()()
. Eventually, I'd like to present a new library I'm working on called haqs which is a JavaScript library which has an emphasis on closures and functional programming.
Here at Hackernoon, I plan on describing the more simplified versions of some of these function. The haqs library version is a bit more complicated because I was a bit braggadocio with demonstrating the functional programming aspects. I would really like to see just an itty-bit of object-oriented programming to reduce the amount of repetition and static structuring.
I also want to apply functional patterns to write code with a concise behavior. Haqs isn't quite ready yet, but I integrate some of the stuff I'm talking about here into it as I would like for it to be used in my Codepen work.
We'll see how it turns out. Until next time, keep hacking!

Tags

Comments

September 5th, 2019

Hi JR - an awesome function - takes something I’ve been doing to a whole new level. Thank you!

Small issue - you have a typo in your updated function:

var qs = parent => query => {let q; return (typeof(parent) === “string”) ? qs(qs()(parent))(query) : (q = (parent||document).querySelector(query)).length > 1 ? q : (q[0]||null);};

You forgot the word “All” after .querySelector…

Should be:
var qs = parent => query => {let q; return (typeof(parent) === “string”) ? qs(qs()(parent))(query) : (q = (parent||document).querySelectorAll(query)).length > 1 ? q : (q[0]||null);};

Thanks again - I’ll be grabbing this and utilsing it. Does it still work if you decide to use “use strict”? It currently seems to - but is there some issue if one does that?

marty_m

More by Jason Charney

Topics of interest