Archives par étiquette : js

Utilisez la méthode Function.prototype.bind() de Javascript

Chaque développeur javascript a un jour ou l’autre rencontré un problème avec le contexte associé à this.

Prenons l’exemple de code simpliste suivant :

var myObject = {

  crazyMessage: 'gouzigouza',

  doSomethingCrazy: function() {
    alert(this.crazyMessage);
  }
};

myObject.doSomethingCrazy();

Mon objet contient une propriété crazyMessage et une méthode doSomethingCrazy qui utilise la propriété crazyMessage.
Jusqu’ici tout va bien, le contexte de this au sein de la méthode doSomethingCrazy est bien l’objet myObject.

Maintenant, dans un élan de folie, je veux implémenter la méthode doSomeAsyncCrazyness :

var myObject = {

  crazyMessage: 'gouzigouza',

  doSomethingCrazy: function() {
    alert(this.crazyMessage);
  },

  doSomeAsyncCrazyness: function() {
    setTimeout(function() {
      this.doSomethingCrazy();
    }, 1000);
  }
};

myObject.doSomeAsyncCrazyness();

Aussi surprenant que cela puisse paraître, je récupère une erreur dans ma console :

Uncaught TypeError: undefined is not a function 

Cette erreur est due à this qui est désormais associé au contexte global et non plus à mon objet myObject, à cause de l’appel à setTimeout.

Mettre this en « cache »

Pour pallier ce problème, l’approche très souvent adoptée par le développeur est d’assigner this à une variable intermédiaire temporaire de type that ou self de la manière suivante :

var myObject = {

  crazyMessage: 'gouzigouza',

  doSomethingCrazy: function() {
    alert(this.crazyMessage);
  },

  doSomeAsyncCrazyness: function() {

    var that = this;
    setTimeout(function() {
      that.doSomethingCrazy();
    }, 1000);
  }
};

myObject.doSomeAsyncCrazyness();

De cette manière, l’appel à la méthode doSomeAsyncCrazyness fonctionne en effet comme prévu.

Une méthode bien plus élégante existe cependant et ce depuis ECMAScript 5.

.bind() magic

La méthode .bind() disponible sur le prototype de Function résoud notre problème en permettant, entre autres choses, de définir explicitement le contexte associé à this.

Nous pouvons donc réécrire notre code précédent de la manière suivante :

var myObject = {

  crazyMessage: 'gouzigouza',

  doSomethingCrazy: function() {
    alert(this.crazyMessage);
  },

  doSomeAsyncCrazyness: function() {
    setTimeout(function() {
      this.doSomethingCrazy();
    }.bind(this), 1000);
  }
};

myObject.doSomeAsyncCrazyness();

Avouez que c’est quand même plus joli.

Support des vieux navigateurs

Et si mon code doit s’exécuter dans un navigateur qui ne supporte pas ES5 ? Internet Explorer < 9 pour ne pas le nommer.
Dans ce cas, plusieurs solutions.

  • Continuer avec la technique de mise en cache de this
  • Utiliser la méthode .bind implémentée dans la plupart des frameworks JS (Angular.bind() par exemple)
  • Implémenter un fallback quand la méthode n’est pas disponible

Mozilla notamment propose le fallback suivant (Mozilla polyfill Function.prototype.bind()) :

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - 
           what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

Pour aller plus loin

Quelques ressources pour aller plus loin :

Transpiler son code ES6 avec Gulp et Traceur

L’actualité est de plus en plus riche pour les développeurs web ces derniers temps. Entre Hack, HVVM, Symfony 3, Angular 2.0, Ember 1.6 ou ECMAScript 6, on ne sait plus où donner de la tête.

Alors que dans certaines entreprises, on se bat encore pour abandonner les navigateurs types IE6/7/8 et enfin pouvoir utiliser ES5 sans problème, ailleurs, on commence déjà à développer en ES6. C’est notamment le cas de la team AngularJs qui a annoncé que Angular 2.0 serait écrit en ES6. La démo du travail en cours sur le nouveau framework de DI d’Angular a même été faite avec des bouts de code en ECMAScript6.

