Passer votre site web MVC à AngularJS

Il arrive sur le marché de plus en plus de framework javascript permettant de rendre la programmation graphique de plus en plus aisée. Nous allons nous intéresser à l’un d’entre eux, et pas des moindres puisqu’il s’agit de AngularJS qui n’est autre que le petit bébé de Google. Je vais vous montrer comment intégrer AngularJS à un site MVC.

Passez votre site web MVC à AngularJS

Le but de l’exercice est simple : modifier les vues MVC pour utiliser AngularJS sans modifier une ligne de C#.

Pour cela je vais reprendre l’exemple du Twitter-like basé sur SignalR présenté précédemment par Nicholas Suter que vous retrouverez à ces liens :
Article 1
Article 2
Article 3

Première partie du travail : récupérer la solution telle quelle sur GitHub (https://github.com/nicholassuter/TwignalR).

Commençons par analyser le contenu de la page :

Twitter-like SignalR

Nous trouvons ici plusieurs données :

  • L’utilisateur courant
  • Le message à envoyer
  • Une liste de messages (publics, personnels ou privés)
  • Une liste de notifications
  • Des compteurs

Nous allons placer ces données dans un modèle qui sera utilisé par la vue.
Première action avant de commencer quelconque développement : ajouter la librairie AngularJS, soit via NuGet soit CDN, à votre convenance. Dans notre exemple j’utiliserai NuGet.

Ensuite créons un nouveau fichier javascript MainController.js avec le code suivant :

var MainController = function ($scope) {

    $scope.displayname = "";

    $scope.messageToSend = "";

    $scope.messages = new Array();
    $scope.countNewMessage = 0;
    $scope.countNewMention = 0;
    $scope.countNewPrivate = 0;

    $scope.notifs = new Array();
    $scope.countNewNotif = 0;

    $scope.repondre = function (name) {
        $scope.messageToSend = "@" + name + " :";
        $('#message').focus();
    };

    $scope.dm = function (name) {
        $scope.messageToSend = "d " + name + " :";
        $('#message').focus();
    };
};

Maintenant modifions la vue, il est nécessaire d’ajouter évidemment le script AngularJS et celui que nous venons de créer. Il est également impératif d’ajouter de nouvelles directives liées à AngularJS afin d’activer le moteur AngularJS sur le code html sous-jacent, ce qui nous donne :

<script src="~/Scripts/angular.js"></script>
<script src="~/Scripts/MainController.js"></script>
<div ng-app >
    <div ng-controller="MainController">
    <h2 id="h2Title">Twitter with SignalR</h2>
    // code d’origine
    </div>
</div>

La directive ng-app indique que le code html inclus dans cette balise sera interprété par Angular.
La directive ng-controller indique que l’on utilisera ici le controller spécifié. Ceci permet un meilleur découpage de votre code à travers différents controller ainsi que de les réutiliser dans d’autres vues par exemple.

Auparavant le stockage du nom de l’utilisateur était fait grâce à une balise cachée :

<input type="hidden" id="displayname" />

Désormais cette balise est inutile, nous allons utiliser notre modèle à la place :

<h3 ng-show="displayname" class="ng-hide">Bienvenue {{displayname}} !</h3>

La classe ng-hide permet de cacher la balise tandis que la directive ng-show permet d’afficher la balise si la condition spécifiée est vraie. Le text {{displayname}} indique à Angular que l’on souhaite binder sur le champ displayname du context courant (ici le MainController). Et oui le mot est lâché, nous faisons bien du binding.

Maintenant c’est au tour du message à envoyer, voici le nouveau code :

<form>
    <input type="text" id="message" ng-model="messageToSend"/>
    <input type="submit" id ="sendmessage" value="Send" />
</form>

Pour ce faire j’ai utilisé la directive ng-model qui permet de binder un élément dans les deux sens. Au passage j’ai ajouté la balise form.

Il faut à présent afficher les messages envoyés :

<div class="widget-title" id="discussion" ng-click="countNewMessage=0">
	<h3 id="h3Messages">Fil de discussion <span ng-show="countNewMessage>0">({{countNewMessage}})</span></h3>
	<div ng-repeat="mess in messages | orderBy:'-messageId' | filter:{isPrivate:false}">
		<div style="border: solid 1px {{mess.borderColor}}; margin-top:5px; padding:5px">
			<strong>{{mess.messageId}} - {{mess.name}} </strong>> {{mess.message}}
			| <a ng-click="repondre(mess.name)">Répondre</a>
			| <a ng-click="dm(mess.name)">Message privé</a>
		</div>
	</div>
</div>

Comme on peut le voir, il y a beaucoup de changement. Le code n’est plus injecté à l’aide du javascript, mais directement écrit en HTML avec l’aide de AngularJS. Tout d’abord, le nombre de nouveaux messages (countNewMessage) est affiché à l’aide d’une balise span. La fonction vider a été remplacée par une affectation du compteur associé à l’aide de la directive ng-click.
Afin d’afficher tous les messages contenus dans le tableau, on utilise la directive ng-repeat qui fonctionne comme un foreach. Ici j’ai appliqué un filtre (orderBy:’-messageId’) qui permet de trier les éléments par ordre décroissant (-) de messageId.

La liste des mentions n’est jamais qu’un sous ensemble de la liste des messages:

<div class="widget-title" id="mentions" ng-click="countNewMention=0">
	<h3 id="h3Mentions">Mentions <span ng-show="countNewMention>0">({{countNewMention}})</span></h3>
	<div ng-repeat="mess in messages | filter:{ isMention:true} | orderBy:'-messageId'">
		<div style="border: solid 1px {{mess.borderColor}}; margin-top:5px; padding:5px">
			<strong>{{mess.messageId}} - {{mess.name}} </strong>> {{mess.message}}
			| <a ng-click="repondre(mess.name)">Répondre</a>
			| <a ng-click="dm(mess.name)">Message privé</a>
		</div>
	</div>
</div>

On utilise donc la même liste de messages, mais avec un filtre (filter:{ isMention:true}). C’est Angular qui filtrera automatiquement la liste selon le ou les critères que l’on passe.
Le principe reste le même pour les autres encarts (privés et notifications), inutile de détailler plus.
Au final on obtient le code html suivant :

@{
    ViewBag.Title = "Twitter with SignalR & AngularJS";
}

<script src="~/Scripts/angular.js"></script>
<script src="~/Scripts/MainController.js"></script>

<div ng-app>
    <div id="MainControllerDiv" ng-controller="MainController">
    <h2 id="h2Title">Twitter with SignalR & AngularJS</h2>
    <h3 ng-show="displayname" class="ng-hide">Bienvenue {{displayname}} !</h3>

        <div class="container" >
            <form>
                <input type="text" id="message" ng-model="messageToSend"/>
                <input type="submit" id ="sendmessage" value="Send" />
            </form>
           <br />
            <br />

            <div class="widget-title" id="discussion" ng-click="countNewMessage=0">
                <h3 id="h3Messages">Fil de discussion <span ng-show="countNewMessage>0">({{countNewMessage}})</span></h3>
                <div ng-repeat="mess in messages | orderBy:'-messageId' | filter:{isPrivate:false}">
                    <div style="border: solid 1px {{mess.borderColor}}; margin-top:5px; padding:5px">
                        <strong>{{mess.messageId}} - {{mess.name}} </strong>> {{mess.message}}
                        | <a ng-click="repondre(mess.name)">Répondre</a>
                        | <a ng-click="dm(mess.name)">Message privé</a>
                    </div>
                </div>
            </div>

            <div class="widget-title" id="mentions" ng-click="countNewMention=0">
                <h3 id="h3Mentions">Mentions <span ng-show="countNewMention>0">({{countNewMention}})</span></h3>
                <div ng-repeat="mess in messages | filter:{ isMention:true} | orderBy:'-messageId'">
                    <div style="border: solid 1px {{mess.borderColor}}; margin-top:5px; padding:5px">
                        <strong>{{mess.messageId}} - {{mess.name}} </strong>> {{mess.message}}
                        | <a ng-click="repondre(mess.name)">Répondre</a>
                        | <a ng-click="dm(mess.name)">Message privé</a>
                    </div>
                </div>
            </div>

            <div class="widget-title" id="messagesprives" ng-click="countNewPrivate=0">
                <h3 id="h3MessagesPrives">Messages privés <span ng-show="countNewPrivate>0">({{countNewPrivate}})</span></h3>
                <div ng-repeat="mess in messages | filter:{ isPrivate:true} | orderBy:'-messageId'">
                    <div style="border: solid 1px {{mess.borderColor}}; margin-top:5px; padding:5px">
                        <strong>{{mess.messageId}} - {{mess.name}} </strong>> {{mess.message}}
                        | <a ng-click="repondre(mess.name)">Répondre</a>
                    </div>
                </div>
            </div>

            <div class="widget-title" id="system" ng-click="countNewNotif=0">
                <h3 id="h3System">Notifications <span ng-show="countNewNotif>0">({{countNewNotif}})</span></h3>
                <div ng-repeat="mess in notifs">
                    <div style="border: solid 1px {{mess.borderColor}}; margin-top:5px; padding:5px">
                        {{mess.message}}
                    </div>
                </div>
            </div>

        </div>
    </div>
</div>

Effectivement le code HTML est plus imposant. Par contre il ne faut pas oublier que ce code était placé dans le javascript auparavant. L’avantage d’utiliser Angular, c’est que l’on a une vue plus proche du code final fourni au navigateur, ce qui permet une meilleure compréhension.

Dernière partie du travail, la modification du code javascript qui est embarqué dans la page.
Pour pouvoir interagir avec le modèle Angular, il est nécessaire de récupérer le modèle associé à une balise HTML via l’instruction suivante :

var scope = angular.element($("#MainControllerDiv")).scope();

On peut utiliser directement la variable scope en lecture, par contre en écriture il est impératif d’utiliser le pattern suivant :

scope.$apply(function () {
    scope.displayname =”ma_valeur”;
});

Ainsi après modification, voici le code JS obtenu :

<script>
        $(function () {

            var scope = angular.element($("#MainControllerDiv")).scope();

            scope.$apply(function () {
                scope.displayname = prompt('Enter your name:', '');
            });

            // Reference the auto-generated proxy for the hub.
            var chat = $.connection.twignalRHub;
            // Create a function that the hub can call back to display messages.
            chat.client.sendToClients = function (name, message, messageId, to) {

                var borderColor;
                var displayNameWithAt = "@@" + scope.displayname;
                var isMention = false;
                if ($.inArray(displayNameWithAt, to) > -1) {
                    borderColor = '#FF0000';
                    scope.$apply(function () {
                        scope.countNewMention++;
                    });
                    isMention = true;
                } else {
                    borderColor = '#dad7d7';
                }
                scope.$apply(function () {
                    scope.messages.push({
                        "name": name,
                        "message": message,
                        "messageId": messageId,
                        "borderColor": borderColor,
                        "isMention": isMention,
                        "isPrivate": false
                    });
                    scope.countNewMessage++;
                });
            };

            chat.client.sendDm = function (from, message, messageId) {
                scope.$apply(function () {
                    scope.messages.push({
                        "name": from,
                        "message": message,
                        "messageId": messageId,
                        "borderColor": "#dad7d7",
                        "isMention": false,
                        "isPrivate": true
                    });
                    scope.countNewPrivate++;
                });

            };

            chat.client.systemNotification = function (message, color) {
                scope.$apply(function () {
                    scope.notifs.push({
                        "message": message,
                        "borderColor": color
                    });
                    scope.countNewNotif++;
                });

            };

            // Set initial focus to message input box.
            $('#message').focus();

            // Start the connection.
            $.connection.hub.start().done(function () {

                chat.server.notifyConnection(scope.displayname);

                $('#sendmessage').click(function () {
                    if (scope.messageToSend) {
                        // Call the Send method on the hub.
                        chat.server.sendToHub(scope.displayname, scope.messageToSend);
                        // Clear text box
                        scope.$apply(function () {
                            scope.messageToSend = "";
                        });
                    }
                });
            });

        });

</script>

Et voilà, le tour est joué.
Personnellement je trouve la syntaxe beaucoup plus simple à utiliser que Knockout qui est un autre framework javascript concurrent.
Le découpage par controller est très intéressant car il permet d’éviter un problème récurrent en javascript : les conflits de variables entre différents scopes.
Bien évidemment AngularJS va beaucoup plus loin, et je vous invite d’ailleurs à continuer l’aventure sur angularjs.org

Pas de commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *