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,576