Et la bonne nouvelle, c’est que si les navigateurs modernes sont encore loin de supporter le futur ES6 (beaucoup de nouvelles features restent à l’état de draft), rien n’empêche de le tester dès aujourd’hui. Et pourquoi pas même, en production… Madness?! Un peu je dois bien l’avouer… Mais si vous avez le goût du risque, après tout, pourquoi pas… J’ai bien eu l’occasion de travailler sur une grosse application avec Ember-Data en v0.13…

Quelles solutions pour tester ES6 ?

Alors comment faire pour tester ES6 ? Plusieurs solutions s’offrent à nous. Une des plus simples si on souhaite uniquement jouer avec les nouveaux modules ou s’essayer au modèle objet, c’est d’utiliser un équivalent à jsfiddle, compatible ECMA6. C’est le cas par exemple de es6fiddle.

Utiliser Gulp et Traceur pour « traduire » son code ES6 vers ES5

En revanche, si l’on souhaite commencer à coder une appli réelle en ES6 et la voir s’exécuter dans un navigateur moderne, il faut se tourner vers des solutions telles que le projet Traceur de Google.

Traceur is a JavaScript.next-to-JavaScript-of-today compiler that allows you to use features from the future today.

Traceur est ce qu’on appelle un transpiler, c’est à dire, un compilateur qui compile le code source d’un langage vers le code source d’un autre. En l’occurence donc, un transpiler ECMAScript6 vers ECMAScript5.

Traceur supporte une liste importante des features populaires d’ES6 et, cerise sur le gâteau, il a son propre plugin Gulp.

Commençons donc par se créer un répertoire pour notre projet et y installer gulp et son plugin gulp-traceur :

mkdir projet-es6 && cd projet-es6
npm init
npm install --save-dev gulp gulp-traceur
mkdir dist src

npm init

On crée ensuite un gulpfile simpliste :

// gulpfile.js
var gulp = require('gulp'),
    traceur = require('gulp-traceur')
    paths = {
       scripts: ['src/*.js', 'src/**/*.js']
    };

gulp.task('scripts', function () {
    gulp.src(paths.scripts)
        .pipe(traceur())
        .pipe(gulp.dest('dist'));
});

gulp.task('watch', function() {
  gulp.watch(paths.scripts, ['scripts']);
});

gulp.task('default', ['scripts', 'watch']);

Et voilà, nous sommes prêts à tester ES6 et à le transpiler vers ES5 grâce à Gulp.

On peut essayer avec une Arrow Function par exemple :

// src/app.js
var square = x => x * x;

Après avoir lancé la commande gulp scripts, notre fonction arrow est transformée en une bonne « vieille » fonction ES5 :

"use strict";
var __moduleName = "app";
var square = (function(x) {
  return x * x;
});

Ci-dessous, le résultat de transpiling avec une classe ES6 toute simple :

Screencast Gulp Traceur ES6

Il ne nous reste plus qu’à jouer avec les autres features spécifiques ES6 :

  • Let et Const
  • Paramètres par défaut
  • Proxies
  • Modules
  • Destructuring

Pour aller plus loin

On trouve toute une flopée d’exemples et de resources à propos d’ECMAScript 6. Je trouve ce document assez bien fait : https://github.com/lukehoban/es6features pour se lancer.

Quelques autres resources intéressantes :

Designez des API asynchrones VRAIMENT asynchrones

Je suis tombé récemment sur un vieil article d’Isaac Zimmitti Schlueter à lire ABSOLUMENT.

Pour résumer rapidement le propos, IZS explique suite à un avertissement on ne peut plus clair (Do Not Release Zalgo) que lors du design d’une API Asynchrone, il faut éviter à tout prix la situation suivante :

var cachedData;
function getSomeData(callback) {
  if (cachedData === undefined) {
    $.get('some/random/api').then(function(data) {
      callback(cachedData = data);
    });
  } else {
    callback(cachedData);
  }
}

Quel est le problème avec ce bout de code ?

Si vous êtes développeur AngularJs / EmberJs / votre framework javascript préféré, je suis certain que vous avez vous-même été amené à écrire du code similaire ou au moins à en rencontrer au sein de vos applications.

Alors quel est le problème ?

Mon API fait un calcul coûteux (requête HTTP vers une API tiers) au premier appel, stocke la donnée retournée dans une variable servant de cache et appelle le callback. Au deuxième appel, mon API constate que la donnée est déjà en cache et appelle immédiatement le callback.

