Des webapps suivant le modèle Android ICS

En parallèle de la mise en place d’une maquette de webapp pour mes projets personnels, j’ai récemement développé un début de maquette pour permettre de développer des webapps à la manière Android ICS (4.0 – Ice Cream Sandwich).

A l’heure actuelle, la maquette ne propose qu’un nombre limité de fonctionnalités à savoir :

  • Header android ICS Bleu
  • Menu latéral
  • Options de l’application
  • Blocs à la Google Now

L’objectif étant à terme d’arriver à proposer une maquette beaucoup plus complète, avec notamment

  • Indicateur de chargement Fait
  • Header dans les autres couleurs ICS Fait
  • Onglets Fait
  • Gestion des formulaires
  • Toute suggestion pertinente qui sera proposée.

Le projet est partagé en open source sur Github, n’hésitez pas à ajouter votre pierre à l’édifice et récupérer les sources pour vos propres développements !

Pour un premier aperçu des fonctionnalités, voici quelques captures d’écran :
Webapp Android - page par défaut

Webapp Android - menu latéral

Webapp Android - options

Edit 12/07/2013 : La démonstration online est maintenant disponible ici.

Publicités

Quick Tip – Optimisation simple de boucles JavaScript

N’étant pas resté au bout de la soirée webperf du 21 avril, je ne me risquerais pas à faire un compte rendu qui risquerait d’être incomplet.

Néanmoins, lors de cette soirée nous avons découvert un morceau de code assez surprenant.

Pour se mettre dans le contexte, nous étions en train d’analyser le site de 20 minutes, et
Vincent Voyer
à décidé de lancer un dynatrace.

En regardant les résutlats, nous avons remarqué qu’un script avait un temps d’execution de… 1.1s, ce qui est bien entendu énorme ! Ce script vennait du tag xiti. Celui-ci sera peut-être corrigé, mais il pourra servir de cas d’exemple sur une mauvaise pratique.

En fouillant dans le code on découvre (en réindenté) le code suivant :

var elts = target.getElementsByTagName('*');
for (var i = 0; i < elts.length; i++) {
...
}

Pourquoi est-ce un problème ? Avant de commencer à expliquer le problème, il est important de connaitre un point important. Pour celui-là, j’ai utilisé statsy.

JS in HTML attributes: 1351 bytes
CSS in HTML attributes: 2790 bytes
JS in SCRIPT tag: 4111 bytes
CSS in STYLE tag: 69039 bytes
All innerHTML: 215207 bytes
# SCRIPT tag: 54
# STYLE tag: 1
# DOM elements: 2143
Cookie size: 340 bytes

Dans la liste des résultats ci-dessus, on remarque que la page comporte 2143 élements. Le problème est simple :
Le getElementsByTagName récupère tout les éléments DOM. Ce qui demande beaucoup de traitement sur une page kilométrique comme celle d’un site de news.

Second problème, la boucle effectuée sur ces éléments recalcule cette selection à chaque itération de la boucle. Les élements sont donc recalculés 2143 fois. C’est simplement énorme.

Comment corriger le problème ? Pour le premier, il faut savoir si cette selection est vraiment importante. Qu’à-t-on besoin de faire sur tout l’arbre DOM ?
Pour le second, il suffit de mettre en cache la taille du tableau, par exemple comme ceci :

var elts = target.getElementsByTagName('*');
for (var i = 0, len = elts.length; i < len; i++) {
...
}

Et dans ces conditions, le recalcul ne sera pas refait pour chaque itération.

Conclusion :
De plus en plus, les sites sont remplis de JavaScript. Ces améliorations peuvent sembler superflues, mais c’est ce qui peut faire la différence entre deux site.
Si vous n’en êtes pas convaincus, pensez aux utilisateurs de netbook, de tablettes ou de mobile : Vous allez leur faire gagner un temps fou !

Mettre en place un placeholder et son failback

Le HTML5 apporte un nouvel attribut aux champs input text et assimilés : placeholder.

Le principe de cet attribut est simple : lorsqu’un champ est vide, on affiche un texte de remplacement pour indiquer ce que doit contenir le champ. Il est très utilisé au niveau applicatif, comme dans l’omnibar de Firefox par exemple :

Placeholder de l'omnibar de Firefox

Le HTML5 nous permet de l’utiliser nativement. Cependant, cet attribut n’est pas encore disponible sous tout les navigateurs. L’objectif fixé par cet article est de mettre en place le système ainsi qu’un bon failback.

Pour effectuer tout ceci, j’utiliserais jQuery. Le script peut bien entendu être étendu à tout les framework, et je vous invite à mettre en commentaire le système avec d’autres systèmes.

Si l’on veut faire simple et tout gérer en JavaScript, il suffit d’écrire un code tel que le suivant :

