Programmation appliquée en Scala

Copyright © Cay S. Horstmann 2015 Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License

Polyglot Programming

Scala for the JavaScript VM

Scala.js

Canvas

Lab

Scary looking lab

Part 1: Canvas

This follows the excellent tutorial by Li Haoyi

  1. Run git clone https://github.com/lihaoyi/workbench-example-app, or if for whatever reason, you don't have git on your computer, download and unzip this zip file.
  2. In a command shell, type

    cd workbench-example-app
    ../activator-1.3.2/activator ~fastOptJS
    (adjusting the directory paths and version number if necessary)
    Note: Here, we use the activator where Haoyi's instructions use sbt. The activator is a superset of the sbt tool.

  3. When you see
     1. Waiting for source changes... (press enter to interrupt)
    point your browser to http://localhost:12345/target/scala-2.11/classes/index-dev.html. What happens?
  4. Now let's look at the code. First hit Enter in the console window to terminate the activator. Edit the file workbench-example-app/project/build.sbt and add a line
    addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "3.0.0")
    to the end. Run
    ../activator-1.3.2/activator eclipse
    
    Then run the Scala IDE and import the project at workbench-example-app.
  5. Run

    ../activator-1.3.2/activator ~fastOptJS
    again. In the IDE, find the file src/main/scala/example/ScalaJSExample. You'll find the source code for the fractal, which isn't very illuminating. Let's do something simpler instead. Change the run method to
    def run = {
          count += 1
          ctx.fillStyle = "green"
          ctx.fillRect(0, 0, count, 100)      
        }
    Refresh your browser window. What happens? Why?

  6. That's nice, but it doesn't really show off the power of Scala. Like, it doesn't use for/yield anywhere. Let's fix that. We want to draw a bar chart, like this:

    So, let's say we have our data in a list
    val data = List(2, 3, 5, 7, 11, 13)
    For now, comment out the line
    dom.setInterval(() => run, 50)
    Add this class
    case class Rect(x: Double, y: Double, width: Double, height: Double) {
      def fill(ctx: CanvasRenderingContext2D) { ctx.fillRect(x, y, width, height)}
    }
    
    and transform data to a list of Rect, using for/yield. Put the code outside the run method. What is your code?
  7. Then add
    rects foreach { _.fill(ctx) } 
    and refresh the browser. Did it work?
  8. If you are like me, it didn't. Add a call
    println(rects)
    . Where do you think the printout shows up? (Hint: It's JavaScript—look into the console of your browser dev tools...)
  9. Using that, you should be able to fix the rectangles and enjoy a bar chart.

DOM and JQuery

  1. Drawing with Canvas is fun, but the money is with web apps.
  2. More and more, web app clients are written in JavaScript
  3. ... which we will generate with Scala
  4. Can dynamically change web page with DOM interface:
    def main(target: html.Div) = {
      var p = document.createElement("p")
      val t = document.createTextNode("Hello, World!")
      p.appendChild(t)
      target.appendChild(n)
    }
    
  5. Ugh—that's why so many people use JQuery.
  6. With the ScalaJS bindings, use jQuery instead of $:
    target.append(jQuery("<p/>").text("Hello, World"))
    
  7. Not bad, but not as typesafe as one would like. Potential alternative: ScalaTags

AJAX/JSON

  1. We want to make calls from the JavaScript client to the server
  2. Use AJAX. Simplest form:
    Ajax.get(url).onSuccess { case xhr =>
      val str = xhr.responseText
      ...
    }
  3. Want support for POST/PUT/DELETE
  4. Want to send/receive JSON data
  5. JSON.parse turns response string into an object of type scalajs.js.Dynamic
  6. Can dynamically invoke properties, e.g.
    val response = JSON.parse(xhr.responseText) // has type Dynamic
    val tasks = response.tasks // Works if JSON has field with name "tasks"
  7. May need to cast for further processing
    val tasksAsArray = tasks.asInstanceOf[scalajs.js.Array[String]]
      // Works if it was a JavaScript array of strings
    

What's Missing?

Lab

Scary looking lab

Part 2: DOM

  1. Change the ScalaJSExample.scala file so that it contains
    package example
    import org.scalajs.dom
    import dom.html
    import scalajs.js.annotation.JSExport
    @JSExport
    object ScalaJSExample   {
      @JSExport
      def main(target: html.Div) ={
        target.innerHTML = s"""
        <div>
          <h1>Hello World!</h1>
        </div>
        """
      }
    }
    In resources/index-dev.html, change the div containing the canvas into
    <div id='div'></div>
    
    and in the call to getElementById, change canvas to div. Reload the page. What happens?
  2. For generating dynamic HTML, we can use the wrapper over the DOM library. Try this in the body of main:
        import dom.document
        val items = List("Hello", "World")
        var n1 = document.createElement("ul")
        for (i <- items) {
          val t = document.createTextNode(i)
          val n = document.createElement("li")
          n.appendChild(t)
          n1.appendChild(n)
        }
        target.appendChild(n1)
    
    What happens? Why?

Part 3: JQuery

  1. Oh yes—that's why we detest the DOM library. So let's use JQuery. In resources/index-dev.html, add
    <script type="text/javascript" src="http://cdn.jsdelivr.net/jquery/2.1.3/jquery.js"></script>
    inside the head element. In workbench-example-app/build.sbt (not the project/build.sbt file!), add
    , "be.doeraene" %%% "scalajs-jquery" % "0.8.0"
    to the Seq of library dependencies. Then
  2. Now replace the body of main with
        val items = List("Hello", "World")
        var n1 = jQuery("<ol/>").appendTo(jQuery("#div"))
        for (i <- items) 
          n1.append(jQuery("<li/>").text(i))
    
    Reload. What happens?

Part 4: Event Handling

  1. Change the contents of the div in index-dev.html to
    <h1>Capital Box!</h1>
    <p id='cap'>Type below and have it capitalized!</p>
    <input id='box' type="text" value='Type here'>
    
    Change the contents of main to
        val box = jQuery("#box")
        box.keyup((ev: JQueryEventObject) => {
          val t = box.value()
          jQuery("#cap").text(t.toString().toUpperCase()) 
        })
  2. Reload the page and type something into the input field. What happens? Why?
  3. What is the type of box? What is the type of t?
  4. Why is the toString necessary?
  5. Why can't the JQueryEventObject parameter type be inferred?

Part 5: Connecting with a Web App

  1. Start up the task list app from last week, by running the activator in the unit15 directory (or whereever you had it), and pointing a browser to http://localhost:9000. Add a few tasks.
  2. Here, we are using server-generated HTML forms. That's sooo last millennium. These days, we just want to make REST calls from the JavaScript client. Add a class
    package controllers
    
    import play.api._
    import play.api.mvc._
    import play.api.libs.json._
    import models.Task
    
    object Api extends Controller {
      def tasks = Action {
        Ok(Json.toJson(Json.obj("tasks" -> Task.all().map(t => Json.obj("id" -> t.id, "label" -> t.label)))))
      }
    }
    and a route
    GET	   /api/tasks				controllers.Api.tasks
    Point a browser window to http://localhost:9000/api/tasks. What do you get? Why?
  3. Now we want to see those tasks in our JavaScript client. In the ScalaJS example app, change the div to
    <div id='div'>
    	<ul id='tasks'>
    	</ul>
    </div>
    and the main method to
        val tasks = jQuery("#tasks")
        val prefix = "http://localhost:9000/api/" 
        val url = prefix + "tasks"
        def updateTasks() {
          tasks.empty()
          Ajax.get(url).onSuccess { case xhr =>
          JSON.parse(xhr.responseText).tasks.asInstanceOf[scalajs.js.Array[scalajs.js.Dynamic]] map { t =>
                val task = jQuery("<li/>")
                task.text(t.label.asInstanceOf[String])
                tasks.append(task)    
              }
            }
        }
        updateTasks()
      
    Add this to the top of the Scala file:
    import dom.ext._
    import scalajs.js.JSON
    import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
  4. Now refresh the browser window that pointed to the client page. What happens? (Look into the console for any error messages.)
  5. That's a problem—for security reasons, cross-site AJAX is disabled by default. It's because we don't yet know how to combine a Play and a ScalaJS project. Here is a remedy:
    Start Chrome/Chromium from the command line with the flag --disable-web-security. Point it to http://localhost:12345/target/scala-2.11/classes/index-dev.html. What happens now?

Part 6: Completing the Web App

  1. Let's add the buttons to delete tasks. Here is the code.
                val button = jQuery("<input type='button' value='Delete' style='margin: 0.2em'/>")
                task.append(button)
                button.click((ev: JQueryEventObject) => {
                  Ajax.post(prefix + "tasks/" + t.id + "/delete", "").onSuccess { case xhr => 
                    println(xhr.responseText)
                  }
                })
    Where do you put it?
  2. What happens when you click a Delete button? (Watch the console!)
  3. That's not surprising. We have to add that to the web app's API. Add a route
    POST   /api/tasks/:id/delete    controllers.Api.deleteTask(id: Long)
    and a method
     def deleteTask(id: Long) = Action {
        Task.delete(id)
        Ok("deleted " + id)
      }
    Now what happens when you delete a task?
  4. Actually, the task is deleted, as you can tell by refreshing the client window. But that should happen in the client app. How do you fix that? (Hint: updateTasks.)
  5. If you ran out of tasks while testing, you had to go back to the unit15 app to add more. That's no good. Let's put it into the JavaScript client. First, add this to the HTML:
    <p>Label <input id='label' type="text"/>
    	<input id='create' type="button" value="Create"/></p>
    And add a button handler:
        jQuery("#create").click((ev: JQueryEventObject) => {
          val label = jQuery("#label").value().toString
                
          Ajax.post(prefix + "tasks/" + label +  "/create", "").onSuccess { case xhr => 
            println(xhr.responseText)
            updateTasks()
          }
          
        })
    
    What will happen when you click the button?
  6. You need to implement the API again. Just follow the previous steps and the implementation of newTask from Unit 15. (It's a bit wrong to put the label into the URL—it really should go with the POST data, but that's not so easy due to the immaturity of the libraries, so we'll skip it.) What did you do to make creating new tasks possible?
  7. Have a final look at the JavaScript client and the client that we did in Unit 15. First, notice that the JavaScript client is more fluid. It doesn't have the “page flip” that you have in web applications. And it is far easier to prettify because you can just write the HTML/JavaScript/JQueryUI code to do so, instead of writing the server-side code that generates the client-side code.
  8. And appreciate that both the server-side and the client-side code are Scala, not two different languages. And not JavaScript.

Homework

This was a cutting-edge lab, and there were two weak points. The transfer of data between the client and server are not really satisfactory. As already mentioned, that's something that can be fixed with some libraries.

But what is less satisfactory is running two projects and hacking the browser to allow connections to two ports. We just didn't have the time in the lab to do this right. Your homework assignment is to fix it.

Follow this or this post and deliver a single project that includes both the Play app and the Scala JS client. Make it so that connecting to localhost:9000 serves up the single page of the app.