0

Completely experimental and only for learning I am trying to reinvent the wheel, a wheel called jquery. The original purpose of doing that was to understand chainable functions. So I wrote a function which is doing this, and very well as far as I can see, like so: (a bit shortened)

var $ = function(s) {
    var el = document.querySelectorAll(s);

    var myobj = {
        addClass(className) {
            el.forEach(elem => {elem.classList.add(className)});
            return this;
        },
        toggleClass(className) {
            el.forEach(elem => {elem.classList.toggle(className)});
            return this;
        },
        removeClass(className) {
            el.forEach(elem => {elem.classList.remove(className)});
            return this;
        },
        html(html) {
            if (typeof html == 'string') {
                el.forEach(elem => {elem.innerHTML = html});
                return this;
            } else {
                return el[0].innerHTML;
            }
        }
    };

    return myobj;
};

(this is really a working example) Now I can use jquery-like syntax for manipulate classes and html of an element (or a node list of elements) like so: $('#test').html('new html content'); or chained like $('a').html('my new link').addClass('newclassname');, which is really cool.

For those who are interested, here's the complete script snapshot of today including css() and attr(), and including the missing jquery function id().

My questions are:

  • [RESOLVED] How can I get this function to return jquery-like just the element itself like var elm = $('div#test');, now I am getting back the entire object.

  • [RESOLVED] (plz. see comments below) How can I write a fallback function, to prevent getting error message like $(...).animate is not a function, if I am using a method which is currently not covered in this function, like $('div#test').addClass('newclass').animate({top:'2.3em',background:'red'});, since animate() isn't implemented at the moment. In php there is a __get() function, is there something similar in javascript ?

Please don't ask me for why I am doing this all, as I mentioned it's for learning and understanding

Thanks.

