Here's a simple example. I'm using Bootstrap (of course), and I'm using the table-striped class to add a little bit of interest to tabular data. The setup of an HTML table is quite verbose and definitely doesn't need to be repeated, so I started with the following basic structure:
@(items:Seq[_], headings:Seq[String] = Nil) <table class="table table-striped"> @if(headings.nonEmpty) { <thead> <tr> @for(heading <- headings) { <th>@heading</th> } </tr> </thead> } <tbody> @for(item <- items) { <tr> ??? </tr> } } </tbody> </table>Which neatens up the call-site from 20-odd lines to one:
@stripedtable(userList, Seq("Name", "Age")
Except. How do I render each row in the table body? That differs for every use case!
What I really wanted was to be able to map over each of the items, applying some client-provided function to render a load of <td>...</td> cells for each one. Basically, I wanted stripedtable to have this signature:
@(items:Seq[T], headings:Seq[String] = Nil)(fn: T => Html)With the body simply being:
@for(item <- items) { <tr> @fn(item) </tr> }and client code looking like this:
@stripedtable(userList, Seq("Name", "Age") { user:User => <td>@user.name</td><td>@user.age</td> }...aaaaand we have a big problem. At least at time of writing, Twirl templates cannot be given type arguments. So those [T]'s just won't work. Loosening off the types like this:
@(items:Seq[_], headings:Seq[String] = Nil)(fn: Any => Html)will compile, but the call-site won't work because the compiler has no idea that the _ and the Any are referring to the same type. Workaround solutions? There are two, depending on how explosively you want type mismatches to fail:
Option 1: Supply a case as the row renderer
@stripedtable(userList, Seq("Name", "Age") { case user:User => <td>@user.name</td><td>@user.age</td> }This works fine, as long as every item in userList is in fact a User - if not, you get a big fat MatchError.
Option 2: Supply a case as the row renderer, and accept a PartialFunction
The template signature becomes:@(items:Seq[_],hdgs:Seq[String] = Nil)(f: PartialFunction[Any, Html])and we tweak the body slightly:
@for(item <- items) { @if(fn.isDefinedAt(item)) { <tr> @fn(item) </tr> } }In this scenario, we've protected ourselves against type mismatches, and simply skip anything that's not what we expect. Either way, I can't currently conceive of a more succinct, reusable, and obvious way to drop a consistently-built, styled table into a page than this:
@stripedtable(userList, Seq("Name", "Age") { case user:User => <td>@user.name</td><td>@user.age</td> }