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