11550 sujets

JavaScript, DOM et API Web HTML5

Bonjour,

Il y a certaines fonctions que j'aimerais bien essayé de "prototyper", quand il s'agit d'agir sur une Fonction, un String, un Array ou un Number, il n' y a pas de problème particulier, en revanche quand il s'agit d'un objet de type Element tout ce complique.

Imaginons un prototype de la fonction addEvent (dont l'utilité peut être mise en doute, mais ce n'est pas le sujet), normalement il suffirais de faire ça :

Element.prototype.addEvent(type, fn){
  if(this.addEventListener){
    this.addEventListener(type, fn, false)
  }else if(this.attachEvent){
    this.attachEvent('on'+type, fn):
  }else{
    this['on'+type] = fn;
  }
}


Mais cela ne marchera pas complètement parce qu' Internet Explorer ne reconnais pas le type d'objet Element.

Donc ce que je cherche c'est une manière de contourner le problème, pour l'instant la seul solution que j'ai trouvé est la suivante :

Il faut tout d'abord créer une fonction Element pour les navigateurs qui ne reconnaissent pas cet Element :

if(!window["Element"]) Element = function() {};


jusque là rien de méchant, là ou je trouve que cela devient confus c'est que je suis obligé de passer par une fonction pour pouvoir appliquer mon prototype a mon élément (une fonction dollar par exemple) :

function $(el){
  
  var element;
  
  if(typeof el == 'string'){
    element = (document.getElementById(el))? document.getElementById(el) : null;
  }else{
    element = (el)? el : null;
  }

  if(element){
    for (var property in Element.prototype) {
      if (!(property in element)) element[property] = Element.prototype[property];
    }
  }
  return element;
};


Ensuite seulement je peux utiliser ma fonction:

$(el).addEvent('click',function(){});


C'est, il faut bien l'avouer, un peu foireux, tout d'abord parce que l'intérêt que ce soit au niveau de la sintaxe comme au niveau des performance (qui sont les deux intérêts principaux du prototypage) ici sont quasi nul

Effectivement entre $(el).addEvent('click',function(){}) el addEvent(el,'click',function(){}) je préfère la deuxième solution, même si entre $('id_name').addEvent('click',function(){}) et addEvent($('id_name'),'click',function(){}) ça se discute.

La plupart des frameworks le font avec une méthode un peu similaire a celle décrite plus haut mais plus beaucoup plus complète qui permet notamment d'appliquer les prototypes sur window et document, je pourrais m'en inspirer mais c'est d'un niveau d'abstraction qui me dépasse bien souvent. Dans le cas d'un framework c'est justifié par le fait que les fonctions qui sont utilisés pour résoudre le problème sont en partie utilisées par d'autre fonctions.

Donc la question est, existe t'il une solution simple ou bien il est tout simplement inutile de "prototyper" les fonctions sur les objets de type Element?

<edit>

Je viens de me rendre compte à la relecture, que mon exemple n'est pas très approprié, en effet le addEventListener natif prenant comme premier paramètre l'élément il n'est pas trés logique d'en changer le fonctionnement, par contre imaginons des fonctions de traitement de classes prototypées ce serait sympa. Par exemple El.addClass('nomdelaclass') ...

</edit>
Modifié par matmat (18 Apr 2008 - 18:03)
matmat a écrit :
Je viens de me rendre compte à la relecture, que mon exemple n'est pas très approprié, en effet le addEventListener natif prenant comme premier paramètre l'élément il n'est pas trés logique d'en changer le fonctionnement

Ah bon ? On en apprend tous les jours. Smiley cligne

C'est un sujet inépuisable sur le forum... Pour résumer : ne pas chercher à étendre les objets natifs, et tout le monde s'en portera mieux. Smiley smile
matmat a écrit :
La plupart des frameworks le font avec une méthode un peu similaire a celle décrite plus haut mais plus beaucoup plus complète qui permet notamment d'appliquer les prototypes sur window et document

