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

10

I would like to create a very simple library containing only the functions I use most, following an idea similar to the ones 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, in 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 jQuery, for example:

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

I think I'm kind of far from it because my function only returns me an element in the document and what I'm looking for would be 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 do not know if it's correct. Here's 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 into the HTML element ( innerHTML ). How would I do to call it like this:

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

?

    
asked by anonymous 29.03.2015 / 01:11

2 answers

10

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;

In order to work as in jQuery, where $ (or jQuery ) is a function, it needs to return an object that represents the selected element, not the element itself as you do today. This object will give you access to the methods you want to chain. Your code does not seem to want to handle collections, so that's one less problem to solve.

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

The solution is to force $ to be called as a constructor function, even if the call was not new $() . This is simple to verify. When you execute new $() , this is defined as the object being instantiated, an object whose type is the constructor function itself. Just check the type of 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;
}

There are two things left: wrap the element in this new object and create 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 these 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>
    
29.03.2015 / 05:38
6

The JQuery code is public, why not take a peek 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.

link
link

Returning to your question, you were right to predict that what you need is a "builder" instead of returning the querySelectorAll response directly. And that's what jQuery does: jQuery("foo") is an object of the "jQuery" class and you need to jQuery("foo").get(0) to access the DOM element directly.

To implement this is not very difficult. Just create a class for your builder and put all the methods you want into it. "Linkable" methods such as your "DoSomething ()" and "MakeSomethingCase ()" also return builders (usually this itself) and "accessor" methods like "get" and "toArray" return miscellaneous values and end chain of methods.

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 this face. The biggest change that is missing is to allow you to create 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 constructor or you can change the RenanQuery constructor to something lower level and put the whole magic in the "$".

    
29.03.2015 / 05:11