$('input[type="text"]').blur(function() {
	if (this.value == '') {
		this.value = this.defaultValue;
	}
}).focus(function() {
	if (this.value == this.defaultValue) {
		this.value = '';
	}
})

Ce modèle présente une limitation, mais nous verons ça plus tard.

Le premier point à mettre en place est la reconnaissance de la présence du placeholder. On va ici utiliser la même méthode que Modernizr :

var input = document.createElement('input');
if (!('placeholder' in input)) {
	// Faire ici le traitement pour les navigateurs ne reconnaissant pas le placeholder
	// Il suffit d'y placer le code précédent par exemple !
} else {
	// On supporte le placeholder : on vide la valeur par défaut
	$('input[type="text"]').val('');
}

Et voilà, le système est en place et il suffit de mettre en application le placeholder sur le champ input :


Aller plus loin :

Les plus perspicaces vont tout de suite remarquer la limitation de cette méthode : Obligation de vider la valeur du champ de formulaire, dont pas de valeur par défaut possible, Impossible de rentrer le même texte que le placeholder, …

On peut aussi parler du l’interdiction du JavaScript, mais je considèrerais cela comme un détail sans importance : les plus grands sites (gmail, facebook, …) obligeant à l’utiliser, j’ommetrais les visteurs récalcitrants.

Les problèmes exposés, attaquons la première amélioration :

Afin d’éviter les problèmes de valeurs par défaut, je vais utiliser la trop peu connue fonction de jQuery data.

$('input[type="text"]').each(function () {
	var $this = $(this);
	var $default = $this.data('placeholder');
	if ($default === undefined) {
		return;
	}
	$this.data('placeholderactive', true);
	$this.focus(function () {
		if ($this.data('placeholderactive') === true && $this.val() == $default) {
			$this.val('').removeClass('placeholderactive').addClass('placeholderinactive');
			$this.data('placeholderactive', false);
		}
	}).blur(function () {
		if ($this.data('placeholderactive') === false && $this.val() == '') {
			$this.val($default).removeClass('placeholderinactive').addClass('placeholderactive');
			$this.data('placeholderactive', true);
		}
	}).val($default).removeClass('placeholderinactive').addClass('placeholderactive');
});

Et son nouveau champ input associé :


Pour mettre un style différent sur le champ placeholder, j’ai utilisé deux classes : placeholderactive et placeholderinactive qui s’occupent respectivement d’avoir un style en mode « placeholder » et un style hors de ce mode.

A vous de jouer et d’implémenter ce nouvel attribut HTML5 sur vos site !

N’hésitez pas à partager vos méthodes pour les autres framework JS ou les améliorations à apporter à mon script jQuery.

Statsy v2 – Quelques ajouts

Stoyan Stefanov à présenté Stasty, un bookmarklet qui permet d’obtenir très rapidement des informations notamment sur les style et script présents en inline sur une page.
Cependant, il manque sur ce script quelques informations importantes, facilement récupérables.
J’ai donc créé un nouveau bookmarklet avec des infomations un peu plus complètes.

Stoyan Stefanov à présenté Stasty, un bookmarklet qui permet d’obtenir très rapidement des informations notamment sur les style et script présents en inline sur une page.

Cependant, il manque sur ce script quelques informations importantes, facilement récupérables.

J’ai donc créé un nouveau bookmarklet avec des infomations un peu plus complètes :

Statsy 2
(function(){
    var jsattribs = [
        'onbeforeunload',
        'onclick',
        'ondblclick',
        'onerror'
        'onload',
        'onmousedown',
        'onmousemove',
        'onmouseout',
        'onmouseover',
        'onmouseup',
        'onunload',
    ],
    cssattribs = [
        'style'
    ];

    var all_elems = document.getElementsByTagName('*');

    function getAttribsSize(attribs) {
        var value = '',
            attr = '',
            cnt = 0;

        for (var i = 0; i < all_elems.length; i++) {
            for (var j = 0; j < attribs.length; j++) {
                attr = attribs[j];
                value = all_elems[i].getAttribute(attr);
                if (value && typeof value === 'string') {
                    cnt += attr.length;
                    cnt += 3; // ="..."
                    cnt += value.length;
                }
            }
        }
        return cnt;
    }

    function getInlineSize(tag) {
        var s = 0,
            all = document.getElementsByTagName(tag);
        for (var i = 0; i < all.length; i++) {
            s += all[i].innerHTML.length;
        }
        return s;
    }

    var jsatt = getAttribsSize(jsattribs);
    var cssatt = getAttribsSize(cssattribs);

    var msg = [];
    msg.push('JS in HTML attributes: '+jsatt+' bytes');
    msg.push('CSS in HTML attributes: '+cssatt+' bytes');
    msg.push('JS in SCRIPT tag: '+getInlineSize('script')+' bytes');
    msg.push('CSS in STYLE tag: '+getInlineSize('style')+' bytes');
    msg.push('All innerHTML: '+document.documentElement.innerHTML.length+' bytes');
    msg.push('# SCRIPT tag: '+document.getElementsByTagName('script').length);
    msg.push('# STYLE tag: '+document.getElementsByTagName('style').length);
    msg.push('# DOM elements: '+all_elems.length);
    msg.push('Cookie size: '+document.cookie.length+' bytes');

    alert(msg.join("n"));
})();