Non, je ne crois pas. Les objets natifs sont plutôt étendus au chargement de la bibliothèque, voire même pas du tout pour certaines bibliothèques qui ont compris que c'était préférable (jQuery par exemple).
Julien Royer a écrit :

Ah bon ? On en apprend tous les jours. cligne


Voilà ce qui arrive quand on utilise des fonctions non prototyper... donc il est bien plus logique même pour addEvent de faire une fonction de forme $(el).addEvent('click',function(){}) que addEvent(el,'click',function(){}) !

Julien Royer a écrit :

Non, je ne crois pas. Les objets natifs sont plutôt étendus au chargement de la bibliothèque, voire même pas du tout pour certaines bibliothèques qui ont compris que c'était préférable (jQuery par exemple).

C'est un sujet inépuisable sur le forum... Pour résumer : ne pas chercher à étendre les objets natifs, et tout le monde s'en portera mieux. smile


Je crois que si, sinon explique moi comment fait il pour faire ça? :

$("p").click(function () { 
   $(this).slideUp(); 
});


ou ça :

$("p:last").addClass("selected");


Toute les fonctions sont des extensions des éléments natifs!
matmat a écrit :
Toute les fonctions sont des extensions des éléments natifs!

Pas vraiment. Je ne suis pas expert en bibliothèques JavaScript, mais pour jQuery, on crée juste un wrapper autour du noeud de l'arbre DOM que l'on manipule. En aucun cas on ne modifie un objet natif.
Modifié par Julien Royer (17 Apr 2008 - 17:15)
Effectivement, je viens de regarder et la méthode est la suivante :

function $(el){

  var elem = document.getElementById(el);
  this[0] = elem;
  this.length = 1;
  return this;

};


Tout simplement un objet est retourné au lieu de l'élément ce qui permet ensuite de "travailler" dessus.

Donc si on reprend cette méthode sur mon exemple du début, cela reviendrait a faire :

1. créer une fonction
Commons = function() {};


2. lui assigner les prototypes que l'on veut :

Commons.prototype = {

  addEvent: function(type, fn){

  },

  addClass: function(Klass){

  },

  makeCoffee: function(Klass){

  }
  
};



3. Assigner ces fonctions a nos élément récupéré par la fonction dollar:
function $(el){
  
  var element;

  if(typeof el == 'string'){
    element = (document.getElementById(el))? document.getElementById(el) : null;
  }else{
    element = (el)? el : null;
  }
  if(element){
    for (var property in Commons.prototype) {
      if (!(property in element)) element[[property]] = Commons.prototype[[property]];
    }
  }

	return element;
};


J'ai juste là?
Modifié par matmat (17 Apr 2008 - 18:29)
matmat a écrit :
J'ai juste là?

Ca dépend de ce que tu appelles "juste". Smiley cligne

Tu ne crées pas un wrapper autour d'un noeud du DOM, tu modifies un noeud du DOM pour lui ajouter des méthodes.
Modérateur
Salut, Smiley smile

Si je ne dis pas de bêtise, il faut faire un merge plutôt qu'un extend, c'est à dire, créer un nouvel objet qui reprend les propriétés de cet objet d'origine auquel tu appliques de nouvelles propriétés.

Dans Mootools, la fonction $merge(), permettant de ne pas altérer l'objet original, donne :
function $merge(){
	var mix = {};
	for (var i = 0; i < arguments.length; i++){
		for (var property in arguments[ i]){
			var ap = arguments[ i][ property];
			var mp = mix[ property];
			if (mp && $type(ap) == 'object' && $type(mp) == 'object') mix[ property] = $merge(mp, ap);
			else mix[ property] = ap;
		}
	}
	return mix;
};
alors que la fonction $extend(), elle, modifie directement l'objet d'origine :
var $extend = function(){
	var args = arguments;
	if (!args[1]) args = [this, args[0]];
	for (var property in args[1]) args[0][ property] = args[1][ property];
	return args[0];
};

