Monday, July 2, 2012

Contour (a Scala+GWT framework) Tutorial

In my spare time I've been writing a client+server web-framework in Scala + Google Web Toolkit. It is in a very early stage right now, but is already enough to make the code of the GWT's tutorial two times shorter, yet easier to read and understand.

I mean two times shorter when compared to the Scala version of the StockWatcher tutorial from the previous article, which itself was shorter and easier to read than the original Java version of the code.

The framework is called Contour (github.com/kazachonak/contour). It is worth mentioning that it uses reactive programming under the covers. Here is a Contour-based version of our app in action:



Let's start by looking at the app code in Contour which is functionally equivalent to the StockWatcher tutorial code from the previous article. And then explain the code line by line.

package org.contourweb.examples.stockwatcher2

import java.util.Date
import org.contourweb.client.model.client
import org.contourweb.client.view.gwtView
import org.contourweb.common.model.localStore
import com.google.gwt.i18n.client.DateTimeFormat
import com.google.gwt.user.client.Random
import com.google.gwt.user.client.ui.TextBox
import reactive.Timer


object stockWatcher extends localStore with client with gwtView {

  
class Stock extends Stock.Model {
  val symbol = new StringField {
    override def update(s: String) = super.update(s.toUpperCase.trim)
    override def unique = true
    override def validation = Valid(is.matches("^[0-9A-Z\\.]{1,10}$"),
                                "'" + is + "' is not a valid symbol.")
  }
  val price = new DoubleField {
    override def format = "#,##0.00"
  }
  val change = new DoubleField {
    override def format = "+#,##0.00;-#,##0.00"
  }
  val changePercent = new DoubleField {
    override def is = 100.0 * change / price
    override def format = "+#,##0.00;-#,##0.00"
  }
}
  
object Stock extends MetaModel with LocalStore {
  type M = Stock
  def create = new Stock // TODO: Remove it
}


class StockWatcherWindow extends GwtRootWindow {
  def main = new StockWatcherView
}


class StockWatcherView extends View {
  import Widgets._
  
  private val REFRESH_INTERVAL = 5000 // ms
  private val newStock = new Stock
  private def dateNow = DateTimeFormat.getMediumDateTimeFormat
                                      .format(new Date)
  private var newSymbolTextBox: TextBox = _
  
  val widget = VerticalPanel()(
    FlexTable(Stock.all)( stock => List(
      Column("Symbol")(Label(stock.symbol)),
      Column("Price")(Label(stock.price.toString)),
      Column("Change")(
        Label(stock.change.toString +
              " (" + stock.changePercent.toString + "%)")
      ),
      Column("Remove")(
        Button(stock.delete, "x")
      )
    )),
    Form(onSubmit = addStock)(implicit form =>
      HorizontalPanel()(
        TextBoxValid(newStock.symbol,
                     onEnter = form.submit,
                     init = newSymbolTextBox=_),
        Button(form.submit, "Add")
      )
    ),
    Label(Stock.all.deltas.map(_ => "Last updated: "+dateNow).hold(""))
  )

  override def onLoad {
    // Move cursor focus to the input box.
    newSymbolTextBox.setFocus(true)
    
    // Setup timer to refresh list automatically.
    new Timer(0, REFRESH_INTERVAL).foreach(_ => refreshWatchList)
  }

  private def addStock {
    newSymbolTextBox.setFocus(true)
    Stock.create.symbol(newStock.symbol).save
    newStock.symbol() = ""
    refreshWatchList
  }
  
  /**
   * Generate random stock prices.
   */
  private def refreshWatchList {
    val MAX_PRICE = 100.0 // $100.00
    val MAX_PRICE_CHANGE = 0.02 // +/- 2%

    Stock.all.now.foreach{ stock =>
      stock.price(Random.nextDouble * MAX_PRICE)
           .change(stock.price * MAX_PRICE_CHANGE
                               * (Random.nextDouble * 2.0 - 1.0))
           .save
    }
  }
}


}




That's it. Let's look at the code again.

object stockWatcher extends localStore with client with gwtView {

Those top-level trait and object definitions starting from the small letter are used as packages for the code. It is better than just packages, because it allows us to define and inject dependencies. Actually, this is the famous Cake pattern, which eliminates the need for dependency injection frameworks' magic. And it is fully typesafe. You can find more about it here: warski.org/blog/2010/12/di-in-scala-cake-pattern/

class Stock extends Stock.Model {
  val symbol = new StringField {
    override def update(s: String) = super.update(s.toUpperCase.trim)
    override def unique = true
    override def validation = Valid(is.matches("^[0-9A-Z\\.]{1,10}$"),
                                "'" + is + "' is not a valid symbol.")
  }
  val price = new DoubleField {
    override def format = "#,##0.00"
  }
  val change = new DoubleField {
    override def format = "+#,##0.00;-#,##0.00"
  }
  val changePercent = new DoubleField {
    override def is = 100.0 * change / price
    override def format = "+#,##0.00;-#,##0.00"
  }
}

This is a domain model definition. I think it is quite self-explanatory. The thing worth mentioning is that there are no XML / Annotations / Source code generation / Runtime byte-code generation. All abstractions at every level are just plain old Scala. This is the principle I want Contour to adhere to as much as possible.
object Stock extends MetaModel with LocalStore {
  type M = Stock
  def create = new Stock // TODO: Remove it
}
This defines a local store for stocks. The idea is that we can change "with LocalStore" to "with DistributedStore" or some other implementation, even our own. And if there are some differences, for example, the "unique" field property is not supported, the Scala compiler will tell us just that right away.

class StockWatcherWindow extends GwtRootWindow {
GwtRootWindow extends GWT's EntryPoint. So we can tell GWT to use this class to start our app.

