Construindo aplicativos Web modernos com AngularJS e Play Framework

Publicados: 2022-03-11

Escolher a ferramenta certa para a finalidade certa ajuda muito, especialmente quando se trata de criar aplicativos da Web modernos. Muitos de nós estão familiarizados com o AngularJS e como ele facilita o desenvolvimento de front-ends robustos de aplicativos da Web. Embora muitos argumentem contra o uso desse popular framework da web, ele certamente tem muito a oferecer e pode ser uma escolha apropriada para uma ampla gama de necessidades. Por outro lado, os componentes que você usa no back-end ditarão muito sobre o desempenho do aplicativo da Web, pois influenciam a experiência geral do usuário. Play é um framework web de alta velocidade para Java e Scala. Ele é baseado em uma arquitetura leve, sem estado e amigável à web e segue padrões e princípios MVC semelhantes ao Rails e Django.

aplicações web com angularjs e play framework

Neste artigo, veremos como podemos usar o AngularJS e o Play para criar um aplicativo de blog simples com um mecanismo básico de autenticação e a capacidade de fazer postagens e comentários. O desenvolvimento do AngularJS, com alguns brindes do Twitter Bootstrap, nos permitirá alimentar uma experiência de aplicativo de página única em cima de um back-end de API REST baseado em Play.

Aplicativos da Web - Introdução

Esqueleto de aplicativo AngularJS

Os aplicativos AngularJS e Play residirão nos diretórios do cliente e do servidor de acordo. Por enquanto, vamos criar o diretório “cliente”.

 mkdir -p blogapp/client

Para criar um esqueleto de aplicativo AngularJS, usaremos o Yeoman - uma incrível ferramenta de andaimes. Instalar o Yeoman é fácil. Usá-lo para montar um simples aplicativo AngularJS esquelético é provavelmente ainda mais fácil:

 cd blogapp/client yo angular

A execução do segundo comando será seguida por algumas opções que você precisa escolher. Para este projeto, não precisamos de “Sass (with Compass)”. Vamos precisar do Boostrap junto com os seguintes plugins AngularJS:

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

Neste ponto, depois de finalizar suas seleções, você começará a ver a saída do NPM e do Bower em seu terminal. Quando os downloads estiverem completos e os pacotes instalados, você terá um esqueleto de aplicação AngularJS pronto para ser usado.

Esqueleto de aplicativo do Play Framework

A forma oficial de criar um novo aplicativo Play envolve o uso da ferramenta Typesafe Activator. Antes de poder usá-lo, você deve baixá-lo e instalá-lo em seu computador. Se você estiver no Mac OS e usar o Homebrew, poderá instalar esta ferramenta com uma única linha de comando:

 brew install typesafe-activator

Criar um aplicativo Play a partir da linha de comando é super fácil:

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

Importando para um IDE

Para importar o aplicativo em um IDE como Eclipse ou IntelliJ, você precisa “eclipsificar” ou “idealizar” seu aplicativo. Para isso, execute o seguinte comando:

 activator

Depois de ver um novo prompt, digite “eclipse” ou “idea” e pressione Enter para preparar o código do aplicativo para Eclipse ou IntelliJ, respectivamente.

Por brevidade, abordaremos apenas o processo de importação do projeto para o IntelliJ neste artigo. O processo de importação para o Eclipse deve ser igualmente simples. Para importar o projeto para o IntelliJ, comece ativando a opção “Projeto de fontes existentes…” encontrada em “Arquivo -> Novo”. Em seguida, selecione seu arquivo build.sbt e clique em “OK”. Ao clicar em “OK” novamente na próxima caixa de diálogo, o IntelliJ deve começar a importar seu aplicativo Play como um projeto SBT.

O Typesafe Activator também vem com uma interface gráfica do usuário, que você pode usar para criar esse código de aplicativo esquelético.

Agora que importamos nosso aplicativo Play para o IntelliJ, também devemos importar nosso aplicativo AngularJS para o espaço de trabalho. Podemos importá-lo como um projeto separado ou como um módulo para o projeto existente onde o aplicativo Play reside.

