1

From How to create a jQuery plugin with methods?

How to do the same thing in javascript Without JQuery

JQuery

    (function ( $ ) {
 
    $.fn.greenify = function() {
        this.css( "color", "green" );
        return this;
    };
 
}( jQuery ));

Js

?
  • What do yo mean by "the same thing"? You mean adding methods to DOM elements? – T.J. Crowder Jul 17 '21 at 07:58
  • @Nithish unfortunately TJ has guided you into the trap he points out. there are many techniques available to you that prevent tampering with native prototypes. i don't know why he neglected to demonstrate those. i asked but he didn't care to offer an explanation. – Mulan Jul 18 '21 at 16:55

2 Answers2

2

plain old functions

jQuery is nothing more than JavaScript itself. You could easily write greenify and boldify as plain functions. This is a good approach because each simple function is easy to write, test, and reuse in any part of your program that needs this specific behaviour -

function greenify (elem) {
  elem.style.color = "green"
}

function boldify (elem) {
  elem.style.fontWeight = "bold"
}

greenify(document.querySelector("#foo"))
boldify(document.querySelector("#bar"))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
html render: example 1
render-preview-1

classes and caveats

jQuery uses an object-oriented style which allows for "chaining" of methods. This interface is desirable because it enables us to write programs as long chained expressions, much like we can write sentences. Most people would accomplish this using a class, which is syntactic sugar for defining a function and adding "methods" to the function's prototype -

class MyTools {
  constructor (elem) {
    this.elem = elem
  }
  greenify() {
    this.elem.style.color = "green"
    return this
  }
  boldify() {
    this.elem.style.fontWeight = "bold"
    return this
  }
}

function mytools (elem) {
  return new MyTools(elem) 
}

mytools(document.querySelector("#foo")).greenify()
mytools(document.querySelector("#bar")).boldify()
mytools(document.querySelector("#qux")).boldify().greenify()
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
html render: example 2
render-preview-2

But what if we had a lot of methods like greenify and boldify in our mytools class? You can see how the class could become quite large, and repeating return this for every method is a bit of burden. And what if someone wanted to use just part of your tool library in the future? Using a monolithic class makes it impossible to use dead code elimination, meaning the entire module must be included even if one function is needed.


higher-order functions

Can we build an intuitive interface without such drawbacks? One approach to address these issues is to use higher-order functions. Such a function can take a function as input and/or return a new function as output. You may have used built-in higher-order functions like Array.prototype.map, Array.prototype.filter, Array.prototype.reduce, or others, but we can easily make our own, too -

const effect = f =>
  x => { f(x); return x }

function identity (value) {
  return { value, map: f => identity(f(value)) }
}

const boldify =
  effect(elem => elem.style.fontWeight = "bold")

const greenify =
  effect(elem => elem.style.color = "green")

// prefix style; ordinary function calls
greenify(boldify(document.querySelector("#foo")))

// "chaining" style; by use of identity
identity(document.querySelector("#qux"))
  .map(boldify)
  .map(greenify)
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
html render: example 3
render-preview-3

modules

Even the "slim" version of jQuery 3.6.0 is over 72 KB, and there's no way to import just a piece of it. This is a consequence of how jQuery choose to implement everything through a single object-oriented interface, the jQuery function, aliased as $. However the future of JavaScript lies in its design for modules. They are reusable, flexible, and can be loaded on-the-fly, resulting in highly-optimized and streamlined scripts that are instantly ready to respond to the user.

We've written a few functions above. Let's see how we might start to organize the modules for our own program -

// func.js

function identity (value) {
  return { value, map: f => identity(f(value)) }
}

const effect = f =>
  x => { f(x); return x }


export { effect, identity }
// css.js

import { effect } from "./func.js"

const boldify =
  effect(elem => elem.style.fontWeight = "bold")

const greenify =
  effect(elem => elem.style.color = "green")

export { boldify, greenify }

When we use a module, we import only the parts we need. This gives the compiler (transpiler, "packer", "bundler", etc) an opportunity to remove dead code and dramatically cut down the size of a completed production-ready program -

// main
import { greenify } from "./css.js"

greenify(document.querySelector("#some-elem"))

