DEV Community

Cover image for How to Make Any Method Chainable in JavaScript
Ezell Frazier
Ezell Frazier

Posted on • Updated on

How to Make Any Method Chainable in JavaScript

TLDR;

  • jQuery pushed the web and JavaScript forward, but its method chaining is greatly missed
  • What if I told you ther is a way to bring this back?
  • This may be a representation of something that I'm not here to talk about ๐Ÿ‘€

Why Should Anyone Care About This?

Regardless of one's experience with JavaScript, they may have heard of jQuery. During the early-to-mid 2000's, the web had reached a level of maturity allowing developers to create fluid user experiences compared to a collection of web pages.

But, this was a tedious task given how bare bones JavaScript and web browser APIs were compared to other programming languages. Imagine not having fetch or document.querySelector. That's pretty rough right? Well, jQuery filled in all the gaps, and then some. For some, jQuery was the standard library of client-side web development. But, that was then; JavaScript and the web has evolved.

However, with all the significant improvements enhancing JavaScript and web APIs, jQuery's method chaining was largely left behind. And because of this, jQuery isn't leaving the tool-belt of some developers. Can one blame them for that? jQuery provided a clean developer experience, while providing tools for building similarly clean user experiences. What's a relatively quick and painless way to bring this back?

What's Method Chaining?

$("#p1").css("color", "red").slideUp(2000).slideDown(2000);
Enter fullscreen mode Exit fullscreen mode

Chaining methods like .css, .slideUp, and slideDown is highly expressive and concise. jQuery's implementation represents a Fluent Interface, providing a level of expressiveness where code almost reads like plain English.

Wouldn't Native Method Chaining be Neat?

document.querySelector("#p1")
  .setCss({ transition: 'height 2s', height: '0px' })
  .setCss({ height: '100px' });
Enter fullscreen mode Exit fullscreen mode

This could be achieved, but one would need to know and care about implementation details between the DOM and one's app, which may introduce far more complexity than is required for most use cases.

Introducing Generic Method Chaining with The Box

Box(document.getElementById('p1'))
    .modifyContents(slideUp(2000))
    .modifyContents(slideDown(2000, '100px'));
Enter fullscreen mode Exit fullscreen mode

The objective is to place whatever one wants inside of a Box. Its two methods replaceContents and modifyContents allows one to temporarily take an item outside of the Box, perform an action, and place it into another Box.

This approach allows one to have a clear separation between what's desired (method chaining) and what one's already writing (DOM manipulation). Additionally, highly modular, and independent code is easier to compose and test.

import { Box } from './box' // highly modular
import { slideUp, slideDown } from './dom' // bring your own implementation details
Enter fullscreen mode Exit fullscreen mode

Is this Form of Method Chaining Really Generic?

Numbers

const four = Box(4);
const eight = four
  .replaceContents((num) => num * 2)
  .modifyContents(console.log); // 8

const ten = eight
  .replaceContents((num) => num + 2)
  .modifyContents(console.log); // 10
Enter fullscreen mode Exit fullscreen mode

Arrays

const nums = Box([1, 2, 3, 4, 5]);
const evens = nums
  .replaceContents((numArr) => numArr.map((x) => x + 2))
  .modifyContents(console.log) // [3, 4, 5, 6, 7]
  .replaceContents((sums) => sums.filter((x) => x % 2 === 0))
  .modifyContents(console.log); // [4, 6]
Enter fullscreen mode Exit fullscreen mode

Mixed Types (Map, Array)

const gteTo2 = Box(new Map([["a", 1], ["b", 2], ["c", 3]]))
  .replaceContents((table) => [...table.entries()])
  .replaceContents((arr) => arr.filter(([, value]) => value >= 2))
  .replaceContents((arr) => new Map(arr))
  .modifyContents(console.log); // Map { 'b' => 2, 'c' => 3 }
Enter fullscreen mode Exit fullscreen mode

Yes!

The Box works with any type. Its two methods replaceContents and modifyContents have a single parameter, which is whatever item is inside of The Box.

The Box can contain a primitive or an object. The difference between its two methods is that replaceContents must return a value, and modifyContents does not. In other words, replaceContents is great for ensuring immutability.

Here's the interface for TypeScript or other languages.

interface IBox<T> {
  replaceContents: <V>(fn: (item: T) => V) => IBox<V>;
  modifyContents: (fn: (item: T) => void) => IBox<T>;
}
Enter fullscreen mode Exit fullscreen mode

How Does The Box compare to Fluent Interfaces?

The Box Fluent Interfaces
Method Chaining โœ… โœ…
Highly Expressive โœ… โœ…
Supports immutability โœ… โœ…
Debugger-friendly โœ… โŒ
Logging-friendly โœ… โŒ
Works with any data type โœ… โŒ
Module-friendly โœ… โŒ

Sold? Here's What The Box Looks Like

function Box(item) {
    const replaceContents = (fn) => Box(fn(item));
    const modifyContents = (fn) => {
        fn(item);
        return Box(item);
    };
    return { replaceContents, modifyContents };
};
Enter fullscreen mode Exit fullscreen mode

Wait a Minute, Is The Box a You-Know-What?

๐Ÿ‘€

Discussion (0)