From 058e9bac2dd8166051bafa16d51f27ee5856929a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 30 Oct 2014 22:37:51 -0700 Subject: web pages --- book/canvas-app.tw | 18 ++--- book/getting-started.tw | 2 +- book/index.tw | 2 +- book/src/main/scala/book/Book.scala | 1 + book/web-page.tw | 132 ++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 book/web-page.tw (limited to 'book') diff --git a/book/canvas-app.tw b/book/canvas-app.tw index ba440e4..dc1b113 100644 --- a/book/canvas-app.tw +++ b/book/canvas-app.tw @@ -20,7 +20,7 @@ @p To begin with, lets remove all the existing stuff in our @code{.scala} file and leave only the @hli.scala{object} and the @hli.scala{main} method. Let's start off with some necessary boilerplate: - @hl.ref("examples/src/main/scala/ScratchPad.scala", "/*setup*/", end = "/*code*/") + @hl.ref("examples/src/main/scala/canvasapp/ScratchPad.scala", "/*setup*/", end = "/*code*/") @p As described earlier, this code uses the @hli.javascript{document.getElementById} function to fish out the @code{canvas} element that we interested in from the DOM. It then gets a rendering context from that @code{canvas}, and sets the height and width of the canvas to completely fill its containing element. Lastly, it fills out the canvas light-gray, so that we can see it on the page. @@ -30,7 +30,7 @@ @div(cls:="pure-g") @div(cls:="pure-u-1 pure-u-md-13-24") - @hl.ref("examples/src/main/scala/ScratchPad.scala", "/*code*/") + @hl.ref("examples/src/main/scala/canvasapp/ScratchPad.scala", "/*code*/") @div(cls:="pure-u-1 pure-u-md-11-24") @canvas(id:="canvas2", display:="block") @@ -55,7 +55,7 @@ @p Again, we need roughly the same boilerplate as just now to set up the canvas: - @hl.ref("examples/src/main/scala/Clock.scala", "/*setup*/", "/*code*/") + @hl.ref("examples/src/main/scala/canvasapp/Clock.scala", "/*setup*/", "/*code*/") @p The only thing unusual here is that I'm going to create a @hli.scala{linearGradient} in order to make the stopwatch look pretty. This is by no means necessary, and you could simply make the @hli.scala{fillStyle} @hli.scala{"black"} if you want to keep things simple. @@ -65,7 +65,7 @@ @div(cls:="pure-g") @div(cls:="pure-u-1 pure-u-md-13-24") - @hl.ref("examples/src/main/scala/Clock.scala", "/*code*/") + @hl.ref("examples/src/main/scala/canvasapp/Clock.scala", "/*code*/") @div(cls:="pure-u-1 pure-u-md-11-24") @canvas(id:="canvas3", display:="block") @@ -98,7 +98,7 @@ It's a relatively simple game, but there should be enough "business logic" in here that we won't be simply gluing together APIs. Let's start! @sect{Setting Up the Canvas} - @hl.ref("examples/src/main/scala/FlappyLine.scala", "/*setup*/", end="/*variables*/") + @hl.ref("examples/src/main/scala/canvasapp/FlappyLine.scala", "/*setup*/", end="/*variables*/") @p This section of the code is peripherally necessary, but not core to the implementation or logic of Flappy Box. We see the same @hli.scala{canvas}/@hli.scala{renderer} logic we've seen in all our examples, along with some logic to make the canvas a reasonable size, and some configuration of how we will render text to the canvas. @@ -107,7 +107,7 @@ In general, code like this will usually end up being necessary in a Scala.js program: the Javascript APIs that the browser provides to do things often ends up being somewhat roundabout and verbose. It's somewhat annoying to have to do for a small program such as this one, but in a larger application, the cost is both spread out over thousands of lines of code and also typically hidden away in some helpers, so you don't have to mess with this stuff unless you really want to. @sect{Defining our State} - @hl.ref("examples/src/main/scala/FlappyLine.scala", "/*variables*/", end="def runDead") + @hl.ref("examples/src/main/scala/canvasapp/FlappyLine.scala", "/*variables*/", end="def runDead") @p This is where we start defining things that are relevant to Flappy Box. There are roughly two groups of values here: immutable constants in the top group, and mutable variables in the bottom. The rough meaning of each variable is documented in the comments, and we'll see exactly how we use them later. @@ -116,7 +116,7 @@ One notable thing is that we're using a @hli.scala{collection.mutable.Queue} to store the list of obstacles. This is defined in the Scala standard library; in general, all the collections in the Scala standard library can be used without issue in Scala.js. @sect{Game Logic} - @hl.ref("examples/src/main/scala/FlappyLine.scala", "def runLive", "def runDead") + @hl.ref("examples/src/main/scala/canvasapp/FlappyLine.scala", "def runLive", "def runDead") @p The @hli.scala{runLive} function is the meat of Flappy Box. In it, we @@ -136,13 +136,13 @@ @p This function basically contains all the game logic, from motion, to collision-detection, to rendering, so it's pretty large. Not that large though! And entirely understandable, even if it takes a few moments to read through. - @hl.ref("examples/src/main/scala/FlappyLine.scala", "def runDead", end="def run") + @hl.ref("examples/src/main/scala/canvasapp/FlappyLine.scala", "def runDead", end="def run") @p This is the function that handles what happens when you're dead. Essentially, we reset all the mutable variables to their initial state, and just count down the @hli.scala{dead} counter until it reaches zero and we're considered alive again. @sect{A Working Product} - @hl.ref("examples/src/main/scala/FlappyLine.scala", "def run") + @hl.ref("examples/src/main/scala/canvasapp/FlappyLine.scala", "def run") @p And finally, this is the code that kicks everything off: we define the @hli.scala{run} function to swap between @hli.scala{runLive} and @hli.scala{runDead}, register an @hli.scala{canvas.onclick} handler to make the player jump by tweaking his velocity, and we call @code{setInterval} to run the @hli.scala{run} function every 20 milliseconds. diff --git a/book/getting-started.tw b/book/getting-started.tw index 9b6b9c7..b04d6b4 100644 --- a/book/getting-started.tw +++ b/book/getting-started.tw @@ -184,7 +184,7 @@ This is the HTML page which our toy app lives in, and the same page that we have so far been using to view the app in the browser. To anyone who has used HTML, most of it is probably familiar. Things of note are the Script tags: @hli.scala{"../example-fastopt.js"} Is the executable blob spat out by the compiler, which we need to include in the HTML page for anything to happen. This is where the results of your compiled Scala code appear. @hli.scala{"workbench.js"} is the client for the Workbench plugin that connects to SBT, reloads the browser and forwards logspam to the browser console. @p - The @hli.scala{ScalaJSExample().main()} call is what kicks off the Scala.js application and starts its execution. Scala.js follows Scala semantics in that @hli.scala{object}s are evaluated lazily, with no top-level code allowed. This is in contrast to Javascript, where you can include top-level statements and object-literals in your code which execute immediately. In Scala.js, nothing happens when @code{../example-fastopt.js} is imported! We have to call the main-method first. + The @hli.scala{ScalaJSExample().main()} call is what kicks off the Scala.js application and starts its execution. Scala.js follows Scala semantics in that @hli.scala{object}s are evaluated lazily, with no top-level code allowed. This is in contrast to Javascript, where you can include top-level statements and object-literals in your code which execute immediately. In Scala.js, nothing happens when @code{../example-fastopt.js} is imported! We have to call the main-method first. In this case, we're passing the canvas object (attained using @hli.javascript{getElementById}) to it so it knows where to do its thing. @p Lastly, only @hli.scala("@JSExport")ed objects and methods can be called from Javascript. Also, although this example only exports the @hli.scala{main} method which is called once, there is nothing stopping you from exporting any number of objects and methods and calling them whenever you need to. In this way, you can easily make a Scala.js "library" which is available to external Javascript as an API. diff --git a/book/index.tw b/book/index.tw index 3cf8df3..c25a711 100644 --- a/book/index.tw +++ b/book/index.tw @@ -17,7 +17,7 @@ @Book.intro @Book.gettingStarted @Book.canvasApp - + @Book.webPage diff --git a/book/src/main/scala/book/Book.scala b/book/src/main/scala/book/Book.scala index c41ab15..10116fe 100644 --- a/book/src/main/scala/book/Book.scala +++ b/book/src/main/scala/book/Book.scala @@ -15,6 +15,7 @@ object Book { lazy val intro = sect("Intro to Scala.js")(twf("book/intro.tw")) lazy val gettingStarted = sect("Getting Started")(twf("book/getting-started.tw")) lazy val canvasApp = sect("Canvas App")(twf("book/canvas-app.tw")) + lazy val webPage = sect("Interactive Web Pages")(twf("book/web-page.tw")) val txt = twf("book/index.tw") val contentBar = { def rec(current: Node, depth: Int): Seq[Frag] = { diff --git a/book/web-page.tw b/book/web-page.tw new file mode 100644 index 0000000..6527340 --- /dev/null +++ b/book/web-page.tw @@ -0,0 +1,132 @@ +@p + Most web applications aren't neat little games which live on a single canvas: they are large, structured HTML pages, which involve displaying data (whether from the user or from the web) in multiple ways, while allowing the user to make changes to the data that can be saved back to whatever remote web-service/database it came from. + +@p + At this point, you are already competent at using Scala.js to make basic, self-contained canvas applications. In this chapter, we will cover how to use Scala.js to build the sort of interactive-web-pages that make up the bulk of the modern-day internet. We'll cover how to use powerful libraries that turn front-end development form the typical fragile-mess into a structured, robust piece of software. + +@sect{Hello World: HTML} + + @p + The most basic way of building interactive web pages using Scala.js is to use the Javascript APIs to blat HTML strings directly into some container div, often @code{document.body}. This approach works, as the following code snippet demonstrates: + + @div(cls:="pure-g") + @div(cls:="pure-u-1 pure-u-md-13-24") + @hl.ref("examples/src/main/scala/webpage/HelloWorld0.scala") + + @div(cls:="pure-u-1 pure-u-md-11-24") + @div(id:="div1") + @script("HelloWorld0().main(document.getElementById('div1'))") + + @p + This approach works, as the above example shows, but has a couple of disadvantages: + + @ul + @li + It is untyped: it is easy to accidentally mistype something, and result in malformed HTML. A typo such as @hli.html{} would go un-noticed at build-time. Depending on where the typo happens, it could go un-noticed until the application is deployed, causing subtle bugs that only get resolved much later. + @li + It is insecure: @a("Cross-site Scripting", href:="http://en.wikipedia.org/wiki/Cross-site_scripting") is a real thing, and it is easy to forget to escape the values you are putting into your HTML strings. Above they're constants like @hli.scala{"dog"}, but if they're user-defined, you may not notice there is a problem until something like @hli.scala{""} sneaks through and your users' accounts & data is compromised. + + @p + There are more, but we won't go deep into the intricacies of these problems. Suffice to say it makes mistakes easy to make and hard to catch, and we have something better... + +@sect{Scalatags} + @p + @a("Scalatags", href:="https://github.com/lihaoyi/scalatags") is a cross-platform Scala.js/Scala-JVM library that is designed to generate HTML. To use Scalatags, you need to add it as a dependency to your Scala.js SBT project, in the @code{build.sbt} file: + + @hl.ref("examples/build.sbt", "com.scalatags") + + @p + With that, the above snippet of code re-written using Scalatags looks as follows: + + @div(cls:="pure-g") + @div(cls:="pure-u-1 pure-u-md-13-24") + @hl.ref("examples/src/main/scala/webpage/HelloWorld1.scala") + + @div(cls:="pure-u-1 pure-u-md-11-24") + @div(id:="div2") + @script("HelloWorld1().main(document.getElementById('div2'))") + + @p + Scalatags has some nice advantages over plain HTML: it's type-safe, so typos like @hli.scala{dvi} get caught at compile-time. It's also secure, such that you don't need to worry about script-tags in strings or similar. The @a("Scalatags Readme", href:="https://github.com/lihaoyi/scalatags#scalatags") elaborates on these points and other advantages. As you can see, it takes just 1 import at the top of the file to bring it in scope, and then you can use all of Scalatags' functionality. + + @p + The Scalatags github page has @a("comprehensive documentation", href:="https://github.com/lihaoyi/scalatags#hello-world") on how to express all manner of HTML fragments using Scalatags, so anyone who's familiar with how HTML works can quickly get up to speed. Instead of a detailed listing, we'll walk through some interactive examples to show Scalatags in action! + + @sect{User Input} + @div(cls:="pure-g") + @div(cls:="pure-u-1 pure-u-md-13-24") + @hl.ref("examples/src/main/scala/webpage/Inputs.scala", "val box") + + @div(cls:="pure-u-1 pure-u-md-11-24") + @div(id:="div3") + @script("Inputs().main(document.getElementById('div3'))") + + @p + In Scalatags, you build up fragments of type @hli.scala{Frag} using functions like @hli.scala{div}, @hli.scala{h1}, etc., and call @hli.scala{.render} on it to turn it into a real @hli.scala{dom.Element}. Different fragments render to different things: e.g. @hli.scala{input.render} gives you a @hli.scala{dom.HTMLInputElement}, @hli.scala{span.render} gives you a @hli.scala{dom.HTMLSpanElement}. You can then access the properties of these elements: adding callbacks, checking their value, anything you want. + + @p + In this example, we render and @hli.scala{input} element and a @hli.scala{span}, wire up the input to set the value of the span whenever you press a key in the input, and then stuff both of them into a larger HTML fragment that forms the contents of our @hli.scala{target} element. + + @sect{Re-rendering} + @p + Let's look at a slightly longer example. While above we spliced small snippets of text into the DOM, here we are going to re-render entire sections of HTML! The goal of this little exercise is to make a filtering search-box: starting from a default list of items, narrow it down as the user enters text into the box. + + @p + To begin with, let's define our list of items: Fruits! + + @hl.ref("examples/src/main/scala/webpage/Search0.scala", "val listings", "def") + + @p + Next, let's think about how we want to render these fruits. One natural way would be as a list, which in HTML is represented by a @hli.html{