Aqui, vamos importar o aplicativo Angular como um módulo. No menu “Arquivo”, selecionaremos a opção “Novo -> Módulo de fontes existentes…”. Na caixa de diálogo, escolheremos o diretório “cliente” e clicaremos em “OK”. Nas próximas duas telas, clique em “Next” e “Finish”, respectivamente.

Gerando Servidores Locais

Neste ponto, deve ser possível iniciar o aplicativo AngularJS como uma tarefa Grunt do IDE. Expanda a pasta do cliente e clique com o botão direito do mouse em Gruntfile.js. No menu pop-up, selecione "Mostrar tarefas do Grunt". Um painel chamado “Grunt” aparecerá com uma lista de tarefas:

Gerando Servidores Locais

Para começar a servir o aplicativo, clique duas vezes em “servir”. Isso deve abrir imediatamente seu navegador da Web padrão e apontá-lo para um endereço de host local. Você deve ver um esboço da página AngularJS com o logotipo da Yeoman.

Em seguida, precisamos iniciar nosso servidor de aplicativos de back-end. Antes de prosseguirmos, devemos abordar algumas questões:

  1. Por padrão, tanto o aplicativo AngularJS (bootstrap pelo Yeoman) quanto o aplicativo Play tentam ser executados na porta 9000.
  2. Em produção, ambos os aplicativos provavelmente serão executados em um domínio e provavelmente usaremos o Nginx para rotear as solicitações de acordo. Mas no modo de desenvolvimento, quando alteramos o número da porta de um desses aplicativos, os navegadores da Web os tratam como se estivessem sendo executados em domínios diferentes.

Para contornar esses dois problemas, tudo o que precisamos fazer é usar um proxy Grunt para que todas as solicitações AJAX para o aplicativo Play sejam proxy. Com isso, em essência, esses dois servidores de aplicativos estarão disponíveis no mesmo número de porta aparente.

Vamos primeiro alterar o número da porta do servidor de aplicativos Play para 9090. Para fazer isso, abra a janela “Run/Debug Configurations” clicando em “Run -> Edit Configurations”. Em seguida, altere o número da porta no campo “Url To Open”. Clique em “OK” para aprovar esta alteração e fechar a janela. Clicar no botão "Executar" deve iniciar o processo de resolução de dependência - os logs desse processo começarão a aparecer.

Gerando Servidores Locais

Feito isso, você pode navegar para http://localhost:9090 em seu navegador da Web e, em alguns segundos, poderá ver seu aplicativo Play. Para configurar um proxy Grunt, primeiro precisamos instalar um pequeno pacote Node.js usando o NPM:

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

Em seguida, precisamos ajustar nosso Gruntfile.js. Nesse arquivo, localize a tarefa “connect” e insira a chave/valor “proxies” depois dela:

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

