Comment se passer de libraries/frameworks JavaScript

bloodyowl
bloodyowl 2013/12/06

** Petite note préalable ** : évidemment que ça ne marche pas sous les vieux navigateurs, cet article s'adresse principalement à toi pour te faire comprendre comment les choses marchent.

De plus en plus, le besoin de légèreté se fait sentir sur les pages. En dépit d'un web plus rapide sur desktop, on a maintenant beaucoup de devices connectés via 3G ou Edge (un petit coucou au métro parisien). Du coup, tu te rendras vite compte qu'embarquer jQuery + jQuery Mobile + jQuery UI et un tas d'autres plug-ins grapillés sur le web, ça commence à peser.

Pour la sélection d'élements

Pour remplacer ton bon vieux $ magique, document.querySelectorAll semble faire l'affaire. En revanche, il retourne une instance de NodeList, pas d'Array, ce qui ne nous arrange pas des masses.

Du coup, on peut écrire une petite function toute con, qui nous retourne un Array et ses méthodes bien utiles.

var nativeSlice = [].slice; // la méthode de conversion

function $(selector) {
  var list = document.querySelectorAll(selector);
  return nativeSlice.call(list);
}

Alternativement, tu peux utiliser Array.apply(null, list) plutôt que nativeSlice.call(list), si vraiment ça te fait plaisir.

Une troisième solution, un peu plus chiante au quotidien (et terriblement laide), c'est d'utiliser directement les méthodes qui t'intéressent (ex. [].forEach) de cette façon :

[].forEach.call(document.querySelectorAll(selector), function(element) {
  // do something w/ element
});

Pour en revenir à notre petite méthode $, on peut du coup faire :

// prends bien l'habitude de garder
// tes nodeLists si tu les réutilises

var elements = $(".my-elements-className");

elements.forEach(function(element) {
  // do something w/ element
});

Pour l'event delegation

L'event delegation, c'est bien, mangez-en. Ça permet, entre autres, de ne pas attacher 150 listeners uniques à 150 éléments différents, mais à attacher un seul listener sur un parent commun, et analyser les sources des évènements à l'intérieur en se basant sur le bubbling (remontée d'évènements de la source au plus haut parent) ou capturing (descente d'évènements du plus haut parent à la source, avant même que la source ne le reçoive).

Pour faire de la délégation, on va procéder en trois temps :

  • Choper event.target
  • Vérifier que le sélecteur qui nous intéresse correspond bien à event.target ou un de ses parents (dans le cas où l'on clique sur le .icon-Arrow dans .js-Button-action)
  • Si ça match, on garde l'élement correspondant au sélecteur, sinon, exit

Les browsers relativement récents possèdent une méthode : matchesSelector (et tous ses alias préfixés). Ce qu'on peut donc faire, c'est ceci :

var docEl = document.documentElement;
// si c'est dans docEl, c'est que c'est dispo
var nativeMatchesSelector =
  docEl.matchesSelector ||
  docEl.webkitMatchesSelector ||
  docEl.mozMatchesSelector ||
  docEl.oMatchesSelector ||
  docEl.msMatchesSelector;
var matchesSelector = nativeMatchesSelector || matchesPolyfill;

// le polyfill utilise querySelectorAll
// et cherche dans le parent de l'élement
function matchesPolyfill(selector) {
  var node = this;
  var parent = node.parentNode;
  var query, index, length;
  if (!parent || parent.nodeType != 1) {
    return false;
  }
  query = parent.querySelectorAll(selector);
  index = -1;
  length = query.length;
  while (++index < length) {
    if (query[index] == node) return true;
  }
  return false;
}

function getCurrentTarget(node, selector) {
  if (matchesSelector.call(node, selector)) return node;
  while ((node = node.parentNode)) {
    if (node.nodeType != 1) return false;
    if (matchesSelector.call(node, selector)) return node;
  }
  return false;
}

Dès lors, dans nos listeners, on pourra directement procéder ainsi :

element.addEventListener("click", function(evt) {
  var currentTarget = getCurrentTarget(evt.target, ".Button-action");
  if (!currentTarget) return;
  // all good with currentTarget
});

Pour l'Ajax

Pourquoi est-ce que l'on appelle ça encore Ajax, d'ailleurs ? Bref.

Simple comme bonjour :

function isSuccessStatus(status) {
  return (status >= 200 && status < 300) || status == 304;
}

function ajax(options) {
  var xhr = new XMLHttpRequest();
  var done = false;
  var async = options.hasOwnProperty("async") ? options.async : true;

  xhr.open(options.method || "GET", options.url, async);

  xhr.onreadystatechange = function() {
    if (done) return;
    if (this.readyState != 4) return;
    done = true;

    if (isSuccessStatus(this.status)) {
      if (options.success) {
        options.success.call(this);
      }
      return;
    }

    if (options.error) {
      options.error.call(this);
    }
  };
  Object.keys(options.headers || {}).forEach(function(key) {
    xhr.setRequestHeader(key, options.headers[key]);
  });
  xhr.send(options.data || null);
  return xhr;
}

Cette fonction offre un support basique de XHR :

var myXHR = ajax({
  url: "api/users",
  success: function() {
    doStuff(this.responseText);
  },
  error: function() {
    showError(this.status);
  },
});

ES5 magic

ECMAScript 5 délivre des petites méthodes très intéressantes pour se simplifier la vie, fortement inspirées par ce qu'on a l'habitude de trouver dans les bibliothèques ayant connu l'âge d'or, comme PrototypeJS ou MooTools.

Dès lors, plutôt qu'un ennuyeux :

var key, item;
for (key in myObject) {
  if (myObject.hasOwnProperty(key)) {
    item = myObject[key];
    // do something
  }
}

on peut se contenter d'un :

Object.keys(myObject).forEach(function(key) {
  var item = myObject[i];
  // do something
});

De même, on bénéficie de méthodes s'avérant très utiles, comme Array.prototype.map, Array.prototype.reduce, Array.prototype.filter, Object.create, Object.getPrototypeOf, Object.getOwnPropertyNames, Object.defineProperty. Si cela t'intéresse, je t'invite vivement à te renseigner sur ces dernières. Et si tu veux avoir plus de détails, read the fucking manual : http://es5.github.io.

Des petits détails cools du côté des events

Avec addEventListener, on peut aussi passer un objet comme listener, avec handleEvent pour interface :

var myElementClickEvents = {
  element: myElement,
  callbacks: [],
  handleEvent: function(evt) {
    var self = this;
    this.callbacks.forEach(function(item) {
      item.call(self.element, evt);
    });
  },
};
myElement.addEventListener("click", myElementClickEvents);

myElementClickEvents.callbacks.push(function(evt) {
  console.log(evt);
});

Avec ça, on peut facilement garder une trace de ce qu'on passé comme listeners.

Du type checking ?

var getClass = function(o) {
  return Object.prototype.toString.call(o);
};
var someString = new String("foo");

typeof someString; // "object"
getClass(someString); // "[object String]", sounds more reasonable

Petit bonus

Un petit bonus rien que pour toi : pour avoir une syntaxe plus sympathique et plus claire que les prototypes.

En principe, on fait comme ça :

function Animal(name) {
  this.name = name;
}

Animal.prototype.getName = function() {
  return this.name;
};

function Cat(name) {
  Animal.call(this, name);
}

function K() {}
K.prototype = Animal.prototype;

Cat.prototype = new K();
Cat.prototype.constructor = Cat;
Cat.prototype.type = "cat";

var myAnimal = new Animal("Foo");
var myCat = new Cat("Bar");

myCat instanceof Cat; // true
myCat instanceof Animal; // true

Maintenant, à l'aide de deux petites méthodes :

// wow
//         many magic
//   very es5
//            wow
function extend(object) {
  var self = Object.create(this);
  if (!object) return self;
  Object.keys(object).forEach(function(key) {
    self[key] = object[key];
  });
  return self;
}

function create() {
  var self = Object.create(this);
  if (typeof self.constructor == "function") {
    self.constructor.apply(self, arguments);
  }
  return self;
}

var klass = {
  create: create,
  extend: extend,
};

Tu peux faire ça :

var animal = klass.extend({
  constructor: function(name) {
    this.name = name;
  },
  getName: function() {
    return this.name;
  },
});

var cat = animal.extend({
  constructor: function(name) {
    animal.constructor.call(this, name);
  },
  type: "cat",
});

var myAnimal = animal.create("Foo");
var myCat = cat.create("Bar");

cat.isPrototypeOf(myCat); // true
animal.isPrototypeOf(myCat); // true

Voilà, j'espère que cela a pu attiser ta curiosité d'en apprendre plus sur le langage lui-même et le DOM.

Vous avez aimé cet article?
Le partager sur Twitter
← Articles
Ne rien rater
Sur les réseaux
Twitter
Facebook
Facebook
Apple Podcast
Soundcloud
Sur le chat
Discord