March 2015

Widok User Manual v0.2.2

Tim Nieradzik

University of Bremen

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.

Introduction

Widok is a reactive web framework for the JVM and Scala.js. Its key concepts are:

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.

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 compile-time. 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.

Getting Started

This chapter will guide you through creating your first Widok project.

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.

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 sub-directory project with the two files plugins.sbt and Build.scala:

logLevel := Level.Warn

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.3")
import sbt._
import sbt.Keys._
import org.scalajs.sbtplugin._
import 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/.

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 {
  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>

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>

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().

Concepts

In this chapter we will mention all key concepts of Widok. The following chapters will deal with these topics in detail.

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

Single-page applications

The application from the previous chapter roughly looked like this:

package example

import org.widok._

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.

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.

Create a new file for each page:

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 hand-written 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.

package example.pages

import org.widok._

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.

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.

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.

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.

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.

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)

  , 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.

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.

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"

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/:

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.

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.

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.

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.

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:

To do so, specify the paths in the build configuration as follows:

val outPath = new File("web")
val jsPath = outPath / "js"
val cssPath = outPath / "css"
val 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/

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)

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.

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.

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

Artifacts

When you issue the sbt task assets, sbt-web will generate your web artifacts, like CSS files.

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(
      /* 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/.

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"

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.

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.

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 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"))
  }
}

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().

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.

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.

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 Notes
section Section
header Header
footer Footer
nav Navigation
h1 Heading.Level1
h2 Heading.Level2
h3 Heading.Level3
h4 Heading.Level4
h5 Heading.Level5
h6 Heading.Level6
p Paragraph
b Text.Bold
strong Text.Bold
i Text.Italic
small Text.Small
br LineBreak
hr HorizontalLine
div Container.Generic
span Container.Inline
raw Raw span with innerHTML
form Form
button Button
label Label
a Anchor
img Image
radio Input.Radio input with type="radio"
checkbox Input.Checkbox input with type="checkbox"
file Input.File input with type="file"
select Input.Select input with type="select"
text Input.Text input with type="text"
textarea Input.Textarea
password Input.Password input with type="password"
number Input.Number input with type="number"
option Input.Select.Option
ul List.Unordered
ol List.Ordered
li List.Item
table Table
thead Table.Head
th Table.HeadColumn
tbody Table.Body
tr Table.Row
td Table.Column
cursor Cursor

Aliases

By importing org.widok.html._ you can use regular HTML tags instead of the more verbose notations.

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:

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.

Forms

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
)

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:

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")

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:

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.

Composed widgets

Widok provides a couple of composed widgets without external rendering dependencies. They are defined in the package org.widok.widgets:

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.

Reactive programming

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-the-fly 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.

Concepts

Reactive programming is a paradigm that focuses on:

  1. propagation of data, specifically changes, and
  2. 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 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:

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 y = mx + b and the user provides the values for m and b, then you would wrap these inputs in channels and express the dependencies using combinators5:

val m = Opt[Int]()
val b = Opt[Int]()

// Produces when user provided `m` and `b`
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:

Change propagation for y = mx + b

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.

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 observables6:

There are extensions to Rx which introduce back-pressure7 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:

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 instantiation8. 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.

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 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 */
}

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 higher-order 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 call9.

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 interfaces 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 defined10. 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:

Traits and layers of a reactive data structure
State Mutation Polling Streaming
Delta no no no yes
Poll no no yes yes11
Read no no yes yes
Write no yes no no
State yes no no no

Reactive data structures

Widok currently implements four reactive data structures:

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:

There are four channel implementations:

Partial channels model optional values:

Note: Opt[T] is merely a convenience type and Var[Option[T]] could be used, too.

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]()  // initialise
ch.attach(println)       // attach observer
ch := 42                 // 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:

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 | operator14:

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.

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:

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      // Opt[Int]
val chVar = ch.cache(42)  // 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 back-propagation:

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   : 2
idMap: two
id   : 3
idMap: 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)

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) })  // prints 0
ch.attach(value => { counter += 1; println(value) })  // prints 1

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 side-effecting.

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 values15. 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

Cycles

Certain propagation flows may lead to cycles:

val todo = Channel[String]()
todo.attach { t =>
    println(t)
    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.

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 size16 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`
      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.

Dictionaries

Dictionaries are unordered maps from A to B. Widok abbreviates the type as Dict.

Sets

Reactive sets are implemented as BufSet17.

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:

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:

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())
)

Tests

The proper functioning of each operation is backed by test cases. These provide complementary documentation.

Bindings

This chapter deals with third-party CSS frameworks for which Widok provides typed bindings.

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.

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"
  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.

Image

Example:

Image("http://lorempixel.com/400/200/")
  .responsive(true)
  .shape(Shape.Circle)

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)

Text style

textStyle() is available on every widget:

span("Text")
  .textStyle(TextStyle.Small)
  .textStyle(TextStyle.Danger)

Glyphicon

Glyphicons are simple function calls, for example: Glyphicon.User(). All Bootstrap glyphicons are supported, although the naming was changed to camel-case.

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("")

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:

Layout

Layout-related widgets are:

Example:

val tab1 = Navigation.Tab("Tab 1")
val tab2 = Navigation.Tab("Tab 2")
val currentTab = Var(tab1)

Navigation.renderTabs(Seq(tab1, tab2), currentTab)

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()))
      , 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.

Alert

Example:

Alert("No occurrences").style(Style.Danger)

Progress bar

Example:

val percentage = Var(0.1)
ProgressBar("Caption")
  .style(percentage.map(p => if (p < 0.5) Style.Warning else Style.Success))
  .progress(percentage)

Panel

Example:

Panel(
  Panel.Heading(Panel.Title3("Panel title"))
, Panel.Body("Panel text")
).style(Style.Danger)

Pagination

Example:

Pagination(
  Pagination.Item(a("«")).disabled(true)
, Pagination.Item(a("1")).active(true)
, Pagination.Item(a("2"))
, Pagination.Item(a("»"))
)

List groups

Example:

ListGroup(
  ListGroup.Item(a("Item 1")).active(true),
, ListGroup.Item(a("Item 2"))
, ListGroup.Item(a("Item 3"))
)

Grids

Example:

Grid.Row(
  Grid.Column(
    "Grid contents"
  ).column(Size.ExtraSmall, 6)
   .column(Size.Medium, 3)
)

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")

Checkbox

Example:

val checked = Var(true)
Checkbox(
  checkbox().bind(checked)
, "Remember me"
)

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 default. */
)

Media

Example:

Media(
  Media.Left(Placeholder("cover", Placeholder.Size(150, 80)))
, Media.Body(
    Media.Heading("Heading")
  , "Description"
  )
)
Breadcrumb(
  Item(a("Item 1"))
, Item(a("Item 2")).active(true)
)

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)
  )
)

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)

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>

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.

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

Form validation example

package example

import org.widok._
import org.widok.html._
import org.widok.validation._
import org.widok.validation.Validations._

case class TestPage() extends Page {
  val username = Var("")
  val password = Var("")
  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), "invalid"),

      label("Repeat password").forId("passwordVerify"),
      text().id("passwordVerify").bind(passwordVerify).cssState(validator.invalid(passwordVerify), "invalid"),

      // 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()) signup())
    )
  )

  def signup() = {}

  override def ready(route: InstantiatedRoute) {}
}

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.

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}")

Developing

If you would like to participate or try out development releases, please read this chapter.

Prerequisites

io.js must be installed.

API

Widok is still in its early stages and the API may be subject to changes. Any recommendations for improvements are welcome.

Compilation

To work on the development version of Widok, run the following commands:

$ git clone git@github.com: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.

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.

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.

Changelog

The changelog lists all major changes between releases. For more details, please see the Git changelog.

Version 0.2.2

General work

Channels

Widgets

Bindings

Version 0.2.1

General work

Widgets

Event propagation

Bindings

Routing

Version 0.2.0

General work

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:

Widgets

Some work also went into the widget subsystem:

Newly added widgets are:

Code generators were introduced for higher reliability of the bindings. sbt-web 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:

Routing

Trivia

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.


  1. An application cannot define more than one entry point.

  2. In contrast to a <span> or <div>, an Inline view cannot be controlled using CSS stylesheet rules.

  3. Partials are composed widgets.

  4. These functions do not access the state in any way.

  5. The types in the code only serve illustration purposes

  6. Source: leecampbell.blogspot.de (4th February 2015)

  7. For instance, Monifu implements this feature.

  8. This function is called by attach() and produces multiple values which is necessary for some reactive data structures like lists.

  9. This is largely inspired by Scala’s SeqView.

  10. For example, a Delta.Clear may only be generated after Delta.Insert.

  11. 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.

  12. 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.

  13. 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.

  14. It is an alias for the method merge()

  15. n must be greater than 0.

  16. size returns a ReadChannel[Int].

  17. This name was chosen as Set would have collided with Scala’s implementation.