Archives par étiquette : clientside application

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