mercredi 18 novembre 2015

Un proto d'appli Web en moins de 2 jours grâce à AngularJS


Retour d'expérience et pourquoi j'ai pu réaliser un prototype d'application Web en 2 jours grâce à AngularJS et un peu d'outillage.

Le code source "exemple" (légèrement remanié et simplifié par rapport à l'original) est en ligne :
https://github.com/xnopre/proto-with-angular

Contexte

Début octobre, j'ai attaqué une nouvelle mission au sein d'une startup d'état au SGMAP à Paris. Je dois développer une application Web pour fournir aux personnes qui vont partir en retraite, une liste planifiée des démarches qu'elles doivent effectuer. L'application doit se composer d'une succession de 4-5 écrans permettant à l'utilisateur de saisir quelques informations, de répondre à quelques questions, puis de consulter la liste des démarches à effectuer.

Le groupe de travail du projet se regroupe toutes les 2 semaines, et il fallait rapidement leur montrer un prototype de l'application Web pour qu'ils puissent mieux se rendre compte de ce à quoi l'application pourrait ressembler.

J'avais donc 3 jours pour produire un prototype !

Les besoins, les contraintes et les concessions

J'ai commencé par recenser les besoins, et notamment de montrer au groupe de travail comment pourraient se passer les points suivants :

  • Enchainements des différents écrans
  • Saisie de données de l'utilisateur (étape 1) 
  • Poser des questions simples comme le choix du régime (étape 2)
  • Afficher certaines questions selon les réponses à d'autres questions (étape 4)
  • Présentation des résultats de la dernière étape, avec un découpage en chapitre ayant chacun un numéro, un titre, une échéance, et la possibilité "d'ouvrir" le chapitre pour consulter son contenu

J'ai rapidement évalué que je n'avais pas forcément besoin d'un véritable serveur (au sens applicatif, comme par exemple Play! ou NodeJS), ni d'une base de données. Les données des différentes écrans pouvaient être en dur, et le proto jetable.

Solution initiale : des pages statiques

La première solution retenue était de faire des pages statiques, avec textes et liens en dur. Mais finalement, très rapidement les contraintes sont apparues, et instinctivement, je me suis dis que je gagnerai du temps en faisant une "Single Page Application" (SPA) avec AngularJS.

Solution finale : AngularJS

Je vais essayer de synthétiser en quoi AngularJS m'a fait gagner du temps.

Pas besoin de serveur d'application

Avec Grunt, et la commande "Grunt serve", un serveur NodeJS est lancé sur le port 9100, la page de mon application est automatiquement ouverte dans mon navigateur, et puisque je suis en "live reload", dès que j'enregistre des changements dans mon éditeur (j'aime bien Brackets pour les applis JavaScript et Angular), la page en cours est rechargée automatiquement dans le navigateur : très productif !

A noter également que l'opération "grunt serve" (tout comme un "grunt build", non configuré dans le proto), grâce à la tâche "injector", permet d'injecter automatiquement dans le fichier "index.html" tous les fichiers CSS et JS, issues dans dépendances Bower, mais également créées dans l'architecture de l'application. Ainsi, les sections suivantes (entre les "injector" et "endinjector") sont automatiquement remplies et mises à jour :

