How to implement a library that has features similar to jQuery?

Asked

Viewed 280 times

11

I would like to create a very simple library containing only the functions I most use, following an idea similar to those in jQuery. For example:

I created the following function to get an element:

var $ = function(el, index){
    if(index)
        return document.querySelectorAll(el)[index-1];
    return document.getElementById(el) || document.querySelector(el);
};

Some tests I did on the following code snippet:

var $ = function(el, index){
    if(index)
        return document.querySelectorAll(el)[index-1];
    return document.getElementById(el) || document.querySelector(el);
};

$("minha-div-com-id").innerHTML = "pegou pelo ID";
$(".minha-div-com-class").innerHTML = "pegou pela classe";
$(".minhas-divs", 1).innerHTML = "pegou o primeiro elemento com classe '.minha-divs'";
$(".minhas-divs", 3).innerHTML = "pegou o terceiro elemento com classe '.minha-divs'";
<p id="minha-div-com-id"></p>                  <!-- pelo id -->
<p class="minha-div-com-class"></p>            <!-- pela classe -->

<p class="minhas-divs" style='color:blue'></p> <!-- 1 -->
<p class="minhas-divs"></p>                    <!-- 2 -->
<p class="minhas-divs" style="color:red"></p>  <!-- 3 -->

This already meets the basics of what I would like to do. But I wanted to include some functions like in jQuery, for example:

$("foo").fazAlgo().eFazMaisAlgumaCoisa().eOutra();.

I think I’m a little far from it because my function only returns me an element in the document and what I’m looking for is a Builder. Maybe the term is wrong, but I know this technique by that name in other languages.

I even got something "more or less", but I don’t know if it’s correct. Follow what I got:

(function(__window){

    var lib = {};
    __window.minhaBibioteca = lib;
    
    lib.alerta = function(args){
        alert(args);
        return lib;
    };
    
    lib.outroAlerta = function(args){
        return lib.alerta(args);
    };
    
})(window);


/**
 * Onde consigo usar assim:
 */

minhaBibioteca.alerta("Alerta {1º}")
              .outroAlerta("Outro alerta {1º}")
              .alerta("Alerta {2º}");

/**
 * ... que ainda não é o que estou buscando,
 * com o alert é simples, mas quando trata-se de manipular um
 * elemento, eu não sei como fazer...
 **/

Assuming I have a function called inner(str) which basically inserts the content/value of str within the HTML element (innerHTML). How would I call her that:

$("foo").inner("Algum Texto");

?

2 answers

12


What you want is called chaining. You need to have an object (while in your code you have a function), and all its methods return the object itself. A simple example of the principle, using a literal object:

var obj = {
    umMetodo: function() {
        console.log('um método');
        return this;
    },
    outroMetodo: function() {
        console.log('outro método');
        return this;
    }
}

var retorno = obj.umMetodo().outroMetodo();
//                  ^-- retorna obj!  ^-- idem!

// Isto é verdadeiro:
retorno == obj;

To be able to function as in jQuery, where $ (or jQuery) is a function, it is necessary that this function returns an object that represents the selected element, and not the element itself as you do today. This object is what will give you access to the methods you want to chain. Your code doesn’t seem to want to deal with collections, so that’s one less problem to solve.

The first attempt would be to make the function need to return this, but that alone is no use. In a direct function call such as $(), the this will always be the object window, which does not help. You need the this is something that makes sense, that represents an element of the GIFT.

The solution is to force $ is called as the constructor function, even if the call was not new $(). This is simple to check. When you run new $(), the this is defined as the object being instantiated, an object whose type is the constructor function itself. Just check the type of the this, using this principle:

function $(param, outro) {
    // chama novamente com new se o tipo não estiver certo
    if(!(this instanceof $)) return new $(param, outro);

    // ... faz o que precisar ...

    // só chega aqui se chamou com new
    return this;
}

Two things are still missing: wrapping the element in this new object of yours and creating the methods. The easiest way to wrap is to store the element in a property of your object. In all the methods you want to chain, it will be available through the property. Putting those two things, your code will look like this:

function $(el, index) {
    if(!(this instanceof $)) return new $(el, index);
    
    if(index) {
        this.el = document.querySelectorAll(el)[index-1];
    } else {
        this.el = document.getElementById(el) || document.querySelector(el);
    }
    
    return this;
}

// No protótipo do tipo, você define todos os métodos:
$.prototype.inner = function(txt) {
    this.el.innerHTML = txt;
    return this;
}
$.prototype.azul = function() {
    this.el.style.color = 'blue';
    return this;
}

// Teste
$('paragrafo').inner('testando').azul();
<p id="paragrafo"></p>

7

The Jquery code is public, why not go take a look at how they do it to take inspiration? : ) If you want a hint, these files here contain the part that defines the "Builder" of jQuery.

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

Returning to your question, you were right to predict that what you need is a "Builder" rather than returning the answer from querySelectorAll directly. And that’s even what jQuery does: jQuery("foo") is an object of the class "jQuery" and it is necessary to make a jQuery("foo").get(0) to access the DOM element directly.

To implement this is not too difficult. Just create a class for your Uilder and put all the methods you want in it. The "chaining" methods like your "fazAlgo()" and "eFazMaisAlgumaCoisa()" also return Builders (usually the same this) and "accessors" methods such as "get" and "toArray" return various values and end the method chain.

function toArray(x){
    return Array.prototype.slice.call(x);
}


function RenanQuery(el, index){
    //this._els são os elementos selecionados pelo seletor `el`.
    //Para ser uniforme, sempre armazenamos como um vetor.
    if(index != null){
        this._els = [ document.querySelectorAll(el)[index] ]
    }else{
        this._els = toArray( document.querySelectorAll(el) );
    }
}

$ = function(el, index){
    return new RenanQuery(el, index);
}

//Métodos acessadores retornam valores quaisquer
RenanQuery.prototype.get = function(n){
    return this._els[n];
}

//Métodos "encadeáveis" retornam o próprio this ou um outro objeto "RenanQuery"
RenanQuery.prototype.each = function(callback){
    for(var i=0; i<this._els.length; i++){
        callback.call(this._els[i], i, this._els[i]);
    }
    return this;
}

RenanQuery.prototype.inner = function(txt){
    for(var i=0; i<this._els.length; i++){
        setInnerText(this._els[i], txt);
    }
    return this;
}

The full version will have more or less that face. The biggest change left is to allow creating Renanquery objects from things that are not selector strings. For example, it would be nice to be able to create a Renanquery element from a DOM node or a list of DOM nodes.

To do this you can put a lot of ifs inside the Renanquery builder or you can change the Renanquery builder to something lower level and put the whole magic in the "$".

  • I’m going to check :D

  • +1, is exactly what jQuery does, and includes the mechanism for treating sets of elements.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.