Angular.js
Angular Friendly Server Side
Skinny Framework provides Angular.js server side API feature.
We also follow Angular’s pragmatic practices such as ngResource APIs, XSRF and JSON vulnerability protection.
Example
src/main/scala/controller/api/ProgrammersController.scala
This is an Angular flavored JSON API controller.
AngularXHRServerFeature
trait provides you all the basic features which is required to be Angular compatible JSON APIs.
package controller.api
import org.json4s._
import skinny._
import skinny.controller.SkinnyApiResource
import skinny.controller.feature.AngularXHRServerFeature
import skinny.validator._
import model._
class ProgrammersController extends SkinnyApiResource with AngularXHRServerFeature {
// ----
// XSRF protection
// https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection
protectFromForgery()
override def model = Programmer
override def resourcesName = "programmers"
override def resourceName = "programmer"
override def resourcesBasePath = s"/api/${resourcesName}"
override def createParams = Params(params +
("hashedPassword" -> params.getAs[PlainPassword]("plainTextPassword").map(_.hash("dummy salt")))
)
override def createForm = validation(createParams,
paramKey("name") is required & maxLength(64),
paramKey("favoriteNumber") is required & numeric,
paramKey("plainTextPassword") is required & minLength(8)
)
override def createFormStrongParameters = Seq(
"name" -> ParamType.String,
"favoriteNumber" -> ParamType.Long,
"hashedPassword" -> ProgrammerParamType.HashedPassword
)
override def updateParams = Params(params)
override def updateForm = validation(updateParams,
paramKey("id") is required,
paramKey("name") is required & maxLength(64),
paramKey("favoriteNumber") is required & numeric
)
override def updateFormStrongParameters = Seq(
"name" -> ParamType.String,
"favoriteNumber" -> ParamType.Long)
private object ProgrammerParamType {
val HashedPassword = ParamType { case v: HashedPassword => v.value }
}
}
src/main/scala/controller/Controllers.scala
object Controllers {
def mount(ctx: ServletContext): Unit = {
apiProgrammers.mount(ctx)
}
object apiProgrammers extends controller.api.ProgrammersController with Routes {
}
}
src/main/webapp/WEB-INF/views/programmers/index.html.ssp
This is the first content to be accessed.
This Scalate template will returns a simple HTML which includes Angular and application JS files.
For simplicity, this example contains controllers, directives and so on in the index HTML file.
Of course, it’s not a best practice. Separating JS files is preferred.
<div ng-app="myApp">
<hr/>
<div ng-view>Loading....</div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular-resource.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular-route.min.js"></script>
<script>
'use strict';
// -----------
// Angular app for this resource CRUD
var app = angular.module('myApp', ['ngRoute', 'ngResource']);
// routes
app.config(function($routeProvider) {
$routeProvider.when('/', {templateUrl: 'partials/list.html', controller: 'ListController'});
$routeProvider.when('/new', {templateUrl: 'partials/new.html', controller: 'NewController'});
$routeProvider.when('/:id', {templateUrl: 'partials/show.html', controller: 'ShowController'});
$routeProvider.when('/:id/edit', {templateUrl: 'partials/edit.html', controller: 'EditController'});
$routeProvider.otherwise({redirectTo: '/'});
});
// form params instead of JSON request body
// Skinny's Angular support basically works with both of them
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.transformRequest = function(data) {
if (data === undefined) { return data; }
var params = '', delim = '';
for (var key in data) {
if (key.substr(0, 1) === '$') { continue; }
if (data.hasOwnProperty(key)) {
var val = data[key];
val = (val === null || val === undefined) ? '' : val;
params += delim + encodeURIComponent(key) + '=' + encodeURIComponent(val);
delim = '&';
}
}
return params;
};
$httpProvider.defaults.headers.post = {'Content-Type': 'application/x-www-form-urlencoded'};
}]);
// -----------
// resource
app.factory('Resource', ['$resource', function($resource) {
return $resource('/api/programmers/:id.json', {}, {});
}]);
// -----------
// controllers
app.controller('ListController', function($scope, Resource) {
$scope.load = function() {
var items = Resource.query(function () {
$scope.items = items;
});
};
$scope.$on('refreshList', $scope.load);
});
app.controller('NewController', function($scope) {
});
app.controller('ShowController', function($scope, $routeParams, Resource) {
$scope.load = function() {
var item = Resource.get({id: $routeParams.id}, function() {
$scope.item = item;
});
};
});
app.controller('EditController', function($scope, $routeParams, Resource) {
$scope.load = function() {
var item = Resource.get({id: $routeParams.id}, function() {
$scope.item = item;
});
};
});
// -----------
// directives
app.directive('saveItem', function(Resource) {
return {
restrict: 'A',
controller: function($scope, $location) {
$scope.submit = function(item) {
Resource.save(item,
function() { // success
$location.path('/');
},
function(response) { // failure
if (response.data) {
// validation errors
$scope.formErrors = {};
for (var name in response.data) {
$scope.formErrors[name] = response.data[name];
}
} else {
window.alert('Failed to access server - ' + JSON.stringify(response));
}
}
);
};
$scope.cancel = function() {
$location.path('/');
}
}
}
});
app.directive('deleteItem', function(Resource) {
return {
restrict: 'A',
controller: function($scope) {
$scope.submit = function(id) {
if (window.confirm('Are you sure?')) {
Resource.delete({id: id},
function (item, headers) {
// success
$scope.$emit('refreshList');
},
function(response) {
// failure
}
);
}
};
}
}
});
</script>
src/main/webapp/programmers/partials/list.html
<div ng-app="myApp" ng-controller="ListController" ng-init="load()">
<h3>List</h3>
<hr/>
<table class="table table-bordered">
<tr>
<th>Name</th>
<th>FavoriteNumber</th>
<th>HashedPassword</th>
<th></th>
</tr>
<tr ng-repeat="item in items">
<td>{{item.name}}</td>
<td>{{item.favoriteNumber}}</td>
<td>{{item.hashedPassword.value}}</td>
<td>
<a href="#/{{item.id}}" class="btn btn-default">Detail</a>
<a href="#/{{item.id}}/edit" class="btn btn-info">Edit</a>
<button delete-item ng-click="submit(item.id)" class="btn btn-danger">Delete</button>
</td>
</tr>
</table>
<p><a href="#/new" class="btn btn-primary">New</a></p>
</div>
src/main/webapp/programmers/partials/_form.html
<form>
<div class="form-group">
<label class="control-label">
Name
</label>
<div class="controls row">
<div class="col-xs-12">
<div ng-class="formErrors['name'].length > 0 ? 'has-error' : ''">
<input type="text" name="name" class="form-control" ng-model="item.name" />
<span ng-show="formErrors['name']">
<p class="help-block" ng-repeat="error in formErrors['name']">{{error}}</p>
</span>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label">
Plain Text Password
</label>
<div class="controls row">
<div class="col-xs-12">
<div ng-class="formErrors['plainTextPassword'].length > 0 ? 'has-error' : ''">
<input type="text" name="plainTextPassword" class="form-control" ng-model="item.plainTextPassword" />
<span ng-show="formErrors['plainTextPassword']">
<p class="help-block" ng-repeat="error in formErrors['plainTextPassword']">{{error}}</p>
</span>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label">
Favorite Number
</label>
<div class="controls row">
<div class="col-xs-12">
<div ng-class="formErrors['favoriteNumber'].length > 0 ? 'has-error' : ''">
<input type="text" name="favoriteNumber" class="form-control" ng-model="item.favoriteNumber" />
<span ng-show="formErrors['favoriteNumber']">
<p class="help-block" ng-repeat="error in formErrors['favoriteNumber']">{{error}}</p>
</span>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary" save-item ng-click="submit(item)">Submit</button>
<button class="btn btn-default" save-item ng-click="cancel()">Cancel</button>
</div>
</form>
src/main/webapp/programmers/partials/new.html
<div ng-app="myApp" ng-controller="NewController" ng-init="load()">
<h3>New</h3>
<hr/>
<div ng-include="'partials/_form.html'"></div>
<hr/>
</div>
src/main/webapp/programmers/partials/edit.html
<div ng-app="myApp" ng-controller="EditController" ng-init="load()">
<h3>Edit</h3>
<hr/>
<div ng-include="'partials/_form.html'"></div>
<hr/>
</div>
src/main/webapp/programmers/partials/show.html
<div ng-app="myApp" ng-controller="ShowController" ng-init="load()">
<h3>Detail</h3>
<hr/>
<table class="table table-bordered">
<thead>
<tr>
<th>Id</th>
<td>{{item.id}}</td>
</tr>
<tr>
<th>Name</th>
<td>{{item.name}}</td>
</tr>
<tr>
<th>Favorite Number</th>
<td>{{item.favoriteNumber}}</td>
</tr>
</tbody>
</table>
<hr/>
<div class="form-actions">
<a href="#/{{item.id}}/edit" class="btn btn-info">Edit</a>
<button delete-item ng-click="submit(item.id)" class="btn btn-danger">Delete</button>
<a href="#/" class="btn btn-default">Back to list</a>
</div>
</div>
If you find a typo or mistake in this page, please report or fix it. How?