<!doctype html>
<html lang="fr">

    <head>
        
    ...

    <!-- injector:bowercss -->
    <link rel="stylesheet" href="lib/bootstrap-css-only/css/bootstrap.css">
    <link rel="stylesheet" href="lib/textAngular/src/textAngular.css">
    <!-- endinjector -->

    <!-- injector:css -->
    <link rel="stylesheet" href="css/main.css">
    <!-- endinjector -->
    </head>
    
    <body ng-app="Proto" >
        
    ...

    <!-- injector:bowerjs -->
    <script src="lib/jquery/dist/jquery.js"></script>
    <script src="lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="lib/angular/angular.js"></script>
    <script src="lib/angular-resource/angular-resource.js"></script>
    <script src="lib/angular-cookies/angular-cookies.js"></script>
    <script src="lib/angular-animate/angular-animate.js"></script>
    <script src="lib/angular-touch/angular-touch.js"></script>
    <script src="lib/angular-sanitize/angular-sanitize.js"></script>
    <script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>
    <script src="lib/angular-ui-utils/ui-utils.js"></script>
    <script src="lib/angular-ui-router/release/angular-ui-router.js"></script>
    <script src="lib/rangy/rangy-core.js"></script>
    <script src="lib/rangy/rangy-classapplier.js"></script>
    <script src="lib/rangy/rangy-highlighter.js"></script>
    <script src="lib/rangy/rangy-selectionsaverestore.js"></script>
    <script src="lib/rangy/rangy-serializer.js"></script>
    <script src="lib/rangy/rangy-textrange.js"></script>
    <script src="lib/textAngular/src/textAngular.js"></script>
    <script src="lib/textAngular/src/textAngular-sanitize.js"></script>
    <script src="lib/textAngular/src/textAngularSetup.js"></script>
    <script src="lib/angular-mocks/angular-mocks.js"></script>
    <!-- endinjector -->

    <!-- injector:js -->
    <script src="js/app.js"></script>
    <script src="js/app.module.config.js"></script>
    <script src="js/main-controller.js"></script>
    <!-- endinjector -->

    </body>
</html>


Et la configuration de la tâche "injector" pour Grunt est la suivante :

        injector: {
            options: {
                addRootSlash: false,
                ignorePath: 'app/',
                bowerPrefix: 'bower',
            },
            localDependencies: {
                files: {
                    'app/index.html': [
                        'app/js/**/*.js',
                        'app/css/{,*/}*.css'
                    ]
                }
            },
            bowerDependencies: {
                files: {
                    'app/index.html': ['bower.json'],
                }
            }
        },

Moins d'HTML

Avec une SPA, j'ai simplement besoin d'un fichier index.html pour le cadre général, puis de petits morceaux d'HTML pour chaque page ("templates").

Le module 'Proto'

La déclaration du module applicatif Angular est très simple :
'use strict';

angular.module('Proto', [
    'ui.bootstrap',
    'ui.router',
    'ngSanitize'
]);

Il faut alors indiquer avec l'attribut "ng-app" dans la page principale index.html qu'on veut utiliser ce module :

    <body ng-app="Proto">


Et toujours dans cette page, indiquer avec "ui-view" l'endroit où doivent être insérés les templates de chaque vue :

    <div class="container">
        <div ui-view="">
        </div>
    </div>


Le routage

Pour le routage avec AngularJS, j'aime bien ui-router qui permet de travailler avec des "états". Pour mon proto, je déclare donc 5 états avec pour chacun : son nom, son URL, son template HTML, et le même controller pour tous les états, je ne fais pas comme cela dans la "vraie vie", mais dans le cas présent, ça me suffit et me simplifie le code :

'use strict';

angular.module('Proto').config(function ($urlRouterProvider, $stateProvider) {
    
    $urlRouterProvider
        .otherwise("/step1");

    $stateProvider
        .state ('step1', {
            url:'/step1',
            templateUrl: 'views/step1.html',
            controller: 'MainCtrl'
        })
        .state ('step2', {
            url:'/step2',
            templateUrl: 'views/step2.html',
            controller: 'MainCtrl'
        })
        .state ('step3', {
            url:'/step3',
            templateUrl: 'views/step3.html',
            controller: 'MainCtrl'
        })
        .state ('step4', {
            url:'/step4',
            templateUrl: 'views/step4.html',
            controller: 'MainCtrl'
        })
        .state ('step5', {
            url:'/step5',
            templateUrl: 'views/step5.html',
            controller: 'MainCtrl'
        });

});


Enchainement des écrans

