Here I’m demonstrating a Login flow consisting of the following:
1. showing a Login form to the user
2. submitting the form to the server via Angular and Ajax
3. validating it on the server and returning a JSON response
4. redirecting to a welcome page on successful submission
This is a flow I encountered recently using Spring MVC and, while not a single-page application was effective at displaying whether the login was successful without a page refresh.
A quick refresher of each technology:
Scala Play: Server-side technology – satisfies HTTP requests returning HTML via templates or JSON, XML etc.
AngularJS: Client-side technology which is like an extension to the HTML specification.
Twitter Bootstrap: Gives a professional look-and-feel to a website, helps sites handle different browsers, screen sizes.
Step 1: Create a Skeleton Scala Play App
I outline that here: https://scalaplayschool.wordpress.com/2014/08/10/hello-world-using-scala-play/
I download activator (that runs the Play framework), unzip it, put it on my Path and use these commands:
$ cd $ activator new lesson12 play-scala $ cd lesson12 $ activator eclipse $ activator run
Step 2: Update the main template to include Angular, Bootstrap and jQuery
There is no need to download and extract source files for these technologies – we can reference public hosted versions. Let’s update our app/views/main.scala.html file as shown:
@(title: String)(content: Html) <!DOCTYPE html> <html> <head> <title>@title</title> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/> <script type="text/javascript" src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> </head> <body> <div style="padding:20px"> @content </div> </body> </html>
Now we can update our app/views/index.scala.html with a simple test to demonstrate that it’s worked.
@() @main("Login") { <div data-ng-app=""> <h1>Login Form</h1> <form role="form"> <div class="form-group"> <label for="username">Username</label> <input class="form-control" data-ng-model="username" autofocus> </div> </form> <div class="alert alert-danger well"> {{ username }} </div> </div> }
We took out the title parameter from the index() function so let’s remove it from app/controllers/Application.scala :
package controllers import play.api._ import play.api.mvc._ object Application extends Controller { def index = Action { Ok(views.html.index()) } }
We should see this:
The sans-serif font and full-width textbox show that Bootstrap has loaded ok and if we type we should see our input reflected in the grey box below the form as we type:
Step 3: Build the Login form
We’ll update our app/views/index.scala.html file again:
@() @main("Login") { <div data-ng-app="" data-ng-controller="LoginController"> <h1>Login Form</h1> <form role="form" data-ng-submit="postLoginForm('@routes.Application.login', '@routes.Application.welcome()')"> <div class="form-group"> <label for="username">Username</label> <input class="form-control" data-ng-model="username" data-ng-change="feedback = ''" autofocus> </div> <div class="form-group"> <label for="password">Password</label> <input class="form-control" data-ng-model="password" type="password" data-ng-change="feedback = ''"> </div> <input type="submit" value="Login"> </form> <div class="alert alert-danger well" data-ng-show="feedback != ''"> {{ feedback }} ! </div> </div> <script type="text/javascript" src="@routes.Assets.at("javascripts/login.js")"></script> }
Note the following:
• data-ng-app will make Angular aware of this form and data-ng-controller will link to a specific class we are providing. We called it LoginController and it will be in login.js referenced at the end of the template.
• role=”form” is to tell Bootstrap about our form so that it can style it correctly.
• data-ng-submit=”…” tells Angular about our form and it will handle its submission.
• form-group, form-control, alert, alert-danger – all of the classes used are ones used by Bootstrap.
• data-ng-show means that AngularJS will show / hide an element based on the expression provided, in this case it is ‘does a non-empty feedback field exist?’ The feedback field belongs to the $scope variable as documented here: https://docs.angularjs.org/guide/scope
We need to update our conf/routes file, adding /login for the processing action and a /welcome to show to authenticated users:
GET / controllers.Application.index POST /login controllers.Application.login GET /welcome controllers.Application.welcome GET /assets/*file controllers.Assets.at(path="/public", file)
Now we need the other view, app/views/welcome.scala.html :
@(username:String) @main(s"Welcome $username") { <h1>Welcome @username</h1> Got your name from the session. }
And our JavaScript file, public/javascripts/login.js which contains the postLoginForm function we bind to our form with data-ng-submit. This function performs the HTTP POST and redirects on successful submission or shows an error message.
function LoginController($scope, $http) { $scope.feedback = ''; $scope.postLoginForm = function(loginUrl, successUrl) { var data = { username : $scope.username, password : $scope.password }; $scope.successUrl = successUrl; $http.post(loginUrl, data).success(function(data, status, headers, config) { if (data.valid) { document.location.href = successUrl; } else { $scope.feedback = "Invalid username / password. Try again."; } }).error(function(data, status, headers, config) { $scope.feedback = 'error: ' + data + ", " + status; }); }; }
Last but not least is a hefty app/controllers/Application.scala :
package controllers import play.api.libs.functional.syntax.functionalCanBuildApplicative import play.api.libs.functional.syntax.toFunctionalBuilderOps import play.api.libs.json._ import play.api.libs.json.JsPath import play.api.libs.json.Json.toJson import play.api.libs.json.Reads import play.api.libs.json.Reads._ import play.api.libs.json.Reads.StringReads import play.api.libs.json.Reads.functorReads import play.api.mvc.Action import play.api.mvc.Controller object Application extends Controller { // Shows the login screen and empties the session: def index = Action { Ok(views.html.index()).withNewSession } // Handles the username-password sent as JSON: def login = Action(parse.json) { request => // Creates a reader for the JSON - turns it into a LoginRequest implicit val loginRequest: Reads[LoginRequest] = Json.reads[LoginRequest] /* * Call validate and if ok we return valid=true and put username in session */ request.body.validate[LoginRequest] match { case s: JsSuccess[LoginRequest] if (s.get.authenticate) => { Ok(toJson(Map("valid" -> true))).withSession("user" -> s.get.username) } // Not valid case _ => Ok(toJson(Map("valid" -> false))) } } def welcome = Action { implicit request => request.session.get("user").map { user => { Ok(views.html.welcome(user)) } }.getOrElse(Redirect(routes.Application.index())) } } case class LoginRequest(username: String, password: String) { // Simple username-password map in place of a database: val validUsers = Map("sysadmin" -> "password1", "root" -> "god") def authenticate = validUsers.exists(_ == (username, password)) }
It does a lot. I’ll explain each action:
• index() – shows the login form using the index.scala.html template and empties out the user’s session if it exists (so we can demonstrate handling users not logged in)
• login() – handles the form submission. Turns the JSON input into a LoginRequest object using a Reader (see the Play documentation) and checks the simple authenticate() method. It returns a JSON object with the key ‘valid’ and the value returned by authenticate(). If a valid user it stores the username in the session, to be used by the welcome action.
• welcome() – if the username is present in the session it shows a welcome page, otherwise it redirects back to the login screen.
Let’s run it:
If we type in values other than those hard-coded to work ( root/god and sysadmin/password1) and submit it (pressing Enter or clicking Login) then we should see our error message:
But if we provide a valid login we get a welcome page:
What happens if we go back to the login page and refresh it? We show the page again but removing our session. If we now try to get to /welcome we are sent back to the login page.