Cask


Table of Contents

Cask: a Scala HTTP micro-framework

Main Customization

Build Status
Gitter Chat
Patreon

package app
object MinimalApplication extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    new String(request.readAllBytes()).reverse
  }

  initialize()
}

Cask is a simple Scala web framework inspired by Python's Flask project. It aims to bring simplicity, flexibility and ease-of-use to Scala webservers, avoiding cryptic DSLs or complicated asynchrony.

Getting Started

The easiest way to begin using Cask is by downloading the example project above.

Unzip one of the example projects available on this page (e.g. above) into a folder. This should give you the following files:

build.sc
app/src/MinimalExample.scala
app/test/src/ExampleTests.scala
./cask -w app.runBackground

This will server up the Cask application on http://localhost:8080. You can immediately start interacting with it either via the browser, or programmatically via curl or a HTTP client like Requests-Scala:

val host = "http://localhost:8080"

val success = requests.get(host)

success.text() ==> "Hello World!"
success.statusCode ==> 200

requests.get(host + "/doesnt-exist").statusCode ==> 404

requests.post(host + "/do-thing", data = "hello").text() ==> "olleh"

requests.get(host + "/do-thing").statusCode ==> 404

These HTTP calls are part of the test suite for the example project, which you can run using:

./cask -w app.test

To configure your Cask application to work with IntelliJ, you can use:

./cask mill.scalalib.GenIdea/idea

This will need to be re-run when you re-configure your build.sc file, e.g. when adding additional modules or third-party dependencies.

Cask is just a Scala library, and you can use Cask in any existing Scala project via the following coordinates:

// Mill
ivy"com.lihaoyi::cask:0.1.9"

// SBT
"com.lihaoyi" %% "cask" % "0.1.9"

The ./cask command is just a wrapper around the Mill build tool; the build.sc files you see in all examples are Mill build files, and you can use your own installation of Mill instead of ./cask if you wish. All normal Mill commands and functionality works for ./cask.

The following examples will walk you through how to use Cask to accomplish tasks common to anyone writing a web application. Each example comes with a downloadable example project with code and unit tests, which you can use via the same ./cask -w app.runBackground or ./cask -w app.test workflows we saw above.

Minimal Example

package app
object MinimalApplication extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    new String(request.readAllBytes()).reverse
  }

  initialize()
}

The rough outline of how the minimal example works should be easy to understand:

In most cases, Cask provides convenient helpers to extract exactly the data from the incoming HTTP request that you need, while also de-serializing it into the data type you need and returning meaningful errors if they are missing. Thus, although you can always get all the data necessary through cask.Request, it is often more convenient to use another way, which will go into below.

As your application grows, you will likely want to split up the routes into separate files, themselves separate from any configuration of the Main entrypoint (e.g. overriding the port, host, default error handlers, etc.). You can do this by splitting it up into cask.Routes and cask.Main objects:

package app

object MinimalRoutes extends cask.Routes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    new String(request.readAllBytes()).reverse
  }

  initialize()
}
object MinimalMain extends cask.Main(MinimalRoutes)

You can split up your routes into separate cask.Routes objects as makes sense and pass them all into cask.Main.

Variable Routes

package app
object VariableRoutes extends cask.MainRoutes{
  @cask.get("/user/:userName")
  def showUserProfile(userName: String) = {
    s"User $userName"
  }

  @cask.get("/post/:postId")
  def showPost(postId: Int, param: Seq[String]) = {
    s"Post $postId $param"
  }

  @cask.get("/path", subpath = true)
  def showSubpath(request: cask.Request) = {
    s"Subpath ${request.remainingPathSegments}"
  }

  initialize()
}

You can bind variables to endpoints by declaring them as parameters: these are either taken from a path-segment matcher of the same name (e.g. postId above), or from query-parameters of the same name (e.g. param above). You can make param take a : String to match ?param=hello, an : Int for ?param=123 a Seq[T] (as above) for repeated params such as ?param=hello&param=world, or : Option[T] for cases where the ?param=hello is optional.