Dans chaque écran (step1.html, step2.html, ...), je mets un formulaire "form", avec en bas, un bouton "Etape suivante" de type "submit", et j'indique à AngularJS que lors de la soumission du formulaire, je veux exécuter la fonction "nextStep()" :

<form ng-submit="nextStep()">

        ...

        <input class="btn btn-success" type="submit" value="Etape suivante" />

</form>

Comme je suis fainéant, et que les états se suivent (1, 2, 3...), j'ajoute le numéro de "step" dans chaque état du routage :

    $stateProvider
        .state ('step1', {
            url:'/step1',
            step: 1,
            templateUrl: 'views/step1.html',
            controller: 'MainCtrl'
        })
        .state ('step2', {
            url:'/step2',
            step: 2,
            templateUrl: 'views/step2.html',
            controller: 'MainCtrl'
        })
        ...
});

Il suffit alors d'ajouter la fonction "nextStep()" dans le $scope du controller, cette fonction récupère l'état courant (ex : step2), ajoute 1 à son numéro, et demande au routeur de naviguer vers le nouvel état (ex : step3) :

'use strict';

angular.module('Proto').controller('MainCtrl', function ($scope, $state) {
    
    ...

    $scope.nextStep = function() {
        var currentStep = $state.current;
        $state.go('step'+(currentStep.step+1));
    };
    
    ...
    
});

Par la suite, j'ai facilement ajouté des boutons "Etape précédente" en appelant une fonction "prevStep()" qui exécute la navigation vers "step - 1".

Données pour chaque écran