Even * imports can be optimized as a dependency resolver can determine only particular functions from the css-named import are used -

import { identity } from "./func.js"
import * as css from "./css.js"

identity(document.querySelector("#some-elem"))
  .map(css.boldify)
  .map(css.greenify)

growing your modules

So you want to expand your module to support some other colours, like bluify, purplify, and more. You might try something like this -

// css.js
// DON'T DO THIS!

const bluify =
  effect(elem => elem.style.color = "green")

const greenify =
  effect(elem => elem.style.color = "green")

const purplify =
  effect(elem => elem.style.color = "purplify")

// ...

export { bluify, greenify, purplify, ... }

Instead, notice how only the ... portion of elem.style.color = ... changes in each function. To avoid repeating ourself over and over, we make functions -

// css.js

import { effect } from "./func.js"

// any color, not just green!
const color = value =>
  effect(elem => elem.style.color = value)

// any font-weight, not just bold!
const fontWeight = value =>
  effect(elem => elem.style.fontWeight = value)
  
// other CSS properties...
const fontSize = value =>
  effect(elem => elem.style.fontSize = value)

// ...

export { color, fontWeight, fontSize, ... }

Notice how color, fontWeight and fontSize each have a value parameter. We reduced code duplication and made our functions more useful -

// main.js

import { identity } from "./func.js"
import * as css from "./css.js"

identity(document.querySelector("#bar"))
  .map(css.color("limegreen"))
  .map(css.fontSize("24pt"))
  
identity(document.querySelector("#qux"))
  .map(css.color("dodgerblue"))
  .map(css.fontWeight("900"))
html render: example 4
render-preview-4

Expand snippet 4 below to verify this result in your own browser -

// func.js
const effect = f =>
  x => { f(x); return x }

function identity (value) {
  return { value, map: f => identity(f(value)) }
}

// css.js
const fontWeight = value =>
  effect(elem => elem.style.fontWeight = value)
  
const fontSize = value =>
  effect(elem => elem.style.fontSize = value)

const color = value =>
  effect(elem => elem.style.color = value)

// main.js
identity(document.querySelector("#bar"))
  .map(color("limegreen"))
  .map(fontSize("24pt"))
  
identity(document.querySelector("#qux"))
  .map(color("dodgerblue"))
  .map(fontWeight("900"))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>

jQuery Lite

Maybe you like jQuery because it accepts a CSS selector and doesn't require you to use identity or document.querySelector. jQuery made a choice and so can you. It's your program and you're free to invent any convenience you desire -

// myquery.js

import { identity } from "./func.js"

function $(selector) {
  const loop = t => plugin => loop(t.map(plugin))
  return loop(identity(document.querySelector(selector)))
}

export { $ }

We can start to see our very own $ evolving! -

// main.js

import { $ } from "./myquery.js"
import { color, fontSize, fontWeight } from "./css.js"

// example expressions
$("#bar")(color("gold"))(fontSize("8pt"))

$("#qux")(fontSize("14pt"))(color("hotpink"))(fontWeight(900))
html render: example 5
render-preview-5

Expand snippet 5 below to verify the results in your browser -

// func.js
const effect = f =>
  x => { f(x); return x }

function identity (value) {
  return { value, map: f => identity(f(value)) }
}

// css.js
const fontWeight = value =>
  effect(elem => elem.style.fontWeight = value)
  
const fontSize = value =>
  effect(elem => elem.style.fontSize = value)

const color = value =>
  effect(elem => elem.style.color = value)
  
// myquery.js
function $(selector) {
  const loop = t => plugin => loop(t.map(plugin))
  return loop(identity(document.querySelector(selector)))
}

// main.js
$("#bar")(color("gold"))(fontSize("8pt"))
$("#qux")(fontSize("14pt"))(color("hotpink"))(fontWeight(900))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>

Maybe you would like to operate on multiple elements, similar to jQuery? We can do that by using querySelectorAll and apply plugin to all elements in an iterable using Array.from. Note we also can bypass the need for identity as loop already closes over its context -

// myquery.js (updated)

function $(selector) {
  const loop = context => plugin => loop(Array.from(context, plugin))
  return loop(document.querySelectorAll(selector))
}
// main.js

// target all <p> elements
$("p")(color("orchid"))