If you need to capture the entire sub-path of the request, you can set the flag subpath=true and ask for a : cask.Subpath (the name of the param doesn't matter). This will make the route match any sub-path of the prefix given to the @cask decorator, and give you the remainder to use in your endpoint logic.

Multi-method Routes

package app
object HttpMethods extends cask.MainRoutes{
  @cask.route("/login", methods = Seq("get", "post"))
  def login(request: cask.Request) = {
    if (request.exchange.getRequestMethod.equalToString("post")) "do_the_login"
    else "show_the_login_form"
  }

  initialize()
}

Sometimes, you may want to handle multiple kinds of HTTP requests in the same endpoint function, e.g. with code that can accept both GETs and POSTs and decide what to do in each case. You can use the @cask.route annotation to do so

Receiving Form-encoded or JSON data

package app
object FormJsonPost extends cask.MainRoutes{
  @cask.postJson("/json")
  def jsonEndpoint(value1: ujson.Js.Value, value2: Seq[Int]) = {
    "OK " + value1 + " " + value2
  }

  @cask.postForm("/form")
  def formEndpoint(value1: cask.FormValue, value2: Seq[Int]) = {
    "OK " + value1 + " " + value2
  }

  @cask.postForm("/upload")
  def uploadFile(image: cask.FormFile) = {
    image.fileName
  }

  initialize()
}

If you need to handle a JSON-encoded POST request, you can use the @cast.postJson decorator. This assumes the posted request body is a JSON dict, and uses its keys to populate the endpoint's parameters, either as raw ujson.Js.Values or deserialized into Seq[Int]s or other things. Deserialization is handled using the uPickle JSON library, though you could write your own version of postJson to work with any other JSON library of your choice.

Similarly, you can mark endpoints as @cask.postForm, in which case the endpoints params will be taken from the form-encoded POST body either raw (as cask.FormValues) or deserialized into simple data structures. Use cask.FormFile if you want the given form value to be a file upload.

Both normal forms and multipart forms are handled the same way.

If the necessary keys are not present in the JSON/form-encoded POST body, or the deserialization into Scala data-types fails, a 400 response is returned automatically with a helpful error message.

Processing Cookies

package app
object Cookies extends cask.MainRoutes{
  @cask.get("/read-cookie")
  def readCookies(username: cask.Cookie) = {
    username.value
  }

  @cask.get("/store-cookie")
  def storeCookies() = {
    cask.Response(
      "Cookies Set!",
      cookies = Seq(cask.Cookie("username", "the username"))
    )
  }

  @cask.get("/delete-cookie")
  def deleteCookie() = {
    cask.Response(
      "Cookies Deleted!",
      cookies = Seq(cask.Cookie("username", "", expires = java.time.Instant.EPOCH))
    )
  }

  initialize()
}

Cookies are most easily read by declaring a : cask.Cookie parameter; the parameter name is used to fetch the cookie you are interested in. Cookies can be stored by setting the cookie attribute in the response, and deleted simply by setting expires = java.time.Instant.EPOCH (i.e. to have expired a long time ago)

Serving Static Files

package app
object StaticFiles extends cask.MainRoutes{
  @cask.get("/")
  def index() = {
    "Hello!"
  }

  @cask.staticFiles("/static/file")
  def staticFileRoutes() = "app/resources/cask"

  @cask.staticResources("/static/resource")
  def staticResourceRoutes() = "cask"

  @cask.staticResources("/static/resource2")
  def staticResourceRoutes2() = "."

  initialize()
}

You can ask Cask to serve static files by defining a @cask.staticFiles endpoint. This will match any subpath of the value returned by the endpoint (e.g. above /static/file.txt, /static/folder/file.txt, etc.) and return the file contents from the corresponding file on disk (and 404 otherwise).

Similarly, @cask.staticResources attempts to serve a request based on the JVM resource path, returning the data if a resource is present and a 404 otherwise.

Redirects or Aborts

package app
object RedirectAbort extends cask.MainRoutes{
  @cask.get("/")
  def index() = {
    cask.Redirect("/login")
  }

  @cask.get("/login")
  def login() = {
    cask.Abort(401)
  }

  initialize()
}

Cask provides some convenient helpers cask.Redirect and cask.Abort which you can return; these are simple wrappers around cask.Request, and simply set up the relevant headers or status code for you.

HTML Rendering

Cask doesn't come bundled with HTML templating functionality, but it makes it really easy to use community-standard libraries like Scalatags to render your HTML. Simply adding the relevant ivy"com.lihaoyi::scalatags:0.6.7" dependency to your build.sc file is enough to render Scalatags templates:

package app
import scalatags.Text.all._
object Scalatags extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "<!doctype html>" + html(
      body(
        h1("Hello World"),
        p("I am cow")
      )
    )
  }

  initialize()
}