L’utilisation d’une API de ce type est extrêmement complexe et source de bugs. Pourquoi ? Son comportement est difficile à prédire…

L’auteur l’explique en ces termes sur son article :

If you have an API which takes a callback, and sometimes that callback is called immediately, and other times that callback is called at some point in the future, then you will render any code using this API impossible to reason about, and cause the release of Zalgo.

Prenons l’exemple du code javascript suivant :

// Quelque part dans mon code
getSomeData(function(result) {
  // Faire quelque chose avec mon résultat
});

function clickHandler() {
  var data;
  getSomeData(function(result) {
    data = result;
  });

  if (data.id === 1) {
    // Traitement important
    alert('gouzigouza');
  }
}

Ligne 2, on appelle notre API asynchrone. Un peu plus bas, on définit une fonction appelée lors d’un clic sur un bouton dans une page web. Au sein de ce clickHandler on utilise l’objet data en partant du principe qu’il est disponible immédiatement. L’erreur saute ici évidemment aux yeux. Rien ne garantit que notre objet data ait déjà été retourné arrivé à la condition if.

Malheureusement, le design de notre API et l’utilisation qui en a été faite ici fait que le bug n’est pas identifiable immédiatement. En revanche, si le développeur supprime le premier appel à notre API ligne 2 ou si le clic sur le bouton au sein de la page web intervient avant que le résultat de notre API asynchrone n’ait été mis en cache, alors on se retrouvera avec une belle erreur javascript :

TypeError: Cannot read property 'id' of undefined

Quelle solution ?

Pour une API asynchrone, l’objectif est donc de toujours respecter le caractère asynchrone.
Pour se faire, une approche simpliste est de modifier notre API de la manière suivante :

var cachedData;
function getSomeData(callback) {
  if (cachedData === undefined) {
    $.get('some/random/api').then(function(data) {
      callback(cachedData = data);
    });
  } else {
    setTimeout(function() {
      callback(cachedData = data);
    }, 0);
  }
}

Ici, l’utilisation du setTimeout() est une astuce qui permet de forcer l’appel du callback de manière asynchrone. Si l’API était codée avec nodejs, il serait bien plus judicieux d’utiliser process.nextTick() qui permet de mettre en place un comportement similaire.

Ainsi, à l’usage, il est désormais systématiquement impossible de faire fonctionner le code suivant :

var data;
getSomeData(function(result) {
  data = result;
});

if (data.id === 1) {
  // Bug systématique
  alert('gouzigouza');
}

Le développeur identifie immédiatement son erreur et est obligé alors de modifier son code de la manière suivante :

getSomeData(function(result) {
  var data = result;
  if (data.id === 1) {
    alert('gouzigouza'); // OK
  }
});

Pour aller plus loin

Voici quelques liens pour approfondir le sujet. Je vous conseille évidemment la lecture de l’article de IZS qui ajoute des exemples de design pour des API où les données sont généralement présentes immédiatement et où les performances sont très importantes.
Vous pouvez également aller jeter un oeil à la partie Keeping callbacks truly asynchronous de cet article sur la fonction process.nextTick de nodejs.

EmberJs – Construire une application autour de panels et d’onglets

Le router de emberjs se base massivement sur des conventions de nommage qui rendent ultra rapide le développement d’une application, notamment si celle-ci est basée sur un workflow standard de type CRUD.
Je liste mes entités, je peux voir une entité, je peux éditer une entité…
Exemples d’URLs de ce type :

  • /posts
  • /posts/1/show
  • /posts/1/edit
  • /authors
  • /authors/1/show
  • /authors/1/edit

Dans ce genre de cas, il n’est presque jamais nécessaire de surcharger les routes générées en mémoire par ember.

En revanche, quand une application devient un peu plus complexe dans sa structure, il devient indispensable de définir des routes spécifiques.

Aujourd’hui, je vais présenter une approche permettant de construire une application emberjs autour d’un layout découpé en panneaux (panels), chacun de ces panneaux étant capable d’afficher un ou plusieurs onglets. Evidemment, en ayant comme objectif que l’état de ces différents onglets soit géré par le router pour pouvoir le conserver via l’URL sur un reloading de la page.

Ci-dessous un petit schéma pour expliciter visuellement le contexte :

