lift-ng is the most powerful, most secure AngularJS backend available today.
The design philosophy of lift-ng is to capture the spirit of both Lift and AngularJS into one package. The result is a secure-by-default framework facilitating powerful and robust client/server interactions for building today's modern web applications. By utilizing Scala's powerfully-flexible language, lift-ng provides a DSL that is natural to use for AngularJS developers and Scala developers alike.
Lift as a backend should appeal to AngularJS developers for the following reasons:
- Lift's templating is unlike most web application frameworks in that it is plain HTML. There is no massaging of your Angular templates to make the framework happy.
- The approach of manipulating the templates on the server by Lift is similar to how you manipulate them on the client with AngularJS. Hence you can manipulate the DOM at the time you know the information, while on the client or earlier while on the server.
- Security is handled for you, making it virtually impossible to have your http endpoints successfully attacked. (More on Lift's security)
- Your application will be built on the rock-solid JVM as a time-tested Java servlet on the most mature Scala web framework.
AngularJS as a front end should appeal to Scala and Lift developers for the following reasons:
- JQuery is a pain for non-trivial applications.
- AngularJS does a fantastic job of managing complex client-side interactions for you.
- With lift-ng in particular, you get to utilize Lift's reactive features such as asynchronous comet updates to the client and returning
LAFuture[T]
results for services.
If you are not comfortable with either Lift or AngularJS, we recommend that you view original contributor Doug Roper's youtube demo of the original functionality of this plugin. See also the sample project as seen in the youtube video.
The quickest jump start you can get on lift-ng is via the giter8 template. Otherwise, you should first get started with Lift and configure the project to include the lift-ng module as outlined below.
You can configure an existing Lift project to use lift-ng manually.
Add lift-ng
as a dependency in your build.sbt
or Build.scala
as appropriate.
Optionally add lift-ng-js
as a dependency if you would like us to manage the delivery of your AngularJS library javascript files.
libraryDependencies ++= {
val liftVersion = "2.5.1" // Also supported: "2.6" and "3.0"
val liftEdition = liftVersion.substring(0,3)
val ngVersion = "1.2.21" // If using lift-ng-js
Seq(
// Other dependencies ...
"net.liftmodules" %% ("ng_"+liftEdition) % "0.4.6" % "compile",
"net.liftmodules" %% ("ng-js_"+liftEdition) % ("0.1_"+ngVersion) % "compile" // If using lift-ng-js
)
}
And invoke Angular.init()
in your Boot
class (shown here with the default values).
package bootstrap.liftweb
class Boot {
def boot {
// Other stuff...
net.liftmodules.ng.Angular.init(
// Set to true if you plan to use futures. False otherwise to avoid an unneeded comet
futures = true,
// Set to the CSS selector for finding your apps in the page.
appSelector = "[ng-app]",
// Set to true to include a script tag with the src set to the path for liftproxy.js. Set to
// false if you want to handle that yourself by referring to the path in net_liftmodules_ng.
includeJsScript = true
)
}
}
If you want to handle the downloading of javascript assets yourself with a library such as head.js, then you should initialize with includeJsScript = false
.
This will prevent our Angular
snippet from including the liftproxy.js
file.
Instead, you can use the global ng_liftmodules_ng
object to include the file yourself.
You can get the full path to the liftproxy.js
file via net_liftmodules_ng.path
or get just the lift-ng version alone with net_liftmodules_ng.version
.
lift-ng is built and released to support Lift edition 2.5 with Scala versions 2.9.1, 2.9.2, and 2.10; Lift edition 2.6 with Scala versions 2.9.1, 2.9.2, and 2.10; and Lift edition 3.0 with Scala version 2.10. This project's scala version is purposefully set at the lowest common denominator to ensure each version compiles.
Below are usage examples of each of the major features of lift-ng. Be sure to check out the aforementioned sample project or the test project for fully functional examples
Most AngularJS backends provide RESTful http endpoints for the application to receive data from the server. lift-ng is certainly no exception by providing a server-side DSL for creating services. Compare the following AngularJS factory
angular.module('lift.pony', [])
.factory("ponyService", function() {
return {
getBestPony: function(arg) {
// Return the best pony (client-side)
return BestPony;
}
};
});
... to this lift-ng DSL in Scala
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.jsonCall("getBestPony", (arg) => {
// Return the best pony (server-side)
Full(BestPony)
})
)
Both will create an angular module named lift.pony
with a service named ponyService
with a function named getBestPony
, yet the former runs on the client-side, and the latter runs on the server-side.
To flesh out this example completely, you will define a Lift snippet to provide the service to the resulting HTML.
Here, we have elected to create an object named NgPonyService
, delivering the snippet via Lift's default snippet method name, render
object NgPonyService {
def render = renderIfNotAlreadyDefined(
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.jsonCall("getBestPony", (arg) => {
// Return the best pony (server-side)
try {
Full(BestPony)
} catch {
case e:Exception => Failure(e.getMessage)
}
})
)
)
}
This renderIfNotAlreadyDefined
returns a scala.xml.NodeSeq
. Hence you will need to add script tags to your target HTML page(s) for the services as well as some plumbing from this module.
<!-- The angular library (manually) -->
<script type="text/javascript" src="/scripts/angular.js"></script>
<!-- Or better yet, via lift-ng-js. (https://github.com/joescii/lift-ng-js) -->
<script data-lift="AngularJS"></script>
<!-- Prerequisite stuff the module needs -->
<script data-lift="Angular"></script>
<!-- The NgPonyService snippet defined above -->
<script data-lift="NgPonyService"></script>
<!-- Your angular controllers, etc -->
<script type="text/javascript" src="/scripts/pony.js"></script>
The resulting angular service returns a $q
promise (see AngularJS: ng.$q). When you call the service, you register callbacks for success, error, and notify (not currently utilized).
angular.module('pony', ['lift.pony'])
.controller('PonyCtrl', function ($scope, ponyService) {
$scope.onClick = function () {
ponyService.getBestPony().then(function(pony) {
// We have our pony!
$scope.pony = pony;
},
function(err) {
// No pony!
$scope.error = err;
},
function(progress) {
// We're still working on getting that pony...
// Well, not really... we don't use this today...
});
};
});
Just like with Lift's SHtml.ajaxInvoke
, you can make a service which takes no arguments. Hence we could have defined our ponyService.getBestPony
like the following...
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.jsonCall("getBestPony", {
// Return the best pony (server-side)
try {
Full(BestPony)
} catch {
case e:Exception => Failure("No Pony!")
}
})
)
Or we can accept a String
...
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.jsonCall("getPonyByName", (name:String) => {
// Return the matching pony
try {
Full(BestPony)
} catch {
case e:Exception => Failure("No Pony!")
}
})
)
Finally, perhaps most importantly, we expect a case class to be sent to the server. Note that the case class must extend NgModel
for this to work.
case class Pony (name:String, img:URL) extends NgModel
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.jsonCall("setBestPony", (pony:Pony) => {
// Nothing to return
Empty
})
)
All of the above functions can be part of the same service...
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.jsonCall("getBestPony", {
// Return the best pony (server-side)
Full(BestPony)
})
.jsonCall("getPonyByName", (name:String) => {
// Return the matching pony
Full(MyPony)
.jsonCall("setBestPony", (pony:Pony) => {
// Nothing to return
Empty
})
)
All of the examples thus far have assumed the value can be calculated quickly without expensive blocking or asynchronous calls. Since it is quite common to perform expensive operations or call APIs which return a Future[T]
, it is important that lift-ng likewise supports returning an LAFuture
.
The same signatures for jsonCall
are supported for futures:
angular.module("lift.pony")
.factory("ponyService", jsObjFactory()
.future("getBestPony", {
// Create the future
val f = new LAFuture[Box[Pony]]()
// Do something to get the result
futurePonyGetter(f)
// Return the future that will contain a Pony
f
})
.future("getPonyByName", (name:String) => {
// Return a future containing the matching pony
futurePonyGetter(name)
.future("setBestPony", (pony:Pony) => {
// Nothing to return
val f = new LAFuture[Box[Pony]]()
f.satisfy(Empty)
f
})
)
Because the underlying Lift library does not currently support returning futures for AJAX calls (as of 2.5.1), we had to circumvent this limitation by utilizing comet. As a result, if you want to utilize futures in your angular app, we must be able to locate your app in the DOM. By default, we look for any elements containing the ng-app
attribute. This can be overridden in the Angular.init()
call via the appSelector
property. This allows us to hook in to your app via comet and send messages asynchronously back to the lift-proxy service.
Testing services provided by lift-ng with Jasmine (etc) can be accomplished in the same manner as you would test any Angular service.
describe("pony", function(){
// Service mock
var ponyService = {};
// Handle to the rootScope
var rootScope = {};
// Handle to the scope
var scope = {};
// Set up the pony module
beforeEach(function(){module('pony');});
// Mock out the service
beforeEach(function() {
angular.mock.module(function($provide) {
$provide.value('lift.pony', ponyService);
});
});
// Build the mock with a $q promise
beforeEach(inject(function($q) {
ponyService.defer = $q.defer();
ponyService.getBestPony = function() {
return this.defer.promise;
};
}));
// Create a controller for each test
beforeEach(inject(function($rootScope, $controller) {
rootScope = $rootScope;
scope = $rootScope.$new();
$controller('PonyCtrl', {
$scope: scope,
ponyService: ponyService
});
}));
// Write a test
it('should call the service when onClick is called', function() {
// Before onClick, the pony will be undefined.
expect(scope.pony).toBeUndefined();
// Provide a pony to be returned
var pony = {
name: 'Doug',
img: 'doug.jpg'
};
ponyService.defer.resolve(pony);
// Simulate the click
scope.onClick();
// This call lets the $q callback happen
rootScope.$digest();
// Expect that pony has now been set.
expect(scope.pony).toEqual(pony);
});
});
Unlike most AngularJS RESTful http backends, you have no further work to do to secure your application. Rather than a fixed named endpoint, lift-ng dynamically creates an http endpoint for each service per page load. The name given to the end points is a securely-randomized number that is difficult to predict for an attacker attempting to utilize cross-site scripting techniques for instance.
In regards to testing the server-side code, we recommend implementing your service logic in a standalone module which handles all of the complex business logic. Then utilize lift-ng to merely expose those services to your angular app. This way your business logic is decoupled from lift-ng and easily testable.
Sometimes the value you want to provide in a service is known at page load time and should not require a round trip back to the server.
Typical examples of this are configuration settings, session values, etc.
To provide a value at page load time, just use JsonObjFactory
's string
, anyVal
, or json
methods.
angular.module("StaticServices")
.factory("staticService", jsObjFactory()
.string("string", "FromServer1")
.anyVal("integer", 42)
.json("obj", StringInt("FromServer2", 88))
)
The above produces a simple service equivalent to the following JavaScript
angular.module("StaticServices",["zen.lift.proxy"])
.factory("staticService", function(liftProxy) { return {
string: function() {return "FromServer1"},
integer: function() {return "42"},
obj: function() {return {str:"FromServer2",num:88}}
}});
Now we can take a look at how to utilize Lift's comet support to asynchronously send angular updates from the server
First you should write a new class which extends the AngularActor
trait, which is a sub-trait of Lift's CometActor
. Thus you can do anything you can normally with a CometActor
, as well as get access the $scope
where the actor is defined in the DOM and the $rootScope
of the angular application. Currently we support $emit
, $broadcast
, and assignment of arbitrary fields on the given scope object.
class CometExample extends AngularActor {
override def lowPriority = {
case ("emit", msg:String) => rootScope.emit("emit-message", msg)
case ("emit", obj:AnyRef) => rootScope.emit("emit-object", obj)
case ("broadcast", msg:String) => scope.broadcast("emit-message", msg)
case ("broadcast", obj:AnyRef) => scope.broadcast("emit-object", obj)
case ("assign", msg:String) => rootScope.assign("my.str.field", msg)
case ("assign", obj:AnyRef) => scope.assign("my.obj.field", obj)
}
}
Now add the comet actor into your HTML DOM within the scope you wish to belong to within the ng-application
.
<div ng-app="ExampleApp">
<div data-lift="comet?type=CometExample"></div>
<!-- other stuff -->
</div>
Then do whatever you need in your angular application to listen for events, watch for changes, etc.
angular.module('ExampleApp', [])
.controller('ExampleController', ['$rootScope', '$scope', function($rootScope, $scope) {
$rootScope.$on('emit-message', function(e, msg) {
$scope.emitMessage = msg;
});
$rootScope.$on('emit-object', function(e, obj) {
$scope.emitObject = obj;
});
$scope.$on('broadcast-message', function(e, msg) {
$scope.broadcastMessage = msg;
});
$scope.$on('broadcast-object', function(e, obj) {
$scope.broadcastObject = obj;
});
$rootScope.$watch('my.str.field', function(e, msg) {
// ...
});
$scope.$watch('my.obj.field', function(e, obj) {
// ...
});
}]);
Note that messages sent prior to a page bootstrapping the Angular application will be queued up and released in order once the application is ready. The retry interval defaults to 100 milliseconds and can be configured in your Lift application's props files with the net.liftmodules.ng.AngularActor.retryInterval
Int property.
If your app doesn't require sophisticated internationalization capabilities (i.e., Java resource bundles will suffice), then you can inject your resource bundles as a service into your app.
Given a resource bundle named bundleName.properties
:
hello=Howdy!
goodbye=Goodbye, {0}!
Add it as a service available to your Angular app with this HTML:
<script id="my-i18n_js" data-lift="i18n?name=bundleName"></script>
Your bundle is made available via the i18n
module with service/factory name coinciding with the bundle name. In this example, the object will have a string field named hello
and a function named goodbye
:
angular.module('ExampleApp', ['i18n'])
.controller('ExampleController', ['$scope', 'bundleName', function($scope, i18n) {
$scope.hello = i18n.hello;
$scope.goodbye = i18n.goodbye($scope.username);
}]);
You may also specify multiple bundle names. Here we include the default Lift bundle:
<script id="my-i18n_js" data-lift="i18n?names=bundleName,lift"></script>
Each bundle is another service available in the i18n
bundle. Also notice in this example we show keys which aren't valid JavaScript identifiers are also available.
angular.module('ExampleApp', ['i18n'])
.controller('ExampleController', ['$scope', 'bundleName', 'lift', function($scope, bundle, lift) {
$scope.hello = bundle.hello;
$scope.goodbye = bundle.goodbye($scope.username);
$scope.lostPasswd = lift["lost.password"];
}]);
For more details about this resource bundle object, see j2js-i18n.
The latest version of scaladocs are hosted thanks to cloudbees continuous integration services. There should not be any differences among the supported versions of Scala. Nonetheless all are listed here for good measure.
Need help? Hit us up on the Lift Google group. We'd love to help you out and hear about what you're building.
As with any open source project, contributions are greatly appreciated. If you find an issue or have a feature idea, we'd love to know about it! Any of the following will help this effort tremendously.
- Issue a Pull Request with the fix/enhancement and unit tests to validate the changes. OR
- Issue a Pull Request with failing tests in the test-project to show what needs to be changed OR
- At a minimum, open an issue to let us know about what you've discovered.
Below is the recommended procedure for git:
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Try to include as much as you are able, such as tests, documentation, updates to this README, etc.
Part of contributing your changes will involve testing. The test-project sub-directory contains and independent sbt project for thoroughly testing the lift-ng module via selenium. At a minimum, we ask that you run the tests with your changes to ensure nothing gets inadvertently broken. If possible, include tests which validate your fix/enhancement in any Pull Requests.
Here are things we would like in this library. It's not a road map, but should at least give an idea of where we plan to explore soon.
- 2-way client-server model binding (See binding branch for the source. Grab version 0.5.0-SNAPSHOT for a preview)
- Produce an error message when an attempt is made to use a model which does not extend
NgModel
. (Currently this silently fails) - Support handling parameters of type
json.JValue
. - Support returning values of type
JsExp
. - Initial value/first resolve value for services. The reason for providing a first value will allow the page load to deliver the values rather than require an extra round trip.
- Make the DSL prettier
AngularActor.scope.parent
support
- 0.4.6: Minor correction to resolution for Issue #1 to correctly allow messages to begin dequeuing without waiting for a new message.
Added
includeJsScript
parameter toAngular.init()
to give developers the ability to download theliftproxy.js
their own way, such as via head.js. Updated closure compiler. - 0.4.5: Now queues and releases async (
AngularActor
) messages arriving prior to Angular bootstrapping, resolving Issue #1. - 0.4.4: Fixed the last version, which would serve the same i18n locale resource for every requested locale.
- 0.4.3: Enhanced i18n service to be served restfully, allowing the browser to cache the service if it has not changed. Dropped 2.9.1-1 support. Began compiling 2.10 with 2.10.4.
- 0.4.2: Reverted a change made in 0.4.0 which allows more flexibility in the placement of angular services in the DOM, fixing part 2 of Issue #2
- 0.4.1: Now works for web apps running in a non-root context, fixing Issue #2.
- 0.4.0: Now only requires
Angular
snippet in your template(s). - 0.3.1: Added i18n service.
- 0.3.0: Implemented support for a factory/service to return an
LAFuture[Box[T]]
- 0.2.3: Implemented
string
,anyVal
, andjson
onJsonObjFactory
to allow providing values which are known at page load time and do not otherwise change. - 0.2.2: Implemented
AngularActor.assign
for assigning scope variables.Failure(msg)
message is sent to the client for$q.reject
. Changed interpretation ofEmpty
to meanResolve
rather thanReject
. - 0.2.1: Implemented
scope.broadcast
andscope.emit
forAngularActor
- 0.2.0: Introduction of
AngularActor
featuringrootScope.broadcast
androotScope.emit
as the first comet-backed features - 0.1.1: First working release
- 0.1: First release featuring AJAX services invoked from the client
lift-ng is licensed under APL 2.0.
Copyright 2014 net.liftweb
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.