Introduction
Nous avons vu précédemment comment filtrer un ou plusieurs éléments en fonction d’un sélecteur, maintenant nous allons appliquer ces connaissances afin de développer des fonctions de navigation d’un élément enfant vers ses éléments parents, dans le but de remplacer les fonctions jQuery correspondantes.
Il est important d’avoir lu l’article précédent sur le filtrage des éléments, notamment la partie sur le matching d’élément avec la fonction matches(‘selector’), car cette fonction sera utilisée un peu partout ici. Il ne faut pas oublier de la polyfiller pour supporter les vieux navigateurs !
Note : comme la plupart des codes sources proposés sur le blog, le code présent dans cet article est compatible avec Internet Explorer à partir de la version 11.
Parent direct : remplacement de $.parent()
Côté jQuery
Avec jQuery, récupérer le parent d’un élément se fait avec la méthode parent(‘selector’) présente sur les objets jQuery
. Le sélecteur fourni permet de ne récupérer le parent que s’il satisfait sa condition, dans le cas contraire un objet jQuery
vide est renvoyé. Ce sélecteur est optionnel, s’il n’est pas transmis le parent sera récupéré sans filtrage. Comme d’habitude, les sélecteurs CSS et les sélecteurs spécifiques jQuery sont supportés.
var enfant = $("[…]"); // on sélectionne un élément
var parent = null;
parent = enfant.parent(); // on prend le parent direct
parent = enfant.parent(".classe"); // on prend le parent direct s'il a la classe donnée
parent = enfant.parent(":visible"); // on prend le parent direct s'il est visible
A noter que parent('selector')
fonctionne sur les objets jQuery
contenant plusieurs éléments, et dans ce cas renvoie un objet contenant les parents directs de chaque élément qui satisfont le sélecteur.
Côté VanillaJS
En javascript natif, une telle fonction n’existe pas par défaut, il va falloir créer la nôtre. Cela ne sera pas difficile, si on se rappelle qu’il faut utiliser matches(‘selector’) pour tester un sélecteur et que l’on sait que récupérer le parent d’un élément peut se faire avec la propriété parentElement des éléments. J’ai choisi de ne pas nommer la fonction parent()
, au cas où la spécification Javascript évoluerait et inclurait officiellement une fonction nommée ainsi.
Element.prototype.queryParentSelector = function(selector){
var parent = this.parentElement;
if(typeof selector == "undefined" || selector === "") return parent;
return parent && parent.matches && parent.matches(selector) ? parent : null;
};
L’utilisation se fait de façon analogue à jQuery (mis à part le changement de nom) :
var enfant = document.querySelector("[…]"); // on sélectionne un élément
var parent = null;
parent = enfant.parentElement; // on prend le parent direct (javascript natif)
parent = enfant.queryParentSelector(); // on prend le parent direct (fonction custom)
parent = enfant.queryParentSelector(".classe"); // on prend le parent direct s'il a la classe donnée
Cela ne fonctionne que sur les Element
(tels que renvoyés par querySelector('selector')
) et non les NodeList
(telles que renvoyées par querySelectorAll('selector')
).
L’adaptation aux NodeList
pourra être le sujet d’un autre article, celui-ci sera assez long comme ça.
Comme d’habitude, les sélecteurs spécifiques jQuery ne sont pas supportés.
Parents indirects : optimisation de $.parents()
Côté jQuery
Pour travailler avec l’ensemble des parents d’un élément, on utilise la fonction parents(‘selector’) des objets jQuery
. Le sélecteur optionnel sert de filtre, et les parents sont renvoyés dans l’ordre du plus proche de l’élément au plus éloigné (donc du plus profond dans le document au plus proche de la racine).
var enfant = $("[…]"); // on sélectionne un élément
var parents = null;
parents = enfant.parents(); // on prend tous les parents
parents = enfant.parents(".classe"); // on prend les parents s'ils ont la classe donnée
parents = enfant.parents(":visible"); // on prend les parents s'ils sont visibles
var parent = enfant.parents(".classe").eq(0); // on prend le parent le plus proche qui a la classe donnée
Comme pour parent('selector')
, les objets jQuery contenant plus d’un élément sont supportés, et dans ce cas la recherche aura lieu sur les parents de chacun des éléments.
Côté VanillaJS
Comme précédemment, il va falloir créer notre propre fonction. Il s’agit simplement de boucler sur les parents, et de retourner ceux (ou celui) qui satisfont le sélecteur. Comme précédemment, la fonction matches('selector')
doit être disponible, et j’ai choisi de ne pas nommer les fonctions parents()
, au cas où la spécification Javascript évoluerait.
// Sélectionner un seul parent
Element.prototype.queryParentsSelector = function(selector){
for(var parent = this.parentElement; parent != null; parent = parent.parentElement) {
if(parent.matches && parent.matches(selector)) return parent;
}
return null;
};
// Sélectionner plusieurs parents
Element.prototype.queryParentsSelectorAll = function(selector){
var result = [];
for(var parent = this.parentElement; parent != null; parent = parent.parentElement) {
if(typeof selector == "undefined" || selector === "")
result.push(parent);
else if(parent.matches && parent.matches(selector))
result.push(parent);
}
return result;
};
Du côté de l’utilisation, c’est facile :
var enfant = document.querySelector("[…]"); // on sélectionne un élément
var parents = null;
parents = enfant.queryParentsSelectorAll(); // on prend tous les parents
parents = enfant.queryParentsSelectorAll(".classe"); // on prend les parents s'ils ont la classe donnée
var parent = enfant.queryParentsSelector(".classe"); // on prend le parent le plus proche qui a la classe donnée
Comme précédemment, on ne gère ni les NodeList
ni les sélecteurs jQuery.
La fonction sélectionnant plusieurs parents renvoyant un tableau, on peut boucler dessus avec Array.forEach()
pour traiter les éléments les uns après les autres, comme avec notre implémentation de forEach() pour les NodeList.
Au plus proche : le cas $.closest()
Côté jQuery
La fonction closest(‘selector’) fournie par les objets jQuery
permet de trouver à partir d’un élément l’élément le plus proche satisfaisant un sélecteur, parmi lui-même et ses parents.
var enfant = $("[…]"); // on sélectionne un élément
var parent = enfant.closest("form"); // on prend le formulaire le plus proche, enfant inclus
Côté VanillaJS
Pour ce qui est du remplacement, à première vue ca parait plutôt simple, les versions récentes des navigateurs web proposent la fonction closest(‘selector’) qui fait la même chose. La fondation Mozilla nous offre un polyfill pour les vieux navigateurs (il nécessite que la fonction matches('selector')
soit supportée, soit nativement, soit elle-aussi via un polyfill) :
if (!Element.prototype.closest){
Element.prototype.closest = function(s) {
var el = this;
if (!document.documentElement.contains(el)) return null;
do {
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType == 1);
return null;
};
}
Cependant, le polyfill ne supporte pas les éléments qui sont détachés du document (par exemple, s’ils ont été créés par script mais n’ont pas encore été attachés). Ca n’est pas une erreur, le comportement au sein des navigateurs est lui aussi erratique : dans les premières versions de Microsoft Edge les éléments détachés ne sont pas supportés non-plus, bien que la fonction closest('selector')
soit présente ; mais dans les versions suivantes ils le sont, sans doute suite à la fusion avec Chromium… Mozilla nous propose donc une sorte de plus petit dénominateur commun, dont le résultat est cohérent entre navigateurs récents et anciens pour les éléments attachés uniquement.
Nous pouvons ajouter notre propre fonction (polyfill Mozilla facultatif) si nous souhaitons gérer les éléments détachés en plus des éléments attachés. On lui donnera un nom différent pour éviter les conflits :
Element.prototype.queryClosestSelector = function(selector){
if(this.matches && this.matches(selector)) return this;
if(this.closest && document.documentElement.contains(this)) return this.closest(selector);
for(var parent = this.parentElement; parent != null; parent = parent.parentElement) {
if(parent.matches && parent.matches(selector)) return parent;
}
return null;
};
Quelle que soit la méthode, l’utilisation est aisée :
var enfant = document.querySelector("[…]"); // on sélectionne un élément
var parent = null;
parent = enfant.closest("form"); // on prend le formulaire le plus proche, méthode polyfill
parent = enfant.queryClosestSelector("form"); // même chose, méthode custom
Quelle implémentation utiliser ? Personnellement, cela dépend de la taille du projet et des conditions de travail… Au sein d’une grande équipe de développeurs qui ignorent les détails de closest('selector')
concernant les éléments détachés j’utiliserai plutôt queryClosestSelector('selector')
– pour éviter les mauvaises surprises – surtout si en plus le temps manque pour former tout le monde. Dans les autres cas j’utiliserai les 2, pour bénéficier du support natif de closest('selector')
sur les éléments attachés, et de queryClosestSelector('selector')
sur les éléments détachés.
Conclusion
Et voila, on peut aller plus loin dans l’optimisation de nos sites en retirant de nouveaux appels aux fonctions jQuery.
Etant donné combien il est fréquent de devoir trouver le parent d’un élément, cela devrait être applicable dans de nombreux projets.
Bonne optimisation, et à bientôt pour de nouveaux articles !
Références
Mozilla Developers – Node.parentElement
Mozilla Developers – Element.closest()
jQuery Documentation – $.parent()
jQuery Documentation – $.parents()
jQuery Documentation – $.closest()