Application ember panes with tabs

Commençons par définir notre application et le layout associé :

// main.js
App = Ember.Application.create({});
// index.html
<script type="text/x-handlebars" data-template-name="application">
  <h1>This is a sample app</h1>
  {{outlet leftPanel}}
  {{outlet rightPanel}}
</script>

Pour savoir en permanence quels sont les onglets sélectionnés dans chacun des panneaux de gauche et de droite, il nous faut les stocker quelque part. L’endroit idéal est le contrôleur d’application qui se trouve hiérarchiquement au dessus de nos deux panels.
On lui ajoute donc deux propriétés leftTab et rightTab auxquelles on associe les onglets à afficher par défaut.
Ensuite, il nous faut définir les contrôleurs pour le panneau de gauche et le panneau de droite. Ils restent vides dans un premier temps mais ce sont eux qui s’occuperont de notifier le router lorsque l’utilisateur souhaite changer l’un ou l’autre des onglets sélectionnés.

App.ApplicationController = Ember.Controller.extend({
    leftTab: 'leftPanelTab1',
    rightTab: 'rightPanelTab1'
});
 
App.LeftPanelController = Ember.Controller.extend({
    // ...
});
 
App.RightPanelController = Ember.Controller.extend({
    // ...
});

Ajoutons les templates associés à nos panneaux. Ces derniers contiennent les liens vers les différents onglets et un outlet dans lequel notre router va injecter l’onglet sélectionné :

<script type="text/x-handlebars" data-template-name="leftPanel">
<div>Left panel
    <a href="#" {{action selectTab "leftPanelTab1"}}>Left panel tab 1</a>
    <a href="#" {{action selectTab "leftPanelTab2"}}>Left panel tab 2</a>
    {{outlet}}
</div>
</script>

<script type="text/x-handlebars" data-template-name="rightPane">
<div>Right panel
    <a href="#" {{action selectTab "rightPanelTab1"}}>Right panel tab 1</a>
    <a href="#" {{action selectTab "rightPanelTab2"}}>Right panel tab 2</a>
    {{outlet}}
</div>
</script>

Pour injecter nos panneaux au sein de notre layout, il est nécessaire d’implémenter la route ApplicationRoute :

App.ApplicationRoute = Ember.Route.extend({
    renderTemplate: function() {
        this.render();
        this.render('leftPanel', {
            outlet: 'leftPanel',
            into: 'application'
        });
 
        this.render('rightPanel', {
            outlet: 'rightPanel',
            into: 'application'
        });
    }
});

Pour notifier notre application que l’utilisateur souhaite changer l’onglet de l’un ou l’autre des panneaux, on utilise ici un helper action pour déclencher une action au niveau de nos contrôleurs, en lui spécifiant en paramètre l’onglet cliqué.

Il nous faut donc maintenant intercepter ces actions dans les contrôleurs et remonter l’information au niveau de notre router en les modifiant ainsi :

App.LeftPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('leftTabChanged', tab);
    }
});
 
App.RightPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('rightTabChanged', tab);
    }
});

Pour représenter l’état de nos panneaux (quels onglets sont affichés), nous allons implémenter une route prenant en paramètres l’onglet à afficher dans chaque zone.

App.PanelsRoute = Ember.Route.extend({
    serialize: function(params, paramNames) {
        return params;
    },
    renderTemplate: function(controller, params) {
 
        this.render(params.leftTab, {
            into: 'leftPanel'
        });
 
 
        this.render(params.rightTab, {
            into: 'rightPanel'
        });
    }
});

Et pour obtenir une belle URL associée, nous pouvons définir le mapping suivant :

App.Router.map(function() {
    this.resource('panels', { path: '/:leftTab/:rightTab' });
});

Enfin, pour terminer et lier l’ensemble, nous allons modifier notre ApplicationRoute afin d’y gérer les évènements associés aux changements d’onglets.
Quand un évènement nous notifiant du changement d’onglet dans le panneau de gauche remonte jusqu’au router, on redirige vers la route panels en lui injectant les nouveaux onglets sélectionnés.

