skip to Main Content

What are some techniques for creating a jQuery-like fluent interface?

At the heart of this question is the notion of abstracting over a selection of NodeLists and doing DOM changes on all the nodes in them. jQuery is usually exposed by the name $, and typical code might look like this1:

$ ('.nav li:first-child a') 
  .color ('red')
  .size (2, 'em')
  .click ((evt) => {
    evt .preventDefault (); 
    console .log (`"${evt .target .closest ('a') .textContent}" link clicked`)
  })

Note the fluent interface there. The nodes are selected once, and then the functions color, size, and click are executed against each matching node. The question, then, is how can we build such a system that allows us to write simple versions of color, size, and click which are bound together in a common chained interface?

In response to a similar question (now deleted), I wrote my own version, and user @WillD linked to a JSFiddle, which I’m hoping will become another answer.

For this pass, I’m suggesting that we don’t try to mimic other parts of jQuery’s functionality; certainly not things like .ajax or $(document).ready(...), but also to not bother with the plug-in mechanism (unless it comes for free) or the ability to generate a new NodeList from the current one.

How would one go about turning a collection of functions into a function that generates a collection of nodes from a selector and allows you to chain those functions as methods on this collection.


1This library long predated document.querySelector/querySelectorAll, and I’m assuming it was a large part of the inspiration for the DOM methods.

2

Answers


  1. Chosen as BEST ANSWER

    Here's my first attempt at this problem:

    const $ = ((fns = {
        color: (color) => (node) => node .style .color = color,
        size: (size, units = '') => (node) => node .style .fontSize = size + units,
        click: (fn) => (node) => node .addEventListener ('click', fn)
        // more here
      }, 
      nodes = Symbol(),
      proto = Object .fromEntries (
        Object .entries (fns) .map (([k, fn]) => [
          k, 
          function (...args) {this [nodes] .forEach ((node) => fn (...args) (node)); return this}
        ])
      )
    ) => (selector) => 
      Object .create (proto, {[nodes]: {value: document .querySelectorAll (selector)}})
    ) ()
    
    $ ('.nav li:first-child a') 
      .color ('red')
      .size (2, 'em')
      .click ((evt) => {
        evt .preventDefault (); 
        console .log (`"${evt.target.closest('a').textContent}" link clicked`)
      })
    div {display: inline-block; width: 40%}
    <div>
      <h3>First nav menu</h3>
      <ul class="nav">
        <li><a href="#">foo</a></li>
        <li><a href="#">bar</a></li>
        <li><a href="#">baz</a></li>
      </ul>
    </div>
    <div>
      <h3>Second nav menu</h3>
      <ul class="nav">
        <li><a href="#">qux</a></li>
        <li><a href="#">corge</a></li>
        <li><a href="#">grault</a></li>
      </ul>
    </div>

    Most importantly, the functions are quite simple:

    {
      color: (color) => (node) => node .style .color = color,
      size: (size, units = '') => (node) => node .style .fontSize = size + units,
      click: (fn) => (node) => node .addEventListener ('click', fn),
    }
      
    

    And we can add as many of them as we like. These are transformed into methods of an object -- which we will use as a prototype -- by creating functions that accept the same arguments, then to each node in our collection, we call this function with the same arguments, and call the resulting function with the node, eventually returning our same object.

    This whole thing returns a function which accepts a selector, calls querySelectorAll with it, and wraps the resulting NodeList in an object with our given prototype.

    Although I haven't tried, I think this would be easy to extend to allow a plug-in architecture. It would be more difficult to allow for functions that could alter the NodeList or return an entirely different one. At that point, we might look to the jQuery source code for inspiration.


  2. Here is the potential solution I advanced before, with the size and click methods now added.

    Here’s how it works. I use the class keyword to create a class which accepts a selector as its constructor argument. It runs a querySelectorAll to get a nodelist for that selector and saves it as a property called nodelist. Each method does a specific thing for every item in this nodelist and once done, returns the selfsame object instance to enable a jquery-like chaining support. Such that you could call each method on the return value of each previous method and so on, and they would all reference the same initial nodelist.

    class JqueryLikeObject {
      constructor(selector) {
        this.nodelist = document.querySelectorAll(selector);
      }
    
      recolor(color) {
        this.nodelist.forEach((item) => {
          item.style.color = color;
        })
        return this;
      }
    
      setText(text) {
        this.nodelist.forEach((item) => {
          item.innerHTML = text;
        })
        return this;
      }
      
      click(callback) {
        this.nodelist.forEach((item) => {
          item.addEventListener('click', callback);
        });
        return this;
      }
    
      size(size, unites) {
        this.nodelist.forEach((item) => {
          item.style.fontSize = size + unites;
        });
        return this;
      }
    }
    
    function $$(selector) {
      return new JqueryLikeObject(selector);
    }
    
    
    $$('.a').recolor('pink');
    $$('.group').recolor('green').setText('Foo!');
    $$('div').size('2','em').click(()=> alert('You clicked it'));
    <div class="a">
    a
    </div>
    
    <div class="b group">
    b
    </div>
    
    <div class="c group">
    c
    </div>

    Alternative:

    In effort to satisfy your desire for the method definitions to be as terse as possible. Here is an alternative setup for the methods in the Jquerylikeobject class.

    Basically it loops through a list of defined methods and as it assigns them as member props to the object it wraps them in the foreach loop that is needed to execute for each item in the nodelist. Passing any arguments along to the inner function.

    class JqueryLikeObject {
      constructor(selector) {
        this.nodelist = document.querySelectorAll(selector);
    
        const methods = {
          recolor: (item, color) => { item.style.color = color },
          setText: (item, text) => { item.innerHTML = text },
          click: (item, callback) => { item.addEventListener('click', callback) },
          size: (item, size, units) => { item.style.fontSize = size + units }
        }
    
        for (const method in methods) {
          const theFunction = methods[method];
          this[method] = function() {
            this.nodelist.forEach((item) => {
              theFunction(item, ...arguments)
            })
            return this
          }
        }
      }
    }
    
    function $$(selector) {
      return new JqueryLikeObject(selector);
    }
    
    
    $$('.a').recolor('pink');
    $$('.group').recolor('green').setText('Foo!');
    $$('div').size('2', 'em').click(() => alert('You clicked it'));
    <div class="a">
      a
    </div>
    
    <div class="b group">
      b
    </div>
    
    <div class="c group">
      c
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search