If you prefer to use the Twirl templating engine, you can use that too:

package app
object Twirl extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "<!doctype html>" + html.hello("Hello World")
  }

  initialize()
}

With the following app/views/hello.scala.html:

@(titleTxt: String)
<html>
    <body>
        <h1>@titleTxt</h1>
        <p>I am cow</p>
    </body>
</html>

Extending Endpoints with Decorators

package app
object Decorated extends cask.MainRoutes{
  class User{
    override def toString = "[haoyi]"
  }
  class loggedIn extends cask.Decorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate): Returned = {
      delegate(Map("user" -> new User()))
    }
  }
  class withExtra extends cask.Decorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate): Returned = {
      delegate(Map("extra" -> 31337))
    }
  }

  @withExtra()
  @cask.get("/hello/:world")
  def hello(world: String)(extra: Int) = {
    world + extra
  }

  @loggedIn()
  @cask.get("/internal/:world")
  def internal(world: String)(user: User) = {
    world + user
  }

  @withExtra()
  @loggedIn()
  @cask.get("/internal-extra/:world")
  def internalExtra(world: String)(user: User)(extra: Int) = {
    world + user + extra
  }

  @withExtra()
  @loggedIn()
  @cask.get("/ignore-extra/:world")
  def ignoreExtra(world: String)(user: User) = {
    world + user
  }

  initialize()
}

You can write extra decorator annotations that stack on top of the existing @cask.get/@cask.post to provide additional arguments or validation. This is done by implementing the cask.Decorator interface and it's getRawParams function. getRawParams:

Each additional decorator is responsible for one additional parameter list to the right of the existing parameter lists, each of which can contain any number of parameters.

Decorators are useful for things like:

For decorators that you wish to apply to multiple routes at once, you can define them by overriding the cask.Routes#decorators field (to apply to every endpoint in that routes object) or cask.Main#mainDecorators (to apply to every endpoint, period):

package app
object Decorated2 extends cask.MainRoutes{
  class User{
    override def toString = "[haoyi]"
  }
  class loggedIn extends cask.Decorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate): Returned = {
      delegate(Map("user" -> new User()))
    }
  }
  class withExtra extends cask.Decorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate): Returned = {
      delegate(Map("extra" -> 31337))
    }
  }

  override def decorators = Seq(new withExtra())

  @cask.get("/hello/:world")
  def hello(world: String)(extra: Int) = {
    world + extra
  }

  @loggedIn()
  @cask.get("/internal-extra/:world")
  def internalExtra(world: String)(user: User)(extra: Int) = {
    world + user + extra
  }

  @loggedIn()
  @cask.get("/ignore-extra/:world")
  def ignoreExtra(world: String)(user: User)(extra: Int)  = {
    world + user
  }

  initialize()
}

This is convenient for cases where you want a set of decorators to apply broadly across your web application, and do not want to repeat them over and over at every single endpoint.

Custom Endpoints

package app

class custom(val path: String, val methods: Seq[String]) extends cask.Endpoint{
  type Output = Int
  def wrapFunction(ctx: cask.Request, delegate: Delegate): Returned = {
    delegate(Map()).map{num =>
      cask.Response("Echo " + num, statusCode = num)
    }
  }

  def wrapPathSegment(s: String) = Seq(s)

  type Input = Seq[String]
  type InputParser[T] = cask.endpoints.QueryParamReader[T]
}

object Endpoints extends cask.MainRoutes{


  @custom("/echo/:status", methods = Seq("get"))
  def echoStatus(status: String) = {
    status.toInt
  }

  initialize()
}

When you need more flexibility than decorators allow, you can define your own custom cask.Endpoints to replace the default set that Cask provides. This allows you to

Generally you should not be writing custom cask.Endpoints every day, but if you find yourself trying to standardize on a way of doing things across your web application, it might make sense to write a custom endpoint decorator: to DRY things up , separate business logic (inside the annotated function) from plumbing (in the endpoint function and decorators), and enforcing a standard of how endpoint functions are written.

Gzip & Deflated Responses

package app
object Compress extends cask.MainRoutes{

  @cask.decorators.compress
  @cask.get("/")
  def hello() = {
    "Hello World! Hello World! Hello World!"
  }

