Inviting ScalaJS to the Party

For now Slimetrail is a plain old JVM-only project. We need to make it able to produce both JVM and JavaScript outputs: it will become a cross-project.

Cross-projectization

  • Recommended time to spend on this part: 5 minutes
  • Recommended finishing before: 10:15am

Add the following lines to project/plugins.sbt .

// ScalaJS
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.25")

A large amount of Scala libraries are available both as JVM and ScalaJS artifacts. So will we! The Text UI needs the toolbox and slimetrail to be compiled for the JVM while the Web UI needs them to be compiled in JavaScript. Cross-projects can define libraryDependencies using %%% instead of %% to ensure the correct artifacts are fetched.

In build.sbt, replace any %% in commonSettings by %%% .

We now need to adapt the toolbox and slimetrail project definitions to define these as cross-projects. To do so:

  • At the beginning of build.sbt add these lines:

    // shadow sbt-scalajs' crossProject and CrossType until Scala.js 1.0.0 is released
    import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType}
    
  • In the toolbox, project definition, replace project by:

    crossProject(JSPlatform, JVMPlatform)
      .crossType(CrossType.Pure)
    
  • After the toolbox, project definition, add:

    lazy val toolboxJS = toolbox.js
    lazy val toolboxJVM = toolbox.jvm
    
  • In the slimetrail, project definition, replace project by:

    crossProject(JSPlatform, JVMPlatform)
      .crossType(CrossType.Pure)
    
  • After the slimetrail, project definition, add:

    lazy val slimetrailJS = slimetrail.js
    lazy val slimetrailJVM = slimetrail.jvm
    
  • In the text, project definition, replace slimetrail by slimetrailJVM .

toolbox and slimetrail projects now exists in two flavors each:

  • toolboxJVM and slimetrailJVM are JVM projects.
  • toolboxJS and slimetrailJS are ScalaJS projects.

Run sbt text/run to ensure everything is still working fine.

Setting up the Web UI

  • Recommended time to spend on this part: 5 minutes
  • Recommended finishing before: 10:20am

Add the new project definition for the Web UI:

// Web Interface
lazy val web =
  project
    .in(file("web"))
    .enablePlugins(ScalaJSPlugin)
    .settings(commonSettings: _*)
    .settings(
      name := "slimetrail-web",
      libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "0.9.6",
      scalaJSUseMainModuleInitializer := true
    )
    .dependsOn(slimetrailJS)

Create the file web/src/main/scala/Main.scala whose content is:

package slimetrail.web

import org.scalajs.dom._
import scala.scalajs.js
import slimetrail._

/** Tree strucure to represent the DOM */
sealed trait Html[+A] {
  final def render: Node =
    ??? // Replace by actual code

  final def map[B](f: A => B): Html[B] =
    ??? // Replace by actual code
}
final case class ATextNode(value: String) extends Html[Nothing]
final case class AnElement[+A](
    namespace: String,
    tag: String,
    attributes: Map[(Option[String], String), String],
    eventListeners: List[(String, js.Function1[_ <: Event, A])],
    children: List[Html[A]]
  ) extends Html[A]

/** Generic Web Application */
trait WebApplication extends Application {

  def view(model: Model): Html[Msg]

  final def run(initialNode: Node): Unit =
    ??? // Replace by actual code
}

/** The Slimetrail Web Application */
final class SlimetrailWebApp(size: Int)
    extends SlimetrailApp(size)
    with WebApplication {

  def view(m: GameState): Html[Msg] =
    ??? // Replace by actual code
}

object Main {
  def onLoading(a: => Unit): Unit =
    document.addEventListener("DOMContentLoaded", (_: Event) => a)

  def main(args: Array[String]): Unit =
    onLoading {
      new SlimetrailWebApp(10)
        .run(document.getElementById("scalajs-controlled-node"))
    }
}

The WebApplication trait is the exact Web counterpart of the TextApplication trait. The view method of WebApplication, just like in TextApplication, takes the current state of the application (GameState for Slimetrail) as input and produce events (Action for Slimetrail) that will be sent to update in order to compute the new state of the application and so on.

The two differences with TextApplication are:

  • the Web UI needs to render some HTML/SVG/CSS in a browser page via the DOM instead of lines of text in a terminal console.
  • the Web UI needs to produce user Action from DOM Events instead of reading lines from the keyboard.

To produce the JavaScript you have two options:

  • sbt web/fastOptJS will compile the web project into a JavaScript file at web/target/scala-2.12/slimetrail-web-fastopt.js fast but without much optimizations.
  • sbt web/fullOptJS will compile the web project into an optimized JavaScript file at web/target/scala-2.12/slimetrail-web-opt.js.

Run bin/genHtml.sh to produce both fast.html and full.html which run the application using the corresponding JavaScript file.

Implementing the Web UI

  • Recommended time to spend on this part: 1 hour and 40 minutes
  • Recommended finishing before: 12:00am

Using the ScalaJS library scaladoc and ScalaJS DOM scaladoc you can now finish the Web UI by implementing the following functions:

  • (Optional) Implements Html.map .

    It applies the function f on any value of type A.

  • Implements Html.render .

    It creates a new Node corresponding to this HTML/SVG tree using the DOM.

  • Implements WebApplication.run .

    It runs a Web Application by replacing the initialNode by the rendering of the current state of the application.

  • Implements SlimetrailWebApp.view .

    It renders the current state of the Slimetrail application as an Html[Action] tree.