Vous pouvez directement récupérer le bookmarklet ici :

Statsy 2

N’hésitez pas à faire vos suggestions !

UPDATE 30/11/2010

Suite à la demande de jpvincent, petit explicatif des différentes informations remontées :

JS in HTML attributes : Cette information remonte tout le code JavaScript présent directement dans les balises HTML en tant qu’attribut. Par exemple, une page qui contiendrait <h1 onmouseover= »alert(‘H1’); »></h1> ajouterait 26 bits à JS in HTML attributes

CSS in HTML attributes : Même procédé que pour les JavaScript détaillés un peu plus haut, en utilisant cette fois le contenu des attributs style (ex. style= »color:#f00; »)

JS in SCRIPT tag : Cette information remonte le nombre de bits du code JavaScript présent au sein de balises

JCSS in STYLE tag : Même principe que la propriété précédente, cette information remonte le nombre de bits du code CSS présent au sein de balises <style></style>

# SCRIPT tag : Compte le nombre d’occurrence d’utilisation des balises <script> au sein de la page

# STYLE tag : Compte le nombre d’occurrence d’utilisation des balises <style> au sein de la page

# DOM elements : Compte le nombre d’éléments DOM au sein de la page

Cookie size : Retourne la taille en bit des cookies présent sur le fichier HTML, et donc probablement sur tout les fichiers statiques s’ils ne sont pas sur un domaine externe

Rendre l’URL de recherche de Google plus lisible

Matt Cutts, responsable de la webspam team de google à posté sur son twitter Une méthode pour rendre les URL de recherche de Google plus jolies.

Lorsque vous faites une recherche, vous tombez facilement sur une URL du type http://www.google.com/search?source=ig&hl=en&rlz=&q=google&aq=f&oq=&aqi=g-p3g7 qui n’est au final pas très lisible. Cette méthode permet de transformer cette URL en une plus compréhensible pour l’humain et aillant les mêmes résultats : http://www.google.com/search?q=google.

Malheureusement, dans l’exemple que je donne, cette méthode est buggée et ne retourne pas un bon résultat. J’ai donc corrigé ce bookmarklet afin d’avoir un résultat correct en toute situation. Le bookmarklet est disponible ici : Trim Google Result.

Pour les personnes qui voudraient éventuellement analyser le code, le voici en version non minimifiée :

javascript:(function(){
	var windowLocationHref = window.location.href;
	var domainName = window.location.hostname;
	var myPathname = window.location.pathname;
	var searchArray = windowLocationHref.split('&');
	var mySearch = '';
	for(myElement in searchArray){
		if(searchArray[myElement].match(/^q=/)){
			mySearch = searchArray[myElement];
		}
	}
	var searchOnly = domainName + myPathname + '?' + mySearch;
	self.location.href='http://' + searchOnly;
})();

Erreur Javascript Internet Explorer : Opération abandonnée

Erreur Javascript Internet Explorer : Opération abandonnée

Si l’on prend des navigateurs différents, on n’obtient pas forcement la même chose avec le même code. C’est régulièrement le cas avec Internet Explorer et Mozilla Firefox.

J’ai récemment eu des complications avec un code JavaScript. Le message indiquait « Internet Explorer ne peut pas ouvrir le site » « Opération abandonnée ! »

La raison de se problème réside dans le fait que du code JavaScript est exécuté avant le chargement complet de la page. Internet Explorer ne le supporte pas et bloque son chargement. Pour parer ce problème, il existe plusieurs méthodes, suivant s’il l’on utilise un Framework tel que prototype.js ou si l’on n’insère que quelques bout indépendant de script dans ses pages.

Pour la première solution, je pars du principe que l’on utilise propotype.js comme framework.
Il suffit de remplacer le code actuel par :

event.observe(window,'load',function () { 
     	// Insérer le script ici 
});

Ainsi, la fonction faisant planter Internet Explorer ne s’exécute qu’après le chargement de la page et l’on peut souffler.

La deuxième manière consiste à modifier soi-même le load de la fonction.

window.onload = function () {
      // Insérer le script ici 
}

Afin de coder proprement et permettre des ajouts futurs simple, nous allons ajouter la fonction au chargement de la page et non la remplacer comme il est habituellement fait et comme je l’ai montré ci-dessus. A la première syntaxe, préférez :

window.onload += function () { 
      // Insérer le script ici 
}

Nous faisons ici une concaténation et la fonction est ajoutée aux autres exécutés en cas de chargement de la page.

Et voilà, un problème de compatibilité réglé !