App.ApplicationRoute = Ember.Route.extend({
    events: {
        leftTabChanged: function(tab) {
            this.controllerFor('application').set('leftTab', tab);
            this.transitionTo('panels', {
                leftTab: tab,
                rightTab: this.controllerFor('application').get('rightTab')
            });
 
        },
        rightTabChanged: function(tab) {
            this.controllerFor('application').set('rightTab', tab);
            this.transitionTo('panels', {
                leftTab:this.controllerFor('application').get('leftTab'),
                rightTab:tab
            });
        }
    },

    renderTemplates: // ...
}

Une autre solution pour répondre à ce même problème serait d’injecter dans nos controlers de panels l’onglet sélectionné dans le panneau opposé. On peut dans ce cas simplifier le code et utiliser directement des helpers linkTo. La solution présentée ici offre néanmoins une grande souplesse pour opérer des traitements intermédiaires lors de la selection par l’utilisateur d’un onglet dans un ou l’autre des panneaux.

Ci-dessous le code complet de cet exemple :

App = Ember.Application.create({});

App.Router.map(function() {
    this.resource('panels', { path: '/:leftTab/:rightTab' });
});

App.ApplicationRoute = Ember.Route.extend({
    events: {
        leftTabChanged: function(tab) {
            this.controllerFor('application').set('leftTab', tab);
            this.transitionTo('panels', {
                leftTab: tab,
                rightTab: this.controllerFor('application').get('rightTab')
            });

        },
        rightTabChanged: function(tab) {
            this.controllerFor('application').set('rightTab', tab);
            this.transitionTo('panels', {
                leftTab:this.controllerFor('application').get('leftTab'),
                rightTab:tab
            });
        }
    },

    renderTemplate: function() {
        this.render();
        this.render('leftPanel', {
            outlet: 'leftPanel',
            into: 'application'
        });

        this.render('rightPanel', {
            outlet: 'rightPanel',
            into: 'application'
        });
    }
});

App.IndexRoute = Ember.Route.extend({
    redirect: function() {
        this.transitionTo('panels', {
            leftTab: this.controllerFor('application').get('leftTab'),
            rightTab: this.controllerFor('application').get('rightTab')
        });
    }
});

App.PanelsRoute = Ember.Route.extend({
    serialize: function(params, paramNames) {
        return params;
    },
    renderTemplate: function(controller, params) {

        this.render(params.leftTab, {
            into: 'leftPanel'
        });


        this.render(params.rightTab, {
            into: 'rightPanel'
        });
    }
});

App.ApplicationController = Ember.Controller.extend({
    leftTab: 'leftPanelTab1',
    rightTab: 'rightPanelTab1'
});

App.LeftPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('leftTabChanged', tab);
    }
});

App.LeftPanelView = Ember.View.extend({
    classNames: ['pane']
});

App.RightPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('rightTabChanged', tab);
    }
});

App.RightPanelView = Ember.View.extend({
    classNames: ['pane']
});
<script type="text/x-handlebars" data-template-name="application">
  <h1>This is a sample app</h1>
  {{outlet leftPanel}}
  {{outlet rightPanel}}
</script>

<script type="text/x-handlebars" data-template-name="leftPanel">
    <a href="#lefttab1" {{action selectTab "leftPanelTab1"}}>Left pane tab 1</a>
    <a href="#lefttab2" {{action selectTab "leftPanelTab2"}}>Left pane tab 2</a>
    <div class="tab-container">{{outlet}}</div>
</script>

<script type="text/x-handlebars" data-template-name="leftPanelTab1">
Left Panel Tab1
</script>

<script type="text/x-handlebars" data-template-name="leftPanelTab2">
Left Panel Tab2
</script>

<script type="text/x-handlebars" data-template-name="rightPanel">
    <a href="#righttab1" {{action selectTab "rightPanelTab1"}}>Right pane tab 1</a>
    <a href="#righttab2" {{action selectTab "rightPanelTab2"}}>Right pane tab 2</a>
    <div class="tab-container">{{outlet}}</div>
</script>

<script type="text/x-handlebars" data-template-name="rightPanelTab1">
    Right Panel Tab1
</script>

<script type="text/x-handlebars" data-template-name="rightPanelTab2">
Right Panel Tab2
</script>

Le résultat est visible sur ce jsfiddle : http://jsfiddle.net/obalais/L78yu
Et le gist associé ci-besoin : https://gist.github.com/bobey/5118200