Creación de aplicaciones web modernas con AngularJS y Play Framework

Publicado: 2022-03-11

Elegir la herramienta adecuada para el propósito correcto es muy importante, especialmente cuando se trata de crear aplicaciones web modernas. Muchos de nosotros estamos familiarizados con AngularJS y lo fácil que es desarrollar interfaces de usuario de aplicaciones web sólidas. Aunque muchos argumentarán en contra del uso de este popular marco web, ciertamente tiene mucho que ofrecer y puede ser una opción adecuada para una amplia gama de necesidades. Por otro lado, los componentes que utilice en el back-end dictarán mucho sobre el rendimiento de la aplicación web, ya que influyen en la experiencia general del usuario. Play es un marco web de alta velocidad para Java y Scala. Se basa en una arquitectura liviana, sin estado y amigable con la web y sigue patrones y principios de MVC similares a Rails y Django.

aplicaciones web con angularjs y play framework

En este artículo, veremos cómo podemos usar AngularJS y Play para crear una aplicación de blog simple con un mecanismo de autenticación básico y la capacidad de hacer publicaciones y comentarios. El desarrollo de AngularJS, con algunas ventajas de Twitter Bootstrap, nos permitirá potenciar una experiencia de aplicación de una sola página sobre un back-end de API REST basado en Play.

Aplicaciones web - Primeros pasos

Esqueleto de la aplicación AngularJS

Las aplicaciones AngularJS y Play residirán en los directorios del cliente y del servidor en consecuencia. Por ahora, crearemos el directorio "cliente".

 mkdir -p blogapp/client

Para crear un esqueleto de aplicación AngularJS, usaremos Yeoman, una herramienta de andamiaje increíble. Instalar Yeoman es fácil. Usarlo para montar una aplicación AngularJS esquelética simple es probablemente aún más fácil:

 cd blogapp/client yo angular

Ejecutar el segundo comando será seguido por algunas opciones entre las que debe elegir. Para este proyecto, no necesitamos "Sass (con Compass)". Necesitaremos Boostrap junto con los siguientes complementos de AngularJS:

  • angular-animate.js
  • angular-cookies.js
  • recurso angular.js
  • angular-route.js
  • angular-sanitize.js
  • angular-touch.js

En este punto, una vez que finalice sus selecciones, comenzará a ver la salida NPM y Bower en su terminal. Cuando se completen las descargas y se hayan instalado los paquetes, tendrá un esqueleto de aplicación AngularJS listo para usar.

Esqueleto de la aplicación Play Framework

La forma oficial de crear una nueva aplicación Play implica el uso de la herramienta Typesafe Activator. Antes de poder usarlo, debe descargarlo e instalarlo en su computadora. Si está en Mac OS y usa Homebrew, puede instalar esta herramienta con una sola línea de comando:

 brew install typesafe-activator

Crear una aplicación Play desde la línea de comandos es muy fácil:

 cd blogapp/ activator new server play-java cd server/

Importando a un IDE

Para importar la aplicación en un IDE como Eclipse o IntelliJ, debe "eclipsar" o "idealizar" su aplicación. Para hacer eso, ejecute el siguiente comando:

 activator

Una vez que vea un nuevo mensaje, escriba "eclipse" o "idea" y presione enter para preparar el código de la aplicación para Eclipse o IntelliJ, respectivamente.

Para abreviar, solo cubriremos el proceso de importar el proyecto a IntelliJ en este artículo. El proceso de importarlo a Eclipse debería ser igualmente simple. Para importar el proyecto a IntelliJ, comience activando la opción "Proyecto de fuentes existentes..." que se encuentra en "Archivo -> Nuevo". A continuación, seleccione su archivo build.sbt y haga clic en "Aceptar". Al hacer clic en "Aceptar" nuevamente en el siguiente cuadro de diálogo, IntelliJ debería comenzar a importar su aplicación Play como un proyecto SBT.

Typesafe Activator también viene con una interfaz gráfica de usuario, que puede usar para crear este código de aplicación esquelético.

Ahora que hemos importado nuestra aplicación Play a IntelliJ, también debemos importar nuestra aplicación AngularJS al espacio de trabajo. Podemos importarlo como un proyecto separado o como un módulo del proyecto existente donde reside la aplicación Play.

