summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlihaoyi <haoyi.sg@gmail.com>2014-11-23 05:37:28 -0800
committerlihaoyi <haoyi.sg@gmail.com>2014-11-23 05:37:28 -0800
commit1c4cb72b209ab11c9e52c3bb490adf759f17fd0c (patch)
treefddc2a4f7b4848902b31aa4fa5c77023045f0166
parentbc640b3b440735a9343185856fdbec2ab064a369 (diff)
downloadhands-on-scala-js-1c4cb72b209ab11c9e52c3bb490adf759f17fd0c.tar.gz
hands-on-scala-js-1c4cb72b209ab11c9e52c3bb490adf759f17fd0c.tar.bz2
hands-on-scala-js-1c4cb72b209ab11c9e52c3bb490adf759f17fd0c.zip
Added autowire section in client-server
-rw-r--r--book/src/main/scalatex/book/handson/ClientServer.scalatex131
-rw-r--r--book/src/main/scalatex/book/indepth/AdvancedTechniques.scalatex30
-rw-r--r--build.sbt8
-rw-r--r--examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala2
-rw-r--r--examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala23
-rw-r--r--examples/crossBuilds/clientserver2/build.sbt37
-rw-r--r--examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala47
-rw-r--r--examples/crossBuilds/clientserver2/project/build.sbt5
l---------examples/crossBuilds/clientserver2/server/shared1
-rw-r--r--examples/crossBuilds/clientserver2/server/src/main/scala/simple/Page.scala21
-rw-r--r--examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala51
-rw-r--r--examples/demos/src/main/scala/advanced/Futures.scala2
-rw-r--r--examples/demos/src/main/scala/scrollmenu/Controller.scala110
-rw-r--r--intro.md70
-rw-r--r--readme.md64
15 files changed, 388 insertions, 214 deletions
diff --git a/book/src/main/scalatex/book/handson/ClientServer.scalatex b/book/src/main/scalatex/book/handson/ClientServer.scalatex
index 247d9ac..3e30d42 100644
--- a/book/src/main/scalatex/book/handson/ClientServer.scalatex
+++ b/book/src/main/scalatex/book/handson/ClientServer.scalatex
@@ -97,6 +97,137 @@
@p
This has always been annoying boilerplate, and Scala.js removes it. With @lnk.github.uPickle, you can simply call @hl.scala{upickle.write(...)} and @hl.scala{upickle.read[T](...)} to convert your collections, primitives or case-classes to and from JSON. This means you do not need to constantly re-invent different ways of making Ajax calls: you can just fling the data right across the network from client to server and back again.
+@sect{What's Left?}
+ @p
+ We've built a small client-server web application with a Scala.js web-client that makes Ajax calls to a Scala-JVM web-server running on Spray. We performed these Ajax calls using uPickle to serialize the data back and forth, so serializing the arguments and return-value was boilerplate-free and correct.
+
+ @p
+ However, there is still some amount of duplication in the code. In particular, the definition of the endpoint name "list" is duplicated 4 times:
+
+ @hl.ref("examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala", """path("ajax" / "list")""", "extract")
+ @hl.ref("examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala", "list(", "}")
+ @hl.ref("examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala", "def list", "val")
+ @hl.ref("examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala", "ajax/list", "val")
+
+ @p
+ Three times on the server and once on the client! What's worse, two of the appearances of @i{list} are in string literals, which are not checked by the compiler to match up with themselves or the name of the method @hl.scala{list}. Apart from this, there is one other piece of duplication that is unchecked: the type being returned from @hl.scala{list} (@hl.scala{Seq[FileData]} is being repeated on the client in @hl.scala{upickle.read[Seq[FileData]]} in order to de-serialize the serialized data. This leaves three wide-open opportunities for error:
+
+ @ul
+ @li
+ You could change the string literals "list" and forget to change the method-name @hl.scala{list}, thus confusing future maintainers of the code.
+ @li
+ You could change one of literal "list"s but forget to change the other, thus causing an error at run-time (e.g. a 404 NOT FOUND response)
+ @li
+ You could update the return type of @hl.scala{list} and forget to update the deserialization call on the client, resulting in a deserialization failure at runtime.
+
+ @p
+
+ @p
+ Neither of these scenarios is great! Although we've already made great progress in making our client-server application type-safe (via Scala.js on the client) and DRY (via shared code in @code{shared/}) we still have this tiny bit of annoying, un-checked duplication and danger lurking in the code-base. The basic problem is that what is normally called the "routing layer" in the web application is still unsafe, and so these silly errors can go un-caught and blow up on unsuspecting developers at run-time. Let's see how we can fix it.
+
+@sect{Autowire}
+
+ @p
+ @lnk("Autowire", "https://github.com/lihaoyi/autowire") is a library that turns your request routing layer from a fragile, hand-crafted mess into a solid, type-checked, boilerplate-free experience. Autowire basically turns what was previously a stringly-typed, hand-crafted Ajax call and route:
+
+ @hl.ref("examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala", """path("ajax" / "list")""", "extract")
+ @hl.ref("examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala", "ajax/list", "val")
+
+ @p
+ Into a single, type-checked function call:
+
+ @hl.ref("examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala", ".call()", "outputBox")
+
+ @p
+ Let's see how we can do that.
+
+ @sect{Setting up Autowire}
+
+ @p
+ To begin with, Autowire requires you to provide three things:
+
+ @ul
+ @li
+ An @hl.scala{autowire.Server} on the Server, set up to feed the incoming request into Autowire's routing logic
+ @li
+ An @hl.scala{autowire.Client} on the Client, set up to take a serialized request and send it across the network to the server.
+ @li
+ An interface (A Scala @hl.scala{trait}) which defines the interface between these two
+
+ @p
+ Let's start with our client-server interface definition
+
+ @hl.ref("examples/crossBuilds/clientserver2/client/shared/main/scala/simple/Shared.scala")
+
+ @p
+ Here, you can see that in addition to sharing the @hl.scala{FileData} class, we are also creating an @hl.scala{Api} trait which contains the signature of our @hl.scala{list} method. The exact name of the trait doesn't matter. We need it to be in @code{shared/} so that the code in both client and server can reference it.
+
+ @p
+ Next, let's look at modifying our server code to make use of Autowire:
+
+ @hl.ref("examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala")
+
+ @p
+ Now, instead of hard-coding the route @hl.scala{"ajax" / "list"}, we now take in any route matching @hl.scala{"ajax" / Segments}, feeding the resultant path segments into the @hl.scala{Router} object:
+
+ @hl.ref("examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala", "path(")
+ @p
+ The @hl.scala{Router} object in turn simply defines how you intend the objects to be serialized and deserialized:
+
+ @hl.ref("examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala", "object Router", "object")
+
+ @p
+ In this case using uPickle. Note how the @hl.scala{route} call explicitly states the type (here @hl.scala{Api}) that it is to generate routes against; this ensures that only methods which you explicitly put in your public interface @hl.scala{Api} are publically reachable.
+
+ @p
+ Next, let's look at the modified client code:
+
+ @hl.ref("examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala")
+
+ @p
+ There are two main modifications here: the existence of the new @hl.scala{Ajaxer} object, and the modification to the Ajax call-site. Let's first look at @hl.scala{Ajaxer}:
+
+ @hl.ref("examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala", "object Ajaxer", "object")
+
+ @p
+ Like the @hl.scala{Router} object, @hl.scala{Ajaxer} also defines how you perform the serialization and deserialization of data-structures, again using uPickle. Unlike the @hl.scala{Router} object, @hl.scala{Ajaxer} also defines how the out-going Ajax call gets sent over the network. Here we're doing it using the @hl.scala{Ajax.post} method.
+
+ @p
+ Lastly, let's look at the modified callsite for the ajax call itself:
+
+ @hl.ref("examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala", "def update", "inputBox")
+
+ @p
+ There are a few things of note here:
+
+ @ul
+ @li
+ The previous call to @hl.scala{Ajax.post} with the path as a string has been replaced by calling @hl.scala{Ajaxer[Api].list(...).call()}, since the logic of actually performing the POST is specified once-and-only-once in the @hl.scala{Ajaxer} object.
+ @li
+ While @hl.scala{Ajax.post} returned a @hl.scala{Future[dom.XMLHttpRequest]} and left us to call @hl.scala{upickle.read} and deserialize the data ourselves, @hl.scala{Ajaxer[Api].list(...).call()} now returns a @hl.scala{Future[Seq[FileData]]}! Thus we don't need to worry about making a mistake in the deserialization logic when we write it by hand.
+
+ @p
+ Other than that, nothing much has changed. If you've done this correctly, the web application will look and behave exactly as it did earlier! So why did we do this in the first place?
+
+ @sect{Why Autowire?}
+ @p
+ Overall, this set up requires some boilerplate to define the @hl.scala{Ajaxer} and @hl.scala{Router} objects, as well as the @hl.scala{Api} trait. However, these can be defined just once and used over and over; while it might be wasteful/unnecessary for making a single Ajax call, the cost is much less amortized over a number of Ajax calls. In a non-trivial web application with dozens of routes being called all over the place, spending a dozen lines setting up things up-front isn't a huge cost.
+
+ @p
+ What have we gotten in exchange? It turns out that by using Autowire, we have eliminated the three failure modes described earlier, that could:
+
+ @ul
+ @li
+ It is impossible for the route and the endpoint method-name to diverge accidentally: if the endpoint is called @hl.scala{list}, the requests will go through the @code{/list} URL. No room for discussion, or to make a mistake
+ @li
+ You cannot accidentally rename the route on the server without changing the client, or vice versa. Attempts to do so will cause a compilation error, and even your IDE should highlight it as red. Try it out!
+
+ @li
+ There is no chance of messing up the serialization/deserialization code, e.g. writing a response of type A on the server and trying to read a data-structure of type B on the client. You have no opportunity to make an error: you pass arguments to the Ajax call, and they are serialized/deserialized automatically, such that by the time you get access to the value on the server, it is already of the correct type! The same applies to serializing/deserializing the return-value on the client. There is simply no place for you as a developer to accidentally make a mistake!
+
+ @p
+ Although the functionality of the web application is the same, it is mostly in terms of @i{safety} that we have made the biggest gains. All of the common failure modes described earlier have been guarded against, and you as a developer will have a hard time trying to make a mal-formed Ajax call. It's worth taking some time to poke at the source code to see the boundaries of the type-safety provided by autowire, as it is a very different experience from the traditional "route it manually" approach to making interactive client-server applications.
+
@hr
@p
diff --git a/book/src/main/scalatex/book/indepth/AdvancedTechniques.scalatex b/book/src/main/scalatex/book/indepth/AdvancedTechniques.scalatex
index 21622ba..ae56fa3 100644
--- a/book/src/main/scalatex/book/indepth/AdvancedTechniques.scalatex
+++ b/book/src/main/scalatex/book/indepth/AdvancedTechniques.scalatex
@@ -10,8 +10,6 @@
@li
@sect.ref("Functional-Reactive UIs", "Functional-reactive user interfaces")
@li
- @sect.ref("Transparent RPCs", "Transparent client-server RPCs")
- @li
@sect.ref("Asynchronous Workflows", "Asynchronous user-interation workflows")
@p
@@ -126,13 +124,6 @@
@p
Hopefully this has given you a sense of how you can use Scala.Rx to help build complex, interactive web applications. The implementation is tricky, but the basic value proposition is clear: you get to write your code top-to-bottom, like the most old-fashioned static pages, and have it transformed by Scala.Rx into an interactive, always-consistent web app. By abstracting away the whole event-propagation, manual-updating process inside the library, we have ensured that there is no place where the developer can screw it up, and the application's UI will forever be in sync with its data.
-@sect{Transparent RPCs}
- @p
- In our chapter on @sect.ref{Integrating Client-Server}, we built a small client-server web application with a Scala.js web-client that makes Ajax calls to a Scala-JVM web-server running on Spray. We performed these Ajax calls using uPickle to serialize the data back and forth, so serializing the arguments and return-value was boilerplate-free and correct.
-
- @p
- However, there is still some amount of duplication in the code. In particular,
-
@sect{Asynchronous Workflows}
@p
In a traditional setting, Scala applications tend to have a mix of concurrency models: some spawn multiple threads and use thread-blocking operations or libraries, others do things with Actors or Futures, trying hard to stay non-blocking throughout, while most are a mix of these two paradigms.
@@ -189,7 +180,12 @@
@sect{Direct Use of XMLHttpRequest}
- @div(id:="div123", display:="block")
+ @div(
+ id:="div123",
+ display:="block",
+ height:="200px",
+ overflow:="scroll"
+ )
@script("Futures().main0(document.getElementById('div123'))")
@hl.ref("examples/demos/src/main/scala/advanced/Futures.scala", "def handle0", "main")
@@ -202,7 +198,12 @@
This solution is basically equivalent to the initial code given in the @sect.ref{Raw Javascript} section of @sect.ref{Interactive Web Pages}, with the additional code necessary for aggregation. As described in @sect.ref{dom.extensions}, we can make use of the @hl.scala{Ajax} object to make it slightly tidier.
@sect{Using dom.extensions.Ajax}
- @div(id:="div124", display:="block")
+ @div(
+ id:="div124",
+ display:="block",
+ height:="200px",
+ overflow:="scroll"
+ )
@script("Futures().main1(document.getElementById('div124'))")
@hl.ref("examples/demos/src/main/scala/advanced/Futures.scala", "def handle1", "main")
@@ -212,7 +213,12 @@
However, we still have the messiness inherent in the result aggregation: we don't actually want to perform our action (writing to the @hl.scala{output} div) when one @hl.scala{Future} is complete, but only when @i{all} the @hl.scala{Future}s are complete. Thus we still need to do some amount of manual book-keeping in the @hl.scala{results} buffer.
@sect{Future Combinators}
- @div(id:="div125", display:="block")
+ @div(
+ id:="div125",
+ display:="block",
+ height:="200px",
+ overflow:="scroll"
+ )
@script("Futures().main2(document.getElementById('div125'))")
@hl.ref("examples/demos/src/main/scala/advanced/Futures.scala", "def handle2", "main")
diff --git a/build.sbt b/build.sbt
index e0b9f22..ece1f30 100644
--- a/build.sbt
+++ b/build.sbt
@@ -75,7 +75,15 @@ lazy val simple = project.in(file("examples/crossBuilds/simple"))
lazy val simple2 = project.in(file("examples/crossBuilds/simple2"))
+lazy val clientserver = project.in(file("examples/crossBuilds/clientserver"))
+
lazy val client = ProjectRef(file("examples/crossBuilds/clientserver"), "client")
lazy val server = ProjectRef(file("examples/crossBuilds/clientserver"), "server")
+lazy val clientserver2 = project.in(file("examples/crossBuilds/clientserver2"))
+
+lazy val client2 = ProjectRef(file("examples/crossBuilds/clientserver2"), "client")
+
+lazy val server2 = ProjectRef(file("examples/crossBuilds/clientserver2"), "server")
+
diff --git a/examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala b/examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala
index f066923..c008a87 100644
--- a/examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala
+++ b/examples/crossBuilds/clientserver/client/src/main/scala/simple/Client.scala
@@ -13,7 +13,7 @@ object Client extends{
def main(container: dom.HTMLDivElement) = {
val inputBox = input.render
val outputBox = ul.render
- def update() = Ajax.post("/ajax", inputBox.value).foreach{ xhr =>
+ def update() = Ajax.post("/ajax/list", inputBox.value).foreach{ xhr =>
val data = upickle.read[Seq[FileData]](xhr.responseText)
outputBox.innerHTML = ""
for(FileData(name, size) <- data){
diff --git a/examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala b/examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala
index c9cc526..4d1ece2 100644
--- a/examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala
+++ b/examples/crossBuilds/clientserver/server/src/main/scala/simple/Server.scala
@@ -20,23 +20,24 @@ object Server extends SimpleRoutingApp{
getFromResourceDirectory("")
} ~
post{
- path("ajax"){
+ path("ajax" / "list"){
extract(_.request.entity.asString) { e =>
complete {
- val (dir, last) = e.splitAt(e.lastIndexOf("/") + 1)
- val files =
- Option(new java.io.File("./" + dir).listFiles())
- .toSeq.flatten
- upickle.write(
- for{
- f <- files
- if f.getName.startsWith(last)
- } yield FileData(f.getName, f.length())
- )
+ upickle.write(list(e))
}
}
}
}
}
}
+ def list(path: String) = {
+ val (dir, last) = path.splitAt(path.lastIndexOf("/") + 1)
+ val files =
+ Option(new java.io.File("./" + dir).listFiles())
+ .toSeq.flatten
+ for{
+ f <- files
+ if f.getName.startsWith(last)
+ } yield FileData(f.getName, f.length())
+ }
} \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver2/build.sbt b/examples/crossBuilds/clientserver2/build.sbt
new file mode 100644
index 0000000..c64a6fd
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/build.sbt
@@ -0,0 +1,37 @@
+import utest.jsrunner.JsCrossBuild
+import scala.scalajs.sbtplugin.ScalaJSPlugin._
+import ScalaJSKeys._
+val sharedSettings = Seq(
+ unmanagedSourceDirectories in Compile +=
+ baseDirectory.value / "shared" / "main" / "scala",
+ libraryDependencies ++= Seq(
+ "com.scalatags" %%% "scalatags" % "0.4.2",
+ "com.lihaoyi" %%% "upickle" % "0.2.5",
+ "com.lihaoyi" %%% "autowire" % "0.2.3"
+ ),
+ scalaVersion := "2.11.4"
+)
+
+lazy val client = project.in(file("client"))
+ .settings(scalaJSSettings:_*)
+ .settings(sharedSettings:_*)
+ .settings(
+ libraryDependencies ++= Seq(
+ "org.scala-lang.modules.scalajs" %%% "scalajs-dom" % "0.6"
+ )
+)
+
+lazy val server = project.in(file("server"))
+ .settings(sharedSettings:_*)
+ .settings(
+ libraryDependencies ++= Seq(
+ "io.spray" %% "spray-can" % "1.3.2",
+ "io.spray" %% "spray-routing" % "1.3.2",
+ "com.typesafe.akka" %% "akka-actor" % "2.3.6"
+ ),
+ (resources in Compile) += {
+ (fastOptJS in (client, Compile)).value
+ (artifactPath in (client, Compile, fastOptJS)).value
+ }
+)
+
diff --git a/examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala b/examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala
new file mode 100644
index 0000000..f10eede
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/client/src/main/scala/simple/Client.scala
@@ -0,0 +1,47 @@
+//js/src/main/scala/simple/Platform.scala
+package simple
+import scalatags.JsDom.all._
+import org.scalajs.dom
+import scala.scalajs.js.annotation.JSExport
+import scalajs.concurrent.JSExecutionContext.Implicits.runNow
+import autowire._
+
+object Ajaxer extends autowire.Client[String, upickle.Reader, upickle.Writer]{
+ override def doCall(req: Request) = {
+ dom.extensions.Ajax.post(
+ url = "/ajax/" + req.path.mkString("/"),
+ data = upickle.write(req.args)
+ ).map(_.responseText)
+ }
+
+ def read[Result: upickle.Reader](p: String) = upickle.read[Result](p)
+ def write[Result: upickle.Writer](r: Result) = upickle.write(r)
+}
+
+@JSExport
+object Client extends{
+ @JSExport
+ def main(container: dom.HTMLDivElement) = {
+ val inputBox = input.render
+ val outputBox = ul.render
+ def update() = Ajaxer[Api].list(inputBox.value).call().foreach{ data =>
+ outputBox.innerHTML = ""
+ for(FileData(name, size) <- data){
+ outputBox.appendChild(
+ li(
+ b(name), " - ", size, " bytes"
+ ).render
+ )
+ }
+ }
+ inputBox.onkeyup = (e: dom.Event) => update()
+ update()
+ container.appendChild(
+ div(
+ h1("File Search"),
+ inputBox,
+ outputBox
+ ).render
+ )
+ }
+} \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver2/project/build.sbt b/examples/crossBuilds/clientserver2/project/build.sbt
new file mode 100644
index 0000000..7c60a91
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/project/build.sbt
@@ -0,0 +1,5 @@
+/*project/build.sbt*/
+addSbtPlugin("org.scala-lang.modules.scalajs" % "scalajs-sbt-plugin" % "0.5.5")
+
+addSbtPlugin("com.lihaoyi" % "utest-js-plugin" % "0.2.4")
+
diff --git a/examples/crossBuilds/clientserver2/server/shared b/examples/crossBuilds/clientserver2/server/shared
new file mode 120000
index 0000000..f32be42
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/server/shared
@@ -0,0 +1 @@
+../client/shared \ No newline at end of file
diff --git a/examples/crossBuilds/clientserver2/server/src/main/scala/simple/Page.scala b/examples/crossBuilds/clientserver2/server/src/main/scala/simple/Page.scala
new file mode 100644
index 0000000..ce6617c
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/server/src/main/scala/simple/Page.scala
@@ -0,0 +1,21 @@
+package simple
+import scalatags.Text.all._
+
+object Page{
+ val boot =
+ "Client().main(document.getElementById('contents'))"
+ val skeleton =
+ html(
+ head(
+ script(src:="/client-fastopt.js"),
+ link(
+ rel:="stylesheet",
+ href:="http://yui.yahooapis.com/pure/0.5.0/pure-min.css"
+ )
+ ),
+ body(
+ onload:=boot,
+ div(id:="contents")
+ )
+ )
+}
diff --git a/examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala b/examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala
new file mode 100644
index 0000000..df5f877
--- /dev/null
+++ b/examples/crossBuilds/clientserver2/server/src/main/scala/simple/Server.scala
@@ -0,0 +1,51 @@
+package simple
+
+import akka.actor.ActorSystem
+import spray.http.{HttpEntity, MediaTypes}
+import spray.routing.SimpleRoutingApp
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object Router extends autowire.Server[String, upickle.Reader, upickle.Writer]{
+ def read[Result: upickle.Reader](p: String) = upickle.read[Result](p)
+ def write[Result: upickle.Writer](r: Result) = upickle.write(r)
+}
+
+object Server extends SimpleRoutingApp with Api{
+ def main(args: Array[String]): Unit = {
+ implicit val system = ActorSystem()
+ startServer("localhost", port = 8080){
+ get{
+ pathSingleSlash{
+ complete{
+ HttpEntity(
+ MediaTypes.`text/html`,
+ Page.skeleton.render
+ )
+ }
+ } ~
+ getFromResourceDirectory("")
+ } ~
+ post{
+ path("ajax" / Segments){ s =>
+ extract(_.request.entity.asString) { e =>
+ complete {
+ Router.route[Api](Server)(
+ autowire.Core.Request(s, upickle.read[Map[String, String]](e))
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ def list(path: String) = {
+ val (dir, last) = path.splitAt(path.lastIndexOf("/") + 1)
+ val files =
+ Option(new java.io.File("./" + dir).listFiles())
+ .toSeq.flatten
+ for{
+ f <- files
+ if f.getName.startsWith(last)
+ } yield FileData(f.getName, f.length())
+ }
+} \ No newline at end of file
diff --git a/examples/demos/src/main/scala/advanced/Futures.scala b/examples/demos/src/main/scala/advanced/Futures.scala
index 6602c61..0d7107d 100644
--- a/examples/demos/src/main/scala/advanced/Futures.scala
+++ b/examples/demos/src/main/scala/advanced/Futures.scala
@@ -22,8 +22,6 @@ object Futures {
}
container.appendChild(
div(
- height:="200px",
- overflow:="scroll",
i("Press Enter in the box to fetch temperatures "),
myInput,
output
diff --git a/examples/demos/src/main/scala/scrollmenu/Controller.scala b/examples/demos/src/main/scala/scrollmenu/Controller.scala
index 119b8d4..cb0fc63 100644
--- a/examples/demos/src/main/scala/scrollmenu/Controller.scala
+++ b/examples/demos/src/main/scala/scrollmenu/Controller.scala
@@ -28,63 +28,65 @@ object Controller{
val snippets = dom.document.getElementsByClassName("highlight-me")
snippets.foreach(js.Dynamic.global.hljs.highlightBlock(_))
- val scrollSpy = new ScrollSpy(structure, main)
- val list = ul(cls:="menu-item-list collapsed")(
- scrollSpy.domTrees.map(_.value.frag)
- ).render
-
- def updateScroll() = scrollSpy(main.scrollTop + main.clientHeight)
- val expandIcon = i(cls:="fa fa-caret-down").render
- val expandLink =
- a(
- expandIcon,
- href:="javascript:",
- marginLeft:="0px",
- paddingLeft:="15px",
- paddingRight:="15px",
- cls:="pure-menu-selected",
- onclick := { (e: dom.Event) =>
- expandIcon.classList.toggle("fa-caret-down")
- expandIcon.classList.toggle("fa-caret-up")
- list.classList.toggle("collapsed")
- scrollSpy.clean = !scrollSpy.clean
- updateScroll()
- }
+ def rest() = {
+ val scrollSpy = new ScrollSpy(structure, main)
+ val list = ul(cls := "menu-item-list collapsed")(
+ scrollSpy.domTrees.map(_.value.frag)
).render
-
- menu.appendChild(
- div(
- zIndex:=10,
- position:="absolute",
- cls:="pure-menu pure-menu-open",
- ul(cls:="menu-item-list")(
- li(
- width:="43px",
- float:="right",
- expandLink
+ def updateScroll() = scrollSpy(main.scrollTop + main.clientHeight)
+ val expandIcon = i(cls := "fa fa-caret-down").render
+ val expandLink =
+ a(
+ expandIcon,
+ href := "javascript:",
+ marginLeft := "0px",
+ paddingLeft := "15px",
+ paddingRight := "15px",
+ cls := "pure-menu-selected",
+ onclick := { (e: dom.Event) =>
+ expandIcon.classList.toggle("fa-caret-down")
+ expandIcon.classList.toggle("fa-caret-up")
+ list.classList.toggle("collapsed")
+ scrollSpy.clean = !scrollSpy.clean
+ updateScroll()
+ }
+ ).render
+
+
+ menu.appendChild(
+ div(
+ zIndex := 10,
+ position := "absolute",
+ cls := "pure-menu pure-menu-open",
+ ul(cls := "menu-item-list")(
+ li(
+ width := "43px",
+ float := "right",
+ expandLink
+ )
)
- )
- ).render
- )
-
- menu.appendChild(
- div(cls:="pure-menu pure-menu-open")(
- a(cls:="pure-menu-heading")(
- "Contents"
- ),
- list
- ).render
- )
-
- menuLink.onclick = (e: dom.MouseEvent) => {
- layout.classList.toggle("active")
- menu.classList.toggle("active")
- menuLink.classList.toggle("active")
+ ).render
+ )
+
+ menu.appendChild(
+ div(cls := "pure-menu pure-menu-open")(
+ a(cls := "pure-menu-heading")(
+ "Contents"
+ ),
+ list
+ ).render
+ )
+
+ menuLink.onclick = (e: dom.MouseEvent) => {
+ layout.classList.toggle("active")
+ menu.classList.toggle("active")
+ menuLink.classList.toggle("active")
+ }
+
+ main.onscroll = (e: dom.UIEvent) => updateScroll()
+ updateScroll()
}
-
- main.onscroll = (e: dom.UIEvent) => updateScroll()
- updateScroll()
+ dom.setTimeout(rest _, 10)
}
-
}
diff --git a/intro.md b/intro.md
deleted file mode 100644
index 6536a54..0000000
--- a/intro.md
+++ /dev/null
@@ -1,70 +0,0 @@
-Hands-On Scala.js
-
- - Some Scala experience
- - Some Javascript/Web experience
-
- Intro to Scala.js
- What
- Why
- Where
-
- Hands On
- Getting Started
- Opening the Project
- The Application Code
- The Project Code
- Publishing
-
- Making an HTML5 Canvas Game
-
- Accessing DOM APIs
- Using js.Dynamic
- Using scala-js-dom for type-safety
-
- Input, Output
- Publishing
- Looking through the generated code
-
- Interactive Web Apps
- Managing HTML using the DOM
- Managing HTML using Scalatags
- Wiring up DOM events and interactions
- Publishing
-
- Building Cross-platform Libraries
- Shared code organization
- Shared tests using uTest
- Publishing
-
- Client-Server Integration
- Hello World client/server project
- Serving the client javascript from the server
-
- Sharing code
- Using shared libraries
- Writing shared application logic
-
- Ajax calls
- Using the DOM APIs
- Using uPickle for serialization
- Using Autowire for routing
-
- Deployment
-
- Reference
- The Scala.js File Encoding
-
- Javascript Interop
- Calling Javascript from Scala.js
- Writing your own FFI facades
- Calling Scala.js from Javascript
- Mapping of Types
-
- Library Dependencies
-
- Differences from Scala/JVM
-
- Internals
- Optimization Phases
-
-
diff --git a/readme.md b/readme.md
deleted file mode 100644
index db717a0..0000000
--- a/readme.md
+++ /dev/null
@@ -1,64 +0,0 @@
-Twist
-=====
-
-Twist is a lightweight, whitespace-delimited, markdown-like relative of the [Twirl](https://github.com/playframework/twirl) templating engine. It allows you to write
-
-```
-@(titleString: String)(sidebar: Html)(content: Html)
-
-@html
- @head
- @title @titleString
- @body
- @section(cls:="sidebar") @sidebar
- @section(cls:="content") @content
-```
-
-Instead of the more verbose Twirl-template
-
-```html
-@(title: String)(sidebar: Html)(content: Html)
-<!DOCTYPE html>
-<html>
- <head>
- <title>@title</title>
- </head>
- <body>
- <section class="sidebar">@sidebar</section>
- <section class="content">@content</section>
- </body>
-</html>
-```
-
-Apart from the syntactic difference, Twirlite templates are just as type-safe as Twirl templates. Furthermore, they use the [Scalatags](https://github.com/lihaoyi/scalatags) HTML-generation library under the hood, making them render around [4x faster](https://github.com/lihaoyi/scalatags#performance) than the equivalent Twirl template. Like Twirl templates, you can use arbitrary function calls, control-flow structures like `if`/`for`, and other Scala expressions within your templates.
-
-Why Twirlite?
--------------
-
-Twirlite emerged out of the deficiencies of other languages used for marking up text.
-
-###[Markdown](http://en.wikipedia.org/wiki/Markdown)
-
-is nice to use but too inflexible: it is impossible to define abstractions in the language, and if you want any sort of automation, e.g. substituting in sections from somewhere else, or re-usable blocks of title+paragraph, you're left with performing hacky string-replacements on the markdown source. With Twirlite, tags are simply functions, and you can define additional tags yourself to abstract away arbitrary patterns within your markup.
-
-###[Scalatags](https://github.com/lihaoyi/scalatags)
-
-is perfect for forming heavily structured markup: since in Scalatags the tags/structure is "raw" while the text is quoted, it facilitates structure-heavy markup at the cost of making text-heavy markup very cumbersome to write. With Twirlite, the situation is reversed: text is left raw, while the tags are quoted (using `@`), lending itself to easily marking-up text-heavy documents. As Twirlite is built on Scalatags, we still get all the other advantages of speed and composability.
-
-###[Twirl](https://github.com/playframework/twirl)
-
-is almost what I want for a markup language, as it doesn't suffer from the same problems Scalatags or Markdown does. However, it has a rather noisy syntax for a langauge meant for marking up text: you need to surround blocks of text in curly braces `{...}` to pass them into functions/tags. Furthermore, Twirl by default uses Scala.XML both in syntax and in implementation, resulting in an odd mix of <XML>-tags and @twirl-tags. Twirlite solves the first by using whitespace as a delimiter, and solves the second by using Scalatags to provide the HTML structure, making all tags uniformly @twirl-tags.
-
-----------
-
-Twirlite also has some other design decisions which are unique, for better or for worse:
-
-###Twirlite as a Macro
-
-Twirlite is usable as a macro right inside your code; that means if you have a text-heavy section of your Scalatags code, you can simply drop into a `twl(...)` macro and start writing long-form text, while still using `@` to interpolate tags as necessary, or splicing in variables in the enclosing scope.
-
-###Direct Text Generation
-
-Twirlite works as a direct-text-generator rather than as a markup-language that is interpreted to generate text. This has advantages in speed and simplicity-of-abstraction, since creating re-usable tags is as simple as defining a function `f(inner: Frag*): Frag` and making it available in scope.
-
-However, it makes it more difficult to do certain kinds of whole-program-analysis, since there never really is a syntax-tree available that you can analyze. Twirlite compiles to Scala source code, which when evaluated spits out HTML `String`s, with nothing in between. Furthermore, since Twirlite can run arbitrary Scala code, it makes it much more difficult to sandbox than Markdown or similar languages. \ No newline at end of file