O Grunt agora fará proxy de todas as solicitações para “/app/*” para o aplicativo Play de back-end. Isso nos poupará de ter que colocar todas as chamadas na lista de permissões para o back-end. Além disso, também precisamos ajustar nosso comportamento 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; } } },

Finalmente, precisamos adicionar uma nova dependência “'configureProxies:server” à tarefa “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' ]); });

Ao reiniciar o Grunt, você deverá observar as seguintes linhas em seus logs indicando que o proxy está em execução:

 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

Criando um formulário de inscrição

Começaremos criando um formulário de inscrição para nosso aplicativo de blog. Isso também nos permitirá verificar se tudo está funcionando como deveria. Podemos usar o Yeoman para criar um controlador de inscrição e visualizar no AngularJS:

 yo angular:controller signup yo angular:view signup

Em seguida, devemos atualizar o roteamento de nosso aplicativo para fazer referência a essa visualização recém-criada e remover o controlador e a visualização redundantes “about” gerados automaticamente. De dentro do arquivo “app/scripts/app.js”, remova as referências a “app/scripts/controllers/about.js” e “app/views/about.html”, deixando-o com:

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

Da mesma forma, atualize o arquivo “app/index.html” para remover os links redundantes e adicione um link à página de inscrição:

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

Além disso, remova a tag de script 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>

Em seguida, adicione um formulário ao nosso arquivo “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>

Precisamos fazer com que o formulário seja processado pelo controlador Angular. Vale a pena notar que não precisamos adicionar especificamente o atributo “ng-controller” em nossas visualizações, pois nossa lógica de roteamento em “app.js” aciona um controlador automaticamente antes de nossa visualização ser carregada. Tudo o que temos que fazer para conectar este formulário é ter uma função de “inscrição” adequada definida em $scope. Isso deve ser feito no arquivo “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); }); }; });

Agora vamos abrir o console do desenvolvedor do Chrome, mudar para a guia "Rede" e tentar enviar o formulário de inscrição.

Exemplo de formulário de inscrição do Angular Play

Veremos que o back-end do Play responde naturalmente com uma página de erro "Ação não encontrada". Isso é esperado, pois ainda não foi implementado. Mas o que isso também significa é que nossa configuração de proxy do Grunt está funcionando corretamente!

Em seguida, adicionaremos uma “Ação” que é essencialmente um método no controlador do aplicativo Play. Na classe “Application” no pacote “app/controllers”, adicione um novo método “signup”:

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

Agora abra o arquivo “conf/routes” e adicione a seguinte linha:

 POST /app/signup controllers.Application.signup

Por fim, retornamos ao nosso navegador da Web, http://localhost:9000/#/signup. Clicar no botão “Enviar” desta vez deve resultar em algo diferente:

Exemplo de formulário de inscrição do Angular Play no navegador

Você deve estar vendo o valor codificado permanentemente retornado, aquele que escrevemos no método de inscrição. Se for esse o caso, estamos prontos para seguir em frente, pois nosso ambiente de desenvolvimento está pronto e funcionando para os aplicativos Angular e Play.

Definindo modelos Ebean em jogo

Antes de definir os modelos, vamos primeiro escolher um armazenamento de dados. Neste artigo, usaremos o banco de dados em memória H2. Para habilitar isso, localize e descomente as seguintes linhas no arquivo “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.*"

E adicione a seguinte linha:

 applyEvolutions.default=true

Nosso modelo de domínio de blog é bastante simples. Em primeiro lugar, temos usuários que podem criar postagens e, em seguida, cada postagem pode ser comentada por qualquer usuário conectado. Vamos criar nossos modelos Ebean.

Do utilizador

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

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

Postar comentário

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

Ação de inscrição real

Agora vamos criar nossa primeira ação real, permitindo que os usuários se cadastrem:

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

Observe que a autenticação usada neste aplicativo é muito básica e não é recomendada para uso em produção.

A parte interessante é que estamos usando os formulários do Google Play para lidar com os formulários de inscrição. Definimos algumas restrições em nossa classe de formulário SignUp. A validação será feita para nós automaticamente sem necessidade de lógica de validação explícita.

Se voltarmos ao nosso aplicativo AngularJS no navegador da web e clicarmos em “Enviar” novamente, veremos que o servidor agora responde com um erro apropriado - que esses campos são obrigatórios.

Enviar formulário de jogo

Manipulando erros do servidor em AngularJS

Portanto, estamos recebendo um erro do servidor, mas o usuário do aplicativo não tem ideia do que está acontecendo. O mínimo que podemos fazer é exibir o erro para nosso usuário. Idealmente, precisaríamos entender que tipo de erro estamos recebendo e exibir uma mensagem amigável. Vamos criar um serviço de alerta simples que nos ajudará a exibir o erro.

Primeiro, precisamos gerar um modelo de serviço com Yeoman:

 yo angular:service alerts

Em seguida, adicione 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; } );

Agora, vamos criar um controller separado responsável pelos alertas:

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

Agora precisamos realmente mostrar boas mensagens de erro do Bootstrap. A maneira mais fácil é usar a interface do usuário angular. Podemos usar o Bower para instalá-lo:

 bower install angular-bootstrap --save

No seu “app.js”, acrescente o módulo Angular UI:

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

Vamos adicionar a diretiva de alerta ao nosso arquivo “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, precisamos atualizar o 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!'); } }) }; });

Agora, se enviarmos o formulário vazio novamente, veremos erros exibidos acima do formulário:

Erros de reprodução angular

Agora que os erros foram tratados, precisamos fazer algo quando a inscrição do usuário for bem-sucedida. Podemos redirecionar o usuário para uma página do painel onde ele pode adicionar postagens. Mas primeiro, devemos criá-lo:

 yo angular:view dashboard yo angular:controller dashboard

Modifique o método de inscrição do controlador “signup.js” para que, em caso de sucesso, ele redirecione o usuário:

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

Adicione uma nova rota em “apps.js”:

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

Também precisamos rastrear se o usuário está logado. Vamos criar um serviço separado para isso:

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

E também modifique o controlador de inscrição para definir o usuário para aquele que acabou de se registrar:

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

Antes de adicionarmos a funcionalidade principal de adicionar postagens, vamos cuidar de alguns outros recursos importantes, como a capacidade de fazer login e logout, exibir informações do usuário no painel e também adicionar suporte à autenticação no back-end.

Autenticação básica

Vamos pular para o nosso aplicativo Play e implementar as ações de login e logout. Adicione essas linhas 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; }

Em seguida, vamos adicionar a capacidade de permitir chamadas de back-end específicas apenas para usuários autenticados. Crie “Secured.java” com o seguinte 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 essa classe mais tarde para proteger novas ações. Em seguida, devemos ajustar o menu principal do aplicativo AngularJS para que ele exiba o nome de usuário e os links de logout. Para isso, precisamos criar o 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; } }); });

Também precisamos de uma view e um controller para a página de login:

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

Em seguida, ajustamos o menu para que ele possa exibir os dados do usuário:

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

Agora, se você fizer login no aplicativo, poderá ver a seguinte tela:

Reproduzir captura de tela

Adicionando postagens

Agora que temos mecanismos básicos de inscrição e autenticação, podemos começar a implementar a funcionalidade de postagem. Vamos adicionar uma nova view e controller para adicionar posts.

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

Em seguida, atualizamos “app.js” para incluir:

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

Em seguida, modificamos “index.html” para adicionar um link para nossa visualização “addpost” no menu do painel:

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

Agora no lado do aplicativo Play, vamos criar um novo controller Post com o 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; } }

Adicione uma nova entrada ao arquivo de rotas para poder lidar com métodos recém-adicionados no roteamento:

 POST /app/post controllers.Post.addPost

Neste ponto, você deve ser capaz de adicionar novas postagens.

Adicionando novas postagens no Play

Exibindo postagens

Adicionar postagens tem pouco valor, se não pudermos exibi-las. O que queremos fazer é listar todos os posts na página principal. Começamos adicionando um novo método em nosso controlador de aplicação:

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

E registrando-o em nosso arquivo de rotas:

 GET /app/posts controllers.Application.getPosts

Em seguida, em nosso aplicativo AngularJS, modificamos nosso 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, remova tudo de “main.html” e adicione isto:

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

Agora, se você carregar a página inicial do seu aplicativo, deverá ver algo semelhante a isto:

Exemplo Angularjs Play carregado

Provavelmente também deveríamos ter uma visão separada para postagens individuais.

 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>

E a rota AngularJS:

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

Como antes, adicionamos um novo método ao nosso controlador de aplicação:

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

… E uma nova rota:

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

Agora, se você navegar para http://localhost:9000/#/viewpost/1, poderá carregar uma visualização para uma postagem específica. Em seguida, vamos adicionar a capacidade de ver as postagens do usuário no painel:

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

Adicione também um novo método ao Post controller, seguido por uma rota correspondente 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

Agora, quando você criar postagens, elas serão listadas no painel:

Novas postagens listadas no painel

Funcionalidade de comentários

Para implementar a funcionalidade de comentários, começaremos adicionando um novo método no 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.

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)