  initialize()
}

Cask provides a useful @cask.decorators.compress decorator that gzips or deflates a response body if possible. This is useful if you don't have a proxy like Nginx or similar in front of your server to perform the compression for you.

Like all decorators, @cask.decorators.compress can be defined on a level of a set of cask.Routes:

package app

object Compress2 extends cask.Routes{
  override def decorators = Seq(new cask.decorators.compress())

  @cask.get("/")
  def hello() = {
    "Hello World! Hello World! Hello World!"
  }

  initialize()
}

object Compress2Main extends cask.Main(Compress2)

Or globally, in your cask.Main:

package app

object Compress3 extends cask.Routes{

  @cask.get("/")
  def hello() = {
    "Hello World! Hello World! Hello World!"
  }

  initialize()
}

object Compress3Main extends cask.Main(Compress3){
  override def mainDecorators = Seq(new cask.decorators.compress())
}

Websockets

package app

import io.undertow.websockets.WebSocketConnectionCallback
import io.undertow.websockets.core.{AbstractReceiveListener, BufferedTextMessage, WebSocketChannel, WebSockets}
import io.undertow.websockets.spi.WebSocketHttpExchange

object Websockets extends cask.MainRoutes{
  @cask.websocket("/connect/:userName")
  def showUserProfile(userName: String): cask.WebsocketResult = {
    if (userName != "haoyi") cask.Response("", statusCode = 403)
    else new WebSocketConnectionCallback() {
      override def onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel): Unit = {
        channel.getReceiveSetter.set(
          new AbstractReceiveListener() {
            override def onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) = {
              message.getData match{
                case "" => channel.close()
                case data => WebSockets.sendTextBlocking(userName + " " + data, channel)
              }
            }
          }
        )
        channel.resumeReceives()
      }
    }
  }

  initialize()
}

Cask's Websocket endpoints are very similar to Cask's HTTP endpoints. Annotated with @cask.websocket instead of @cask.get or @cast.post, the primary difference is that instead of only returning a cask.Response, you now have an option of returning a io.undertow.websockets.WebSocketConnectionCallback.

The WebSocketConnectionCallback allows you to pro-actively start sending websocket messages once a connection has been made, and it lets you register a AbstractReceiveListener that allows you to react to any messages the client on the other side of the websocket connection sends you. You can use these two APIs to perform full bi-directional, asynchronous communications, as websockets are intended to be used for.

Returning a cask.Response immediately closes the websocket connection, and is useful if you want to e.g. return a 404 or 403 due to the initial request being invalid.

Cask intentionally provides a relatively low-level websocket interface. It leaves it up to you to manage open channels, react to incoming messages, or pro-actively send them out, mostly using the underlying Undertow webserver interface. While Cask does not model streams, backpressure, iteratees, or provide any higher level API, it should not be difficult to take the Cask API and build whatever higher-level abstractions you prefer to use.

TodoMVC Api Server

package app
object TodoMvcApi extends cask.MainRoutes{
  case class Todo(checked: Boolean, text: String)
  object Todo{
    implicit def todoRW = upickle.default.macroRW[Todo]
  }
  var todos = Seq(
    Todo(true, "Get started with Cask"),
    Todo(false, "Profit!")
  )

  @cask.get("/list/:state")
  def list(state: String) = {
    val filteredTodos = state match{
      case "all" => todos
      case "active" => todos.filter(!_.checked)
      case "completed" => todos.filter(_.checked)
    }
    upickle.default.write(filteredTodos)
  }

  @cask.post("/add")
  def add(request: cask.Request) = {
    todos = Seq(Todo(false, new String(request.readAllBytes()))) ++ todos
  }

  @cask.post("/toggle/:index")
  def toggle(index: Int) = {
    todos = todos.updated(index, todos(index).copy(checked = !todos(index).checked))
  }

  @cask.post("/delete/:index")
  def delete(index: Int) = {
    todos = todos.patch(index, Nil, 1)
  }

  initialize()
}

This is a simple self-contained example of using Cask to write an in-memory API server for the common TodoMVC example app.

This minimal example intentionally does not contain javascript, HTML, styles, etc.. Those can be managed via the normal mechanism for Serving Static Files.

TodoMVC Database Integration

package app
import cask.internal.Router
import com.typesafe.config.ConfigFactory
import io.getquill.{SqliteJdbcContext, SnakeCase}


