AngularJS Binding for Scala.js
Introduction
scalajs-angular aims to help developers build AngularJS based applications in type safe manner with Scala language.
To achieve this goal, it depends on Scala.js to provide bindings to core AngularJS classes and functions, as well as its own APIs to enable Scala developers to access them in more natural manner.
It's still at the very early stage of development, so the most parts of the project are subject to frequent and extensive changes.
And the bindings are by no means comprehensive or exhaustive for now, so please use it at your discretion.
How to Use
SBT Settings
Add the following lines to your sbt
build definition:
libraryDependencies += "com.greencatsoft" %%% "scalajs-angular" % "0.4"
If you want to test the latest snapshot version instead, change the version to
0.5-SNAPSHOT
and add Sonatype snapshot repository to the resolver as follows:
resolvers +=
"Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
Or more simply as,
resolvers += Resolver.sonatypeRepo("snapshots")
Note: This guide is based on the latest 0.5-SNAPSHOT version, which has introduced significant changes in API from the earlier version. You might want to check the history of this document and the example application if you're looking for the guide for the earlier versions or want to upgrade your application from them.
Defining a Module
You can define an AngularJS module in the following manner:
val module = Angular.module("myproject", Seq("ngRoute", "ui.bootstrap"))
And you can either register your Angular components like a controller either as a class, or as a singleton object like below:
// In case of registering a class
module.config[RoutingConfig]
module.factory[UserServiceFactory]
module.controller[UserDetailController]
module.directive[UserInfoDirective]
module.run[AppInitializer]
module.filter[UpperCaseFilter]
// In case of registering a singleton object
module.config(RoutingConfig)
module.factory(UserServiceFactory)
module.controller(UserDetailController)
module.directive(UserInfoDirective)
module.run(AppInitializer)
module.filter(UpperCaseFilter)
Any classes or objects which is to be registered as an Angular component should be inheriting
from Service
trait (or one of its subtypes, like Controller
). And in case with
injectable services, like controllers, factories, directives, or filters, you need to specify
its name using the @injectable
annotation like shown below:
@injectable("todoEscape")
class EscapeDirective extends AttributeDirective {
...
}
In case of a factory, both the factory itself and its product type should be annotated with
@injectable
using the same name:
@injectable("taskService")
class TaskService(http: HttpService) extends Service {
...
}
@injectable("taskService")
class TaskServiceFactory(http: HttpService) extends Factory[TaskService] {
...
}
Note: Unlike the original API, Module
does not supports method chaining, due to a
limitation in the current macro implementation (possibly caused by
this issue).
Managing Dependencies
You can find core AngularJS services like HttpService
or Location
in the
core
package, while those from any third party modules reside in the extensions
package.
And such dependencies can be injected into any object which inherits from the Service
trait, including Controller
, Directive
, Factory
, and more.
From 0.5
version onward, it also supports constructor based dependency injection (which
was first attempted by an alternate implementation of the Angular.js API in Scala.js,
scalajs-angulate) as well as the traditional
property based approach.
In general, the constructor based approach should be preferred, as it adheres to the Scala's principle with using immutable properties. However, if you want to declare your Angular module as a singleton object, you need to use the property based method instead, as a singleton does not have a constructor.
Note that you can mix both types of the injection method in a single component, which might be
useful if you're declaring each dependencies as separate traits, like LocationAware
.
Constructor Based Dependency Injection
You can declare any dependent objects as constructor arguments of the target class you want them to be injected into:
@injectable("todoCtrl")
class TodoCtrl(scope: TodoScope, location: Location, service: TaskService)
extends AbstractController[TodoScope](scope) {
scope.todoItems = service.getItems()
...
}
Using this method, you can inject any Angular components (including your own services) into
any other components, provided that they have the @injectable
annotation.
As the class instantiation is handled by Angular itself, you can only include valid Angular components in your constructor argument list.
Note that the @injectable
annotation need not be declared on the immediate type of an
argument. For example, TodoScope
in the above example inhertis from Scope
trait
which is annotated with @injectable("$scope")
, so you don't have to declare another
annotation on the TodoScope
trait itself.
Property Based Dependency Injection
In case of using a Scala object as Angular component, you need to inject any dependencies as properties (variables) instead of constructor arguments.
To inject a specific dependency, you can declare a variable with the @inject
annotation like the following example:
@injectable("exampleCtrl")
object ExampleController extends Controller[Scope] {
@inject
var location: Location = _
@inject
override var scope: Scope = _
// You can assume all dependencies to be resolved
// after this method is invoked.
override def initialize() {
super.initialize()
val url = location.absUrl + "/example"
}
}
All injected types must have a valid @injectable
annotation in one of the types in
their class hierarchy as mentioned previously.
One of the notable differences from the constructor based method is that, you cannot access injected objects in the constructor block of the object because they are not available at the time of the object creation.
In order to solve this problem, the Service
trait extends from Initializable
which provides a method def initialized(): Unit
which is invoked after all dependencies
are injected to the service (like @PostConstruct
in Java).
Using Controllers and Scopes
Controller is a special type of Angular service, which is used to communicate with a view
template by manipulating a scope object. As such, they are usually tightly coupled with
associated scope objects, so their relationship is reflected in the signature of the trait
which represents Angular controllers, as Controller[A <: Scope]
.
Normally, you would define a scope trait and declare any properties or functions you want to
access from your controller class, then write a matching controller class or an object using
the name of the scope trait as the type parameter of the Controller
trait (or
the AbstractController
class for convenience).
A typical scope would look like an example below:
trait UserScope extends Scope {
var id: String = js.native
var name: String = js.native
var email: String = js.native
var friends: js.Array[String] = js.native
var delete: js.Function = js.native
}
Note that you cannot specify a default value for a property, or write an implementation
of a function of your scope trait, since Scope
inherits from js.Object
and Scala.js
does not support such an use case yet.
So, typically, they are initialized from a constructor block, or inside the initialize
method in case of an object, as shown below:
@injectable("userDetailsCtrl")
class UserDetailsController(scope: UserScope, http: HttpService)
extends AbstractController[UserScope](scope) {
val future: Future[User] = http.get("/users/john")
future onComplete {
case Success(user) => {
scope.id = user.id
scope.name = user.name
scope.email = user.email
scope.friends = user.friends
}
case Failure(t) =>
println("An error has occured: " + t.getMessage)
}
scope.delete = () => userService.delete(scope.id)
}
By default, the controller instance is automatically exported to the associated scope
as controller
variable. So, you can access an arbitrary method after you put
@JSExport
annotation on the method and on the controller class which encloses it:
@JSExport
@injectable("userDetailsCtrl")
class UserDetailsController(scope: UserScope, http: HttpService)
extends AbstractController[UserScope](scope) {
...
@JSExport
def delete(): Unit = userService.delete(scope.id)
}
<div ng-controller="userDetailsCtrl">
...
<button ng-click="controller.delete()">Delete</button>
</div>
The same rule applies to the case when you use the controller-as
syntax, because
you cannot directly refer to the controller instance due to a limitation in the
implementation.
So, if you have declared your controller as TodoCtrl as todo for instance, you can invoke its checkAll() method with todo.controller.checkAll()(instead of todo.checkAll()).
As a final note, Controller
(and Directive
) provides implicit conversion
from Scope
to js.Dynamic
via scope.dynamic
method, so you can use
this feature to attach arbitrary properties or functions to the scope object without
declaring them first:
@injectable("userDetailsCtrl")
class UserDetailsController(scope: Scope, http: HttpService)
extends AbstractController[Scope](scope) {
...
scope.dynamic.delete = () => userService.delete(scope.id)
}
Using Services and Factories
It is recommended to implement service facades (or business delegates) are as plain Scala objects without depending any Angular specific APIs, for the sake of cleaner separation between layers.
But if you need to inject Angular components to your service object, you might want them
to be registered as an Angular service as well, using Module.service
method as
shown below:
@injectable("taskService")
object TaskService extends Service {
var http: HttpService = _
override def initialize() {
...
}
}
...
module.service(TaskService)
Better still, you can rewrite the above example using Factory[A]
instead:
@injectable("taskService")
class TaskService(http: HttpService) {
...
}
@injectable("taskService")
class TaskServiceFactory(http: HttpService) extends Factory[TaskService] {
override def apply() = new TaskService(http)
}
...
module.factory[TaskServiceFactory]
Using Directives
To define a directive, you can declare an object which implements Directive
trait.
You can also mixin such traits as ElementDirective
, AttributeDirective
,
Requires
, and so on to assign more specific behaviors to your directive implementation.
Scope related configuration can also be specified by mixing in one of InheritParentScope
,
UseParentScope
, or IsolatedScope
traits.
IsolatedScope
also provides its own DSL to specify attribute bindings, as specified by
AngularJS API:
@injectable("myCustomer")
class CustomerDirective extends ElementDirective
with TemplatedDirective with IsolatedScope {
override val templateUrl = "my-customer-iso.html"
bindings ++= Seq(
"customerInfo" := "info",
"title" :@ "",
"close" :& "onClose"
)
}
To implement a directive which manipulates DOM elements, you can override the link
method as follows:
@injectable("currentLocation")
class LocationDirective(location: Location) extends AttributeDirective {
override def link(
scope: ScopeType, elems: Seq[Element], attrs: Attributes, controllers: Controller[_]) {
val elem = elems.head.asInstanceOf[HTMLElement]
elem.innerHTML = location.path
}
}
Using Filters
To define a filter, you can declare an object which implements Filter[A]
trait, and
override the filter
method to handle the actual filtering:
@injectable("upper")
class UpperCaseFilter extends Filter[String] {
override def filter(item: String): String = item.toUpperCase
}
As with other types inheriting from the Service
trait, you can inject dependencies
to your filter instance using the @inject
annotation, and it also provides an alternative
filter
method which takes additional arguments:
@injectable("upper")
class UpperCaseFilter(location: Location) extends Filter[String] {
override def filter(item: String, args: Seq[Any]) =
if (location.path.endsWith(args.head.toString))
item.toUpperCase
else
item.toLowerCase
}
Defining Routes
Defining routing rules is quite straight forward, like the following example:
class RoutingConfig(routeProvider: RouteProvider) extends Config {
routeProvider
.when("/", Route("/assets/templates/home.html", "Home"))
.when("/signup", Route("/assets/templates/signup.html", "Sign up", "signupCtrl"))
.when("/users", Route("/assets/templates/users.html", "Users", "usersCtrl"))
}
(Note that you need to 'ngRoute' in your dependency list, and angular-route.js in the html file for the above code to work)
Example Project
There's an example implementation of TodoMvc application as a separate project:
License
This project is provided under the terms of Apache License, Version 2.0.