Créer des applications Web modernes avec AngularJS et Play Framework
Publié: 2022-03-11Choisir le bon outil pour le bon objectif va un long chemin, en particulier lorsqu'il s'agit de créer des applications Web modernes. Beaucoup d'entre nous connaissent AngularJS et la facilité avec laquelle il facilite le développement de frontaux d'applications Web robustes. Bien que beaucoup s'opposeront à l'utilisation de ce framework Web populaire, il a certainement beaucoup à offrir et peut être un choix approprié pour un large éventail de besoins. D'autre part, les composants que vous utilisez sur le back-end dicteront beaucoup sur les performances de l'application Web, car ils ont une influence sur l'expérience utilisateur globale. Play est un framework Web à haute vitesse pour Java et Scala. Il est basé sur une architecture légère, sans état et conviviale pour le Web et suit des modèles et des principes MVC similaires à Rails et Django.
Dans cet article, nous verrons comment nous pouvons utiliser AngularJS et Play pour créer une application de blog simple avec un mécanisme d'authentification de base et la possibilité de publier des messages et des commentaires. Le développement d'AngularJS, avec quelques goodies Twitter Bootstrap, nous permettra d'alimenter une expérience d'application d'une seule page en plus d'un back-end d'API REST basé sur Play.
Applications Web - Mise en route
Squelette d'application AngularJS
Les applications AngularJS et Play résideront dans les répertoires client et serveur en conséquence. Pour l'instant, nous allons créer le répertoire « client ».
mkdir -p blogapp/client
Pour créer un squelette d'application AngularJS, nous utiliserons Yeoman - un outil d'échafaudage incroyable. L'installation de Yeoman est facile. L'utiliser pour échafauder une simple application squelettique AngularJS est probablement encore plus facile :
cd blogapp/client yo angular
L'exécution de la deuxième commande sera suivie de quelques options parmi lesquelles vous devrez choisir. Pour ce projet, nous n'avons pas besoin de "Sass (with Compass)". Nous aurons besoin de Boostrap avec les plugins AngularJS suivants :
- angular-animate.js
- angular-cookies.js
- angular-resource.js
- angular-route.js
- angular-sanitize.js
- angular-touch.js
À ce stade, une fois que vous avez finalisé vos sélections, vous commencerez à voir la sortie NPM et Bower sur votre terminal. Une fois les téléchargements terminés et les packages installés, vous disposerez d'un squelette d'application AngularJS prêt à être utilisé.
Play Framework Application Squelette
La manière officielle de créer une nouvelle application Play implique l'utilisation de l'outil Typesafe Activator. Avant de pouvoir l'utiliser, vous devez le télécharger et l'installer sur votre ordinateur. Si vous êtes sous Mac OS et utilisez Homebrew, vous pouvez installer cet outil avec une seule ligne de commande :
brew install typesafe-activator
Créer une application Play à partir de la ligne de commande est très simple :
cd blogapp/ activator new server play-java cd server/
Importation vers un IDE
Pour importer l'application dans un IDE tel qu'Eclipse ou IntelliJ, vous devez "éclipser" ou "idéaliser" votre application. Pour ce faire, exécutez la commande suivante :
activator
Une fois que vous voyez une nouvelle invite, tapez "eclipse" ou "idée" et appuyez sur Entrée pour préparer le code d'application pour Eclipse ou IntelliJ, respectivement.
Par souci de concision, nous ne couvrirons que le processus d'importation du projet dans IntelliJ dans cet article. Le processus d'importation dans Eclipse devrait être tout aussi simple. Pour importer le projet dans IntelliJ, commencez par activer l'option "Projet à partir de sources existantes…" qui se trouve sous "Fichier -> Nouveau". Ensuite, sélectionnez votre fichier build.sbt et cliquez sur "OK". En cliquant à nouveau sur "OK" dans la boîte de dialogue suivante, IntelliJ devrait commencer à importer votre application Play en tant que projet SBT.
Typesafe Activator est également livré avec une interface utilisateur graphique, que vous pouvez utiliser pour créer ce code d'application squelettique.
Maintenant que nous avons importé notre application Play dans IntelliJ, nous devons également importer notre application AngularJS dans l'espace de travail. Nous pouvons l'importer soit en tant que projet séparé, soit en tant que module dans le projet existant où réside l'application Play.
Ici, nous allons importer l'application Angular en tant que module. Dans le menu "Fichier", nous sélectionnerons l'option "Nouveau -> Module à partir de sources existantes…". Dans la boîte de dialogue, nous allons choisir le répertoire "client" et cliquer sur "OK". Sur les deux écrans suivants, cliquez respectivement sur "Suivant" et "Terminer".
Générer des serveurs locaux
À ce stade, il devrait être possible de démarrer l'application AngularJS en tant que tâche Grunt à partir de l'IDE. Développez votre dossier client et faites un clic droit sur Gruntfile.js. Dans le menu contextuel, sélectionnez "Afficher les tâches de Grunt". Un panneau intitulé "Grunt" apparaîtra avec une liste de tâches :
Pour commencer à servir l'application, double-cliquez sur "servir". Cela devrait immédiatement ouvrir votre navigateur Web par défaut et le pointer vers une adresse localhost. Vous devriez voir une page AngularJS stub avec le logo de Yeoman dessus.
Ensuite, nous devons lancer notre serveur d'applications back-end. Avant de pouvoir continuer, nous devons résoudre quelques problèmes :
- Par défaut, l'application AngularJS (amorcée par Yeoman) et l'application Play tentent de s'exécuter sur le port 9000.
- En production, les deux applications seront probablement exécutées sous un seul domaine, et nous utiliserons probablement Nginx pour acheminer les requêtes en conséquence. Mais en mode développement, lorsque nous modifions le numéro de port de l'une de ces applications, les navigateurs Web les traitent comme si elles s'exécutaient sur des domaines différents.
Pour contourner ces deux problèmes, il nous suffit d'utiliser un proxy Grunt afin que toutes les requêtes AJAX adressées à l'application Play soient transmises par proxy. Avec cela, ces deux serveurs d'applications seront essentiellement disponibles au même numéro de port apparent.
Changeons d'abord le numéro de port du serveur d'application Play en 9090. Pour cela, ouvrez la fenêtre « Run/Debug Configurations » en cliquant sur « Run -> Edit Configurations ». Ensuite, modifiez le numéro de port dans le champ "Url To Open". Cliquez sur « OK » pour approuver cette modification et fermer la fenêtre. Cliquer sur le bouton "Exécuter" devrait démarrer le processus de résolution des dépendances - les journaux de ce processus commenceront à apparaître.
Une fois cela fait, vous pouvez accéder à http://localhost:9090 sur votre navigateur Web et, en quelques secondes, vous devriez pouvoir voir votre application Play. Pour configurer un proxy Grunt, nous devons d'abord installer un petit package Node.js utilisant NPM :
cd blogapp/client npm install grunt-connect-proxy --save-dev
Ensuite, nous devons modifier notre Gruntfile.js. Dans ce fichier, localisez la tâche "connecter" et insérez la clé/valeur "proxies" après celle-ci :
proxies: [ { context: '/app', // the context of the data service host: 'localhost', // wherever the data service is running port: 9090, // the port that the data service is running on changeOrigin: true } ],
Grunt transmettra désormais toutes les requêtes à "/app/*" à l'application principale Play. Cela nous évitera d'avoir à ajouter à la liste blanche tous les appels vers le back-end. De plus, nous devons également modifier notre comportement de livereload :
livereload: { options: { open: true, middleware: function (connect) { var middlewares = []; // Setup the proxy middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest); // Serve static files middlewares.push(connect.static('.tmp')); middlewares.push(connect().use( '/bower_components', connect.static('./bower_components') )); middlewares.push(connect().use( '/app/styles', connect.static('./app/styles') )); middlewares.push(connect.static(appConfig.app)); return middlewares; } } },
Enfin, nous devons ajouter une nouvelle dépendance « 'configureProxies:server » à la tâche « serve » :
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'connect:dist:keepalive']); } grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'configureProxies:server', 'connect:livereload', 'watch' ]); });
Au redémarrage de Grunt, vous devriez remarquer les lignes suivantes dans vos journaux indiquant que le proxy est en cours d'exécution :
Running "autoprefixer:server" (autoprefixer) task File .tmp/styles/main.css created. Running "configureProxies:server" (configureProxies) task Running "connect:livereload" (connect) task Started connect web server on http://localhost:9000
Création d'un formulaire d'inscription
Nous allons commencer par créer un formulaire d'inscription pour notre application de blog. Cela nous permettra également de vérifier que tout fonctionne comme il se doit. Nous pouvons utiliser Yeoman pour créer un contrôleur d'inscription et afficher dans AngularJS :
yo angular:controller signup yo angular:view signup
Ensuite, nous devons mettre à jour le routage de notre application pour référencer cette vue nouvellement créée, et supprimer le contrôleur et la vue "à propos" générés automatiquement et redondants. Dans le fichier "app/scripts/app.js", supprimez les références à "app/scripts/controllers/about.js" et "app/views/about.html", en le laissant avec :
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
De même, mettez à jour le fichier "app/index.html" pour supprimer les liens redondants et ajoutez un lien vers la page d'inscription :
<div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#/">Home</a></li> <li><a ng-href="#/signup">Signup</a></li> </ul> </div> </div>
Supprimez également la balise de script pour "about.js":
<!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="scripts/app.js"></script> <script src="scripts/controllers/main.js"></script> <script src="scripts/controllers/signup.js"></script> <!-- endbuild --> </body> </html>
Ensuite, ajoutez un formulaire à notre fichier "signup.html":
<form name="signupForm" ng-submit="signup()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Sign up!</button> </form>
Nous devons faire en sorte que le formulaire soit traité par le contrôleur angulaire. Il convient de noter que nous n'avons pas besoin d'ajouter spécifiquement l'attribut "ng-controller" dans nos vues, car notre logique de routage dans "app.js" lance automatiquement un contrôleur avant le chargement de notre vue. Tout ce que nous avons à faire pour câbler ce formulaire est d'avoir une fonction "inscription" appropriée définie dans $scope. Cela devrait être fait dans le fichier "signup.js":
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .success(function(data) { $log.debug(data); }); }; });
Maintenant, ouvrons la console du développeur Chrome, passons à l'onglet "Réseau" et essayons de soumettre le formulaire d'inscription.
Nous verrons que le back-end Play répond naturellement avec une page d'erreur "Action introuvable". Ceci est attendu car il n'a pas encore été mis en œuvre. Mais cela signifie également que notre configuration de proxy Grunt fonctionne correctement !
Ensuite, nous allons ajouter une "Action" qui est essentiellement une méthode dans le contrôleur d'application Play. Dans la classe "Application" du package "app/controllers", ajoutez une nouvelle méthode "signup":
public static Result signup() { return ok("Success!"); }
Ouvrez maintenant le fichier « conf/routes » et ajoutez la ligne suivante :
POST /app/signup controllers.Application.signup
Enfin, nous revenons à notre navigateur Web, http://localhost:9000/#/signup. Cliquer sur le bouton "Soumettre" cette fois devrait donner quelque chose de différent :
Vous devriez voir la valeur codée en dur renvoyée, celle que nous avons écrite dans la méthode d'inscription. Si tel est le cas, nous sommes prêts à passer à autre chose car notre environnement de développement est prêt et fonctionne à la fois pour les applications Angular et Play.
Définir les modèles Ebean dans Play
Avant de définir des modèles, choisissons d'abord un magasin de données. Dans cet article, nous utiliserons la base de données en mémoire H2. Pour activer cela, recherchez et décommentez les lignes suivantes dans le fichier "application.conf":
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" db.default.user=sa db.default.password="" ... ebean.default="models.*"
Et ajoutez la ligne suivante :
applyEvolutions.default=true
Notre modèle de domaine de blog est assez simple. Tout d'abord, nous avons des utilisateurs qui peuvent créer des publications, puis chaque publication peut être commentée par n'importe quel utilisateur connecté. Créons nos modèles Ebean.
Utilisateur
// User.java @Entity public class User extends Model { @Id public Long id; @Column(length = 255, unique = true, nullable = false) @Constraints.MaxLength(255) @Constraints.Required @Constraints.Email public String email; @Column(length = 64, nullable = false) private byte[] shaPassword; @OneToMany(cascade = CascadeType.ALL) @JsonIgnore public List<BlogPost> posts; public void setPassword(String password) { this.shaPassword = getSha512(password); } public void setEmail(String email) { this.email = email.toLowerCase(); } public static final Finder<Long, User> find = new Finder<Long, User>( Long.class, User.class); public static User findByEmailAndPassword(String email, String password) { return find .where() .eq("email", email.toLowerCase()) .eq("shaPassword", getSha512(password)) .findUnique(); } public static User findByEmail(String email) { return find .where() .eq("email", email.toLowerCase()) .findUnique(); } public static byte[] getSha512(String value) { try { return MessageDigest.getInstance("SHA-512").digest(value.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }
BlogPost
// BlogPost.java @Entity public class BlogPost extends Model { @Id public Long id; @Column(length = 255, nullable = false) @Constraints.MaxLength(255) @Constraints.Required public String subject; @Column(columnDefinition = "TEXT") @Constraints.Required public String content; @ManyToOne public User user; public Long commentCount; @OneToMany(cascade = CascadeType.ALL) public List<PostComment> comments; public static final Finder<Long, BlogPost> find = new Finder<Long, BlogPost>( Long.class, BlogPost.class); public static List<BlogPost> findBlogPostsByUser(final User user) { return find .where() .eq("user", user) .findList(); } public static BlogPost findBlogPostById(final Long id) { return find .where() .eq("id", id) .findUnique(); } }
Poster un commentaire
// PostComment.java @Entity public class PostComment extends Model { @Id public Long id; @ManyToOne @JsonIgnore public BlogPost blogPost; @ManyToOne public User user; @Column(columnDefinition = "TEXT") public String content; public static final Finder<Long, PostComment> find = new Finder<Long, PostComment>( Long.class, PostComment.class); public static List<PostComment> findAllCommentsByPost(final BlogPost blogPost) { return find .where() .eq("post", blogPost) .findList(); } public static List<PostComment> findAllCommentsByUser(final User user) { return find .where() .eq("user", user) .findList(); } }
Action d'inscription réelle
Créons maintenant notre première action réelle, permettant aux utilisateurs de s'inscrire :
// Application.java public static Result signup() { Form<SignUp> signUpForm = Form.form(SignUp.class).bindFromRequest(); if ( signUpForm.hasErrors()) { return badRequest(signUpForm.errorsAsJson()); } SignUp newUser = signUpForm.get(); User existingUser = User.findByEmail(newUser.email); if(existingUser != null) { return badRequest(buildJsonResponse("error", "User exists")); } else { User user = new User(); user.setEmail(newUser.email); user.setPassword(newUser.password); user.save(); session().clear(); session("username", newUser.email); return ok(buildJsonResponse("success", "User created successfully")); } } public static class UserForm { @Constraints.Required @Constraints.Email public String email; } public static class SignUp extends UserForm { @Constraints.Required @Constraints.MinLength(6) public String password; } private static ObjectNode buildJsonResponse(String type, String message) { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", message); wrapper.put(type, msg); return wrapper; }
Notez que l'authentification utilisée dans cette application est très basique et n'est pas recommandée pour une utilisation en production.
La partie intéressante est que nous utilisons les formulaires Play pour gérer les formulaires d'inscription. Nous avons défini quelques contraintes sur notre classe de formulaire SignUp. La validation sera effectuée pour nous automatiquement sans avoir besoin d'une logique de validation explicite.
Si nous revenons à notre application AngularJS dans le navigateur Web et que nous cliquons à nouveau sur "Soumettre", nous verrons que le serveur répond maintenant avec une erreur appropriée - que ces champs sont obligatoires.
Gestion des erreurs de serveur dans AngularJS
Nous obtenons donc une erreur du serveur, mais l'utilisateur de l'application n'a aucune idée de ce qui se passe. Le moins que nous puissions faire est d'afficher l'erreur à notre utilisateur. Idéalement, nous aurions besoin de comprendre le type d'erreur que nous obtenons et d'afficher un message convivial. Créons un service d'alerte simple qui nous aidera à afficher l'erreur.
Tout d'abord, nous devons générer un modèle de service avec Yeoman :
yo angular:service alerts
Ensuite, ajoutez ce code à "alerts.js":
angular.module('clientApp') .factory('alertService', function($timeout) { var ALERT_TIMEOUT = 5000; function add(type, msg, timeout) { if (timeout) { $timeout(function(){ closeAlert(this); }, timeout); } else { $timeout(function(){ closeAlert(this); }, ALERT_TIMEOUT); } return alerts.push({ type: type, msg: msg, close: function() { return closeAlert(this); } }); } function closeAlert(alert) { return closeAlertIdx(alerts.indexOf(alert)); } function closeAlertIdx(index) { return alerts.splice(index, 1); } function clear(){ alerts = []; } function get() { return alerts; } var service = { add: add, closeAlert: closeAlert, closeAlertIdx: closeAlertIdx, clear: clear, get: get }, alerts = []; return service; } );
Créons maintenant un contrôleur séparé responsable des alertes :
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Nous devons maintenant afficher de beaux messages d'erreur Bootstrap. Le moyen le plus simple consiste à utiliser l'interface utilisateur angulaire. Nous pouvons utiliser Bower pour l'installer :
bower install angular-bootstrap --save
Dans votre "app.js", ajoutez le module d'interface utilisateur angulaire :
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Ajoutons une directive d'alerte à notre fichier "index.html":