ddlab
  • 918
  • 13
  • 28
  • 2
    Not sure what you mean with your first question – when you use jQuery, `$('div#test')` will return you a jQuery collection and not a single DOM element. A jQuery collection is conceptually pretty much the same thing your `myobj` is (although it's build in a different way). – David Feb 04 '19 at 16:14
  • 1
    @David It's not jQuery. OP is trying to write their own code to mimic jQuery's behavior as a learning endeavor. – Tyler Roper Feb 04 '19 at 16:15
  • @TylerRoper Yeah, I know. But I still don't get what he means by saying "... the element itself ..." – David Feb 04 '19 at 16:15
  • You don’t get “just the element itself” with jQuery either – it’s a jQuery object (try `$('div#test') instanceof jQuery`). Maybe you’re referring to how it looks in the console, like an array of elements? – Ry- Feb 04 '19 at 16:17
  • Thanks for the comments, really interesting. So far as I understand `var elm = $('div#test');` refers to the `div#test` as an element I can work with, if I use `elm`. I know that in this case it's just on DOM element, but it could be more nodes like `$('div > a')`. – ddlab Feb 04 '19 at 16:21
  • @TylerRoper: It also doesn’t do that. `innerHTML` isn’t a property on jQuery objects, for example. – Ry- Feb 04 '19 at 16:23
  • @Ry- I suppose the more I try to explain the more trouble I get myself into. That property would be on each element within the collection, ie `$(item)[0]`. – Tyler Roper Feb 04 '19 at 16:24
  • @TylerRoper This would be enable the use of `console.log($('#test').clientHeight);`, but it doesn't. It would say `undefined` – ddlab Feb 04 '19 at 16:25
  • How does jquery do this? By always returning an object that it knows about. That object may be an 'empty collection', but it's still a jquery object, so you never get `.x is not a function` because `.x` always applies to that object (it may not do anything, but that's another matter). You get chaining by ensuring all of your methods return the same type of object. If you're expecting to return a DOM node/element and then be able to manipulate that, with chaining, you'll be in for a much harder time. – freedomn-m Feb 04 '19 at 16:26
  • @ddlab Correct, though realize that wouldn't work in jQuery either. Given that example you'd also have to account for selecting one single element instead of a collection, as `clientHeight` wouldn't be available on an array of elements. – Tyler Roper Feb 04 '19 at 16:28
  • @freedomn-m AS I said, in my example `animate()` doesn't exist, thats why `$(...).animate is not a function` – ddlab Feb 04 '19 at 16:28
  • @TylerRoper Sure I know this. – ddlab Feb 04 '19 at 16:30
  • @TylerRoper Ahhhm, may be I misunderstood you a bit... Will rethink about this. Thanks – ddlab Feb 04 '19 at 16:32
  • I was explaining why jquery doesn't give this error when nothing has been found. "is not a function" .. "for that object". It's not the same as "doesn't exist at all, anywhere". – freedomn-m Feb 04 '19 at 16:36
  • Hey guys, thanks for the help until now. **I need to excuse for the "element itself" confusion.** There was a typo in my workbench project. It turned out that `var el = $('#test'); el.css({backgroundColor: 'yellow'});` works very well and as expected. So only the second question is remaining. – ddlab Feb 04 '19 at 16:40
  • Did you try `__noSuchMethod__`? https://stackoverflow.com/a/2357690/2181514 https://stackoverflow.com/a/994406/2181514 – freedomn-m Feb 04 '19 at 16:46
  • 1
    @freedomn-m I never knew about `__noSuchMethod`. I was going to suggest [a Proxy to capture all gets](https://stackoverflow.com/questions/41886355/capturing-all-chained-methods-and-getters-using-a-proxy-for-lazy-execution). EDIT: seems `__noSuchMethod__` is non-standard and also marked obsolete. So a Proxy is probably the better option. – VLAZ Feb 04 '19 at 16:49
  • @freedomn-m thanks for your suggestion with the proxy. +1 See my comments below. – ddlab Feb 04 '19 at 21:13

2 Answers2

2

How can I get this function to return jquery-like just the element itself like var elm = $('div#test');, now I am getting back the entire object.

jQuery always gives you back an entire jQuery object. If you’re referring to how the methods you’ve defined (addClass, …) don’t appear with the object in the browser console, that’s achieved by sharing those properties with a prototype instead of creating a new set of functions for every object. ES6 classes are a convenient way to make constructors that allow you to define a prototype with methods and create objects that inherit from it:

class MyElementCollection {
    constructor(el) {
        this.el = el;
    }

    addClass(className) {
        this.el.forEach(elem => {elem.classList.add(className)});
        return this;
    }

    // ⋮
}

var $ = function(s) {
    var el = document.querySelectorAll(s);

    return new MyElementCollection(el);
};

Then if you want it to show up like an array on Firefox and have elements accessible by index $('div#test')[0] like jQuery does, you assign the elements to those indexes and give your collection a length:

class MyElementCollection {
    constructor(el) {
        el.forEach((elem, i) => {
            this[i] = elem;
        });

        this.length = el.length;
    }

    each(action) {
        for (let i = 0; i < this.length; i++) {
            action(this[i]);
        }
    }

    addClass(className) {
        this.each(elem => {elem.classList.add(className)});
        return this;
    }

    // ⋮
}

Then if you want it to show up like an array on Chrome, you do the weird hack (still applicable in 2019) of defining a splice method, even if it doesn’t do the same thing that it does on arrays… or do anything at all:

class MyElementCollection {
    // ⋮

    splice() {
        throw new Error('Not implemented');
    }
}

How can I write a fallback function, to prevent getting error message like $(...).animate is not a function, if I am using a method which is currently not covered in this function, like $('div#test').addClass('newclass').animate({top:'2.3em',background:'red'});, since animate() isn't implemented at the moment. In php there is a __get() function, is there something similar in javascript ?

You can do this with proxies in JavaScript, but I wouldn’t recommend it – it’s not very clean. (Your object will pretend to have all properties instead of just ones existing in jQuery that you haven’t implemented, and there won’t be a list of them to find on the prototype. Proxies also come with performance and understandability penalties.) Instead, consider assigning all the unimplemented stuff the same implementation:

class MyElementCollection {
    // ⋮
}

const notImplemented = () => {
    throw new Error('Not implemented');
};

['animate', …].forEach(name => {
    MyElementCollection.prototype[name] = notImplemented;
});
Ry-
  • 218,210
  • 55
  • 464
  • 476
  • Sorry for beeing so late, but thanks for trying to teach me so patiently. +1 for this class model. – ddlab Feb 05 '19 at 08:55
1

Have a look into the github repo of jquery. The relevant files are these:

https://github.com/jquery/jquery/blob/master/src/core.js

https://github.com/jquery/jquery/blob/master/src/core/init.js

jQuery does it's stuff in a more complicated way than I will do here, so this is just a basic explanation.

The jQuery function (and the $ alias of it) call new jQuery.fn.init(selector, root) in their function bodies. This creates a new jQuery collection (that's the term they use in the documentation) from the jQuery.prototype. That prototype has all the usual jQuery methods attached to it (like on, find, animate, etc.).

They do this:

function jQuery (selector, root) {
  return new jQuery.fn.init(selector, root);
}

jQuery.fn = jQuery.prototype = { /* skipped */ }

jQuery.fn.init = function (selector, root) { /* skipped */ }
jQuery.fn.init.prototype = jQuery.fn;

The key here is the last line, where they make the jQuery.prototype also being the prototype of jQuery.fn.init (don't get confused by fn, it's just another alias they use so they don't have to type prototype all the time).

When you scan the jQuery.fn.init constructor function, you'll notice they store all matched DOM elements by their index into the new collection. Simplified:

jQuery.fn.init = function (selector, root) {
  let elms = Array.from(root.querySelectorAll(selector));
  elms.forEach((el, index) => {
    this[index] = el;
  }, this);
}

They do alot more stuff under the hood, but this basically explains what a jQuery collection is: A custom type of object which partially looks like a NodeList but with a custom prototype which has all the jQuery methods attached to it.

This means, your first question is based on the wrong assumption: Using $('div#test') will return a jQuery collection with a single element. You'll never get the "raw" DOM node from jQuery.

As for your second question: You can use proxies for that.

David
  • 3,552
  • 1
  • 13
  • 24
  • To the first question, it was resolved a bit earlier, before your answer. But +1 because correct answer (and understandable for me) **OK, back to the second question:** I've got the proxy thing to work generally. I can let pass through the function, if it exists, or get a custom message. But I still get the browsers error message on console and breaking of the rest of the code. Here's the actual test script (extremely shortened) http://ddlab.de/stackoverflow/jquery-like-chained-js-functions/with_proxy_ver_1.js – ddlab Feb 04 '19 at 19:38
  • **Second question also resolved:** It was not very difficult, only sometimes new things need a bit to be understood. The proxy thing helped me out finally. And the real MAGIC happens in the handler, on the *else*-part: If the passed property is not a function, the from now on let it be one, which only creates the custom message, but suppresses the javascript error and the breaking of the code. I am very happy now with this. Thanks again to those of you who brought me to proxies (or reverse). Here's the final http://ddlab.de/stackoverflow/jquery-like-chained-js-functions/with_proxy_ver_2.js – ddlab Feb 04 '19 at 20:45