Download Widok User Manual v0.2.2
Transcript
March 2015 Widok User Manual v0.2.2 Tim Nieradzik University of Bremen [email protected] Abstract Widok is a reactive web framework for the JVM and Scala.js. It enables you to develop interactive client-server applications entirely in Scala. The client code is transpiled to JavaScript, while sharing the same interfaces on the server. Abstractions such as widgets and reactive data structures allow for concise and reliable code. Widok ships native bindings for popular CSS frameworks which let you iterate faster. 1 Contents Contents 2 1 Introduction 1.1 Comparison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 2 Getting Started 2.1 Prerequisites . . 2.2 Project structure 2.3 Code . . . . . . 2.4 Compilation . . 3 4 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 8 9 Concepts 3.1 Basic application . . . . 3.2 Single-page applications 3.3 Multi-page applications . 3.4 Pages . . . . . . . . . . . 3.5 Widgets . . . . . . . . . 3.6 Reactive programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 11 12 14 15 15 Build process 4.1 JDK . . . . . . . . . . . . . . . . . 4.2 Development releases . . . . . . . 4.3 Production releases . . . . . . . . 4.3.1 Additional optimisations 4.4 Continuous compilation . . . . . 4.5 Configure paths . . . . . . . . . . 4.6 sbt-web . . . . . . . . . . . . . . . 4.6.1 Sass . . . . . . . . . . . . 4.6.2 Font-Awesome . . . . . . 4.6.3 Artifacts . . . . . . . . . . 4.7 Code sharing . . . . . . . . . . . . 4.8 Colours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 17 18 18 18 19 19 20 21 21 21 22 Router . . . . . . . . . . . . . . . . 23 Contents 5.1 5.2 Interface . . . . . . . . . Routes . . . . . . . . . . 5.2.1 Design decisions Application provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 24 25 Widgets 6.1 HTML . . . . . . . . . . 6.1.1 Aliases . . . . . 6.2 Usage . . . . . . . . . . . 6.2.1 Forms . . . . . . 6.3 Writing custom widgets 6.4 Binding to events . . . . 6.5 Composed widgets . . . 6.6 Implicits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 28 28 29 29 30 31 31 Reactive programming 7.1 Motivation . . . . . . . . 7.2 Concepts . . . . . . . . . 7.3 Requirements . . . . . . 7.4 Implementation . . . . . 7.5 Reactive data structures . 7.6 Channels . . . . . . . . . 7.6.1 Operations . . . 7.6.2 State channels . 7.6.3 Call semantics . 7.6.4 Cycles . . . . . . 7.7 Buffers . . . . . . . . . . 7.8 Dictionaries . . . . . . . 7.9 Sets . . . . . . . . . . . . 7.10 Binding to Widgets . . . 7.11 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 33 35 36 39 40 40 41 43 43 44 45 45 45 47 Bindings 8.1 Bootstrap . . . . . . . . . . 8.1.1 External stylesheet 8.1.2 Image . . . . . . . 8.1.3 Label . . . . . . . 8.1.4 Text style . . . . . 8.1.5 Glyphicon . . . . . 8.1.6 Form . . . . . . . . 8.1.7 Layout . . . . . . . 8.1.8 Navigation . . . . 8.1.9 Navigation bar . . 8.1.10 Alert . . . . . . . . 8.1.11 Progress bar . . . 8.1.12 Panel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 49 50 50 50 50 50 52 52 52 53 53 53 5.3 6 7 8 3 4 Contents . . . . . . . . . . . 53 54 54 54 54 55 55 55 56 56 56 Validation 9.1 Validation channels . . . . . . . . . . . . . . . . . . . . . . . . . 9.2 Form validation example . . . . . . . . . . . . . . . . . . . . . . 57 57 57 10 Validations 10.1 Error messages . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 59 11 Developing 11.1 Prerequisites 11.2 API . . . . . 11.3 Compilation 11.4 Releases . . 11.5 Manual . . . 8.2 9 8.1.13 Pagination 8.1.14 List groups 8.1.15 Grids . . . 8.1.16 Button . . . 8.1.17 Checkbox . 8.1.18 Modal . . . 8.1.19 Media . . . 8.1.20 Breadcrumb 8.1.21 Table . . . 8.1.22 Typeahead Font-Awesome . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 61 61 61 61 62 12 Changelog 12.1 Version 0.2.2 . . . . . . . . 12.1.1 General work . . . 12.1.2 Channels . . . . . 12.1.3 Widgets . . . . . . 12.1.4 Bindings . . . . . 12.2 Version 0.2.1 . . . . . . . . 12.2.1 General work . . . 12.2.2 Widgets . . . . . . 12.2.3 Event propagation 12.2.4 Bindings . . . . . 12.2.5 Routing . . . . . . 12.3 Version 0.2.0 . . . . . . . . 12.3.1 General work . . . 12.3.2 Event propagation 12.3.3 Widgets . . . . . . 12.3.4 Routing . . . . . . 12.3.5 Trivia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 63 63 63 63 63 64 64 64 64 64 64 64 64 65 66 67 67 13 Support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 Chapter 1 Introduction Widok is a reactive web framework for the JVM and Scala.js. Its key concepts are: • Pages: Widok enforces modularisation of your web application. You split your application into pages. A router watches the browser query and loads the respective page. • Widgets: The layout is specified in terms of composable widgets. Widgets are bound to dynamically changing values which are rendered on-the-fly in the browser. • Bindings: Widok ships bindings for CSS frameworks like Bootstrap and Font-Awesome. • Reactive programming: Reactive data structures, which implement a simple model of data propagation, are provided. Widok has reactive counterparts for variables, arrays, maps and sets. Instead of dealing with constant values, you specify the data flow as streams, on which you operate with higher-order functions like map() or filter(). 1.1 Comparison In contrast to traditional web frameworks, a Widok application would implement the entire rendering logic and user interaction on the client-side. The sole purpose of the server would be to exchange data with the client. This approach leads to lower latencies and is more suitable for interactive applications. Instead of writing HTML templates and doing manual DOM manipulations, Widok advocates widgets which are inspired by traditional GUI development. Widgets are first-class objects, allowing you to return them in functions. This can be useful when rendering a data stream consisting of widgets, or when you want to display a different widget depending on the device the client is using. Another strength of Widok is that you can develop client-server applications entirely in Scala and CSS. Scala.js transpiles your code to JavaScript. Having only one implementation language reduces redundancy due to code sharing. This is especially useful for protocols. It also lets you develop safer web applications since you could use the same validation code on the client as on the server. 6 Chapter 1. Introduction Widok is fully supported by IntelliJ IDEA. As Scala is a statically typed language you can use your IDE for refactoring and tab completion, increasing your productivity. Similarly, many bugs can be already identified during compiletime. Browser source maps will help you pinpoint run-time errors in the original source code. Scala.js supports continuous compilation which lets you iterate faster. Finally, Widok is not necessarily bound to web applications. As it compiles to regular JavaScript code, you could develop io.js applications or even native user interfaces with NW.js. The JVM build comprises the reactive library, so that you can use it on the server-side as well. Chapter 2 Getting Started This chapter will guide you through creating your first Widok project. 2.1 Prerequisites To develop web applications with Widok the only dependency you will need is sbt. Once installed, it will automatically fetch Scala.js and all libraries Widok depends on. You may also want to use an IDE for development. Widok is well-supported by IntelliJ IDEA with the Scala plugin. The use of an IDE is recommended as the interfaces Widok provides are fully typed, which lets you do tab completion. 2.2 Project structure Your project will have the following structure: ��� application.html ��� project � ��� Build.scala � ��� plugins.sbt ��� src � ��� main � ��� scala � ��� example � ��� Main.scala Create a directory for your project. Within your project folder, create a subdirectory project with the two files plugins.sbt and Build.scala: • plugins.sbt specifies sbt plug-ins, notably Scala.js logLevel := Level.Warn addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.3") 8 Chapter 2. Getting Started • Build.scala is the build configuration of your project. The configuration itself is specified in Scala code, which allows for more flexibility. The chapter ‘Build process’ explains some of the possibilities in the web context. import import import import sbt._ sbt.Keys._ org.scalajs.sbtplugin._ org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ object Build extends sbt.Build { val buildOrganisation = "example" val buildVersion = "0.1-SNAPSHOT" val buildScalaVersion = "2.11.6" val buildScalaOptions = Seq( "-unchecked", "-deprecation" , "-encoding", "utf8" , "-Xelide-below", annotation.elidable.ALL.toString ) lazy val main = Project(id = "example", base = file(".")) .enablePlugins(ScalaJSPlugin) .settings( libraryDependencies ++= Seq( "io.github.widok" %%% "widok" % "0.2.2" ) , organization := buildOrganisation , version := buildVersion , scalaVersion := buildScalaVersion , scalacOptions := buildScalaOptions , persistLauncher := true ) } Your source code goes underneath src/main/scala/example/. 2.3 Code Create a source file named Main.scala with the following contents: package example import org.widok._ import org.widok.bindings.HTML object Main extends PageApplication { 2.4. Compilation 9 def view() = Inline( HTML.Heading.Level1("Welcome to Widok!") , HTML.Paragraph("This is your first application.") ) def ready() { log("Page loaded.") } } Finally, you need to create an HTML file application.html in the root directory. It references the compiled JavaScript sources: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Widok example</title> </head> <body id="page"> <script type="text/javascript" src="./target/scala-2.11/example-fastopt.js" ></script> <script type="text/javascript" src="./target/scala-2.11/example-launcher.js" ></script> </body> </html> 2.4 Compilation This is all you need for a minimal Widok project. To compile your application, run: $ sbt fastOptJS Now you can open application.html in your browser. The page should show a heading with a paragraph. Obviously, the Scala code you wrote translates to: <h1>Welcome to Widok!</h1> <p>This is your first application.</p> 10 Chapter 2. Getting Started Upon page load this gets dynamically inserted into the node with the ID page. When you open up the browser’s web console, it will show the message you specified in ready(). Chapter 3 Concepts In this chapter we will mention all key concepts of Widok. The following chapters will deal with these topics in detail. 3.1 Basic application Consider a one-file project consisting of: object Main extends Application { def main() { stub() } } A global object of type Application defines the entry point of the application1 . You could use methods from Widok’s DOM object to access and modify the DOM. Compile this application: $ sbt fastOptJS Open your application.html in the browser and it will print stub in the web console. The example doesn’t use any browser-related functionality. Therefore, it would also run under io.js. $ cat target/scala-2.11/*js | iojs stub 3.2 Single-page applications The application from the previous chapter roughly looked like this: package example import org.widok._ 1 An application cannot define more than one entry point. 12 Chapter 3. Concepts object Main extends PageApplication { def view() = Inline() def ready() { } } For a single-page application you need to declare an object which inherits from PageApplication, whereby Scala.js knows that it shall be the entry-point of the program. Furthermore, the two methods view() and ready() must be implemented. The views are rendered when the page is loaded. Afterwards, ready() gets called. Note: The Inline() view is a container that groups multiple widgets without affecting the design2 . 3.3 Multi-page applications While for small applications a single-page approach may be sufficient, you should consider making use of the in-built router and split your application into multiple pages for better modularity: package example import org.widok._ object Routes { val main = Route("/" , pages.Main ) val test = Route("/test/:param", pages.Test ) val notFound = Route("/404" , pages.NotFound) val routes = Set(main, test, notFound) } object Main extends RoutingApplication( Routes.routes , Routes.notFound ) A multi-page application must extend RoutingApplication which is passed a list of routes and a fallback route. Here, the Routes objects defines the available routes. The query part of a route can be parameterised by prefixing a colon. For instance, param is a named parameter for the route test. The router only matches strings. Further validations and conversions could be performed in the page itself. 2 rules. In contrast to a <span> or <div>, an Inline view cannot be controlled using CSS stylesheet 3.3. Multi-page applications 13 Create a new file for each page: • pages/Main.scala package example.pages import org.widok._ import org.widok.bindings.HTML import example._ case class Main() extends Page { def view() = HTML.Anchor("Link to second page") .url(Routes.test("param", "first page")) def ready(route: InstantiatedRoute) { log(s"Page 'main' loaded with route '$route'") } override def destroy() { log("Page 'main' left") } } Contrary to single-page applications, ready() needs one parameter which contains information about the chosen route and its parameters. This page uses HTML.Anchor() which is a widget representing a link. The target URL is set to an instantiated route, namely test. Every route can be instantiated, although all parameters according to the route specification must be provided. The apply() method of a route is overloaded. For only one route parameter, the first argument denotes the named parameter and the second its value. If a route has more than one parameter, a map with the values must be passed instead. Instantiating routes is to be preferred over links with handwritten paths. Referencing routes ensures during compile-time that no invalid routes are referred to. During runtime, assertions will verify whether all correct parameters were specified. When clicking the link, the router will notice this change and render the new route. The actual HTML file of the page is not reloaded, though. By default, destroy() is a stub, but may be overridden when navigating between routes requires resource management. • pages/Test.scala package example.pages import org.widok._ 14 Chapter 3. Concepts case class Test() extends Page { val query = Channel[String]() def view() = Inline("Received parameter: ", query) def ready(route: InstantiatedRoute) { query := route.args("param") } } Here, we are registering a channel and pass it the current value of the query parameter param. A channel can be considered as a stream you can send messages to. Each message is then multiplexed to the subscribers. query has one subscriber here. As it is used in view(), it is converted into a view. Whenever a new value is produced on query (using :=), it gets rendered automatically in the browser. If the user changes the query parameter of the current page, the router will detect this and re-render the page. • pages/NotFound.scala package example.pages import org.scalajs.dom import org.widok._ import org.widok.bindings.HTML import example.Routes case class NotFound() extends Page { def view() = HTML.Heading.Level1("Page not found") def ready(route: InstantiatedRoute) { dom.setTimeout(() => Routes.main().go(), 2000) } } NotFound was set as a fall-back route. It is loaded when no other route matches or when the fall-back route is loaded explicitly. Here, we are showing how to call JavaScript functions using the DOM bindings. It redirects to the main page after two seconds by calling go() on the instantiated route. 3.4 Pages As in the multi-page application, it is advisable to put all pages in a package as to separate them from other parts of the application, like models, validators or partials3 . 3 Partials are composed widgets. 3.5. Widgets 15 When the page is loaded, Widok looks for the element with the ID page in the DOM and renders view() in this node. The entire contents is destroyed when the route changes. view() must return the whole layout of the page. To prevent duplication among pages, partials should be defined. Common candidates for partials are navigation bars, panels or layout elements. But as partials are just regular functions returning a widget, they can contain logic and you may render different widgets depending on whether the user accesses the website on a mobile device or on a desktop. For instance, Bootstrap splits pages into header, body and footer. You could create a trait CustomPage that contains all shared elements like header and footer and requires you only to define body() in the pages. 3.5 Widgets The most notable difference to the traditional approach is that instead of writing HTML code you are dealing with type-safe widgets. Widok provides widget bindings for HTML tags, and custom bindings for Bootstrap and Font-Awesome. It is possible to embed HTML code using the HTML.Raw() widget. You could even access DOM elements using DOM.getElementById() as in JavaScript. However, this is discouraged in Widok which provides better ways to interact with elements. 3.6 Reactive programming To showcase some of the capabilities of reactive programming for UI development, take the following example: package example import org.widok._ import org.widok.html._ object App extends PageApplication { val name = Var("") val hasName = name.map(_.nonEmpty) def view() = div( h1("Welcome to Widok!") , p("Please enter your name:") , text().bind(name) , p("Hello, ", name) .show(hasName) 16 Chapter 3. Concepts , button("Change my name") .onClick(_ => name := "tux") .show(name.isNot("tux")) , button("Log out") .onClick(_ => name := "") .show(hasName) ) def ready() { } } The first striking change from the previous examples is that we now use the HTML aliases (import org.widok.html._). More importantly, this example shows that widgets provide methods to interact with channels. For example, the method bind() on textual input fields realises two-way binding, i.e., every key stroke produces a new value on the channel and notifies all other subscribers. Another related method is show() which will only show a certain widget if the passed channel produces the value true. Var() is a channel with an empty value as a default value and is bound to name. Well-known combinators such as map() and filter() are also defined on channels. In the example, map() is used for hasName such that the channel notifies its subscribers whenever name is updated. Chapter 4 Build process The chapter ‘Getting Started’ proposed a simple sbt configuration. sbt is a flexible build tool and can be extended with plug-ins and custom tasks. Some useful advice on using sbt for web development is given here. For more information on the build process, please refer to the Scala.js documentation. 4.1 JDK The Oracle JDK leads to slightly shorter compilation times than OpenJDK. With the default configuration, sbt tends to allocate a lot of memory, so that you may run into out-of-memory situations. This can be mitigated by limiting the heap size with the JAVA_OPTS environment variable: export JAVA_OPTS="$JAVA_OPTS \ -XX:InitialHeapSize=128m \ -XX:MaxHeapSize=512m \ -XX:+CMSClassUnloadingEnabled" export SBT_OPTS="$JAVA_OPTS" 4.2 Development releases Code optimisations are time-consuming and usually negligible during development. To compile the project without optimisations, use the fastOptJS task: $ sbt fastOptJS This generates two files in target/scala-2.11/: • $ProjectName-fastopt.js • $ProjectName-launcher.js The former is the whole project including dependencies within a single JavaScript file, while the latter contains a call to the entry point. It is safe to concatenate these two files and ship them to the client. 18 4.3 Chapter 4. Build process Production releases Scala.js uses Google’s Closure Compiler to apply code optimisations. To create an optimised build, use the fullOptJS task: $ sbt fullOptJS You may want to add a constant to your sbt configuration to toggle compiler settings depending on whether you need a production or development release. For example, -Xelidable-below could be used to remove assertions from production releases for better performance. 4.3.1 Additional optimisations The Scala.js compiler provides settings to fine-tune the build process. To further reduce the build size, class names could be replaced by an empty string. The semantics of a program should never rely on class names. This optimisation is therefore safe to set. However, if your want to retain some class names, you could define exceptions, for example for classes from a certain namespace. Another option is to enable unchecked asInstanceOf casts. A cast should always be well-defined. If this cannot be ensured, a manual isInstanceOf check needs to be performed anyway. Expecting an exception to be thrown is a suboptimal way of dealing with potentially undefined casts. Under this assumption, asInstanceOf casts should work if unchecked. Scala.js lets you change the semantics for the sake of better performance. import org.scalajs.core.tools.sem._ ... scalaJSSemantics ~= (_ .withRuntimeClassName(_ => "") .withAsInstanceOfs(CheckedBehavior.Unchecked) ) Since class names can be useful for debugging purposes and illegal casts may happen during development, these two options should only be set for production releases. 4.4 Continuous compilation sbt can detect changes in source files and recompile only when needed. To do so, prefix ~ to your build task (either fastOptJS or fullOptJS), for example: $ sbt ~fastOptJS This leads to faster development cycles than executing fastOptJS on your own. 4.5. Configure paths 19 4.5 Configure paths If the web server should point directly to the most recently built version, you do not need to copy over the generated files each time. Instead, the paths can be customised. A recommended application hierarchy is the following: • • • • web/index.html: Self-written entry-point of the application web/js/: Generated JavaScript files web/css/: Generated CSS stylesheets web/fonts/: A copy of all font files (for example, Bootstrap glyphicons or Font-Awesome) To do so, specify the paths in the build configuration as follows: val val val val outPath = new File("web") jsPath = outPath / "js" cssPath = outPath / "css" fontsPath = outPath / "fonts" Scala.js’ output path can be remapped using: .settings( ... artifactPath in (Compile, packageScalaJSLauncher) := jsPath / "launcher.js" , artifactPath in (Compile, fastOptJS) := jsPath / "application.js" ) Make sure to also add the following three paths to your .gitignore: web/css/ web/js/ web/fonts/ 4.6 sbt-web Many popular web libraries are published to Maven Central as regular .jar files, so-called WebJars. See the official Scala.js documentation on how to depend on these. sbt-web is an sbt plug-in to manage these WebJars and to produce web artifacts as part of the build process. To enable sbt-web, add two imports: import com.typesafe.sbt.web.SbtWeb import com.typesafe.sbt.web.Import._ And enable the plug-in: .enablePlugins(SbtWeb) 20 Chapter 4. Build process For example, to download the Sass version of the Bootstrap bindings as well as Font-Awesome, add these two lines to libraryDependencies: libraryDependencies ++= Seq( ... "org.webjars" % "bootstrap-sass" % "3.3.1" , "org.webjars" % "font-awesome" % "4.3.0-1" ) Note: sbt-web is not necessary to use Bootstrap or Font-Awesome, albeit it facilitates the customisation and upgrading of web dependencies. The chapter ‘Bindings’ explains how to use a CDN instead. 4.6.1 Sass Sass is a CSS dialect with useful extensions. One of its strengths is that you can modularise your stylesheet and store it in separate files. Since Bootstrap is available as Sass, the sbt-sass plug-in for sbt-web lets you create one monolithic, minified CSS file for your whole application. You may find that the widgets Bootstrap provides are not sufficient for your purposes. Using Sass, you would not end up with additional CSS files that need to be included in your application.html, which in turn will increase load times. Assuming that you want to use Bootstrap and Font-Awesome in your application, create the directory src/main/assets/ with the file application.scss containing: $icon-font-path: "../fonts/"; @import "lib/bootstrap-sass/stylesheets/bootstrap.scss"; $fa-font-path: "../fonts/"; @import "lib/font-awesome/scss/font-awesome.scss"; Then, add to your plugins.sbt: resolvers += Resolver.url( "GitHub repository" , url("http://shaggyyeti.github.io/releases") )(Resolver.ivyStylePatterns) addSbtPlugin("default" % "sbt-sass" % "0.1.9") And configure the output path of the produced CSS files in your Build.scala: resourceManaged in sass in Assets := cssPath Finally, add to your .gitignore: .sass-cache/ sbt-sass requires that the official Sass compiler is installed on your system. 4.7. Code sharing 21 4.6.2 Font-Awesome In order to automatically copy the Font-Awesome files to your configured path fontsPath, you can define a sbt-web task: val copyFontsTask = { val webJars = WebKeys.webJarsDirectory in Assets webJars.map { path => val fonts = path / "lib" / "font-awesome" / "fonts" fonts.listFiles().map { src => val tgt = fontsPath / src.getName IO.copyFile(src, tgt) tgt }.toSeq } }.dependsOn(WebKeys.webJars in Assets) And register it via: sourceGenerators in Assets <+= copyFontsTask 4.6.3 Artifacts When you issue the sbt task assets, sbt-web will generate your web artifacts, like CSS files. 4.7 Code sharing Scala.js provides a simple infrastructure to having separate sub-projects for JavaScript and JVM sources, which can share code. This is quite common for client-server applications which could have a common protocol specified in Scala code. You can work on your entire project in the IDE and easily jump between client and server code. Such projects are called cross-projects in Scala.js. You can find more information in the official documentation. import org.scalajs.sbtplugin.cross.CrossProject object Build extends sbt.Build { lazy val crossProject = CrossProject( "server", "client", file("."), CrossType.Full ) .settings( /* Shared settings */ ) .jsSettings( 22 Chapter 4. Build process /* Scala.js settings */ ) .jvmSettings( /* JVM settings */ ) lazy val js = crossProject.js lazy val jvm = crossProject.jvm } You will also need to move your current src/ folder to js/. The JVM project goes underneath jvm/src/main/scala/ and the shared source files underneath shared/src/main/scala/. 4.8 Colours Colours can be activated in sbt and Scala by setting two environment variables: export JAVA_OPTS="$JAVA_OPTS -Dscala.color" export SBT_OPTS="$JAVA_OPTS" Chapter 5 Router When developing applications that consist of more than one page, a routing system becomes inevitable. The router observes the fragment identifier of the browser URL. For example, in application.html#/page the part after the hash mark denotes the fragment identifier, that is /page. The router is initialised with a set of known routes. A fallback route may also be specified. 5.1 Interface The router may be used as follows: object Main extends Application { val main = Route("/", pages.Main) val test = Route("/test/:param", pages.Test) val test2 = Route("/test/:param/:param2", pages.Test) val notFound = Route("/404", pages.NotFound) val routes = Set(main, test, notFound) def main() { val router = Router(enabled, fallback = Some(notFound)) router.listen() } } routes denotes the set of enabled routes. It should also contain the notFound route. Otherwise, the page could not be displayed when #/404 is loaded. 5.2 Routes To construct a new route, pass the path and its reference to Route(). Pages may be overloaded with different paths as above with test and test2. A path consists of parts which are separated by slashes. For instance, the test route above has two parts: test and :param. A part beginning with a 24 Chapter 5. Router colon is a placeholder. Its purpose is to match the respective value in the fragment identifier and to bind it to the placeholder name. Note that a placeholder always refers to the whole part. A route can be instantiated by calling it, setting all of its placeholders: // Zero parameters val route: InstantiatedRoute = Main.main() // One parameter val route = Main.test("param", "value") // Multiple parameters val route: InstantiatedRoute = Main.test2( Map( "param" -> "value", "param2" -> "value2" ) ) // Redirect to `route` route.go() To query the instantiated parameters, access the args field in the first parameter passed to ready(). case class Test() extends Page { ... def ready(route: InstantiatedRoute) { log(route.args("param")) // Accessing optional parameters with get() // This returns an Option[String] log(route.args.get("param2")) } } 5.2.1 Design decisions Due to its limitations, the router could be efficiently implemented. Matching string-only parts in routes allows for better reasoning than regular expressions. When the router is constructed, it sorts all routes by their length and checks whether there are any conflicts. Also, the restriction that each parameter must be named makes code more readable when referring to parameters of an instantiated route. If validation of parameters is desired, this must be done in ready(). 5.3. Application provider 25 5.3 Application provider As the router defines usually the entry point of an application, Widok provides an application provider that enforces better separation: object Routes { val main = Route("/", pages.Main) ... val notFound = Route("/404", pages.NotFound) val routes = Set(main, ..., notFound) } object Main extends RoutingApplication( Routes.routes , Routes.notFound ) This is to be preferred when no further logic should be executed in the entry point prior to setting up the router. Chapter 6 Widgets A widget is a type-safe abstraction for an element displayed by the browser. The entire page layout is described using widgets. Thus, widget instantiations can be nested. Furthermore, custom widgets can be defined for better code reuse. A custom widget is usually composed of other widgets, changing their attributes such as CSS tags. Instead of accessing DOM elements using getElementById(), a widget doesn’t have any ID by default. Instead, it maintains a reference to the DOM element. This way, widgets that may have the same ID cannot collide and no ill-defined type-casts may occur. Mutation methods on a widget return the instance. This allows to arbitrarily nest widgets and change their attributes by chaining method calls, without the need to store the widget in a local variable. 6.1 HTML Widok provides widgets for many HTML elements. The bindings have a more intuitive naming than their HTML counterparts, although aliases were defined, too. The module the HTML widgets reside in is org.widok.bindings.HTML. If your project doesn’t define any conflicting types, it is safe to import the whole contents into the namespace. Alias Widget section header footer nav h1 h2 h3 h4 h5 h6 p Section Header Footer Navigation Heading.Level1 Heading.Level2 Heading.Level3 Heading.Level4 Heading.Level5 Heading.Level6 Paragraph Notes 28 6.1.1 Chapter 6. Widgets Alias Widget b strong i small br hr div span raw form button label a img radio checkbox file select text textarea password number option ul ol li table thead th tbody tr td cursor Text.Bold Text.Bold Text.Italic Text.Small LineBreak HorizontalLine Container.Generic Container.Inline Raw Form Button Label Anchor Image Input.Radio Input.Checkbox Input.File Input.Select Input.Text Input.Textarea Input.Password Input.Number Input.Select.Option List.Unordered List.Ordered List.Item Table Table.Head Table.HeadColumn Table.Body Table.Row Table.Column Cursor Notes span with innerHTML input with type="radio" input with type="checkbox" input with type="file" input with type="select" input with type="text" input with type="password" input with type="number" Aliases By importing org.widok.html._ you can use regular HTML tags instead of the more verbose notations. 6.2 Usage A widget inherits from the type Widget. Widgets are implemented as case classes and can therefore be used like regular function calls. The simplest widget is Raw() which allows to render HTML markup: 6.3. Writing custom widgets 29 val widget = Raw("<b><i>Text</i></b>") This is equivalent to: val widget = Text.Bold( Text.Italic("Text") ) Most widgets take children. If this is the case, child widgets are passed per convention with the constructor. Widget parameters are set using chainable method calls: Anchor( Text.Bold("Wikipedia") ).url("http://en.wikipedia.org/") .title("en.wikipedia.org") Use the += and -= operators to dynamically append or remove children to/from a widget. 6.2.1 Forms 6.2.1.1 Select boxes val languages = Seq("English", "French", "Spanish") select().options(languages.map(option(_))) Bi-directional binding: val numbers = Buffer("one", "two", "three") val selection = Var(Option.empty[String]) select().default("").bind(numbers, option, selection) Disabling elements: val enabled = Set("one", "three") select().default("").bind(numbers , (s: String) => option(s).enabled(enabled.contains(s)) , selection ) 6.3 Writing custom widgets Widgets should be designed with type-safety in mind. For example, the only children List.Unordered() accepts are instances of List.Item. When creating custom widgets, think of a class hierarchy which closely resembles the intended nesting. This will allow to catch usage errors during compile-time. A custom widget may be defined as follows: 30 Chapter 6. Widgets case class Panel(contents: View*) extends Widget[Panel] { val rendered = DOM.createElement("div", contents) css("panel") css("panel-default") } This corresponds to: <div class="panel panel-default"> ... rendered children ... </div> If a custom widget doesn’t need to be used as a type, it is sufficient to define it as a function: def Panel(contents: View*) = Container.Generic(contents: _*) .css("panel") .css("panel-default") 6.4 Binding to events A widget provides functionality to interact with the DOM. Methods with the prefix on*() exist for all events and take a callback. To listen to JavaScript’s onclick and ondblclick events of a button, write: Button("Click") .onClick(e => println("Click: " + e.timeStamp)) .onDoubleClick(e => println("Double click: " + e.timeStamp)) All DOM events are published as channels. A channel produces data which is passed on to its subscribers. The above is a shortcut for: val btn = Button("Click") btn.click.attach(...) btn.doubleClick.attach(...) This allows for an event to have multiple subscribers. This is important in web applications where data gets propagated to various layers of the application. For example, consider a shopping cart where the user updates the quantity of a certain product. At the same time the header needs to get updated with the newly calculated price. Making the DOM events available as streams widens the range of possibilities. As click is a stream of events, we could decide to take into account only the first event: btn.click.head.attach(e => println(e.timeStamp)) Another prominent use case of channels are dynamic changes of widgets, such as the visibility: 6.5. Composed widgets 31 HTML.Container.Generic("Button clicked") .show(btn.click.head.map(_ => false)) show() expects a Boolean channel. Depending on the values that are sent to the channel a widget is shown or not. Here, the widget is hidden as soon as we click the button. Data propagation mechanisms are explained in more detail in the next chapter ‘Reactive programming’. 6.5 Composed widgets Widok provides a couple of composed widgets without external rendering dependencies. They are defined in the package org.widok.widgets: • LoremIpsum: Prints Lorem Ipsum as a paragraph • Placeholder: Generates placeholder images on-the-fly 6.6 Implicits Widok defines a couple of implicits to make your code more concise. For example, if there is only one element you may drop the Inline() and write: def view() = HTML.Paragraph("Litwo! Ojczyzno moja!") Instead of: def view() = Inline(HTML.Paragraph("Litwo! Ojczyzno moja!")) Another implicit is evaluated here, which converts the string into a widget. There are also implicits to render buffers and channels. Chapter 7 Reactive programming 7.1 Motivation User interfaces are heavily data-driven. Values do not only need to be displayed once, but continuously modified as the user interacts with the interface. Interactivity requires data dependencies which ultimately lead to deeply intertwined code. Imperative code in particular is prone to this shortcoming since dependencies are hard to express. As web applications are increasingly more interactive, a flow-driven approach is desirable. Focussing on flows, the essence of the program will be to specify the data dependencies and how values propagate to the user interface and back. To tackle this issue, Widok follows a reactive approach. Consider an application to visualise stock market data. You are listening to a real-time stream producing values. Then, you want to display only the most current stock price to the user. This is solved by creating a ‘container’ which is bound to a DOM node. Whenever you feed a new stock price to it, an atomic update takes place in the browser, only changing the value of the associated DOM node. Another example is a monitoring service which allows you to control on-thefly the log level of a web application. A select box will list all possible log levels, like debug or critical. When the page is first loaded, it obtains the current log level from the server. Changing its value, however, must back-propagate and send the selection to the server. All other clients that are connected are notified of the change as well. For a simple application that illustrates client-side data propagation, see our TodoMVC implementation. 7.2 Concepts Reactive programming is a paradigm that focuses on: a) propagation of data, specifically changes, and b) data flow. Concretely, a data structure is said to be reactive (or streaming) if it models its state as streams. It does this by defining change objects (deltas) and mapping its 34 Chapter 7. Reactive programming operations onto these. The published stream is read-only and can be subscribed. If the stream does not have any subscribers, the state would not get persisted and is lost. Example: A reactive list implementation could map all its operations like clear() or insertAfter() on the two delta types Insert and Delete. A subscriber can interpret the deltas and persist the computed list in an in-memory buffer. Another property of a reactive data structure is that it does not only stream deltas, but also state observations. Sticking to the reactive list example, the deltas could allow streaming observations on the list’s inherent properties — one being the length, another the existence of a certain element, i.e. contains(value). Finally, a mutable reactive data structure is an extension with the sole difference that it maintains an internal state which always represents the computed result after a delta was received. This is a hybrid solution bridging mutable object-oriented objects with reactive data structures. The mutable variant of our reactive list could send its current state when a subscriber is registering. This ultimately leads to better legibility of code as subscribers can register at any point without caring whether the expected data has been propagated already. The second reason is that otherwise we would need multiple instances of mutual objects that interpret the deltas. This is often undesired as having multiple such instances incurs a memory overhead. To recap, a reactive data structure has four layers: • State: interpretation of the delta stream and “converting” it into a mutable object • Mutation operations: functions to produce deltas on the stream1 • Polling operations: blocking functions to query the state • Streaming operations: publish the state changes as a stream Obviously, the first three layers are the very foundation of object-orientation. It is different in that a) modifications are encoded as deltas and b) there are streaming operations. For now we just covered the first component of reactive programming: data propagation. The second cornerstone, data flow, is equally important, though. Streams describe data flow in terms of dependencies. Considering you want to plot a line as a graph using the formula 𝑦 = 𝑚𝑥 + 𝑏 and the user provides the values for 𝑚 and 𝑏, then you would wrap these inputs in channels and express the dependencies using combinators2 : val m = Opt[Int]() val b = Opt[Int]() // Produces when user provided `m` and `b` 1 2 These functions do not access the state in any way. The types in the code only serve illustration purposes 7.3. Reqirements 35 val mAndB: ReadChannel[(Int, Int)] = m.combine(b) // Function channel to calculate `y` for current input val y: ReadChannel[Int => Int] = mAndB.map { case (m, b) => (x: Int) => m * x + b } The user could listen to y and whenever it receives a new function, it can just call it for all the x in the interval of the shown graph. The example shows that messages in streams are not bound to data objects and even immutable functions could be passed around. The data propagation is illustrated by the following diagram: Figure 7.1 Change propagation for 𝑦 = 𝑚𝑥 + 𝑏 As soon as the user inserts a value for m as well as b, mAndB will produce a tuple. Then, y computes the final function. How channels work in detail is explained in the following sections. This example should only give an intuition of the fundamental concepts and how data dependencies are expressed. 7.3 Requirements The term “stream” was used several times. This term is polysemous and requires further explanation. In reactive programming there are different types of streams with severe semantic differences. Rx (Reactive Extensions) is a contract designed by Microsoft which calls these streams observables and defines rules how to properly interact with these. An observable can be subscribed to with an observer which has one function for the next element and two auxiliary ones for handling errors and the completion of the stream. Furthermore, observables are subdivided into cold and hot observables3 : 3 Source: leecampbell.blogspot.de (4th February 2015) 36 Chapter 7. Reactive programming • Cold observable: Streams that are passive and start publishing on request • Hot observable: Streams that are active and publish regardless of subscriptions There are extensions to Rx which introduce back-pressure4 to deal with streams that are producing values too fast. This may not be confused with back-propagation which describes those streams where the subscribers could propagate values back to the producer. This illustrates the diversity of streams. Due to the nature of Widok, streams had to be implemented differently from the outlined ones. Some of the requirements were: • • • • • lightweight design support for n-way binding usable as the basis for reactive data structures provide functionality for resource management require little boilerplate to define new operations To better differentiate from the established reactive frameworks, a less biased term than observable was called for and the reactive streams are therefore called channels in Widok. The requirements have been implemented as follows: A subscriber is just a function pointer (wrapped around a small object). A channel can have an unlimited number of children whereas each of the child channels knows their parent. A function for flushing the content of a channel upon subscription can be freely defined during instantiation5 . When a channel is destroyed, so are its children. Error handling is not part of the implementation. Similarly, no back-pressure is performed, but back-propagation is implemented for some selected operations like biMap(). For client-side web development only a small percentage of the interaction with streams require the features observables provide and this does not justify a more complex overall design. It is possible to use a full-fledged model like Rx or Monifu for just those areas of the application where necessary by redirecting (piping) the channel output. 7.4 Implementation This section explains how reactive data structures are implemented in Widok. The design decisions will be beneficial for you to better understand the API and to design your own reactive data structures. To leverage the capabilities of Scala’s type system, we decided to separate the logic into distinct traits. Each data structure defines six traits which, when 4 For instance, Monifu implements this feature. This function is called by attach() and produces multiple values which is necessary for some reactive data structures like lists. 5 7.4. Implementation 37 combined using the Cake pattern, yield a mutable reactive object without any additional code needed: ["" ["Read" ["Delta"] ["Poll"]] ["Write"] ["State" ["Disposable"]] ] For a hypothetical reactive data structure X you would define: object X { /* Define delta change type */ } /* Read/write access to state */ trait StateX[T] extends Disposable { /* This could be any kind of mutable storage */ val state: Storage[T] = ... /* Channel needed by the other traits */ val changes: Channel[X.Delta[T]] = ... /* Listen to `changes` and persist these in `state` */ changes.attach { ... } /* Free resources */ def dispose() { changes.dispose() } } /* The name may suggest otherwise, but it does not have any access * to the state; it only produces delta objects */ trait WriteX[T] { val changes: WriteChannel[X.Delta[T]] /* Also define operations to generate delta change objects */ } trait DeltaX[T] { val changes: ReadChannel[X.Delta[T]] /* Also define streaming operations that listen to changes * and process these */ } trait PollX[T] { val changes: ReadChannel[X.Delta[T]] /* Only read-only access is permitted here */ val state: Storage[T] /* Also define streaming operations that need the state */ 38 Chapter 7. Reactive programming } trait ReadX[T] extends DeltaX[T] with PollX[T] case class X[T]() extends ReadX[T] with WriteX[T] with StateX[T] A call to X() now yields a mutable reactive instance of our newly defined data structure. It would have been possible to implement X as a single class, but the chosen approach offers more flexibility. Each of the traits are exchangeable. There are more possibilities for object instantiations. For example, often a change stream is already available. In this case, DeltaX[T] could be instantiated with a custom value for changes. The caller can decide whether it needs any of the operations that PollX defines. Depending on this decision it will either buffer the data or not. This ultimately leads to a more memory-efficient design as the responsibility of memory allocation is often shifted to the caller. It is in some way similar to what Python allows with its yield expression. The delta trait has a read-only version of the change stream. It may define operations that apply transformations directly on the stream without building any complex intermediate results. A prominent example would be the higherorder function map(). As map() works on a per-element basis and does not need any access to the state, it can be implemented efficiently. As a consequence, this allows for chaining: list.map(f).map(g).buffer would compute the final list at the very end with the buffer call6 . Another motivating reason for this design is precisely the immutability of delta objects. The stream could be forwarded directly to the client which may render the elements in the browser on-the-fly. A similar use case would be persistence, for example in an asynchronous database. Scala’s type refinements for traits come in useful. X takes changes from StateX. It points to the same memory address in WriteX and DeltaX even though they are declared with different types. This is because Channel inherits both from WriteChannel and ReadChannel. The type-safety has an enormous benefit: A function can use a mutable stream internally, but returning the stream with writing capabilities would lead to unpredictable results. If the caller accidentally writes to this stream, this operation will succeed and in the worst case other subscribers receive the messages as well. As X inherits from ReadX, the function can be more explicit and revoke some of its capabilities simply by returning ReadX[T]. Similarly, if the caller should get writing capabilities and no read capabilities, this can be made explicit as well. This will make it trivial to find bugs related to reading and writing capabilities of streams directly during compile-time. And it makes in6 This is largely inspired by Scala’s SeqView. 7.5. Reactive data structures 39 terfaces more intelligible as a more specific type reduces the semantic space of a function. The third advantage is correctness: With the functionality separated into different traits, the proper behaviour can be ensured using property-based testing. Rules for the generation of delta objects could be defined7 . This stream is then used in StateX and all other traits can be tested whether they behave as expected. Presently, a very basic approach for property-based testing is implemented, but future versions will explore better ways to achieve a higher coverage. A variety of generally applicable reactive operations were specified as traits in org.widok.reactive. They can be seen as a contract and a reactive data structure should strive to implement as many as possible of these. Depending on conceptual differences, not every operation can be defined on a data structure, though. As the signatures are given, this ensures that all data structures use the operations consistently. Each of the traits group functions that are similar in their behaviour. Furthermore, the traits are combined into sub-packages which follow the properties mentioned at the beginning of the chapter, namely org.widok.reactive.{mutate, poll, stream}. To summarise, for a reactive data structure it is necessary to declare several traits with the following capabilities: Table 7.1 Traits and layers of a reactive data structure Delta Poll Read Write State State Mutation Polling Streaming no no no no yes no no no yes no no yes yes no no yes yes8 yes no no 7.5 Reactive data structures Widok currently implements four reactive data structures: • • • • 7 Channels: single values like T Buffers: lists like Seq[T] Dictionaries: maps like Map[A, B] Sets: reactive Set[T] For example, a Delta.Clear may only be generated after Delta.Insert. This is a practical decision. The Poll trait has direct access to the state. Thus, certain streaming operations can be implemented more efficiently. This should be avoided though as a delta stream would need to be persisted first in order for the Poll trait to be applicable. 8 40 Chapter 7. Reactive programming 7.6 Channels A channel models continuous values as a stream. It serves as a multiplexer for typed messages that consist of immutable values. Messages sent to the channel get propagated to the observers that have been attached to the channel — in the same order as they were added. It is possible to operate on channels with higher-order functions such as map(), filter() or take(). These methods may be chained, such that every produced values is propagated down the observer chain. Widok differentiates between two top-level channel types: • Channel: corresponds to a reactive T • Partial channel: corresponds to a reactive Option[T] There are four channel implementations: • • • • Channel: stream that does not persist its values Var: variable stream; its value is always defined and has an initial value9 LazyVar: stream for lazily evaluated variables PtrVar: stream for generic events10 Partial channels model optional values: • PartialChannel: base type • Opt: stream that has two states, either defined with a value or undefined Note: Opt[T] is merely a convenience type and Var[Option[T]] could be used, too. 7.6.1 Operations Here is a simple example for a channel that receives integers. We register an observer which prints all values on the console: val ch = Channel[Int]() ch.attach(println) ch := 42 // initialise // attach observer // produce value Note: The := operator is a shortcut for the method produce. The return values of operations are channels, therefore chaining is possible. Channels can be used to express data dependencies: 9 In Rx terms, Var would correspond to a cold observer as attaching to it will flush its current value. This is different from Channel which loses its messages when there are no subscribers. 10 It can be used to create delta channels from DOM variables by binding to the corresponding events that triggered by the value changes. For an example see Node.click. 7.6. Channels 41 val ch = Channel[Int]() ch.filter(_ > 3) .map(_ + 1) .attach(println) ch := 42 // 43 printed ch := 1 // nothing printed Use the method distinct to produce a value if it is the first or different from the previous one. A use case is to perform time-consuming operations such as performing HTTP requests only once for the same user input: ch.distinct.attach { query => // perform HTTP request } Considering that you want to observe multiple channels of the same type and merge the produced values, you can use the | operator11 : val a = Channel[String]() val b = Channel[String]() val c = Channel[String]() val merged: ReadChannel[String] = a | b | c It must be noted that streaming operations have different semantics than their non-reactive counterparts. For brevity, only certain combinators are covered by the manual. For the rest, please refer to the ScalaDoc documentation. 7.6.2 State channels For better performance, Channel does not cache the produced values. Some operations cannot be implemented without access to the current value, though. And often it is necessary to poll the current value. For these reasons state channels such as Var or Opt were introduced. The following example visualises the different behaviours: val ch = Var(42) ch.attach(println) // prints 42 val ch2 = Channel[Int]() ch2 := 42 // Value is lost as ch2 does not have any observers ch2.attach(println) update() is an operation that requires that the produced values are persisted. update() takes a function which modifies the current value: 11 It is an alias for the method merge() 42 Chapter 7. Reactive programming val ch = Var(2) ch.attach(println) ch.update(_ + 1) // produces 3 A partially-defined channel (Opt) is constructed as follows: val x = Opt[Int]() x := 42 Alternatively, a default value may be passed: val x = Opt(42) A state channel provides all the methods a channel does. Var[T] and Opt[T] can be obtained from any existing ReadChannel[T] using the method cache: val chOpt = ch.cache val chVar = ch.cache(42) // Opt[Int] // Var[Int] chOpt is undefined as long as no value was produced on ch. chVar will be initialised with 42 and the value is overridden with the first produced value on ch. biMap() allows to implement a bi-directional map, i.e. a stream with backpropagation: val map = Map(1 -> "one", 2 -> "two", 3 -> "three") val id = Var(2) val idMap = id.biMap( (id: Int) => map(id) , (str: String) => map.find(_._2 == str).get._1) id .attach(x => println("id : " + x)) idMap.attach(x => println("idMap: " + x)) idMap := "three" The output is: id : idMap: id : idMap: 2 two 3 three biMap() can be used to implement a lens as a channel. The following example defines a lens for the field b. It has a back channel that composes a new object with the changed field value. case class Test(a: Int, b: Int) val test = Var(Test(1, 2)) val lens = test.biMap(_.b, (x: Int) => test.get.copy(b = x)) test.attach(println) lens := 42 // produces Test(1, 42) 7.6. Channels 43 A LazyVar evaluates its argument lazily. In the following example, it points to a mutable variable: var counter = 0 val ch = LazyVar(counter) ch.attach(value => { counter += 1; println(value) }) ch.attach(value => { counter += 1; println(value) }) // prints 0 // prints 1 7.6.3 Call semantics Functions passed to higher-order operations are evaluated on-demand: val ch = Var(42).map(i => { println(i); i + 1 }) ch.attach(_ => ()) // prints 42 ch.attach(_ => ()) // prints 42 The value of a state channel gets propagated to a child when it requests the value (flush()). In the example, Var delays the propagation of the initial value 42 until the first attach() call. attach() goes up the channel chain and triggers the flush on each channel. In other words, map(f) merely registers an observer, but doesn’t call f right away. f is called each time when any of its direct or indirect children uses attach(). This reduces the memory usage and complexity of the channel implementation as no caching needs to be performed. On the other hand, you may want to perform on-site caching of the results of f, especially if the function is sideeffecting. The current value of a state channel may be read at any time using .get (if available) or flush(). There are operations that maintain state for all observers. For example, skip(n) counts the number of produced values12 . As soon as n is exceeded, all subsequent values are passed on. The initial attach() calls ignore the first value (42), but deal with all values after that: val ch = Var(42) val dch = ch.drop(1) dch.attach(println) dch.attach(println) ch := 23 // produces 23 twice 7.6.4 Cycles Certain propagation flows may lead to cycles: val todo = Channel[String]() todo.attach { t => println(t) 12 n must be greater than 0. 44 Chapter 7. Reactive programming todo := "" } todo := "42" Setting todo will result in an infinite loop. Such flows are detected and will lead to a run-time exception. Otherwise, the application would block indefinitely which makes debugging more difficult. If a cycle as in the above example is expected, use the combinator filterCycles to make it explicit. This will ignore value propagations caused by a cycle. 7.7 Buffers Buffers are reactive lists. State changes such as row additions, updates or removals are encoded as delta objects. This allows to reflect these changes directly in the DOM, without having to re-render the entire list. Buffer[T] is therefore more efficient than Channel[Seq[T]] when dealing with list changes. The following example creates a buffer with three initial rows, observes the size13 and then adds another row: val buf = Buffer(1, 2, 3) buf.size.attach(println) // Prints 3 buf += 4 // Inserts row 4, prints 4 All polling methods have a dollar sign as suffix $: val buf = Buffer(1, 2, 3) println(buf.size$) // Prints 3 An example of using removeAll(): val buf = Buffer(3, 4, 5) val mod2 = buf.filter$(_ % 2 == 0) buf.removeAll(mod2.get) Note: Buffer will identify rows by their value if the row type is a case class. In this case, operations like insertAfter() or remove() will always refer to the first occurrence. This is often not desired. An alternative would be to define a class instead or to wrap the values in a Ref[_] object: val todos = Buffer[Ref[Todo]]() ul( todos.map { case tr @ Ref(t) => li( // Access field `completed` 13 size returns a ReadChannel[Int]. 7.8. Dictionaries 45 checkbox().bind(t.completed) // remove() requires reference , button().onClick(_ => todos.remove(tr)) ) } ) The value of a Ref[_] can be obtained by calling get. However, it is more convenient to do pattern matching as in the example. You can observe the delta objects produced by a buffer: val buf = Buffer(1, 2, 3) buf.changes.attach(println) buf += 4 buf.clear() This prints: Insert(Last(),1) Insert(Last(),2) Insert(Last(),3) Insert(Last(),4) Clear() All streaming operations that a buffer provides are implemented in terms of the changes channel. 7.8 Dictionaries Dictionaries are unordered maps from A to B. Widok abbreviates the type as Dict. 7.9 Sets Reactive sets are implemented as BufSet14 . 7.10 Binding to Widgets Reactive data structures interact with user interfaces. These data structures are usually set up before the widgets, so that they can be referenced during the widget initialisation. The most common use case is binding channels to DOM nodes: 14 This name was chosen as Set would have collided with Scala’s implementation. 46 Chapter 7. Reactive programming val name = Channel[String]() def view() = h1("Hello ", name) This example shows one-way binding, i.e. uni-directional communication. name is converted into a widget, which observes the values produced on name and updates the DOM node with every change. This is realised by an implicit and translates to span().subscribe(name). Another implicit is provided for widget channels, so you can use map() on any channel to create a widget stream. The widgets are rendered automatically. If the widget type stays the same and it provides a subscribe() method, use it instead. On form fields you will need to call subscribe() by yourself: val name = Channel[String]() def view() = text().subscribe(name) Two-way binding is achieved by using the method bind() instead of subscribe(). The only difference is that changes are back-propagated. This lets you define multiple widgets which listen to the same channel and synchronise their values: val ch = Var("Hello world") def view() = Inline( Input.Text().bind(ch) , Input.Text().bindEnter(ch) ) This creates two text fields. When the page is loaded, both have the same content: “Hello world”. When the user changes the content of the first field, the second text field is updated on-the-fly. The second field requires an enter press before the change gets propagated to the first text field. Each widget has methods to control its attributes either with static values or channels. For example, to set the CSS tag of a widget use widget.css("tag1", "tag2"). This method is overloaded and you could also pass a ReadChannel[Seq[String]]. Passing channels is useful specifically for toggling CSS tags with cssState(). It sets CSS tags only when the expected channel produces true, otherwise it unsets the tags: widget.cssState(editing, "editing", "change") Other useful functions are show() and visible(). The former sets the CSS property display to none, while the latter sets visibility to hidden to hide a widget. As reactive data structures provide streaming operations that return channels, these can be used in widgets. Consider the method isEmpty that is defined on buffers. You could show a span depending on whether the list is empty or not: 7.11. Tests 47 val buf = Buffer[Int]() def view() = Inline( span("The list is empty.") .show(buf.isEmpty) , span("The list is not empty.") .show(buf.nonEmpty) , button().onClick(_ => buf += 42) , button().onClick(_ => buf.clear()) ) 7.11 Tests The proper functioning of each operation is backed by test cases. These provide complementary documentation. Chapter 8 Bindings This chapter deals with third-party CSS frameworks for which Widok provides typed bindings. 8.1 Bootstrap Bootstrap is a “framework for developing responsive, mobile first projects on the web.” See the project page for more information. The bindings require Bootstrap v3.3.2. To use the bindings, it may be desirable to import its entire namespace: import org.widok.bindings.Bootstrap._ Bootstrap’s components closely resemble their HTML counterparts. For example: <button type="button" class="btn btn-default"> <span class="glyphicon glyphicon-align-left"></span> </button> This translates to: Button(Glyphicon.AlignLeft()) Bootstrap widgets expect a list of arguments which denotes child widgets. The configuration can be controlled by usual method calls on the widget. If a widget conceptually doesn’t have any children, then its arguments are used for the configuration instead. 8.1.1 External stylesheet For the bindings to work without sbt-web, add the latest Bootstrap stylesheet to the head tag of your application.html file. You can either keep a local copy of the stylesheet or use a CDN: <link rel="stylesheet" 50 Chapter 8. Bindings href="https://maxcdn.bootstrapcdn.com/ bootstrap/3.3.2/css/bootstrap.min.css"> Please keep in mind that the pre-built stylesheet comes with certain restrictions, like the font path being hard-coded. 8.1.2 Image Example: Image("http://lorempixel.com/400/200/") .responsive(true) .shape(Shape.Circle) 8.1.3 Label Every widget is equipped with a method label(value: Style) that allows attaching a Bootstrap label like label-info to it: span("Text").label(Label.Info) 8.1.4 Text style textStyle() is available on every widget: span("Text") .textStyle(TextStyle.Small) .textStyle(TextStyle.Danger) 8.1.5 Glyphicon Glyphicons are simple function calls, for example: Glyphicon.User(). All Bootstrap glyphicons are supported, although the naming was changed to camelcase. 8.1.6 Form Forms can be validated on-the-fly. For each field a custom validator may be written. validator.errors() will render the textual error. Instead of showing the error underneath a field, this call can be placed anywhere, for instance to centralise all errors. validate() is defined on every widget and sets the has-error CSS tag if a field is invalid. The initial validation is triggered when the user presses the submit button. validator.check() will perform the first validation and return true if all fields are valid. If at least one input field was invalid, the submit button is kept disabled as long as the input stays wrong. val username = Var("") val displayName = Var("") 8.1. Bootstrap def validateNonEmpty(value: String) = if (value.trim.isEmpty) Some("Field cannot be empty") else None implicit val validator = Validator( Validation(username, validateNonEmpty) , Validation(displayName, validateNonEmpty) ) Container( FormGroup( InputGroup( InputGroup.Addon(Glyphicon.Globe()) , Input.Text() .placeholder("Display name") .size(Size.Large) .tabIndex(1) .bind(displayName) ) , validator.errors(displayName) ).validate(displayName) , FormGroup( InputGroup( InputGroup.Addon(Glyphicon.User()) , Input.Text() .placeholder("Username") .size(Size.Large) .tabIndex(2) .bind(username) ) , validator.errors(username) ).validate(username) ) , Button("Submit").onClick { _ => if (validator.check()) println("Ok") }.enabled(validator.maySubmit) ) Other widgets related to forms are: • HorizontalForm() • FormGroup() • ControlLabel() 51 52 Chapter 8. Bindings • • • • • • • • • • • • 8.1.7 InputGroup() InputGroup.Addon() Input.Text() Input.Textarea() Input.Password() Input.Select() Input.Number() Button() Button.Group() Button.Toolbar() Checkbox() Radio() Layout Layout-related widgets are: • • • • • 8.1.8 Footer() Container() PageHeader() Lead() PullRight() Navigation Example: val tab1 = Navigation.Tab("Tab 1") val tab2 = Navigation.Tab("Tab 2") val currentTab = Var(tab1) Navigation.renderTabs(Seq(tab1, tab2), currentTab) 8.1.9 Navigation bar Example for the NavigationBar widget: NavigationBar( Container( NavigationBar.Header( NavigationBar.Toggle() , NavigationBar.Brand("Brand name") ) , NavigationBar.Collapse( NavigationBar.Elements( Item(a(Glyphicon.Dashboard(), " Page 1").url(Routes.page1())) 8.1. Bootstrap 53 , Item(a(Glyphicon.Font(), " Page 2").url(Routes.page2())) , NavigationBar.Right( NavigationBar.Navigation( NavigationBar.Form( FormGroup( InputGroup(Input.Text()) , Button(Glyphicon.Search()) ).role(Role.Search) ) ) ) ) ) ) ) As probably more than one page is going to use the same header, you should create a trait for it. For example, you could define CustomPage with the header. Then, it only requires you to define the page title and body for every page. 8.1.10 Alert Example: Alert("No occurrences").style(Style.Danger) 8.1.11 Progress bar Example: val percentage = Var(0.1) ProgressBar("Caption") .style(percentage.map(p => if (p < 0.5) Style.Warning else Style.Su .progress(percentage) 8.1.12 Panel Example: Panel( Panel.Heading(Panel.Title3("Panel title")) , Panel.Body("Panel text") ).style(Style.Danger) 8.1.13 Pagination Example: 54 Chapter 8. Bindings Pagination( Pagination.Item(a("«")).disabled(true) , Pagination.Item(a("1")).active(true) , Pagination.Item(a("2")) , Pagination.Item(a("»")) ) 8.1.14 List groups Example: ListGroup( ListGroup.Item(a("Item 1")).active(true), , ListGroup.Item(a("Item 2")) , ListGroup.Item(a("Item 3")) ) 8.1.15 Grids Example: Grid.Row( Grid.Column( "Grid contents" ).column(Size.ExtraSmall, 6) .column(Size.Medium, 3) ) 8.1.16 Button Example: Button(Glyphicon.User()) .size(Size.ExtraSmall) .onClick(_ => println("Clicked")) .title("Button title") AnchorButton provides the functionality of Button and HTML.Anchor: Button(Glyphicon.User()) .size(Size.ExtraSmall) .url("http://google.com/") .title("Button title") 8.1.17 Example: Checkbox 8.1. Bootstrap 55 val checked = Var(true) Checkbox( checkbox().bind(checked) , "Remember me" ) 8.1.18 Modal It is most convenient to use the ModalBuilder to create modals. On the same page you can define several modals. For example: val modal: ModalBuilder = ModalBuilder( Modal.Header( Modal.Close(modal.dismiss) , Modal.Title("Modal title") ) , Modal.Body("Modal body") , Modal.Footer( Button("Submit").onClick(_ => modal.dismiss()) ) ) def body() = div( Button("Open").onClick(_ => modal.open()) , modal /* Each modal must be added to the body. It is hidden by defa ) 8.1.19 Media Example: Media( Media.Left(Placeholder("cover", Placeholder.Size(150, 80))) , Media.Body( Media.Heading("Heading") , "Description" ) ) 8.1.20 Breadcrumb Breadcrumb( Item(a("Item 1")) , Item(a("Item 2")).active(true) ) 56 Chapter 8. Bindings 8.1.21 Table To use a Bootstrap table, use Table() and Table.Row() which in contrast to table() and tr() provide Bootstrap-related styling options: Table( thead( tr( th("Date") , th("Quantity") ) ) , tbody( Table.Row(td("01.01.2015"), td("23")).style(Style.Info) , Table.Row(td("02.01.2015"), td("42")).style(Style.Danger) ) ) 8.1.22 Typeahead Example: val allMatches = Map(0 -> "First", 1 -> "Second", 2 -> "Third") def matches(input: String): Buffer[(Int, String)] = Buffer.from(allMatches.filter { case (k, v) => v.startsWith(input) }.toSeq) def select(selection: Int) { println(s"Selection: $selection") } Typeahead(Input.Text(), matches, select) 8.2 Font-Awesome The Font-Awesome bindings include all icons in camel-case notation. For convenience, rename the object when you import it: import org.widok.bindings.{FontAwesome => fa} Using the user icon is as simple as writing: fa.User() This translates to: <span class="fa fa-user"></span> Chapter 9 Validation Generic client-side dynamic form field validation can be managed by the Validator class. It is constructed with a number of tuples (ReadChannel, Seq(validators)). When data is read from a channel it is validated against all associated validations. 9.1 Validation channels The validator exposes the following channels that can be used in widgets to add validation in a reactive way: validations ~ Dict that is holding the validation results for any channel that has received updates. This channel can also be used for dirty field-check (all fields present in this Dict are dirty). errors ~ Filtered version of validations that only includes failing validations. valid ~ Boolean channel that indicates if all fields in this validator are valid. valid(channel) ~ Boolean channel that indicates the validation status of channel invalid(channel) ~ Boolean channel that indicates the validation status of channel combinedErrors(channels*) ~ Buffer[String] with the combined validation errors for the given channels 9.2 Form validation example package example import import import import org.widok._ org.widok.html._ org.widok.validation._ org.widok.validation.Validations._ case class TestPage() extends Page { val username = Var("") val password = Var("") 58 Chapter 9. Validation val passwordVerify = Var("") val samePasswords = password.combine(passwordVerify).cache(("", "")) val validator = Validator( username -> Seq(RequiredValidation(), EmailValidation()), password -> Seq(RequiredValidation(), MinLengthValidation(5)), samePasswords -> Seq(SameValidation()) ) override def view(): View = div( form( label("Username").forId("username"), text().id("username").bind(username), // display the validation error messages provided by the failing validation(s) validator.errors.value(username).values.map { case None => div() case Some(v) => div(s"Validation error: ${v.mkString(", ")}") }, // set "invalid" css class on password fields when validation fails label("Password").forId("password"), text().id("password").bind(password).cssState(validator.invalid(password), "inv label("Repeat password").forId("passwordVerify"), text().id("passwordVerify").bind(passwordVerify).cssState(validator.invalid(pas // show span when passwords differs span("Passwords must match").show(validator.invalid(samePasswords)), // only enabled when form is valid. // call validate so that validation is triggered for any non-dirty fields button("Register").enabled(validator.valid).onClick(_ => if (validator.validate ) ) def signup() = {} override def ready(route: InstantiatedRoute) {} } Chapter 10 Validations Validations are classes derived from the Validation base class and are used to validate some input. The input can be of any type, but in the context of form validation they normally validates a ReadChannel[String] channel. The Validation base class has one abstract member (validate) that performs the actual validation of the provided input. There are a number of provided validations in org.widok.validation.Validations. 10.1 Error messages A customised error message can be provided when initialising a Validation. This error message is interpolated using variables that are defined in each Validation. For example: MinValidation(5, "Too few characters, minimum is: #{min}.. You wrote: #{value}") Chapter 11 Developing If you would like to participate or try out development releases, please read this chapter. 11.1 Prerequisites io.js must be installed. 11.2 API Widok is still in its early stages and the API may be subject to changes. Any recommendations for improvements are welcome. 11.3 Compilation To work on the development version of Widok, run the following commands: $ $ $ $ git clone [email protected]:widok/widok.git cd widok npm install source-map-support sbt publish-local This compiles the latest version of Widok and installs it locally. To use it, make sure to also update the version in your project accordingly. Remember that your project’s Scala.js version must match the version Widok is built for. 11.4 Releases The versioning scheme follows the format releaseSeries.relativeVersion. Thus, v0.2.0 defines the version 0 of the release series 0.2. All versions within the same release series must be binary-compatible. If any of the dependencies (like Scala.js) are updated, the release series must be increased as well. Widok releases are published to Maven Central. 62 Chapter 11. Developing 11.5 Manual Since v0.2, the manual is stored in the same repository as the code. This enables you to commit code coupled with the corresponding documentation changes. At any time, the manual should always reflect the current state of the code base. Chapter 12 Changelog The changelog lists all major changes between releases. For more details, please see the Git changelog. 12.1 Version 0.2.2 12.1.1 General work • Ported to Scala.js v0.6.3 • Migrate to scalajs-dom v0.8.1 • Set up Google Group for discussions 12.1.2 Channels • BufSet: Use Set[T] instead of Seq[T] 12.1.3 Widgets • Provide blur and submit event • Input: Trigger value change on blur and change (required for autocompletion) • Input: Provide reactive counterpart for placeholder() • Add Time widget • Take options instead of strings in Select.{options, bind}() • Fix List.Items() in Safari • Add hide() and invisible() (negated show() and visible()) • Add support for field validation 12.1.4 Bindings • Generate functions instead of case classes • Bootstrap: Allow column buffers in grid rows • Bootstrap: Use buffer for Typeahead options (allows updating options list asynchronously) • Bootstrap: Support navbar-left and navbar-text 64 Chapter 12. Changelog 12.2 Version 0.2.1 12.2.1 General work • Ported to Scala.js v0.6.2 • Use io.js for running the test cases 12.2.2 • • • • • • Allow the use of Buffer.map() in tbody Provide autofocus() and placeholder() on Textarea Widget: Add += and -= Widget: Support drag events Widget: Don’t wrap ReadChannel[Widget[_]] in spans Widget: Add focus() 12.2.3 • • • • • Event propagation Opt: Fix undefined$ Dict: Fix size Dict: Generate fine-grained delta objects in toBuffer Dict: Add sortBy() Buffer: Add flatMapSeq() (in consistency with Channel) 12.2.4 • • • • • • • • Widgets Bindings Bootstrap: Implement AnchorButton Bootstrap: Implement Number Bootstrap: Implement Textarea Bootstrap: Implement Radio Bootstrap: Add argument to Checkbox Bootstrap: Fix positioning of Typeahead wrt. scrolling Bootstrap: Backport upstream changes to fix scrolling behaviour in modals Bootstrap: Remove TextContainer, use textStyle() instead 12.2.5 Routing • Support percent-encoded arguments 12.3 Version 0.2.0 12.3.1 General work • Relicensed as Apache v2.0 • Ported to Scala 2.11.5 • Ported to Scala.js v0.6.0 12.3. Version 0.2.0 65 • Ported to scalajs-dom v0.8.0 • Uses minitest for the test cases • Shapeless dependency dropped; the lens macros resulted in slower compilation and had insufficient IDE support. At the time of writing, Shapeless was not yet ported to Scala.js v0.6.0. Var() can be used as a drop-in replacement for more flexibility and better performance. As a result of dropping Shapeless, the Sonatype resolver is not needed anymore. • There is a Gitter channel for conversations about Widok • JVM support: Widok is now built for the JVM, allowing you to use the reactive library on the server, too. Upcoming versions will further focus on network transparency (see #26). In the future, the widget library will be usable under the JVM as well (see #25). • The manual is now part of the code repository. Pandoc is used for compilation. Current targets are PDF, HTML and EPUB. 12.3.2 Event propagation This version includes a complete redesign of the event propagation mechanisms. The previous implementation was merely a proof of concept and therefore had a couple of design issues. Changes include: • More abstraction layers for better type-safety and modularity. The whole API now strictly distinguishes between ReadChannels and WriteChannels. By looking at the types of a function, it is now more predictable what the passed channel is being used for. • Improve naming: Var and Buffer as opposed to CachedChannel and CachedAggregate • Aggregates were dropped. Instead, reactive data structures were introduced, which encode their changes as delta objects and are built on top of channels. Publishing changes as channels allows to persist the stream or send it directly to the client. Furthermore, change transformers could be written, for example one that cancels out common sequences like for better rendering performance: Change.Insert(Position.Last(), element) Change.Remove(element) • Reactive data structures for buffers, dictionaries and sets were introduced. • A reactive Rose tree implementation was added. • Back propagation for aggregates was entirely removed. For sequences it offered little benefit. Previously, back propagation was only used for deletions. Abandoning this feature makes it easier to implement new combinators. For example, filter() became significantly shorter and better to comprehend. 66 Chapter 12. Changelog • Partially defined streams were introduced (Opt) • Created traits that fully specify the functionality of reactive combinators (see Combinators.scala). As most combinators are implemented by more than one class, this will ensure consistency in their usage. They could also be used outside of Widok as the basis for alternative implementations. • Work on resource management has begun; dispose() can be called on channels to clear its subscription in the parent. • Property-based testing for channels and aggregates (basic operations only) • Cycle detection for channels. As cycles may be well-defined under certain circumstances, they can be ignored using filterCycles (see the TodoMVC application for an example) 12.3.3 Widgets Some work also went into the widget subsystem: • New implicits to render numerical values, buffers and sequences without type conversions • DOM event listeners are now available on all widgets and made available as channels. This implies that more than one listener can be attached to the same event. Also, on*() short-hands were created for all events. This makes registering a click event listeners as simple as writing .onClick(ev => ...). • All DOM elements have short aliases which are equal to their HTML tag names. Instead of Input.Text(), you could now write text(). As these tag names may conflict in certain cases, an explicit import is necessary: import org.widok.html._ • The Bootstrap bindings were completely revised. No type casts are necessary anymore and most widgets are now case classes instead of functions. This makes the widgets easier in their usage. The bindings now cover a large percentage of the functionality Bootstrap provides. Newly added widgets are: • • • • • • • • • Bootstrap: Bootstrap: Bootstrap: Bootstrap: Bootstrap: Bootstrap: Bootstrap: Bootstrap: Bootstrap: Modal dialogue Typeahead field Form validation Table Pagination Panel Button group and toolbar Breadcrumb Media object 12.3. Version 0.2.0 67 • • • • • • HTML: File input HTML: Password field HTML: Select box HTML: Radio button HTML: HTML5 number input field Widget container Inline() (can be used when a span() would introduce undesired design glitches) • Image placeholder • Lorem ipsum Code generators were introduced for higher reliability of the bindings. sbtweb is used internally to obtain external web dependencies. As part of the build process, Scala files are then created. Auto-generated bindings are provided for: • Bootstrap’s glyphicons • Font-Awesome 12.3.4 Routing • In v0.1 the DOM nodes of all routes were initialised when the page loads. This may result in cycle errors. Now, a route change also reinitialises the entire page. • The method destroy() can be overridden to react on page changes, for example for manual resource management 12.3.5 Trivia • The API changes significantly shrinked TodoMVC’s file size from 3396 bytes (92 lines) to 3059 bytes (89 lines). • Widok can be used to develop desktop applications with NW.js as shown by poliglot-ui Chapter 13 Support For discussions, please refer to our Google Group. For real-time support, you may also visit our Gitter channel. Bugs should be reported in the GitHub issue tracker.