WARNING: This page is a guide for 1.x series. See here instead to learn about the latest
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?