Creating cutting-edge web applications using AngularJS and Play Framework

Selecting the appropriate tool for the task is crucial, particularly when constructing contemporary web applications. Many of us are acquainted with AngularJS and its ability to simplify the development of robust web application front-ends. While some may oppose the use of this popular web framework, it undeniably offers numerous advantages and can be a suitable choice](/angular-js/a-step-by-step-guide-to-your-first-angularjs-app) for a diverse range of requirements. Conversely, the components employed on the back-end significantly impact the web application’s performance and influence the overall user experience. Play is a high-speed web framework designed for Java and Scala. It is founded on a lightweight, stateless architecture tailored for the web and adheres to [MVC patterns and principles akin to Rails and Django.

web applications with angularjs and play framework

This article explores the utilization of AngularJS and Play to create a straightforward blog application incorporating a basic authentication system and functionalities for posting and commenting. AngularJS development, enhanced with Twitter Bootstrap elements, will empower a single-page application experience built upon a Play-based REST API back-end.

Web Applications - Getting Started

AngularJS Application Skeleton

AngularJS and Play apps will reside within the client and server directories, respectively. Our initial step involves creating the “client” directory.

1
mkdir -p blogapp/client

We’ll employ Yeoman, an exceptional scaffolding tool, to construct an AngularJS application skeleton. Installing Yeoman is straightforward. Utilizing it to scaffold a basic AngularJS application skeleton is arguably even simpler:

1
2
cd blogapp/client
yo angular

Upon executing the second command, you’ll encounter several options to choose from. For this project, we can omit “Sass (with Compass).” However, we’ll require Bootstrap along with the following AngularJS plugins:

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

After finalizing your selections, your terminal will display NPM and Bower output as downloads commence and packages are installed. Subsequently, you’ll have a functional AngularJS application skeleton at your disposal.

Play Framework Application Skeleton

The official method for generating a new Play application entails using the Typesafe Activator tool. Prior to its utilization, download and install it on your system. If you’re using Homebrew on Mac OS, a single command suffices:

1
brew install typesafe-activator 

Creating a Play application via the command line is remarkably easy:

1
2
3
cd blogapp/
activator new server play-java
cd server/

Importing to an IDE

To integrate the application into an IDE like Eclipse or IntelliJ, it needs to be “eclipsified” or “idealized.” Execute the following command:

1
activator

At the new prompt, enter either “eclipse” or “idea” to configure the application code for Eclipse or IntelliJ, respectively.

For conciseness, we’ll solely cover importing the project into IntelliJ. The Eclipse import process should be equally straightforward. In IntelliJ, choose “File -> New” and opt for “Project from existing sources…” Then, select your build.sbt file and click “OK.” After confirming on the subsequent dialog, IntelliJ will import your Play application as an SBT project.

Typesafe Activator also offers a graphical user interface for create this skeletal application code.

With our Play application imported into IntelliJ, we’ll import the AngularJS application, either as a distinct project or a module within the Play application’s project.

Here, we’ll import it as a module. Go to “File -> New -> Module From Existing Sources…” In the dialog, choose the “client” directory and click “OK.” On the following screens, click “Next” and “Finish.”

Spawning Local Servers

You should be able to launch the AngularJS application as a Grunt task from the IDE. Expand the client folder and right-click “Gruntfile.js.” From the menu, select “Show Grunt Tasks.” A “Grunt” panel will appear, listing various tasks:

Spawning Local Servers

Double-click “serve” to initiate the application. Your default web browser will open, directed to a localhost address displaying a rudimentary AngularJS page featuring Yeoman’s logo.

Next, we need to launch our back-end application server. However, two issues require attention:

  1. Both the AngularJS application (bootstrapped by Yeoman) and the Play application default to port 9000.
  2. In production, both applications likely operate under a single domain, with Nginx routing requests. However, in development mode, altering one application’s port leads web browsers to treat them as separate domains.

To circumvent these issues, we’ll employ a Grunt proxy, directing all AJAX requests to the Play application, effectively making both servers accessible on the same apparent port.

First, let’s change the Play application server’s port to 9090. Access “Run -> Edit Configurations” and locate “Run/Debug Configurations.” Modify the “Url To Open” field’s port number. Click “OK” to confirm and close. Clicking “Run” will initiate dependency resolution, with logs appearing accordingly.

Spawning Local Servers

Once complete, navigate to http://localhost:9090 in your browser. Shortly, your Play application should load. To set up the Grunt proxy, install a small Node.js package using NPM:

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

Next, modify your “Gruntfile.js.” Locate the “connect” task and insert the “proxies” key/value pair after it:

1
2
3
4
5
6
7
8
 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 will now proxy requests to “/app/*” to the Play back-end, eliminating the need for individual call whitelisting. Additionally, adjust the livereload behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
   }
 }
}, 

Lastly, incorporate a new dependency, “‘configureProxies:server’,” into the “serve” task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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'
 ]);
});

Upon restarting Grunt, your logs should display lines indicating the proxy’s operation:

1
2
3
4
5
6
7
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

Creating a Sign-up Form

We’ll begin by crafting a signup form for our blog application, verifying that everything functions correctly. We can use Yeoman to generate a Signup controller and view in AngularJS:

1
2
yo angular:controller signup
yo angular:view signup

Update the application routing to reference this new view and remove the auto-generated “about” controller and view. Within “app/scripts/app.js,” eliminate references to “app/scripts/controllers/about.js” and “app/views/about.html,” resulting in:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.config(function ($routeProvider) {
 $routeProvider
   .when('/', {
     templateUrl: 'views/main.html',
     controller: 'MainCtrl'
   })
   .when('/signup', {
     templateUrl: 'views/signup.html',
     controller: 'SignupCtrl'
   })
   .otherwise({
     redirectTo: '/'
   });

Similarly, modify “app/index.html,” removing redundant links and adding one for the signup page:

1
2
3
4
5
6
7
<div class="collapse navbar-collapse" id="js-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>

Also, delete the “about.js” script tag:

1
2
3
4
5
6
7
<!-- 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>

Next, incorporate a form into “signup.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<form name="signupForm" ng-submit="signup()" novalidate>
 <div>
   <label for="email">Email</label>
   <input name="email" class="form-control" type="email" id="email" placeholder="Email"
          ng-model="email">
   </div>
 <div>
   <label for="password">Password</label>
   <input name="password" class="form-control" type="password" id="password"
          placeholder="Password" ng-model="password">

 </div>
 <button type="submit" class="btn btn-primary">Sign up!</button>
</form>

This form should be processed by the Angular controller. Note that explicitly adding the “ng-controller” attribute in our views is unnecessary, as the routing logic in “app.js” automatically triggers a controller before view loading. To connect this form, define a “signup” function within $scope in “signup.js”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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);
           });
     };
   });

Now, open your Chrome developer console, switch to the “Network” tab, and attempt a signup form submission.

Angular Play signup form example

The Play back-end will respond with an “Action not found” error page—expected, as it’s not yet implemented. This confirms our Grunt proxy setup is functioning correctly.

Next, we’ll add an “Action” – a method within the Play application controller. Within the “Application” class in the “app/controllers” package, add a new “signup” method:

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

Now, in “conf/routes,” add the following line:

1
POST        /app/signup          controllers.Application.signup

Finally, return to your browser and http://localhost:9000/#/signup. Clicking “Submit” this time should yield a different result:

Angular Play signup form example in browser

You should see the hardcoded value returned from the “signup” method. If so, our development environment is ready, with both Angular and Play applications working correctly.

Defining Ebean Models in Play

Before defining models, let’s select a datastore. This article utilizes the H2 in-memory database. To enable it, uncomment these lines in “application.conf”:

1
2
3
4
5
6
db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
db.default.user=sa
db.default.password=""
...
ebean.default="models.*"

And add this line:

1
applyEvolutions.default=true

Our blog’s domain model is straightforward: users create posts, and any signed-in user can comment on them. Let’s define our Ebean Models.

User

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// User.java

@Entity
public class User extends Model {

 @Id
 public Long id;

 @Column(length = 255, unique = true, nullable = false)
 @Constraints.MaxLength(255)
 @Constraints.Required
 @Constraints.Email
 public String email;

 @Column(length = 64, nullable = false)
 private byte[] shaPassword;

 @OneToMany(cascade = CascadeType.ALL)
 @JsonIgnore
 public List<BlogPost> posts;

 public void setPassword(String password) {
   this.shaPassword = getSha512(password);
 }

 public void setEmail(String email) {
   this.email = email.toLowerCase();
 }

 public static final Finder<Long, User> find = new Finder<Long, User>(
     Long.class, User.class);

 public static User findByEmailAndPassword(String email, String password) {
   return find
       .where()
       .eq("email", email.toLowerCase())
       .eq("shaPassword", getSha512(password))
       .findUnique();
 }

 public static User findByEmail(String email) {
   return find
       .where()
       .eq("email", email.toLowerCase())
       .findUnique();
 }

 public static byte[] getSha512(String value) {
   try {
     return MessageDigest.getInstance("SHA-512").digest(value.getBytes("UTF-8"));
   }
   catch (NoSuchAlgorithmException e) {
     throw new RuntimeException(e);
   }
   catch (UnsupportedEncodingException e) {
     throw new RuntimeException(e);
   }
 }
}

BlogPost

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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();
 }

}

PostComment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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();
 }

}

Real Signup Action

Let’s create a real signup action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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;
}

Note: This app’s authentication is rudimentary and unsuitable for production.

We use Play forms for handling signup forms. Constraints set on the “SignUp” form class enable automatic validation without explicit logic.

Returning to our AngularJS application, submitting an empty form will now trigger appropriate server-side error messages regarding required fields.

Submit play form

Handling Server Errors in AngularJS

Currently, server errors are invisible to the user. At a minimum, we should display them. Ideally, we’d understand the error type and present a user-friendly message. Let’s create a simple alert service for error display.

First, generate a service template using Yeoman:

1
yo angular:service alerts

Add this code to “alerts.js”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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;
   }
);

Now, create a separate controller responsible for alerts:

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

To display visually appealing Bootstrap error messages, we’ll utilize Angular UI. Install it via Bower:

1
bower install angular-bootstrap --save

Append the Angular UI module to your “app.js”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
angular
   .module('clientApp', [
     'ngAnimate',
     'ngCookies',
     'ngResource',
     'ngRoute',
     'ngSanitize',
     'ngTouch',
     'ui.bootstrap'
   ])

Add the alert directive to “index.html”:

1
2
3
4
5
6
<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>

Finally, update the “SignUp” controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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!');
             }
           })
 
     };
   });

Now, submitting an empty form will display errors above it:

Angular Play errors

With error handling in place, let’s redirect users to a dashboard page upon successful signup. First, create the dashboard:

1
2
yo angular:view dashboard
yo angular:controller dashboard

Modify the “signup” method in “signup.js” to redirect upon success:

1
2
3
4
5
6
7
8
angular.module('clientApp')
   .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) {
// ..
.success(function(data) {
 if(data.hasOwnProperty('success')) {
   $location.path('/dashboard');
 }
});

Add a new route in “apps.js”:

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

We also need to track user login status. Create a separate service for this:

1
yo angular:service user
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// user.js

angular.module('clientApp')
   .factory('userService', function() {
     var username = '';

     return {
       username : username
     };
   });

And modify the signup controller to set the user:

1
2
3
4
5
6
.success(function(data) {
 if(data.hasOwnProperty('success')) {
   userService.username = $scope.email;
   $location.path('/dashboard');;
 }
});

Before implementing posting functionality, let’s address other essentials: login/logout, displaying user information on the dashboard, and adding authentication support to the back-end.

Basic Authentication

Let’s implement login and logout actions in our Play application. Add these lines to “Application.java”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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;
}

Next, restrict specific back-end calls to authenticated users. Create “Secured.java” containing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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();
 }
}

We’ll use this class later to secure new actions. Modify the AngularJS application’s main menu to display username and logout links. First, create a controller:

1
yo angular:controller menu
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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;
       }
     });
   });

We also need a view and controller for the login page:

1
2
yo angular:controller login
yo angular:view login
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- login.html -->

<form name="loginForm" ng-submit="login()" novalidate>
 <div>
   <label for="email">Email</label>
   <input name="email" class="form-control" type="email" id="email" placeholder="Email"
          ng-model="email">
 </div>
 <div>
   <label for="password">Password</label>
   <input name="password" class="form-control" type="password" id="password"
          placeholder="Password" ng-model="password">

 </div>
 <button type="submit" class="btn btn-primary">Log in</button>
</form>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 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');
             }
           });
     };
   });

Now, adjust the menu to display user data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- index.html -->

<div class="collapse navbar-collapse" id="js-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>

Logging into the application should now present the following:

Play screenshot

Adding Posts

With signup and authentication in place, let’s implement posting functionality. Add a new view and controller for adding posts:

1
yo angular:view addpost
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!-- addpost.html -->

<form name="postForm" ng-submit="post()" novalidate>
 <div>
   <label for="subject">Subject</label>
   <input name="subject" class="form-control" type="subject" id="subject" placeholder="Subject"
          ng-model="subject">
 </div>
 <div>
   <label for="content">Post</label>
   <textarea name="content" class="form-control" id="content" placeholder="Content"
             ng-model="content"></textarea>
 </div>

 <button type="submit" class="btn btn-primary">Submit post</button>
</form>
1
yo angular:controller addpost
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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);
         });
   };
 });

Update “app.js” to include:

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

In “index.html,” add a link for our “addpost” view to the dashboard menu:

1
2
3
4
5
6
<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>

On the Play side, create a new controller, “Post,” with an “addPost” method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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;

 }
}

Add a new entry to the routes file for handling the new method:

1
POST        /app/post                   controllers.Post.addPost

You should now be able to add new posts.

Adding new posts in Play

Displaying Posts

Let’s display the added posts. We’ll list them on the main page. Start by adding a new method to our application controller:

1
2
3
4
5
// Application.java

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

And register it in the routes file:

1
GET         /app/posts                  controllers.Application.getPosts

In our AngularJS application, modify the main controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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();

   });

Finally, replace the contents of “main.html” with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<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>

Your application’s home page should now resemble this:

Angularjs Play example loaded

A separate view for individual posts would be beneficial.

1
2
yo angular:controller viewpost
yo angular:view viewpost
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 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();
   });
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- 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>

And the corresponding AngularJS route:

1
2
3
4
5
app.js:
.when('/viewpost/:postId', {
templateUrl: 'views/viewpost.html',
controller: 'ViewpostCtrl'
})

Similar to before, add a new method to the application controller:

1
2
3
4
5
6
7
8
9
// 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));
}

… And a new route:

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

Navigating to http://localhost:9000/#/viewpost/1 will now load the view for a specific post. Let’s enable viewing user posts on the dashboard:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 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();
   });
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- 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>

Add a new method and route to the “Post” controller:

1
2
3
4
5
6
7
8
9
// 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)));
}
1
GET         /app/userposts              controllers.Post.getUserPosts

Created posts will now be listed on the dashboard:

New posts listed on dashboard

Commenting Functionality

To implement commenting, add a new method to the “Post” controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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 register a new route for it:

1
POST        /app/comment                controllers.Post.addComment

In the AngularJS application, add the following to “viewpost.js”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$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();
     });
};

Finally, add these lines to “viewpost.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<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" id="comment" placeholder="Comment"
             ng-model="comment"></textarea>
 </div>

 <button type="submit" class="btn btn-primary">Add comment</button>
</form>

You can now add and view comments on any post.

View comments screenshot

What’s Next?

This tutorial constructed an AngularJS blog with a Play application as its REST API back-end. While lacking robust data validation (particularly client-side) and comprehensive security, this tutorial aimed to showcase one approach to building such an application. The source code is available at a GitHub repository.

For those interested in AngularJS and Play for web development, explore these topics further:

Licensed under CC BY-NC-SA 4.0