object TodoMvcDb extends cask.MainRoutes{
  val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")

  object ctx extends SqliteJdbcContext(
    SnakeCase,
    ConfigFactory.parseString(
      s"""{"driverClassName":"org.sqlite.JDBC","jdbcUrl":"jdbc:sqlite:$tmpDb/file.db"}"""
    )
  )

  class transactional extends cask.Decorator{
    class TransactionFailed(val value: Router.Result.Error) extends Exception
    def wrapFunction(pctx: cask.Request, delegate: Delegate): Returned = {
      try ctx.transaction(
        delegate(Map()) match{
          case Router.Result.Success(t) => Router.Result.Success(t)
          case e: Router.Result.Error => throw new TransactionFailed(e)
        }
      )
      catch{case e: TransactionFailed => e.value}

    }
  }

  case class Todo(id: Int, checked: Boolean, text: String)
  object Todo{
    implicit def todoRW = upickle.default.macroRW[Todo]
  }

  ctx.executeAction(
    """CREATE TABLE todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  checked BOOLEAN,
  text TEXT
);
""".stripMargin
  )
  ctx.executeAction(
    """INSERT INTO todo (checked, text) VALUES
(1, 'Get started with Cask'),
(0, 'Profit!');
""".stripMargin
  )

  import ctx._

  @transactional
  @cask.get("/list/:state")
  def list(state: String) = {
    val filteredTodos = state match{
      case "all" => run(query[Todo])
      case "active" => run(query[Todo].filter(!_.checked))
      case "completed" => run(query[Todo].filter(_.checked))
    }
    upickle.default.write(filteredTodos)
  }

  @transactional
  @cask.post("/add")
  def add(request: cask.Request) = {
    val body = new String(request.readAllBytes())
    run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id))
  }

  @transactional
  @cask.post("/toggle/:index")
  def toggle(index: Int) = {
    run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked))
  }

  @transactional
  @cask.post("/delete/:index")
  def delete(index: Int) = {
    run(query[Todo].filter(_.id == lift(index)).delete)
  }

  initialize()
}

This example demonstrates how to use Cask to write a TodoMVC API server that persists it's state in a database rather than in memory. We use the Quill database access library to write a @transactional decorator that automatically opens one transaction per call to an endpoint, ensuring that database queries are properly committed on success or rolled-back on error. Note that because the default database connector propagates its transaction context in a thread-local, @transactional does not need to pass the ctx object into each endpoint as an additional parameter list, and so we simply leave it out.

While this example is specific to Quill, you can easily modify the @transactional decorator to make it work with whatever database access library you happen to be using. For libraries which need an implicit transaction, it can be passed into each endpoint function as an additional parameter list as described in Extending Endpoints with Decorators.

TodoMVC Full Stack Web

The following code snippet is the complete code for a full-stack TodoMVC implementation: including HTML generation for the web UI via Scalatags, Javascript for the interactivity, static file serving, and database integration via Quill. While slightly long, this example should give you a tour of all the things you need to know to use Cask.

Note that this is a "boring" server-side-rendered webapp with Ajax interactions, without any complex front-end frameworks or libraries: it's purpose is to demonstrate a simple working web application of using Cask end-to-end, which you can build upon to create your own Cask web application architected however you would like.

package app
import cask.internal.Router
import com.typesafe.config.ConfigFactory
import io.getquill.{SnakeCase, SqliteJdbcContext}
import scalatags.Text.all._
import scalatags.Text.tags2

object TodoServer extends cask.MainRoutes{
  val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")

  object ctx extends SqliteJdbcContext(
    SnakeCase,
    ConfigFactory.parseString(
      s"""{"driverClassName":"org.sqlite.JDBC","jdbcUrl":"jdbc:sqlite:$tmpDb/file.db"}"""
    )
  )

  class transactional extends cask.Decorator{
    class TransactionFailed(val value: Router.Result.Error) extends Exception
    def wrapFunction(pctx: cask.Request, delegate: Delegate): Returned = {
      try ctx.transaction(
        delegate(Map()) match{
          case Router.Result.Success(t) => Router.Result.Success(t)
          case e: Router.Result.Error => throw new TransactionFailed(e)
        }
      )
      catch{case e: TransactionFailed => e.value}
    }
  }

  case class Todo(id: Int, checked: Boolean, text: String)