Aquí, importaremos la aplicación Angular como un módulo. En el menú "Archivo", seleccionaremos la opción "Nuevo -> Módulo de fuentes existentes...". Desde el cuadro de diálogo, elegiremos el directorio “cliente” y haremos clic en “Aceptar”. En las siguientes dos pantallas, haga clic en “Siguiente” y “Finalizar”, respectivamente.

Generación de servidores locales

En este punto, debería ser posible iniciar la aplicación AngularJS como una tarea de Grunt desde el IDE. Expanda la carpeta de su cliente y haga clic derecho en Gruntfile.js. En el menú emergente, seleccione "Mostrar tareas Grunt". Aparecerá un panel con la etiqueta "Grunt" con una lista de tareas:

Generación de servidores locales

Para comenzar a servir la aplicación, haga doble clic en "servir". Esto debería abrir inmediatamente su navegador web predeterminado y señalarlo a una dirección localhost. Debería ver una página de código auxiliar de AngularJS con el logotipo de Yeoman.

A continuación, debemos iniciar nuestro servidor de aplicaciones back-end. Antes de que podamos continuar, debemos abordar un par de cuestiones:

  1. De forma predeterminada, tanto la aplicación AngularJS (arrancada por Yeoman) como la aplicación Play intentan ejecutarse en el puerto 9000.
  2. En producción, es probable que ambas aplicaciones se ejecuten en un dominio, y probablemente usaremos Nginx para enrutar las solicitudes en consecuencia. Pero en el modo de desarrollo, cuando cambiamos el número de puerto de una de estas aplicaciones, los navegadores web las tratarán como si se estuvieran ejecutando en diferentes dominios.

Para solucionar estos dos problemas, todo lo que tenemos que hacer es usar un proxy Grunt para que todas las solicitudes AJAX a la aplicación Play sean enviadas por proxy. Con esto, en esencia, ambos servidores de aplicaciones estarán disponibles en el mismo número de puerto aparente.

Primero cambiemos el número de puerto del servidor de aplicaciones Play a 9090. Para hacer esto, abra la ventana "Ejecutar/Depurar configuraciones" haciendo clic en "Ejecutar -> Editar configuraciones". A continuación, cambie el número de puerto en el campo "Url para abrir". Haga clic en "Aceptar" para aprobar este cambio y cerrar la ventana. Al hacer clic en el botón "Ejecutar" se debe iniciar el proceso de resolución de dependencias; comenzarán a aparecer los registros de este proceso.

Generación de servidores locales

Una vez hecho esto, puede navegar a http://localhost:9090 en su navegador web, y en unos segundos debería poder ver su aplicación Play. Para configurar un proxy Grunt, primero debemos instalar un pequeño paquete Node.js usando NPM:

 cd blogapp/client npm install grunt-connect-proxy --save-dev

A continuación, debemos modificar nuestro Gruntfile.js. En ese archivo, ubique la tarea "conectar" e inserte la clave/valor "proxies" después:

 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 ahora enviará todas las solicitudes a "/app/*" a la aplicación Play de back-end. Esto nos evitará tener que incluir en la lista blanca todas las llamadas al back-end. Además, también necesitamos modificar nuestro comportamiento de carga en vivo:

 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; } } },

Finalmente, necesitamos agregar una nueva dependencia “'configureProxies:server” a la tarea “servir”:

 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' ]); });

Al reiniciar Grunt, deberías notar las siguientes líneas en tus registros que indican que el proxy se está ejecutando:

 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

Creación de un formulario de registro

Comenzaremos creando un formulario de registro para nuestra aplicación de blog. Esto también nos permitirá verificar que todo funciona como debería. Podemos usar Yeoman para crear un controlador de registro y ver en AngularJS:

 yo angular:controller signup yo angular:view signup

A continuación, debemos actualizar el enrutamiento de nuestra aplicación para hacer referencia a esta vista recién creada y eliminar el controlador y la vista redundantes "acerca de" generados automáticamente. Desde dentro del archivo "app/scripts/app.js", elimine las referencias a "app/scripts/controllers/about.js" y "app/views/about.html", dejándolo con:

 .config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });

Del mismo modo, actualice el archivo "app/index.html" para eliminar los enlaces redundantes y agregue un enlace a la página de registro:

 <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>

Además, elimine la etiqueta de secuencia de comandos para "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>

A continuación, agregue un formulario a nuestro archivo "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>