Ainsi, en reprenant ce principe, ta fonction $() retourne un nouvel objet possédant toutes les propriétés de l'élément sélectionné ainsi que les méthodes de ton choix.
Modifié par koala64 (18 Apr 2008 - 19:30)
koala64 a écrit :

Ainsi, en reprenant ce principe, ta fonction $() retourne un nouvel objet possédant toutes les propriétés de l'élément sélectionné ainsi que les méthodes de ton choix.


Oui, sauf que avec la fonction $merge() je récupére bien toute les propriété mais pas l'objet!

C'est a dire que si j'applique ensuite mon prototype, il s'applique sur un objet vide.
Modifié par matmat (19 Apr 2008 - 00:39)
Pour ne pas appliqué les propriétés directement sur le noeud DOM, une solution peut être de créer un objet qui aurait comme proprieté notre getElementByID() :
function $(el){
  
  var element = {};

  if(typeof el == 'string'){
    element[0] = (document.getElementById(el))? document.getElementById(el) : null;
    element.length = 1;    
  }else{
    element[0] = (el)? el : null;
  }
    
  if(element[0]){
    for (var prop in Commons.prototype) {
      element[0][[property]] = Commons.prototype[[property]];
    }
  }
  
	return element[0];
};


Cela reprendrais la méthode de jQuery, mais j' avoue ne pas très bien comprendre l'intérêt...
Modifié par matmat (19 Apr 2008 - 00:47)
Modérateur
matmat a écrit :
Oui, sauf que avec la fonction $merge() je récupére bien toute les propriété mais pas l'objet!
mmh... Tu crées quand même un nouvel objet à l'aide de $merge et celui-ci posséde toutes les propriétés de l'élément d'origine ainsi que celles d'autres objets. Smiley rolleyes

Si tu le souhaites, tu peux donc travailler directement sur l'objet créé sans pour autant toucher à l'élément d'origine. Ainsi, tu rajoutes une couche d'abstraction en rendant tes manipulations indépendantes des évolutions de l'élément.

Par exemple, pour montrer l'utilité d'un tel procédé, on pourrait imaginer que tu souhaites faire diverses manips qui vont prendre 10 secondes et qu'au bout de ces 10 secondes, tu souhaites appliquer une classe sur l'élément parent de celui que tu as passé dans ton nouvel objet.

Si, au bout de la 5ième seconde, tu supprimes l'élément d'origine via un autre bout de code, tu peux quand même traiter l'élément parent à la 10ième seconde parce qu'il fait toujours parti des propriétés de l'objet. Smiley murf

Si, en revanche, tu travailles directement sur l'élément, ton script buggue en arrivant sur l'ajout de classe parce que l'élément parent est indéfini à la 10ième seconde vu que tu as supprimé l'élément d'origine à la 5ième seconde.
Merci pour tes explications, je comprend beaucoup mieux l'intérêt de créer une "couche d'abstraction" .

koala64 a écrit :
mmh... Tu crées quand même un nouvel objet à l'aide de $merge et celui-ci posséde toutes les propriétés de l'élément d'origine ainsi que celles d'autres objets. Smiley rolleyes


C'est dans la pratique que j'ai du mal a comprendre ou ce stocke l'élément dans ce nouvel objet. En effet mon probléme est maintenant le suivant, si je travaille avec une fonction type merge, je fais effectivement une copie "abstraite" de mon element DOM auquel je donne toute les propriétées de cet elements :


function $(el){
  
  var element = {};

  if(typeof el == 'string'){
    domElement = (document.getElementById(el))? document.getElementById(el) : null;
    if(domElement){
      for (var domProperty in domElement) {
        element[domProperty] = domElement[domProperty];
      }
    }
  }else{
    element = (el)? el : null;
  }
    
  if(element){
    for (var property in Commons.prototype) {
      element[property] = Commons.prototype[property];
    }
  }

  return element;
};


