fQuery

Well hello, hello! It is the YEAR OF OUR LORD 2017 and I’m still only two blog posts deep.

Meaning I’m hot garbage at updating. This is the last time I will explicitly mention that I’m hot garbage at updating.

About 9 months ago, I was dealing with the ill-fitting API of a certain jQuery 1.6.4, which means, in essence, I was dealing with a giant rotting corpse of code, considering anything I built at that point had to work in IE8+, it was definitely a giant unoptimized square-peg tool for a round-hole job.

That’s how I received the impetus to see how far I could take a hand-coded replacement. I wanted this library to be functional and stateless and stress function composition, meaning I could reuse a lot of my methods elsewhere in the code and ultimately shrink the footprint of the result.

This is about to be a very scatterbrained writeup of some of the features of fQuery, it won’t make sense because it’s a friday and I’m tired, and I probably won’t clean it up. Here’s an autogenerated documentation website if you just want to browse the API. :)

Querying

I thought to myself “I wonder how hard it’d be to replace the query selection engine that jQuery has going on”, knowing full-well that IE8 supported document.querySelector and document.querySelectorAll.

In fact, I was almost basically done at that point. If I’m being real, querySelector and querySelectorAll are actually great native DOM methods that allow you to select elements you want, all using the same syntax you would when using strings for jQuery.

/** These are (for the most part) equivalent: */ 
$('.my-class');
document.querySelectorAll('.my-class');

For the purposes of targeting elements, using querySelector or querySelectorAll can actually yield a 2x-10x performance boost over certain jQuery builds. Which means if selecting large swathes of elements is the bane of your existence, life just got easier (and speedier).

I came up with this method to do most of the querying

/**
 * Finds an element by selector within a context. If child elements in 
 * element match selector, they are added to the return result
 * 
 * @param {Node} element - The element to search for children inside of
 * @param {string} selector - The CSS selector to match elements against
 * 
 * @returns {array|Node} A single element or collection of elements that match the query
 */
export function find(element, selector) {
  return _unwrap(_elements(element.querySelectorAll(selector)));
}

Those underscored functions, _unwrap and _elements are both stateless functional helper functions. Basically what happens is I specify an element to query the descendants of, and then _elements turns the result of that query into only HTMLElement nodes via filtering (IE8 has some weird issues with returning whitespace as childNodes). _unwrap will then convert that to a single element if it’s an array of one, or leave it alone if it’s got a length greater than one.

This was the closest I wanted to get to jQuery. I didn’t want to abstract heavily over the collection, as what I wanted to do here was create a library more in line with native DOM methods without all of the hell of actually using them.

Element Relationships

With jQuery, you can do some really cool things with elements and find relational elements based on them (.parent(), siblings(), children()). Fortunately, these all have a near 1:1 correlation to real native DOM methods, so I made methods for them!

Again, each of these functions takes an element to act on (they’re pure/stateless), so for a given input, the output will always be predictable.

/**
 * Returns the immediate parent of a given node
 * 
 * @param {Node} element - The element from which to return a parent
 * 
 * @return {Node} element - The parent element of the element passed in
 */
export function parent(element) {
  return element.parentNode;
}

/**
 * Returns an array of children of the passed in element. Will return a single element
 * if the array contains only one child. 
 * 
 * @param {Node} element - The element from which to retrieve children
 * 
 * @return {array|Node} element - The children of element
 */
export function children(element) {
  return _unwrap(_elements(element.childNodes));
}

/**
 * Returns siblings of a given node. If a selector is specified, it will
 * return only the siblings that match that selector. 
 * 
 * @param {Node} element - The element from which to return a parent
 * @param {string} selector - the selector to match elements against
 * 
 * @return {array|Node} element - The sibling(s) of the element passed in. 
 */
export function siblings(element, selector) {
  var result = selector ? _exclude(find(parent(element), selector), element) :
                          _exclude(children(parent(element)), element);
  
  return _unwrap(result);
}

This was all done probably in the same day. It was kind of outrageous. Here I was, thinking that a replacement for jQuery in IE8+ world would be impossible. Most of the functionality I used in jQuery was about 8-10 functions. I essentially replaced almost half of what I used the most with jQuery.

Another favorite jQuery function of mine is actually closest(), where given a Node, you traverse its ancestors until you find the matching Node.

/**
 * Finds the closest parent element of element that matches selector
 * starting with the element this function was passed
 * 
 * @param {Node} element - The element from which to begin the search
 * @param {string} selector - The CSS selector to match elements against
 * 
 * @returns {Node} The first ancestor element that matches the query
 */
export function closest(element, selector) {
  var current  = element;

  // Keep looking up the DOM if our current element doesn't match, stop at <html>
  while(current.matches && !current.matches(selector) && current.tagName !== "HTML") {
    current = parent(current);
  }

  return (current.matches(selector)) ? current : [];
}

I found out that there’s a method for HTMLElements called Element.matches, and you can use it to match a query string to an element. If the query string can match to the element, it’s true! So I essentially went full-dogfood on my code and used my parent function to continuously go up the ancestor chain. If it hit <html>, I knew I hadn’t found what I was looking for. Element.matches only works in IE9+, but I supplied my codebase with a polyfill since IE8 compatibility was what I was going for.

Here’s the entire module where all this code is located.

Element utilities

attr(), value(), css(), append(), prepend(), empty(), remove() and classList() were all written in the same file as I found them to be probably the most useful utilities (or at least the ones that could be replicated, all of which clocked in around a 5kb file unminified.

As it turns out, there’s a lot of DOM method support for the stuff that jQuery makes nice. Where jQuery throws in a bunch of checks and extra stuff for compatibility, fQuery basically uses the dom as is and just makes single-word, pure functions, essentially. Here are a few examples of jQuery and fQuery functions that do the same thing, but they look different.

and also fQuery is masking over the DOM in a very shallow way.

attr()

jQuery: $(element).attr("something", true); (setter)
fQuery: attr(element, "something", true) (setter)
DOM: Element.setAttribute

note: The getter for both jQuery and fQuery just leave off the last param.

value()

Purpose: Used for setting values on DOM form elements.
jQuery: $(element).val("something", true);
fQuery: value(element, "something", true)
DOM: Element.value = something

note: The getter for both jQuery and fQuery just leave off the last param.

css()

Purpose: Used for setting values on DOM form elements.
jQuery: $(element).css({background: "blue"});
fQuery: css(element, {background: "blue"});
DOM: Element.style

The rest?

You can view the file containing all of these methods. The API isn’t very difficult, and it’s actually fairly terse.

Events

Yeah, okay, one thing I will give jQuery is the ability to delegate events. I spent weeks in January thinking about events and how they would play out. I have a stubbed utility for this, but I don’t know how great it is.

I think the answer to this is actually using something like tiny-emitter for a pub/sub implementation, but I put this project on hold for a while. Maybe I’ll get back to it.

Conclusion

fQuery was a project that was born out of the want for performance. I wanted to optimize and clean up a codebase at the same time. I thought that this project was a step in that direction. I still haven’t run into any projects that explicitly need the use of fQuery (we are in a golden age of frameworks, for what that’s worth), but this was an incredibly enriching project that allowed me to leverage and learn about a lot of technologies that are much, much closer to actual, painful DOM scripting.

Happy coding!

Written on July 28, 2017