<div class="container"> <div ng-controller="AlertsCtrl"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="alert.close()">{{ alert.msg }}</alert> </div> <div ng-view=""></div> </div>
Enfin, nous devons mettre à jour le contrôleur SignUp :
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location, userService) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } if(status === 500) { alertService.add('danger', 'Internal server error!'); } }) }; });
Maintenant, si nous envoyons à nouveau le formulaire vide, nous verrons des erreurs affichées au-dessus du formulaire :
Maintenant que les erreurs sont gérées, nous devons faire quelque chose lorsque l'inscription de l'utilisateur est réussie. Nous pouvons rediriger l'utilisateur vers une page de tableau de bord où il peut ajouter des publications. Mais d'abord, nous devons le créer:
yo angular:view dashboard yo angular:controller dashboard
Modifiez la méthode d'inscription du contrôleur "signup.js" afin qu'en cas de succès, il redirige l'utilisateur :
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Ajoutez une nouvelle route dans "apps.js":
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
Nous devons également savoir si l'utilisateur est connecté. Créons un service séparé pour cela :
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
Et modifiez également le contrôleur d'inscription pour définir l'utilisateur sur celui qui vient de s'inscrire :
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Avant d'ajouter la fonctionnalité principale d'ajout de publications, prenons soin de certaines autres fonctionnalités importantes telles que la possibilité de se connecter et de se déconnecter, d'afficher les informations utilisateur sur le tableau de bord et d'ajouter également la prise en charge de l'authentification dans le back-end.
Authentification de base
Passons à notre application Play et implémentons les actions de connexion et de déconnexion. Ajoutez ces lignes à "Application.java":
public static Result login() { Form<Login> loginForm = Form.form(Login.class).bindFromRequest(); if (loginForm.hasErrors()) { return badRequest(loginForm.errorsAsJson()); } Login loggingInUser = loginForm.get(); User user = User.findByEmailAndPassword(loggingInUser.email, loggingInUser.password); if(user == null) { return badRequest(buildJsonResponse("error", "Incorrect email or password")); } else { session().clear(); session("username", loggingInUser.email); ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "Logged in successfully"); msg.put("user", loggingInUser.email); wrapper.put("success", msg); return ok(wrapper); } } public static Result logout() { session().clear(); return ok(buildJsonResponse("success", "Logged out successfully")); } public static Result isAuthenticated() { if(session().get("username") == null) { return unauthorized(); } else { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "User is logged in already"); msg.put("user", session().get("username")); wrapper.put("success", msg); return ok(wrapper); } } public static class Login extends UserForm { @Constraints.Required public String password; }
Ajoutons ensuite la possibilité d'autoriser des appels back-end particuliers uniquement aux utilisateurs authentifiés. Créez "Secured.java" avec le code suivant :
public class Secured extends Security.Authenticator { @Override public String getUsername(Context ctx) { return ctx.session().get("username"); } @Override public Result onUnauthorized(Context ctx) { return unauthorized(); } }
Nous utiliserons cette classe plus tard pour protéger de nouvelles actions. Ensuite, nous devons modifier le menu principal de notre application AngularJS afin qu'il affiche les liens de nom d'utilisateur et de déconnexion. Pour cela, nous devons créer un contrôleur :
yo angular:controller menu
// menu.js angular.module('clientApp') .controller('MenuCtrl', function ($scope, $http, userService, $location) { $scope.user = userService; $scope.logout = function() { $http.get('/app/logout') .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = ''; $location.path('/login'); } }); }; $scope.$watch('user.username', function (newVal) { if(newVal === '') { $scope.isLoggedIn = false; } else { $scope.username = newVal; $scope.isLoggedIn = true; } }); });
Nous avons également besoin d'une vue et d'un contrôleur pour la page de connexion :
yo angular:controller login yo angular:view login
<!-- login.html --> <form name="loginForm" ng-submit="login()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Log in</button> </form>
// login.js angular.module('clientApp') .controller('LoginCtrl', function ($scope, userService, $location, $log, $http, alertService) { $scope.isAuthenticated = function() { if(userService.username) { $log.debug(userService.username); $location.path('/dashboard'); } else { $http.get('/app/isauthenticated') .error(function() { $location.path('/login'); }) .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); } }; $scope.isAuthenticated(); $scope.login = function() { var payload = { email : this.email, password : this.password }; $http.post('/app/login', payload) .error(function(data, status){ if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { alertService.add('danger', 'Invalid login or password!'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data){ $log.debug(data); if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); }; });
Ensuite, nous ajustons le menu pour qu'il puisse afficher les données utilisateur :
<!-- index.html --> <div class="collapse navbar-collapse" ng-controller="MenuCtrl"> <ul class="nav navbar-nav pull-right" ng-hide="isLoggedIn"> <li><a ng-href="/#/signup">Sign up!</a></li> <li><a ng-href="/#/login">Login</a></li> </ul> <div class="btn-group pull-right acc-button" ng-show="isLoggedIn"> <button type="button" class="btn btn-default">{{ username }}</button> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button> <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul> </div> </div>
Maintenant, si vous vous connectez à l'application, vous devriez pouvoir voir l'écran suivant :
Ajout de messages
Maintenant que nous avons mis en place des mécanismes d'inscription et d'authentification de base, nous pouvons commencer à mettre en œuvre la fonctionnalité de publication. Ajoutons une nouvelle vue et un contrôleur pour ajouter des publications.
yo angular:view addpost
<!-- addpost.html --> <form name="postForm" ng-submit="post()" novalidate> <div> <label for="subject">Subject</label> <input name="subject" class="form-control" type="subject" placeholder="Subject" ng-model="subject"> </div> <div> <label for="content">Post</label> <textarea name="content" class="form-control" placeholder="Content" ng-model="content"></textarea> </div> <button type="submit" class="btn btn-primary">Submit post</button> </form>
yo angular:controller addpost
// addpost.js angular.module('clientApp') .controller('AddpostCtrl', function ($scope, $http, alertService, $location) { $scope.post = function() { var payload = { subject : $scope.subject, content: $scope.content }; $http.post('/app/post', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'subject' || key === 'content') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { $scope.subject = ''; $scope.content = ''; alertService.add('success', data.success.message); }); }; });
Ensuite, nous mettons à jour "app.js" pour inclure :
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Ensuite, nous modifions "index.html" pour ajouter un lien pour notre vue "addpost" dans le menu du tableau de bord :
<ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li><a ng-href="/#/addpost">Add post</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul>
Maintenant du côté de l'application Play, créons un nouveau contrôleur Post avec la méthode addPost :
// Post.java public class Post extends Controller { public static Result addPost() { Form<PostForm> postForm = Form.form(PostForm.class).bindFromRequest(); if (postForm.hasErrors()) { return badRequest(postForm.errorsAsJson()); } else { BlogPost newBlogPost = new BlogPost(); newBlogPost.commentCount = 0L; newBlogPost.subject = postForm.get().subject; newBlogPost.content = postForm.get().content; newBlogPost.user = getUser(); newBlogPost.save(); } return ok(Application.buildJsonResponse("success", "Post added successfully")); } private static User getUser() { return User.findByEmail(session().get("username")); } public static class PostForm { @Constraints.Required @Constraints.MaxLength(255) public String subject; @Constraints.Required public String content; } }
Ajoutez une nouvelle entrée au fichier routes pour pouvoir gérer les méthodes nouvellement ajoutées dans le routage :
POST /app/post controllers.Post.addPost
À ce stade, vous devriez pouvoir ajouter de nouveaux messages.
Affichage des articles
L'ajout de messages a peu de valeur, si nous ne pouvons pas les afficher. Ce que nous voulons faire, c'est répertorier tous les messages sur la page principale. Nous commençons par ajouter une nouvelle méthode dans notre contrôleur d'application :
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
Et en l'enregistrant dans notre fichier routes :
GET /app/posts controllers.Application.getPosts
Ensuite, dans notre application AngularJS, nous modifions notre contrôleur principal :
// main.js angular.module('clientApp') .controller('MainCtrl', function ($scope, $http) { $scope.getPosts = function() { $http.get('app/posts') .success(function(data) { $scope.posts = data; }); }; $scope.getPosts(); });
Enfin, supprimez tout de "main.html" et ajoutez ceci :
<div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | <a ng-href="/#/viewpost/{{ post.id }}">Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>
Maintenant, si vous chargez la page d'accueil de votre application, vous devriez voir quelque chose de similaire à ceci :
Nous devrions aussi probablement avoir une vue séparée pour les messages individuels.
yo angular:controller viewpost yo angular:view viewpost
// viewpost.js angular.module('clientApp') .controller('ViewpostCtrl', function ($scope, $http, alertService, userService, $location) { $scope.user = userService; $scope.params = $routeParams; $scope.postId = $scope.params.postId; $scope.viewPost = function() { $http.get('/app/post/' + $scope.postId) .error(function(data) { alertService.add('danger', data.error.message); }) .success(function(data) { $scope.post = data; }); }; $scope.viewPost(); });
<!-- viewpost.html --> <div class="panel panel-default" ng-show="post"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>
Et la route AngularJS :
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Comme précédemment, nous ajoutons une nouvelle méthode à notre contrôleur d'application :
// Application.java public static Result getPost(Long id) { BlogPost blogPost = BlogPost.findBlogPostById(id); if(blogPost == null) { return notFound(buildJsonResponse("error", "Post not found")); } return ok(Json.toJson(blogPost)); }
… Et un nouveau parcours :
GET /app/post/:id controllers.Application.getPost(id: Long)
Maintenant, si vous accédez à http://localhost:9000/#/viewpost/1, vous pourrez charger une vue pour un message particulier. Ensuite, ajoutons la possibilité de voir les publications des utilisateurs dans le tableau de bord :
// dashboard.js angular.module('clientApp') .controller('DashboardCtrl', function ($scope, $log, $http, alertService, $location) { $scope.loadPosts = function() { $http.get('/app/userposts') .error(function(data, status) { if(status === 401) { $location.path('/login'); } else { alertService.add('danger', data.error.message); } }) .success(function(data) { $scope.posts = data; }); }; $scope.loadPosts(); });
<!-- dashboard.html --> <h4>My Posts</h4> <div ng-hide="posts.length">No posts yet. <a ng-href="/#/addpost">Add a post</a></div> <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <a ng-href="/#/viewpost/{{ post.id }}">{{ post.subject }}</a> | Comments <span class="badge">{{ post.commentCount }}</span> </div> </div>
Ajoutez également une nouvelle méthode à Post controller, suivie d'une route correspondant à cette méthode :
// Post.java public static Result getUserPosts() { User user = getUser(); if(user == null) { return badRequest(Application.buildJsonResponse("error", "No such user")); } return ok(Json.toJson(BlogPost.findBlogPostsByUser(user))); }
GET /app/userposts controllers.Post.getUserPosts
Désormais, lorsque vous créerez des publications, elles seront répertoriées sur le tableau de bord :
Fonctionnalité de commentaire
Pour implémenter la fonctionnalité de commentaire, nous allons commencer par ajouter une nouvelle méthode dans Post controller :
// Post.java public static Result addComment() { Form<CommentForm> commentForm = Form.form(CommentForm.class).bindFromRequest(); if (commentForm.hasErrors()) { return badRequest(commentForm.errorsAsJson()); } else { PostComment newComment = new PostComment(); BlogPost blogPost = BlogPost.findBlogPostById(commentForm.get().postId); blogPost.commentCount++; blogPost.save(); newComment.blogPost = blogPost; newComment.user = getUser(); newComment.content = commentForm.get().comment; newComment.save(); return ok(Application.buildJsonResponse("success", "Comment added successfully")); } } public static class CommentForm { @Constraints.Required public Long postId; @Constraints.Required public String comment; }
And as always, we need to register a new route for this method:
POST /app/comment controllers.Post.addComment
In our AngularJS application, we add the following to “viewpost.js”:
$scope.addComment = function() { var payload = { postId: $scope.postId, comment: $scope.comment }; $http.post('/app/comment', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'comment') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { alertService.add('success', data.success.message); $scope.comment = ''; $scope.viewPost(); }); };
And finally add the following lines to “viewpost.html”:
<div class="well" ng-repeat="comment in post.comments"> <span class="label label-default">By: {{ comment.user.email }}</span> <br/> {{ comment.content }} </div> <div ng-hide="user.username || !post"><h4><a ng-href="/#/login">Login</a> to comment</h4></div> <form name="addCommentForm" ng-submit="addComment()" novalidate ng-show="user.username"> <div><h4>Add comment</h4></div> <div> <label for="comment">Comment</label> <textarea name="comment" class="form-control" placeholder="Comment" ng-model="comment"></textarea> </div> <button type="submit" class="btn btn-primary">Add comment</button> </form>
Now if you open any post, you will be able to add and view comments.
Et après?
In this tutorial, we have built an AngularJS blog with a Play application serving as a REST API back-end. Although the application lacks robust data validation (especially on the client side) and security, these topics were out of the scope of this tutorial. It was aiming to demonstrate one of the many possible ways of building an application of this kind. For convenience, the source code of this application has been uploaded to a GitHub repository.
If you find this combination of AngularJS and Play in web application development interesting, I highly recommend you review the following topics further:
- AngularJS documentation
- Play documentation
- Recommended security approach in one page JS app with Play as back-end (contains example)
- Secure REST API without OAuth
- Ready Play authentication plug-in (might be not fully usable for single-page JavaScript applications, but can be used as a good example)