// target #foo element
$("#foo")(fontSize("18pt"))(fontWeight("bold"))

// target #bar and #qux elements
$("#bar,#qux")(fontSize("12pt"))
html render: example 6
render-preview-6

Expand snippet 6 below to verify the results in your browser -

// func.js
const effect = f =>
  x => { f(x); return x }

function identity (value) {
  return { value, map: f => identity(f(value)) }
}

// css.js
const fontWeight = value =>
  effect(elem => elem.style.fontWeight = value)
  
const fontSize = value =>
  effect(elem => elem.style.fontSize = value)

const color = value =>
  effect(elem => elem.style.color = value)
  
// myquery.js
function $(selector) {
  const loop = t => plugin => loop(t.map(v => Array.from(v, plugin)))
  return loop(identity(document.querySelectorAll(selector)))
}

// main.js
$("p")(color("orchid"))
$("#foo")(fontSize("18pt"))(fontWeight("bold"))
$("#bar,#qux")(fontSize("12pt"))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>

eagle eyes

Did you notice how color, fontWeight, and fontSize looked almost exactly the same? We can continue to use abstraction to remove additional duplication. By capturing the smallest identifiable essence of a behaviour, we gain the most flexibility, allowing us to easily build sophisticated behaviours from simple ones -

// css.js

import { effect } from "./func.js"

const style = property => value =>
  effect(elem => elem.style[property] = value)

const color = style("color")
const fontWeight = style("fontWeight")
const fontSize = style("fontSize")

// ...

export { color, fontWeight, fontSize, style, ... }
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • This plug-in mechanism is beautiful! I can easily see applying it in many other scenarios. – Scott Sauyet Jun 12 '23 at 12:55
  • thanks @Scott. i think modules might be the best thing to happen to JS since its inception. my only complaint is they're not first-class! maybe in 10 years or so haha – Mulan Jun 12 '23 at 15:05
0

If you mean "how do I add my own methods to DOM element," my recommendation is: Don't, because your methods can conflict with ones added to the standard later. (And absolutely don't in a library, only do it — if you do it — in code for a specific page or application.) Instead, do what jQuery did and put wrappers around elements.

But if you want to do it anyway, you'd add functions to the prototype of the elements you want them to appear on. The base HTML element is HTMLElement, so to add a method to all HTML elements, you'd add it there.

// Ensure that your method isn't enumerable by using `Object.defineProperty`
Object.defineProperty(HTMLElement.prototype, "greenify", {
    configurable: true,
    writable: true,
    value() {
        this.style.color = "green";
    }
});

Example:

// Ensure that your method isn't enumerable by using `Object.defineProperty`
Object.defineProperty(HTMLElement.prototype, "greenify", {
    configurable: true,
    writable: true,
    value() {
        this.style.color = "green";
    }
});

// Use it
document.getElementById("a").greenify();
document.getElementById("b").greenify();

// Since it's on the prototype, it applies regardless of
// whether the element already existed:
const c = document.createElement("div");
c.greenify();
c.textContent = `This is "c"`;
document.body.appendChild(c);
<div id="a">This is "a"</div>
<div id="b">This is "b"</div>

For now, collections of elements are either NodeList instances (querySelectorAll and other newer collections) or HTMLCollection instances (getElementsByXYZ), although that may evolve further. So if you wanted to be able to call addClass on a collection, you'd add it to one or both of those prototypes. Let's move from greenify to a more useful example: Adding a class to all elements in the collection:

// Ensure that your method isn't enumerable by using `Object.defineProperty`
function addClass(...classes) {
    for (let n = 0; n < this.length; ++n) {
        this[n].classList.add(...classes);
    }
}
Object.defineProperty(NodeList.prototype, "addClass", {
    configurable: true,
    writable: true,
    value: addClass
});
Object.defineProperty(HTMLCollection.prototype, "addClass", {
    configurable: true,
    writable: true,
    value: addClass
});

// Use it
document.querySelectorAll("div").addClass("accent");
document.getElementsByClassName("x").addClass("emphasize");
.accent {
    color: green;
}
.emphasize {
    font-weight: bold;
}
<div class="x">This is "a"</div>
<div class="x">This is "b"</div>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875