  def main = new StockWatcherView
}
The View returned by main will be rendered when the application is loaded.

class StockWatcherView extends View {
  import Widgets._
We import widget factories, so that we can use them as 'Button' instead of 'Widgets.Button'. Thankfully to the Cake pattern, we can use a different Widgets implementation during in-JVM testing.

  private val REFRESH_INTERVAL = 5000 // ms
  private val newStock = new Stock
  private def dateNow = DateTimeFormat.getMediumDateTimeFormat
                                      .format(new Date)
  private var newSymbolTextBox: TextBox = _
Just some fields, which are used in our view

  val widget = VerticalPanel()(

This is the start of a declarative view definition. All our components here reside in a VerticalPanel.

    FlexTable(Stock.all)( stock => List(

This starts a FlexTable definition. 'Stock.all' is a reactive collection of stocks.


      Column("Symbol")(Label(stock.symbol)),
      Column(title = "Price")(Label(stock.price.toString)),

Some columns definitions. We can use named parameters if we want to.


      Column("Change")(
        Label(stock.change.toString +
              " (" + stock.changePercent.toString + "%)")
      ),
      Column("Remove")(
        Button(stock.delete, "x")
      )
    )),

We've already seen what this FlexTable does in the previous article. But this time the definition looks much more succint and straitforward. And due to reactive programming, any changes to stocks are reflected in the FlexTable. And FlexTable is not fully re-rendered. Just the required rows are updated.

    Form(onSubmit = addStock)(implicit form =>
      HorizontalPanel()(
        TextBoxValid(newStock.symbol,
                     onEnter = form.submit,
                     init = newSymbolTextBox=_),

'init' param is of type TextBox=>Unit. It is a function, which receives TextBox and returns nothing. Here we just set the TextBox received to 'newSymbolTextBox' variable for later use. This pattern is not encouraged, but can be useful sometimes.

        Button(form.submit, "Add")
      )
    ),
This 'Form' block produces behaviour which is different in comparison to the StockWatcher version from the previous article. Now validation errors are displayed not in an alert window, but under the TextBox. This is the behaviour of TextBoxValid, which is compile-time checked to be inside a Form. This Form is entirely logical - no actual html 'form' is created by default.

    Label(Stock.all.deltas.map(_ => "Last updated: "+dateNow).hold(""))
This will display "Last updated: " label and update it on any Stock update. If you don't fully understand this line, take a look at the Reactive Programming Tutorial in Scala + GWT.
  )

  override def onLoad {
    // Move cursor focus to the input box.
    newSymbolTextBox.setFocus(true)
    
    // Setup timer to refresh list automatically.
    new Timer(0, REFRESH_INTERVAL).foreach(_ => refreshWatchList)
  }

  private def addStock {
    newSymbolTextBox.setFocus(true)
    Stock.create.symbol(newStock.symbol).save
    newStock.symbol() = ""
    refreshWatchList
  }
  
  /**
   * Generate random stock prices.
   */
  private def refreshWatchList {
    val MAX_PRICE = 100.0 // $100.00
    val MAX_PRICE_CHANGE = 0.02 // +/- 2%

    Stock.all.now.foreach{ stock =>
      stock.price(Random.nextDouble * MAX_PRICE)
           .change(stock.price * MAX_PRICE_CHANGE
                               * (Random.nextDouble * 2.0 - 1.0))
           .save
Neccesary changes are reflected in FlexTable on each 'save' call. No more any update-view-by-hand-and-then-debug code.

    }
  }
}


}

The full source code with css styles can be found at
github.com/kazachonak/contour/blob/master/examples/stockwatcher2/src/main/scala/org/contourweb/examples/stockwatcher2/stockWatcher.scala

As you see, Contour doesn't require us to create a pure HTML/CSS template just to get some work done with a bit of css styling here and there.

But sometimes we have pure HTML/CSS templates that are regularly updated by designers. So we just want to use the layout/styling information from those templates directly. It is also easy to do in Contour. In the next article we'll write a famous TodoMVC app, which is heavy HTML/CSS-based.

1 comment:

  1. OK, this has caught my attention. Looks very cool. I will have to play with this in the future so I can understand better.

    Very elegant how you have embedded the Stock Watcher in this page.

    ReplyDelete