Introduction
In the previous article, I demonstrated how to create a JSON-powered web back-end using Java Spring MVC and Jeneva. In this article, I will show some basic tips on how to create a single page front-end side using AngularJS. If you are not familiar with the basics of AngularJS, then, please, refer to this YouTube channel.
Background
Let's assume that we already have working backend, with MVC controller, that provides HTML views, and that gives us handy JSON API.
@Controller
@RequestMapping({"/client"})
public class ClientController {
private IClientService clientService;
@Autowired
public ClientController(IClientService clientService) {
this.clientService = clientService;
}
@RequestMapping(value={"/list"})
public String list() {
return "client/list";
}
@RequestMapping("/create")
public String create() {
return "client/create";
}
@RequestMapping("/edit")
public String edit() {
return "client/edit";
}
@RequestMapping("/getbyid")
@ResponseBody
public Client getById(int id) {
return this.clientService.getById(id);
}
@RequestMapping("/getall")
@ResponseBody
public List<client> getAll() {
return this.clientService.getAll();
}
@RequestMapping("/save")
@ResponseBody
public void save(@RequestBody Client item) {
this.clientService.save(item);
}
@RequestMapping("/update")
@ResponseBody
public void update(@RequestBody Client item) {
this.clientService.update(item);
}
@RequestMapping("/delete/{id}")
@ResponseBody
public void delete(@PathVariable("id") int id) {
this.clientService.delete(id);
}
The controller looks pretty simple and workable. Please refer to my previous article to find out how I created backend. My goal is creating the front-end for these two controllers. The front-end must be a single-page browser application (SPA). Many people think that SPA is not a useful technique, because it does not produce links to different pages and SPA requires you so much effort, that it is not worth it. I will show you that AngularJS makes creating SPA as simple as creating multiple-pages application, and all the benefits of the multiple-page apps are preserved in SPA.
Basic Structure
The most important part of every single page application (SPA) is its structure. We are going to have 3 views: "List of Clients", "Create Client" and "Edit Client". This actually means that we will have three AngularJS controllers for these views - clientListController.js, clientCreateController.js, and clientEditController.js. The "vendor" folder contains all third-party libraries. We also have custom filters. Usually, this kind of structure also has "directives" and "services" folder, but in this simple SPA, I am not using any custom directives and services.
The fundamental part of the front-end is the app.js file located in the root of the js folder. Let's analyze its contents.
var myclients = myclients || {};
myclients.app = angular.module("myclients", ["ngRoute", "jenevamodule"]);
myclients.app.config(function ($routeProvider) {
$routeProvider
.when("/", {
templateUrl: "client/list",
controller: "clientListController"
})
.when("/client/list", {
templateUrl: "client/list",
controller: "clientListController"
})
.when("/client/create", {
templateUrl: "client/create",
controller: "clientCreateController"
})
.when("/client/edit/:id", {
templateUrl: "client/edit",
controller: "clientEditController"
})
.otherwise({
templateUrl: "home/notfound"
});
});
$jeneva.baseUrl = "/api/";
In the first line, I define main namespace of my application. Then, using AngularJS, I create an instance of my main module. This is a common task to every Angular application. And this module is dependant on two different modules: ngRoute
and jenevamodule
, which both come as independent reusable angular modules. The most interesting part is in the middle. As you see, I define several routes here. This is actually the core of any front-end. Simply-saying - here, I tell AngularJS - which js controller and which view should be turned on, depending on the URL in the browser. Every moment, my application will have only one active view and controller, and this will depend on the URL entered in the browser window, that is it. For example, if I type url: client/list
, then client/list
view and clientListController
will be loaded active.
The last part of the app.js is dedicated to Jeneva. I use it because it makes my application easier to develop and support. You can refer to my previous article to find out more about Jeneva. I tell Jeneva that my JSON API controllers are located in the /api/ subfolder.
JS Controllers
Now, let's take a look at controllers. The first one is clientListController.js.
myclients.app.controller(
"clientListController", ["$scope", "jeneva",
function ($scope, jeneva) {
$scope.clientRows = new Array();
$scope.ClientRow = function (id) {
this.id = id;
this.name = null;
this.lastname = null;
this.age = null;
this.logins = new Array();
};
$scope.loadClients = function() {
jeneva.get("client/getall")
.then(function (items) {
angular.forEach(items, function (p, i) {
var row = new $scope.ClientRow(p.id);
row.name = p.name;
row.lastname = p.lastname;
row.age = p.age;
angular.forEach(p.logins, function (q, j) {
row.logins.push(q.name);
});
$scope.clientRows.push(row);
});
});
};
$scope.onDelete = function (clientId) {
jeneva.post("client/delete/" + clientId)
.then(function () {
$scope.loadClients();
});
};
$scope.$on("$routeChangeSuccess", function () {
$scope.loadClients();
});
}]);
Some genius developers can ask - why should I copy the server side json-model to the client side model. In this case, it is redundant, it is easier to just use the server-side json-model as client-side model, without copying it. But in most cases, server and client side models have different structure and by copying it, you avoid any problems in future and make all code consistent through the entire application. Client-side model ($scope
) must be as simple as it can, it must not care about domain model classes. You must take into account domain model classes only when you interact with the server-side, i.e., when you receive or send data to server.
As you see, I refer to the myclients.app module created in the first two lines of the app.js file. My controller is named clientListController
and is injected with 2 variables: $scope
, and jeneva
. In the body of the controller, I define $scope.clientRows
- which is the model of my view - list of clients from the database. Interaction with backend is made in the $scope.loadClients
method, which uses jeneva
service to call JSON API controllers. The reason why I use jeneva
instead of direct angular ($http
) call is - jeneva manages automagically if my backend fails and will display error message in the correct place in my view (based on jvErrorkey
directive). When my API returns list of Clients, I populate them into the $scope.clientRows
field.
The last part of the controller is the most important. I attach handler to the $routeChangeSuccess
event. This event fires every time, when this controller becomes active, i.e., every time user navigates to the "List of Clients" view, this event gets fired. In my case, when this event is fired, the $scope.loadClients
method is called, and data is extracted from backend.
Other controllers work about the same. They are injected with $location
variable, which is used to navigate between view in AngularJS SPA. For example, the clientCreateController.js:
myclients.app.controller(
"clientCreateController",
["$scope", "$location", "jeneva", function ($scope, $location, jeneva) {
$scope.name = null;
$scope.lastname = null;
$scope.age = null;
$scope.loginRows = new Array();
$scope.LoginRow = function () {
this.name = null;
this.password = null;
this.enabled = false;
};
$scope.onRemoveLoginClick = function (item) {
var index = $scope.loginRows.indexOf(item);
$scope.loginRows.splice(index, 1);
};
$scope.onAddLoginClick = function () {
var row = new $scope.LoginRow();
$scope.loginRows.push(row);
};
$scope.onSave = function () {
var data = {};
data.name = $scope.name;
data.lastname = $scope.lastname;
data.age = $scope.age;
data.logins = new Array();
angular.forEach($scope.loginRows, function (p, i) {
var item = {};
item.name = p.name;
item.password = p.password;
item.enabled = p.enabled;
data.logins.push(item);
});
jeneva.post("client/save", data)
.then(function () {
$location.path("client/list");
});
};
$scope.$on("$routeChangeSuccess", function () {
$scope.onAddLoginClick();
});
}]);
Take a look at the $scope.onSave
method. It fires when user clicks on the Save button of the Client Create view. It collects all user-entered data into a big data variable, and sends it as JSON to API controller, using jeneva service. If data is saved successfully, the $location.path()
method is called and user gets navigated to the List of Clients view again. If backend fails or if backend validation fails, the jeneva
service will handle this issue and all validation issues will be displayed in the view in correct places, depending on the jeneva directives (later in this article, you will see how Jeneva handles these failures).
The clientEditController.js works the same way, you can see it in the source code.
HTML Views
As you already know, there are only three views - List of Clients, Create Client, and Edit Client. If you are not familiar with AngularJS, then please refer to this youtube channel. Let's take a look at the List view.
<a href="#/client/create">NEW CLIENT</a>
<h2>Clients</h2>
<span class="error" jv-error-key></span>
<hr />
<table border="1" style="width: 50%;">
<thead>
<tr class="head">
<th>ID</th>
<th>NAME</th>
<th>LASTNAME</th>
<th>AGE</th>
<th>LOGINS</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in clientRows">
<td ng-bind="row.id"></td>
<td ng-bind="row.name"></td>
<td ng-bind="row.lastname"></td>
<td ng-bind="row.age"></td>
<td>
<div ng-repeat="login in row.logins">
<span ng-bind="login"></span><br />
</div>
</td>
<td>
<a ng-href="#/client/edit/{{row.id}}">Edit</a>
</td>
</tr>
</tbody>
</table>
For those who are familiar with AngularJS, it will be pretty simple to understand this code. This view is tied together with clientListController.js. Basically, this code refers to the $scope.clientRows
field from the controller's code, and displays list of clients as HTML table.
The most interesting moment here is links. Take a look at the links:
<a href="#/client/create">NEW CLIENT</a>
As you see, the URL starts with a # sign, which actually means that if you click the link, the browser would not reload the page. Instead, AngularJS will capture this event, and will switch the active view and active controller, based on the routes registered in the app.js file. For example, if you click the NEW CLIENT link, AngularJS will replace contents of the current view with contents of the Create Client view, and active controller will become - clientCreateController.js.
The other interesting moment is the top of the view:
<span class="error" jv-error-key></span>
This span is controlled by the jv-error-key
directive from Jeneva. Which actually means that when failure response is arrived from backend server to browser, the error message that has empty key will be displayed in the body of the span
. Sometimes, you have to use key-less (or path-less) error messages, for example, "Unexpected failure
" message or some more specific message - "You cannot delete this client
" (or you can give some error messages static key). The best thing about key-less (or static key) failure messages is that they don't require any JSON data to be posted to server. In addition to key-less messages, you are always free to define your custom keys, for example, you can define "server_error
" key (path), then you can register failures with this key to validation context, and then you can use the jv-error-key
directive to display that failure message anywhere on your form. In this example, I use key-less messages for unhandled exceptions and simply display them in the top of the each form. I also use key-less messages to inform that user cannot delete a client due to some reason (see deleting client in the ClientService
).
Now take a look at the Edit Client link:
<a ng-href="#/client/edit/{{row.id}}">Edit</a>
As you see, this link navigates to the Edit Client view, it also contains id
of the client. The editClientController
must know how to extract this id
from the link. Take a look at the app.js file, how the route for the Edit Client view is defined.
.when("/client/edit/:id", {
templateUrl: "client/edit",
controller: "clientEditController"
})
AngularJS routing mechanism is able to handle url parameters. And the clientEditController
can access them using the $routeParams
service. The $routeParams
service must be injected into controller in the same way as other services. Please see the source code to get more details.
Now, let's take a look at the Create Client view.
<a href="#/client/list">ALL CLIENTS</a>|
<h2>New Client</h2>
<span class="error" jv-error-key></span>
<span class="error" jv-error-key="logins"></span>
<hr />
<table ng-form name="form">
<tr>
<td>Name</td>
<td>
<input name="name"
type="text" ng-model="name" jv-path="name" />
<span class="error"
ng-if="form.name.$error.jvpath"
ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
</td>
</tr>
<tr>
<td>Last name</td>
<td>
<input name="lastname"
type="text" ng-model="lastname" jv-path="lastname" />
<span class="error"
ng-if="form.lastname.$error.jvpath"
ng-repeat="msg in form.lastname.$jvlist">{{msg}}</span>
</td>
</tr>
<tr>
<td>Age</td>
<td>
<input name="age"
type="text" ng-model="age" jv-path="age" />
<span class="error"
ng-if="form.age.$error.jvpath"
ng-repeat="msg in form.age.$jvlist">{{msg}}</span>
</td>
</tr>
<tr>
<td>Logins</td>
<td>
<table>
<thead>
<tr>
<th>Login</th>
<th>Password</th>
<th>Enabled</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in loginRows" ng-form name="loginForm">
<td>
<input name="loginName" type="text"
ng-model="row.name"
jv-path="{{'logins['+ $index + '].name'}}" />
<span class="error" ng-if="loginForm.loginName.$error.jvpath"
ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>
</td>
<td>
<input name="password" type="text"
ng-model="row.password" jv-path="{{'logins['+
$index + '].password'}}" />
<span class="error"
ng-if="loginForm.password.$error.jvpath"
ng-repeat="msg in loginForm.password.$jvlist">{{msg}}</span>
</td>
<td>
<input name="enabled" type="checkbox"
ng-model="row.enabled"
jv-path="{{'logins['+ $index + '].enabled'}}" />
<span class="error" ng-if="loginForm.enabled.$error.jvpath"
ng-repeat="msg in loginForm.enabled.$jvlist">{{msg}}</span>
</td>
<td>
<input type="button"
ng-click="onRemoveLoginClick(row)" value="Delete" />
</td>
</tr>
</tbody>
</table>
<div style="padding-bottom: 0.5em;">
<input type="button"
ng-click="onAddLoginClick()" value="Add" />
</div>
</td>
</tr>
</table>
<hr />
<input type="button" ng-click="onSave()" value="Save" />
Every input
element is tied to the corresponding controller's $scope
field using ng-model
Angular directive. Every input
element is followed by error message span
.
<input name="name" type="text"
ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath"
ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
or like this:
<tr ng-repeat="row in loginRows" ng-form name="loginForm">
<td>
<input name="loginName" type="text" ng-model="row.name"
jv-path="{{'logins['+ $index + '].name'}}" />
<span class="error" ng-if="loginForm.loginName.$error.jvpath"
ng-repeat="msg in loginForm.loginName.$jvlist">{{msg}}</span>
As you see, the span
is decorated with ng-if
and ng-repeat
directives. Which basically means, that if validation fails for the name
field of the loginForm
, the span will display error messages. The second case looks strange, but it is pretty simple too. jv-path="{{'logins['+ $index + '].name'}}"
this means if logins[0].name
field is failed - then error message will be placed in the correct place.
View Container
Well, this is basically it. We have views, we have controllers, we have app.js which configures everything. The last step is to create a page, the single page. The page that hosts all views and controllers. This page will be displayed in browser when user navigates to the root of the web applciation. My container page is located here: views/home/Index.cshtml. It looks very simple:
<!DOCTYPE html>
<html ng-app="myclients">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>MyClients Single Page Angular Application</title>
<script type="text/javascript"
src="@Url.Content("/Resources/js/vendor/angular.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/vendor/angular-route.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/vendor/jeneva.angular.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/app.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/filters/idtext.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/filters/datetime.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/controllers/clientCreateController.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/controllers/clientListController.js")"></script>
<script type="text/javascript"
src="@Url.Content("/Resources/js/controllers/clientEditController.js")"></script>
<link href="@Url.Content("~/Resources/css/style.css")" rel="stylesheet"/>
</head>
<body>
<ng-view>
</ng-view>
</body>
</html>
First of all, I have to define application module using the ng-app
directive. My module is called myclients
, this is the module referred in the app.js file. Then, I have to include angular.js, jeneva.angular.js, and all controllers, services, directives and filters.
The body
element contains just one tag - <ng-view>
, as you have already guessed, AngularJS will replace the contents of this tag by currently active view. And that is it.
Conclusion
Using AngularJS, you can create single-page applications without any extra effort. Jeneva makes it even more simple by managing validation routines and back-end business layer.
References
History
- 27th June, 2014: Initial version