Angular2 & Scala.js
Gregg Hernandez <gregg@lucidchart.com>
golucid.co
Why not Scala.js?
TypeScript is pretty good
- Strict JS superset
- Share client code w/Node
- Object-oriented
- Seamless JS interop
- Static types
Compile times
Angulate2
- Early stages
- Missing features
- Unmerged fixes
RxJS
@js.native
trait Observable[T] extends js.Object {
def map[U](selector: js.Function1[T,U]): Observable[U] = js.native
def map[U](selector: Selector[T,U]): Observable[U] = js.native
// TODO: what is the return type of subscribe?
def subscribe[U<:T](observer: Observer[U]): js.Dynamic = js.native
def subscribe[U<:T](onNext: js.Function1[U,Any],
onError: js.Function1[js.Dynamic,Any] = null,
onCompleted: js.Function0[Any] = null) = js.native
}
Why Scala.js?
scala-js.org
The worst mistake of computer science
goo.gl/WfRLFC
null
and undefined
Multiple Paradigm
- Object-oriented
- Functional
Share code with the JVM
// /app/shared/src/main/scala/Data.scala
// Compiled to Bytecode & JavaScript
case class Data(name: String, count: Int)
object Data {
def serialize(d: Data): String = ...
def unserialize(s: String): Data = ...
}
// /app/server/src/main/scala/Server.scala
// Compiled to Bytecode
object Server {
def increment(request: Request) = {
val data = Data.unserialize(request.body)
Ok(data.copy(count = data.count + 1))
}
}
// /app/client/src/main/scala/Client.scala
// Compiled to JavaScript
object Client extends JSApp {
def main(): Unit = {
val data = Data("foo", 10)
val result = http.post("/increment", Data.serialize(data))
println(result) // Data(foo,11)
}
}
Share code with the JVM
// /app/shared/src/main/scala/Data.scala
// Compiled to Bytecode & JavaScript
case class Data(name: String, count: Int)
object Data {
def serialize(d: Data): String = ...
def unserialize(s: String): Data = ...
}
Share code with the JVM
// /app/server/src/main/scala/Server.scala
// Compiled to Bytecode
object Server {
def increment(request: Request) = {
val data = Data.unserialize(request.body)
Ok(data.copy(count = data.count + 1))
}
}
Share code with the JVM
// /app/client/src/main/scala/Client.scala
// Compiled to JavaScript
object Client extends JSApp {
def main(): Unit = {
val data = Data("foo", 10)
val result = http.post(
"/increment",
Data.serialize(data))
println(result) // Data(foo,11)
}
}
Meta-programming
@link class UserLink
case class UserLink(uri: URI)
object UserLink extends
LinkCompanion[UserLink]
The code you write:
Compiler output:
case class Foo(
bar: String,
baz: Int
)
val format = Json.format[Foo]
case class Foo(
bar: String,
baz: Int
)
val format = new Format[Foo] {
def reads(js: JsValue): JsResult[Foo] = {
for {
bar <- (js \ "bar").validate[String]
baz <- (js \ "baz").validate[Int]
} yield Foo(bar, baz)
}
def writes(foo: Foo): JsValue = {
Json.obj(
"bar" -> foo.bar,
"baz" -> foo.baz
)
}
}
Lots of other things
- Type level programming
- Tail call optimization
- Ad-hoc polymorphism
- Immutability
- Robust collection library
- Great IDE support
- Extensible compiler
- Concurrency
- Thriving community (gitter.im/scala-js/scala-js)
Some Stats
Size of output (scalajs 0.6.8)
- Dev (fastOptJS): 1.1M
- Prod (fullOptJS): 296K
Performance
Tour of Heroes
https://angular.io/docs/ts/latest/tutorial/
Dom
import org.scalajs.dom
@Component(...)
class HeroDetailComponent(...) extends OnInit with OnDestroy {
...
def goBack(): Unit = {
dom.window.history.back()
}
...
}
AppComponent
@Component({
selector: 'my-app',
styleUrls: ['app/app.component.css'],
template: 'app/app.component.html',
providers: [HeroService]
})
export class AppComponent {
title = 'Tour of Heroes';
}
@Component(
selector = "my-app",
styleUrls = js.Array("app/app.component.css"),
templateUrl = "app/app.component.html",
providers = @@[HeroService]
)
class AppComponent {
val title = "Tour of Heroes"
}
AppComponent
@Component(
selector = "my-app",
styleUrls = js.Array("app/app.component.css"),
templateUrl = "app/app.component.html",
directives = @@[ROUTER_DIRECTIVES],
providers = @@[HeroService]
)
class AppComponent {
val title = "Tour of Heroes"
}
AppComponent
@Component(
selector = "my-app",
styleUrls = js.Array("app/app.component.css"),
templateUrl = "app/app.component.html",
directives = @@[ROUTER_DIRECTIVES],
providers = @@[HeroService]
)
class AppComponent {
val title = "Tour of Heroes"
}
DashboardComponent
@Component({
selector: 'my-dashboard',
templateUrl: 'app/dashboard.component.html',
styleUrls: ['app/dashboard.component.css']
})
export class DashboardComponent
implements OnInit {
heroes: Hero[] = [];
constructor(
private router: Router,
private heroService: HeroService) {
}
ngOnInit() {
this.heroService.getHeroes()
.then(heroes =>
this.heroes = heroes.slice(1, 5));
}
gotoDetail(hero: Hero) {
let link = ['/detail', hero.id];
this.router.navigate(link);
}
}
@Component(
selector = "my-dashboard",
templateUrl = "assets/dashboard.component.html",
styleUrls = js.Array("assets/dashboard.component.css")
)
class DashboardComponent(
router: Router,
heroService: HeroService
) extends OnInit {
var heroes = js.Array[Hero]()
def ngOnInit(): Unit = {
heroService.getHeroes().map { hs =>
heroes = hs.drop(1).take(4)
}
}
def gotoDetail(hero: Hero): Unit = {
router.navigate(js.Array[js.Any](
"detail", hero.id
))
}
}
DashboardComponent
@Component(
selector = "my-dashboard",
templateUrl = "assets/dashboard.component.html",
styleUrls = js.Array("assets/dashboard.component.css")
)
class DashboardComponent(
router: Router,
heroService: HeroService
) extends OnInit {
var heroes = js.Array[Hero]()
def ngOnInit(): Unit = {
heroService.getHeroes().map { hs =>
heroes = hs.drop(1).take(4)
}
}
def gotoDetail(hero: Hero): Unit = {
router.navigate(js.Array[js.Any](
"detail", hero.id
))
}
}
Scala Collections
// from previous slide
hs.drop(1).take(4)
// extends to many other collection types
List(1, 2, 3, 4, 5, 6, 7).drop(1).take(4)
// List(2,3,4,5)
Map("one" -> 1, "two" -> 2, "three" -> 3).drop(1).take(1)
// Map("two" -> 2)
Scala Collections
// infinite streams
val primes = {
val s = Stream.from(2)
def sieve(s: Stream[Int]): Stream[Int] =
s.head #:: sieve(s.tail.filter(_ % s.head != 0))
sieve(s)
}
primes.take(5).toList
// List(2,3,5,7,11)
primes.take(10).toList
// List(2,3,5,7,11,13,17,19,23,29)
primes.drop(100).take(5).toList
// List(547, 557, 563, 569, 571)
HeroService
@Injectable()
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
getHero(id: number) {
return this.getHeroes()
.then(heroes =>
heroes.find(hero => hero.id === id));
}
}
@Injectable()
class HeroService() {
def getHeroes(): Future[js.Array[Hero]] = {
Future.successful(Heroes.all)
}
def getHero(id: Int): Future[Option[Hero]] = {
getHeroes().map { heroes =>
heroes.find(_.id == id)
}
}
}
HeroDetailComponent
@Component(...)
export class HeroDetailComponent implements OnInit, OnDestroy {
hero: Hero;
sub: any;
constructor(private heroService: HeroService, private route: ActivatedRoute) {
}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
let id = +params['id'];
this.heroService.getHero(id)
.then(hero => this.hero = hero);
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
goBack() {
window.history.back();
}
}
@Component(...)
class HeroDetailComponent(heroService: HeroService, route: ActivatedRoute) extends OnInit with OnDestroy {
var hero: js.UndefOr[Hero] = js.undefined
var sub: js.UndefOr[js.Dynamic] = js.undefined
def goBack(): Unit = {
dom.window.history.back()
}
def ngOnInit(): Unit = {
sub = route.params.subscribe { params =>
val id = params.selectDynamic("id").asInstanceOf[String].toInt
heroService.getHero(id).map { hero =>
hero.map(this.hero = _)
}
}
}
def ngOnDestroy(): Unit = {
sub.map(_.unsubscribe())
}
}
var hero: js.UndefOr[Hero] = js.undefined
println(hero.name)
// Compilation Failed
// value name is not a member of js.UndefOr[Hero]
// println(hero.name)
// ^
js.UndefOr[Hero]
var hero: js.UndefOr[Hero] = js.undefined
hero.map { hero =>
// this code is never executed
println(hero.name)
}
js.UndefOr[Hero]
var hero: js.UndefOr[Hero] = Hero(11, "Mr. Nice")
hero.map { hero =>
println(hero.name) // prints: "Mr. Nice"
}
js.UndefOr[Hero]
js.Dynamic
@Component(...)
class HeroDetailComponent(heroService: HeroService, route: ActivatedRoute) extends OnInit with OnDestroy {
var hero: js.UndefOr[Hero] = js.undefined
var sub: js.UndefOr[js.Dynamic] = js.undefined
def goBack(): Unit = {
dom.window.history.back()
}
def ngOnInit(): Unit = {
sub = route.params.subscribe { params =>
val id = params.selectDynamic("id").asInstanceOf[String].toInt
heroService.getHero(id).map { hero =>
hero.map(this.hero = _)
}
}
}
def ngOnDestroy(): Unit = {
sub.map(_.unsubscribe())
}
}
js.Dynamic
@Component(...)
class HeroDetailComponent(heroService: HeroService, route: ActivatedRoute) extends OnInit with OnDestroy {
var hero: js.UndefOr[Hero] = js.undefined
var sub: js.UndefOr[js.Dynamic] = js.undefined
def ngOnDestroy(): Unit = {
sub.map(dynSub => dynSub.unsubscribe())
}
def goBack(): Unit = {
dom.window.history.back()
}
def ngOnInit(): Unit = {
sub = route.params.subscribe { params =>
val id = params.selectDynamic("id").asInstanceOf[String].toInt
heroService.getHero(id).map { hero =>
hero.map(this.hero = _)
}
}
}
}
js.Dynamic
@Component(...)
class HeroDetailComponent(heroService: HeroService, route: ActivatedRoute) extends OnInit with OnDestroy {
var hero: js.UndefOr[Hero] = js.undefined
var sub: js.UndefOr[js.Dynamic] = js.undefined
def goBack(): Unit = {
dom.window.history.back()
}
def ngOnInit(): Unit = {
sub = route.params.subscribe { params =>
val id = params.selectDynamic("id").asInstanceOf[String].toInt
heroService.getHero(id).map { hero =>
hero.map(this.hero = _)
}
}
}
def ngOnDestroy(): Unit = {
sub.map(dynSub => dynSub.unsubscribe())
}
}
js.Dynamic
val id = params.selectDynamic("id").asInstanceOf[String].toInt
Routes
const routes: RouterConfig = [
{path: '', redirectTo: '/dashboard', pathMatch: 'full'},
{path: 'dashboard', component: DashboardComponent},
{path: 'detail/:id', component: HeroDetailComponent},
{path: 'heroes', component: HeroesComponent}
];
export const appRouterProviders = RouterModule.forRoot(routes);
object Routes {
val routes = RouterConfig(
RDef(path = "", redirectTo = "dashboard", pathMatch = "full"),
RDef(path = "heroes", component = @#[HeroesComponent]),
RDef(path = "dashboard", component = @#[DashboardComponent]),
RDef(path = "detail/:id", component = @#[HeroDetailComponent])
)
@JSExport
val routing = RouterModule.forRoot(routes)
}
Routes
object Routes {
val routes: RouterConfig = RouterConfig(
RDef(path = "", redirectTo = "dashboard", pathMatch = "full"),
RDef(path = "heroes", component = @#[HeroesComponent]),
RDef(path = "dashboard", component = @#[DashboardComponent]),
RDef(path = "detail/:id", component = @#[HeroDetailComponent])
)
@JSExport
val routing = RouterModule.forRoot(routes);
}
Moving beyond the TypeScript API
Validating inputs
@Component(
inputs = js.Array("the-hero")
)
class HeroComponent
"the-hero" is an invalid JS identifier
Generate ngOnInit
var heroes: js.Array[Hero] = js.Array()
def ngOnInit(): Unit = {
heroService.getHeroes().map { hs =>
heroes = hs.drop(1).take(4)
}
}
Generate ngOnInit
var heroes: js.Array[Hero] = ngOnInit {
heroService.getHeroes().map { hs =>
heroes = hs.drop(1).take(4)
}
}
// nothing here now, at compile time
// the above code is rewritten into
// the common form
def ngOnInit(): Unit = {
}
Thanks
github.com/gregghz/angulate2-play-example
github.com/jokade/angulate2
scala-js.org
Other sessions you should check out:
Primer to WebGL - Paul Draper - 4:10pm - Auditorium
Where's my JavaScript? A Close Look at Loading Up Your Application - Matt Swensen - 4:40pm - Room B
Angular2 + Scala.js
By Gregg H
Angular2 + Scala.js
- 5,600