Dans les étapes 1 et 3, j'ai des listes déroulantes pour choisir des dates (jour, mois, année). Toujours comme je suis fainéant, et pour ne pas copier-coller des tas de ligne "<option>", je remplis ces listes dans le contrôleur :

    $scope.listeAnneesNaissance = [];
    for(var i = 1945; i < 2015; i++) {
        $scope.listeAnneesNaissance.push(i);
    }
    
    $scope.listeAnneesDepart = [];
    for(var i = 0; i < 10; i++) {
        $scope.listeAnneesDepart.push(2015+i);
    }
    
    $scope.listeMois =['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];


et je les utilise dans les pages avec des "ng-repeat" sur les "option" :

            <select name='naissance.mois' class='form-control proto-select' required >
                <option ng-repeat='mois in listeMois'>{{mois}}</option>
            </select>
            <select name='naissance.annee' class='form-control proto-select' required >
                <option ng-repeat='annee in listeAnneesNaissance'>{{annee}}</option>
            </select>

De même, j'avais besoin de liste de régimes ("caisses") pour l'étape 2, et d'une checklist et son contenu à afficher à l'étape 5. J'ai donc créé des structures de données dans le contrôleur :

    $scope.caisses = [
        {
            nom: "MSA",
            description: "Exploitants et salariés agricoles"
        },{
            nom: "CNAV",
            description: "Régime général",
            details: [
                "Salariés de l'industrie, du commerce et des services",
                "Agents non titulaires de l'Etat et des collectivités publiques",
                "Artistes et auteurs d'oeuvres originales"
            ]
        },{
            nom: "Je ne sais pas"
        }
    ];
    
    $scope.checkList = [
        {
            titre: "Je reconstitue ma carrière",
            delai: "Dès aujourd'hui",
            infos: "Avez-vous déjà vérifié que les données de votre relevé de situation individuelle sont exactes et complètes ? Si oui, passez à l’étape suivante. Sinon, suivez les étapes ci-dessous",
            actions: [
                "Je crée mon espace privé sur le site Internet de ma caisse MSA pour avoir accès aux services en ligne personnalisés ICI.",
                "Dans mon espace privé « Mon compte », je choisis « Internet » comme préférence de mode de réception des documents et des informations de mon dossier personnel.",
                "Dans mon espace privé, je consulte mon relevé de situation individuelle.",
                "Je vérifie que les données de mon relevé de situation individuelle sont exactes et complètes.",
                "Si j’identifie des périodes manquantes ou informations erronées, je demande une régularisation de ma carrière en envoyant par courrier ou en déposant à mon agence les pièces justificatives correspondantes."
            ],
            precisions: [
                "Je peux à tout moment adresser mes questions par mail à mon conseiller depuis mon espace privé."
            ]
        },{
            titre: "Je contacte mon / mes régime(s) complémentaire(s)",
            delai: "Dès que possible",
            infos: "Nous avons détecté que vous dépendez pour votre retraite complémentaire de [XXX] . Vous devez effectuer une demande distincte auprès de ce régime.",
            actions: [
                "Je prends contact avec mon/mes régimes de retraite complémentaire ICI afficher les coordonnées des régimes concernés telles qu’extraites de l’annuaire retraite.",
                "Je note ci-dessous les démarches à effectuer pour obtenir ma retraite complémentaire."
            ]
        },{
        ...


Ensuite, j'utilise simplement ces données dans chaque page, avec là encore des "ng-repeat" sur différents éléments HTML imbriqués.

J'avais pressenti que pendant ces 2-3 jours de préparation du proto avec le Product Owner, ces données allaient changer, mais également les façons de les présenter. Ca n'a pas raté, et j'étais bien content de ne pas avoir dupliqué des paquets de "div" HTML, mais d'avoir un "ng-repeat" sur un "div", et donc de n'avoir que ce "div" à modifier... :-)

Les animations

A l'étape 4, on pose quelques questions complémentaires à l'utilisateur, et certaines questions doivent apparaitre selon les réponses à la question précédent.

Pour cela, avec AngularJS, c'est très simple sans code. Les boutons radio pour les réponses sont classiques, et j'utilise "ng-model" pour stocker "false" ou "true" dans la variable "inactivite" qui sera automatiquement créée dans le scope (pas besoin de la déclarer explicitement) :

    ...
    <input ng-model="inactivite" type="radio" value="false" name="inactivite" />
    ...
    <input ng-model="inactivite" type="radio" value="true" name="inactivite" />
    ...


Et ensuite, le "div" suivant est complété avec un attribut "ng-show" pour afficher le "div" si la variable "inactivite" a la valeur "true" :

    ...
    <div ng-show="inactivite === 'true'">
    ...

Aussi simple que cela !

Conclusion

Après moins de 2 jours de travail, y compris un peu de CSS non mentionné ici, je disposais d'un prototype qui répondait aux attentes, qui présentait l'enchainement des écrans, des données fictives et quelques animations. Ce prototype a permis un premier travail d'échanges avec le Product Owner, qui a demandé quelques changements. Et dès le lendemain, nous avons pu le présenter au groupe de travail qui a tout de suite pu se projeter plus concrètement dans l'outil imaginé depuis plusieurs réunions de préparation du projet.

C'est après ces 3-4 jours de prototypage et démonstration, et avec un peu de recul, que je me suis vraiment rendu compte que c'est grâce à AngularJS que j'avais pû produire un tel résultat en si peu de temps, et je voulais partager ce retour d'expérience.

A noter que dans les semaines qui ont suivi, ce prototype a fait émerger l'idée de développer un BackOffice pour permettre au Product Owner et aux personnes du métier de configurer en ligne les règles métier de l'application et ses contenus. Et ce BackOffice a été développé avec AngularJS.

Par contre, en parallèle, les développements ont également avancé sur l'application destinée aux usagers, le FrontOffice, mais finalement (pour d'autres raisons, notamment de tests) en mode "classique", avec des pages HTML servies par un serveur Play Framework, et donc sans AngularJS...

1 commentaire:

  1. Même approche de mon coté, j'utilise angular pour faire mes protos (entre 4h et 30h de travail).

    2 objectifs :
    - tester auprès de clients et d'utilisateurs potentiel.
    - une fois validé une spec concrète pour l'équipe des développeurs.

    (Fake it until you make it).

    RépondreSupprimer