Necesitamos hacer que el formulario sea procesado por el controlador Angular. Vale la pena señalar que no necesitamos agregar específicamente el atributo "ng-controller" en nuestras vistas, ya que nuestra lógica de enrutamiento en "app.js" activa un controlador automáticamente antes de que se cargue nuestra vista. Todo lo que tenemos que hacer para conectar este formulario es tener una función de "registro" adecuada definida en $scope. Esto debe hacerse en el archivo “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); }); }; });

Ahora abramos la consola de desarrollo de Chrome, cambiemos a la pestaña "Red" e intentemos enviar el formulario de registro.

Ejemplo de formulario de registro de Angular Play

Veremos que el back-end de reproducción responde de forma natural con una página de error "Acción no encontrada". Esto es de esperar ya que aún no se ha implementado. ¡Pero lo que también significa es que nuestra configuración de proxy Grunt está funcionando correctamente!

A continuación, agregaremos una "Acción", que es esencialmente un método en el controlador de la aplicación Play. En la clase "Aplicación" en el paquete "aplicación/controladores", agregue un nuevo método "registro":

 public static Result signup() { return ok("Success!"); }

Ahora abra el archivo “conf/routes” y agregue la siguiente línea:

 POST /app/signup controllers.Application.signup

Finalmente, volvemos a nuestro navegador web, http://localhost:9000/#/signup. Al hacer clic en el botón "Enviar" esta vez debería producir algo diferente:

Ejemplo de formulario de registro de Angular Play en el navegador

Debería ver el valor codificado devuelto, el que escribimos en el método de registro. Si ese es el caso, estamos listos para continuar, ya que nuestro entorno de desarrollo está listo y funciona para las aplicaciones Angular y Play.

Definición de modelos de Ebean en juego

Antes de definir modelos, primero elijamos un almacén de datos. En este artículo, utilizaremos la base de datos en memoria H2. Para habilitar eso, busque y descomente las siguientes líneas en el archivo "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.*"

Y añade la siguiente línea:

 applyEvolutions.default=true

Nuestro modelo de dominio de blog es bastante simple. En primer lugar, tenemos usuarios que pueden crear publicaciones y luego cada publicación puede ser comentada por cualquier usuario que haya iniciado sesión. Vamos a crear nuestros modelos Ebean.

Usuario

 // 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); } } }

Entrada en el blog

 // 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(); } }

Publicar comentario

 // 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(); } }

Acción de registro real

Ahora vamos a crear nuestra primera acción real, permitiendo que los usuarios se registren:

 // 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; }

Tenga en cuenta que la autenticación utilizada en esta aplicación es muy básica y no se recomienda para uso en producción.

La parte interesante es que estamos usando formularios de Play para manejar los formularios de registro. Establecemos un par de restricciones en nuestra clase de formulario de registro. La validación se realizará por nosotros automáticamente sin necesidad de una lógica de validación explícita.

Si volvemos a nuestra aplicación AngularJS en el navegador web y hacemos clic en "Enviar" nuevamente, veremos que el servidor ahora responde con un error apropiado: que estos campos son obligatorios.

Enviar formulario de juego

Manejo de errores del servidor en AngularJS

Así que recibimos un error del servidor, pero el usuario de la aplicación no tiene idea de lo que está pasando. Lo menos que podemos hacer es mostrar el error a nuestro usuario. Idealmente, necesitaríamos entender qué tipo de error estamos recibiendo y mostrar un mensaje fácil de usar. Vamos a crear un servicio de alerta simple que nos ayude a mostrar el error.

Primero, necesitamos generar una plantilla de servicio con Yeoman:

 yo angular:service alerts

A continuación, agregue este código a "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; } );

Ahora, creemos un controlador independiente responsable de las alertas:

 yo angular:controller alerts
 angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });

Ahora necesitamos mostrar buenos mensajes de error de Bootstrap. La forma más fácil es usar la interfaz de usuario angular. Podemos usar Bower para instalarlo:

 bower install angular-bootstrap --save

En su "app.js", agregue el módulo de interfaz de usuario angular:

 angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])

Agreguemos una directiva de alerta a nuestro archivo "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>

Finalmente, necesitamos actualizar el controlador 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!'); } }) }; });

Ahora, si volvemos a enviar el formulario vacío, veremos errores que se muestran arriba del formulario:

Errores de reproducción angular

Ahora que se manejan los errores, debemos hacer algo cuando el registro del usuario sea exitoso. Podemos redirigir al usuario a una página de panel donde puede agregar publicaciones. Pero primero, debemos crearlo:

 yo angular:view dashboard yo angular:controller dashboard

Modifique el método de registro del controlador "signup.js" para que, en caso de éxito, redireccione al usuario:

 angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });

Agrega una nueva ruta en “apps.js”:

 .when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })

También necesitamos rastrear si el usuario ha iniciado sesión. Vamos a crear un servicio separado para eso:

 yo angular:service user
 // user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });

Y también modifique el controlador de registro para configurar el usuario como uno que acaba de registrarse:

 .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });

Antes de agregar la funcionalidad principal de agregar publicaciones, ocupémonos de otras características importantes, como la capacidad de iniciar y cerrar sesión, mostrar la información del usuario en el tablero y también agregar soporte de autenticación en el back-end.

Autenticación básica

Pasemos a nuestra aplicación Play e implementemos las acciones de inicio y cierre de sesión. Agregue estas líneas a "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; }

A continuación, agreguemos la capacidad de permitir llamadas de back-end particulares solo a usuarios autenticados. Cree "Secured.java" con el siguiente código:

 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(); } }

Usaremos esta clase más adelante para proteger nuevas acciones. A continuación, debemos modificar el menú principal de nuestra aplicación AngularJS para que muestre los enlaces de nombre de usuario y cierre de sesión. Para eso, necesitamos crear un controlador:

 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; } }); });

También necesitamos una vista y un controlador para la página de inicio de sesión:

 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'); } }); }; });

A continuación, modificamos el menú para que pueda mostrar los datos del usuario:

 <!-- 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>

Ahora, si inicia sesión en la aplicación, debería poder ver la siguiente pantalla:

Reproducir captura de pantalla

Agregar publicaciones

Ahora que contamos con mecanismos básicos de registro y autenticación, podemos comenzar a implementar la funcionalidad de publicación. Agreguemos una nueva vista y un controlador para agregar publicaciones.

 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); }); }; });

Luego actualizamos "app.js" para incluir:

 .when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })

A continuación, modificamos "index.html" para agregar un enlace para nuestra vista "addpost" en el menú del tablero:

 <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>

Ahora, en el lado de la aplicación Play, creemos una nueva publicación de controlador con el método 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; } }

Agregue una nueva entrada al archivo de rutas para poder manejar los métodos recién agregados en el enrutamiento:

 POST /app/post controllers.Post.addPost

En este punto, debería poder agregar nuevas publicaciones.

Agregar nuevas publicaciones en Play

Mostrar publicaciones

Agregar publicaciones tiene poco valor si no podemos mostrarlas. Lo que queremos hacer es enumerar todas las publicaciones en la página principal. Comenzamos agregando un nuevo método en nuestro controlador de aplicaciones:

 // Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }

Y registrándolo en nuestro fichero de rutas:

 GET /app/posts controllers.Application.getPosts

A continuación, en nuestra aplicación AngularJS modificamos nuestro controlador 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(); });

Finalmente, elimine todo de "main.html" y agregue esto:

 <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>

Ahora, si carga la página de inicio de su aplicación, debería ver algo similar a esto:

Ejemplo de Angularjs Play cargado

Probablemente también deberíamos tener una vista separada para publicaciones individuales.

 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>

Y la ruta AngularJS:

 app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })

Como antes, agregamos un nuevo método a nuestro controlador de aplicaciones:

 // 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)); }

… Y una nueva ruta:

 GET /app/post/:id controllers.Application.getPost(id: Long)

Ahora, si navega a http://localhost:9000/#/viewpost/1, podrá cargar una vista para una publicación en particular. A continuación, agreguemos la capacidad de ver las publicaciones de los usuarios en el tablero:

 // 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>

También agregue un nuevo método al controlador Post, seguido de una ruta correspondiente a este método:

 // 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

Ahora, cuando cree publicaciones, aparecerán en el panel:

Nuevas publicaciones enumeradas en el tablero

Funcionalidad de comentarios

Para implementar la funcionalidad de comentarios, comenzaremos agregando un nuevo método en el controlador Post:

 // 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.

View comments screenshot

What's Next?

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)