Construindo aplicativos Web modernos com AngularJS e Play Framework
Publicados: 2022-03-11Escolher 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.
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:
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:
- Por padrão, tanto o aplicativo AngularJS (bootstrap pelo Yeoman) quanto o aplicativo Play tentam ser executados na porta 9000.
- 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.
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.
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:
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.
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:

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:
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.
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:
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:
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.
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)