Par contre comme celui ci n'est pas reconnu comme en objet de type Élément ou HTMLelement, il est ne m'est pas possible de lui appliquer la fonction addEvent...

J'ai vu que dans Mootools ils font un émulation des objet HTMLelement pour pouvoir les étendre, donc on revient dans le problème évoqué plus haut.

Donc comment je pourrais faire pour que ma fonction $() me retourne un objet indépendant de mon élément DOM mais auquel je puisse appliquer un événement.
Salut,

En ce qui concerne jQuery, le fonctionnement est le suivant :

L'objet retourné par jQuery (ou $) est un wrapper autour d'une liste de noeuds du DOM. Cet objet ne met pas à disposition les méthodes et propriétés natives du DOM (getElementsByTagName par exemple). Pour accéder à ces méthodes, il faut tout d'abord récupérer le ou les noeuds du DOM (par index, ou avec get).
Si j'ai bien compris :

Notre fonction dollar retourne un tableau qui constitue notre "wrapper":

L'exemple est très simplifier, la fonction dollar ne permet de sélectionner que par id, c'est pour comprendre le principe

function $(el){  
  var elements = [];
  elements[0] = document.getElementById(el);  
  return elements;
};


Ensuite j'applique mes propriétés a mon élement :

var monElement = $('id')[0];//correspond a get(0) de jquery
extend(monElement,commons.prototype)


Et ensuite je peux utiliser :

monElement.addEvent('click',callBack);


Donc là les propriétés sont assignés a monElement qui est une copie "abstraite" du noeud DOM. Cette copie restera donc disponible en mémoire tel quel même au cas ou le noeud DOM est modifier par la suite.