  ctx.executeAction(
    """CREATE TABLE todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  checked BOOLEAN,
  text TEXT
);
""".stripMargin
  )
  ctx.executeAction(
    """INSERT INTO todo (checked, text) VALUES
(1, 'Get started with Cask'),
(0, 'Profit!');
""".stripMargin
  )

  import ctx._

  @transactional
  @cask.post("/list/:state")
  def list(state: String) = renderBody(state).render

  @transactional
  @cask.post("/add/:state")
  def add(state: String, request: cask.Request) = {
    val body = new String(request.readAllBytes())
    run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id))
    renderBody(state).render
  }

  @transactional
  @cask.post("/delete/:state/:index")
  def delete(state: String, index: Int) = {
    run(query[Todo].filter(_.id == lift(index)).delete)
    renderBody(state).render
  }

  @transactional
  @cask.post("/toggle/:state/:index")
  def toggle(state: String, index: Int) = {
    run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked))
    renderBody(state).render
  }

  @transactional
  @cask.post("/clear-completed/:state")
  def clearCompleted(state: String) = {
    run(query[Todo].filter(_.checked).delete)
    renderBody(state).render
  }

  @transactional
  @cask.post("/toggle-all/:state")
  def toggleAll(state: String) = {
    val next = run(query[Todo].filter(_.checked).size) != 0
    run(query[Todo].update(_.checked -> !lift(next)))
    renderBody(state).render
  }

  def renderBody(state: String) = {
    val filteredTodos = state match{
      case "all" => run(query[Todo]).sortBy(-_.id)
      case "active" => run(query[Todo].filter(!_.checked)).sortBy(-_.id)
      case "completed" => run(query[Todo].filter(_.checked)).sortBy(-_.id)
    }
    frag(
      header(cls := "header",
        h1("todos"),
        input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "")
      ),
      tags2.section(cls := "main",
        input(
          id := "toggle-all",
          cls := "toggle-all",
          `type` := "checkbox",
          if (run(query[Todo].filter(_.checked).size != 0)) checked else ()
        ),
        label(`for` := "toggle-all","Mark all as complete"),
        ul(cls := "todo-list",
          for(todo <- filteredTodos) yield li(
            if (todo.checked) cls := "completed" else (),
            div(cls := "view",
              input(
                cls := "toggle",
                `type` := "checkbox",
                if (todo.checked) checked else (),
                data("todo-index") := todo.id
              ),
              label(todo.text),
              button(cls := "destroy", data("todo-index") := todo.id)
            ),
            input(cls := "edit", value := todo.text)
          )
        )
      ),
      footer(cls := "footer",
        span(cls := "todo-count",
          strong(run(query[Todo].filter(!_.checked).size).toInt),
          " items left"
        ),
        ul(cls := "filters",
          li(cls := "todo-all",
            a(if (state == "all") cls := "selected" else (), "All")
          ),
          li(cls := "todo-active",
            a(if (state == "active") cls := "selected" else (), "Active")
          ),
          li(cls := "todo-completed",
            a(if (state == "completed") cls := "selected" else (), "Completed")
          )
        ),
        button(cls := "clear-completed","Clear completed")
      )
    )
  }

  @transactional
  @cask.get("/")
  def index() = {
    cask.Response(
      "<!doctype html>" + html(lang := "en",
        head(
          meta(charset := "utf-8"),
          meta(name := "viewport", content := "width=device-width, initial-scale=1"),
          tags2.title("Template • TodoMVC"),
          link(rel := "stylesheet", href := "/static/index.css")
        ),
        body(
          tags2.section(cls := "todoapp", renderBody("all")),
          footer(cls := "info",
            p("Double-click to edit a todo"),
            p("Created by ",
              a(href := "http://todomvc.com","Li Haoyi")
            ),
            p("Part of ",
              a(href := "http://todomvc.com","TodoMVC")
            )
          ),
          script(src := "/static/app.js")
        )
      )
    )
  }

  @cask.staticResources("/static")
  def static() = "todo"

  initialize()
}


About the Author: Haoyi is a software engineer, an early contributor to Scala.js, and the author of many open-source Scala tools such as Cask, the Ammonite REPL and FastParse.

If you've enjoy using Cask, or enjoyed using Haoyi's other open source libraries, please chip in (or get your Company to chip in!) via Patreon so he can continue his open-source work


Main Customization