S'il vous plait, si je suis complètement a coté de la plaque pouvez écrire un exemple, parce que j'ai un peu du mal avec le concept de wrapper (j'ai beau avoir lu la définition sur Wikipédia).
Modifié par matmat (21 Apr 2008 - 17:02)
matmat a écrit :
Si j'ai bien compris :

Notre fonction dollar retourne un tableau qui constitue notre "wrapper":

Tu as bien compris.
matmat a écrit :
Ensuite j'applique mais propriétés a mon élement :

var monElement = $('id')[0];//correspond a get(0) de jquery
extend(monElement,commons.prototype)

Dans ce cas, tu modifies l'objet natif (le noeud du DOM). On en revient au problème soulevé précédemment, c'est-à-dire que tu fais des modifications qui vont impacter tous les scripts de la page.
Il y a quand même quelque chose que je continue de ne pas comprendre. Dans tout les cas si on veut être sur de récupérer les propriétés d'un élément avant d'éventuelles modifications par d'autre scripts, il faut le faire avant ces modifications. Et si des modifications sont faite sur un noeud DOM, dans tout les cas elle se répercuterons sur les autres scripts vu que c'est la même page donc les mêmes éléments.

Finalement, à l'examen plus approfondis des différentes librairie, je me rend compte que si avec jquery (par exemple) je fais :

var el = $('#get2');
el.click(function(event){
  setTimeout(function(){
    alert('5secondes');
    document.getElementById('parent').removeChild(document.getElementById('get2'));
  },1000);
  setTimeout(function(){
    alert(el.get(0).parentNode.id)
  },2000);
});


Le script bug, parce que l'élément a disparut, l'élément n'a absolument pas était "mergé" dans aucune couche abstraite, le procédé est exactement le même que celui décris plus haut.

J'ai enlevé que ce soit a jQuery ou a Mootools toutes les fonctions jusqu'a n'avoir plus que les fonctions nécessaires pour faire $('mondiv').addEvent() ou $('#mondiv').click(), et la démarche et la même que celle décrite plus haut (pas le première, les suivantes)

Donc au final dans toutes ces librairies les propriétés .click() ou .addEvent() sont ajoutés aux noeuds DOM récuperé par un fonction dollar, la seule différence entre l'une et l'autre c'est que l'une travaille a base de tableaux (jQuery) et l'autre a base de d'objet (Mootools ou Prototype).
Modifié par matmat (21 Apr 2008 - 19:29)
Modérateur
Bon, je n'avais pas trop essayé le chainage jusqu'à maintenant mais, quand bien même j'aurais fait une erreur, me suis bien fendu la gueule Smiley lol
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
	<head>
		<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
		<title>Exemple</title>
		<style type="text/css"><!--

@media screen, projection {
	.blueText {color:blue; cursor:pointer;}
}

		--></style>
		<script type="text/javascript"><!--

(function() {

var Test$ = function() {}, Tp = Test$.prototype = {

	// INITIALISATION DU SCRIPT
	init: function() {
		return Tp.addEvent(window, 'load', function() {
			return Tp.$('toto').
			addClass('blueText').
			alerte('Et voilà ! Le texte est devenu tout bleu... [langue]').
			alerte("... mais le bleu, c'est moche donc clique dessus [cligne]").
			addEvent('click', function() {
				return Tp.myObj.removeClass('blueText');
			});
		});
	},
	
	// OBJET
	myObj: {},
	
	// SELECTION D'UN ELEMENT
	$: function(sId) {
		Tp.myObj.el = typeof(sId) == 'string' ? document.getElementById(sId) : typeof(sId) == 'object' ? sId : null;
		if(Tp.myObj.el) for(var prop in Tp) if(prop != 'init' && prop != '$') Tp.myObj[prop] = Tp[prop];
		return Tp.myObj.el ? Tp.myObj : null;
	},
	
	// AJOUT D'UN GESTIONNAIRE D'EVENEMENT
	addEvent: function() {
		var a = arguments;
		if(Tp.myObj.el) document.addEventListener ?
			Tp.myObj.el.addEventListener(a[0], a[1], a[2] || false):
			Tp.myObj.el.attachEvent ?
				Tp.myObj.el.attachEvent('on' + a[0], a[1]):
				false;
		else document.addEventListener ?
			a[0].addEventListener(a[1], a[2], a[3] || false):
			a[0].attachEvent ?
				a[0].attachEvent('on' + a[1], a[2]):
				false;
		return Tp.myObj || a[0];
	},
	
	// AJOUT D'UNE CLASSE
	addClass: function(sClass) {
		var oEl = Tp.myObj.el || arguments[1] || null;
		if(!Tp.hasClass(oEl, sClass)) oEl.className += oEl.className ? ' ' + sClass : sClass;
		return Tp.myObj || true;
	},
	
	// EXISTENCE D'UNE CLASSE
	hasClass: function() {
		return typeof arguments[1] == 'string' ?
			new RegExp('\\b' + arguments[1] + '\\b').test(arguments[0].className):
			arguments[1].test(arguments[0].className);
	},
	
	// SUPPRESSION D'UNE CLASSE
	removeClass: function(sClass) {
		var oEl = Tp.myObj.el || arguments[1] || null;
		var rep = oEl.className.match(' ' + sClass) ? ' ' + sClass : sClass;
		oEl.className = oEl.className.replace(rep, '');
		return Tp.myObj || true;
	},
	
	// ALERTE
	alerte: function(sMsg) {
		alert(sMsg);
		return Tp.myObj || true;
	}
	
};

var monTest = new Test$;
monTest.init();

})();

		//--></script>
	</head>
	<body>

<h1>Bloc</h1>

<div id="toto">
	<h2>Titre</h2>
	<div>
		<p>Sed certe Romani Romani existimatio quidem parentis neque criminis sed haec defendentibus sed ex cernitis maestitia existimatio neque neque neque his cernitis sentiant iuratis quidem quod et existimatio quod loco opinemur iudicantibus iuratis iuratis nostra cernitis audietis patris pietate defendentibus luctusque luctusque maeror nobis haec maestitia sentiant opinemur criminis Romani.</p>
		<p>Sed certe Romani Romani existimatio quidem parentis neque criminis sed haec defendentibus sed ex cernitis maestitia existimatio neque neque neque his cernitis sentiant iuratis quidem quod et existimatio quod loco opinemur iudicantibus iuratis iuratis nostra cernitis audietis patris pietate defendentibus luctusque luctusque maeror nobis haec maestitia sentiant opinemur criminis Romani.</p>
	</div>
</div>

	</body>
</html>
Améliorations et critiques bienvenues... Smiley smile

PS : Ce n'est qu'un test mais ça m'a donné plein d'idées... Smiley ravi
Cool...

Une petite remarque, si ce script est utilisé comme une bibliothèque, il est donc appelé a être utilisé par d'autres scripts.
Pour cela il faut bien donner un accès a celui-ci au moyen d'une variable, par exemple la fonction dollar.
Le choix est discutable vue que cette fonction est utilisé par d'autre bibliothèques, mais bon partons du principe que si on en utilise cette bibliothèque c'est justement pour pas en utiliser d'autre.

l'idée serait de pouvoir faire :

$(window).addEvent('load',function(){
  var myEl = $('toto'); 
  myEl.addClass('blueText')
  .alerte('Et voilà ! Le texte est devenu tout bleu...')
  .alerte("... mais le bleu, c'est moche donc clique dessus")
  .addEvent('click', function() {
    myEl.removeClass('blueText');
  });
});


pour cela on pourrait réécrire le début de la même forme que jQuery :

(function(){

var Test$ = function(selector){
  return new Test$.prototype.init(selector);
};

window.$ = Test$;

var Tp = Test$.prototype = {

  // OBJET
  myObj: {},

  // SELECTION D'UN ELEMENT
  init: function(sId) {
    //staff
  },
	
  // AJOUT D'UN GESTIONNAIRE D'EVENEMENT
  addEvent: function() {
    //staff
  }

  //more functions...

};

})();


Notre fonction $() devient la seul variable globale qui nous permet de faire tout les chainages de notre bibliothèque sur n'importe quel élément.

C'est très mal fait, parce que dans ce cas là si on sélectionne un nouvel élément il écrase l'ancien...
Modifié par matmat (22 Apr 2008 - 03:59)
Modérateur
Ben là, c'était fait rapidement mais l'objet généré devrait plutôt être créé au sein de $() afin de ne pas l'écraser si tu viens à te resservir de $().

L'accès aux méthodes, quant à lui, devrait plutôt se faire via une variable de ton cru (qui ici pourrait être Tp) et surtout pas par $, car trop utilisé. Autant ne pas prendre de risques dès le début, non ? Smiley cligne

Par ailleurs, ça pourrait être amélioré en ne copiant pas systématiquement toutes les propriétés du prototype mais uniquement celles qui vont être réellement utiles (inutile, par exemple, d'ajouter une méthode slice sur un élément). De même, on pourrait laisser la possibilité d'insérer de nouvelles méthodes mais ça suppose quelques vérifications. Smiley smile

En somme, il peut être intéressant de regrouper tes méthodes par type et de les affecter en fonction du type de l'objet auquel elles s'appliquent.
Salut salut,

Désolé pour l'interruption.

Pour l'instant je vais laisser de coté le prototypage, je crois qu'il faut d'abord que je progresse un peu.

Un autre avantage du chainage, hormis l'écriture rapide c'est de pouvoir encapsuler facilement toutes ces fonctions. Mais bon avant il faut pouvoir résoudre le point de la fonction d'accès ($() ou autre) de manière très propre et robuste vu que tout dépend d'elle.

Comme on l'a vu c'est complexe parce que plus on avance plus il y des nouveaux problèmes à résoudre comme le dernier point que tu as soulevé par exemple.
Modifié par matmat (25 Apr 2008 - 21:38)