Last week I finished writing some Scala code to convert this website from using Drupal 8 to using static web pages. Technically what happens is that I use Drupal at a different location, and then my Scala code reads the Drupal database and uses to the Twirl template system — that comes with the Play Framework — to convert that data into the static HTML pages you see here.
Along the way I learned a lot about the 2020 version of Twirl templates, so I thought I’d share some tips and examples about how to do things with Twirl. If you ever want to use Twirl in standalone mode like I did, or inside the Play Framework, I hope this is helpful.
Configuring SBT to use Twirl
To use Twirl with SBT in a standalone project, first put this line in project/plugins.sbt:
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.0")
Then include a line like this in build.sbt:
lazy val root = (project in file(".")).enablePlugins(SbtTwirl)
For instance, here’s my complete build.sbt file:
name := "StaticDrupal"
version := "0.1"
scalaVersion := "2.12.11"
lazy val root = (project in file(".")).enablePlugins(SbtTwirl)
libraryDependencies ++= Seq(
"org.scalikejdbc" %% "scalikejdbc" % "3.1.0",
"org.scalikejdbc" %% "scalikejdbc-config" % "3.1.0",
"ch.qos.logback" % "logback-classic" % "1.2.3",
"mysql" % "mysql-connector-java" % "8.0.19",
"com.typesafe" % "config" % "1.4.0",
"org.scalactic" %% "scalactic" % "3.1.1",
"org.scalatest" %% "scalatest" % "3.1.1" % "test"
)
scalacOptions += "-deprecation"
Once you have that setup you can write normal Scala applications that use Twirl templates, and run them with sbt run
, and create standalone applications with sbt package
or sbt assembly
.
How to handle Twirl import statements and input parameters
When you first create a Twirl template, you’ll probably want some import
statements at the beginning, and then a parameter list after that:
@import static_drupal.Category
@import static_drupal.Tag
@import static_drupal.WhatsNewLink
@import static_drupal.WhatsRelatedLink
@(
productionMode: Boolean,
nid: Int,
nodeType: String,
dateChanged: String,
title: String,
description: String,
body: play.twirl.api.Html
)
<!DOCTYPE html>
...
...
A Twirl template is just like a function, so you can think of those parameters as being function parameters.
Also:
- Twirl template are named something like nodeBook.scala.html, nodeBlog.scala.html, footer.scala.html, etc.
- Twirl templates go under a src/main/twirl folder.
How to include a Twirl template in another template
To include one Twirl template inside another, do it like this:
@* include a template file named cssIncludes.scala.html *@
@cssIncludes()
How to write Twirl functions
I’m still not an expert on Twirl functions — I’m not even sure they’re called functions (see Reusable Blocks on this page) — but you can write them like this:
@getNextNode(currentBookNode: BookNode, allNodesForBook: Seq[BookNode]) = @{
val idx = allNodesForBook.indexOf(currentBookNode)
if (idx < allNodesForBook.length-2) {
Some(allNodesForBook(idx+1))
} else {
None
}
}
@getPreviousNode(currentBookNode: BookNode, allNodesForBook: Seq[BookNode]) = @{
val idx = allNodesForBook.indexOf(currentBookNode)
if (idx == 0) {
None
} else {
Some(allNodesForBook(idx-1))
}
}
Twirl if/then/else statements
Display some HTML if a condition is true:
@if(description.trim != "") {
<meta name="description" content="@description" />
}
Display the contents of a template named divGoogleAnalytics.scala.html inside an if
statement:
@if(productionMode) {
@divGoogleAnalytics()
}
Here’s a Twirl if/else block:
@if(bookmarkUrl.trim.isEmpty) {
<div class="field__label">URL</div>
<div class="field__item"><a
href=""
rel="nofollow">(the url was not found)</a></div>
</div>
} else {
<div class="field__label">URL</div>
<div class="field__item"><a
href="@bookmarkUrl"
rel="nofollow">@bookmarkUrl</a></div>
</div>
}
Here’s a Twirl if/else-if/else block:
@if(pageNumber == 1) {
<li class="pager__item pager__item--prev">1</li>
} else if(pageNumber == 2) {
<li class="pager__item pager__item--prev">
<a href="/@currCategory.name" title="Previous page">«</a>
</li>
} else {
<li class="pager__item pager__item--prev">
<a href="/@currCategory.name/@{pageNumber-1}" title="Previous page">«</a>
</li>
}
Note that the opening (
needs to be write next to the if
as shown, i.e., as if(...)
, and not if (...)
. I don’t remember what the error message is, but it won’t compile if you have a blank space after the if
.
A Twirl for-loop
Here’s a Twirl for-loop:
<!-- START "content_tags" -->
<div class="content_tags">
<div class="field field--name-tags field--type-entity-reference field--label-hidden field__items">
@for(t <- tags) {
<div class="field__item"><a href="/taxonomy/term/@t.tid" hreflang="en">@t.name</a></div>
}
</div>
<!-- END "content_tags" -->
Here’s another for-loop with UL/LI tags:
<ul>
@for(link <- whatsNewLinks) {
<li><div class="views-field views-field-title"><span class="field-content"><a href="@link.urlAlias" hreflang="en">@link.title</a></span></div></li>
}
</ul>
Using a counter in a Twirl for-loop
When you need to use a counter in a Twirl for-loop, call the zip
method on your sequence. In this code, nodesForPage
is a Seq[Node]
:
@for((node,currentFileNumber) <- nodesForPage.zip(Stream from 1)) {
@defining(currentFileNumber) { pageNumber =>
@if(startRow(pageNumber)) {
<div class="views-row clearfix row-@currentRow(pageNumber)">
}
<div class="views-col col-@getColumn(pageNumber)">
<div class="views-field views-field-field-photo-d8">
<div class="field-content">
<a href="@node.photoNodeUri"><img
src="/sites/default/files/styles/thumbnail_160x160/public/@node.fileUri.drop(9)"
width="160" height="160"
alt="@node.altText" typeof="foaf:Image" class="image-style-thumbnail-160x160" />
</a></div></div></div>
@if(endRow(pageNumber, nodesForPage.length)) {
</div><!-- views-row-->
}
}
}
A Twirl match expression
Here’s a Twirl match
expression:
@photoDetailsOption match {
case None => {
<p>(Sorry, the photo was not found)</p>
}
case Some(p) => {
<a href="/sites/default/files/@p.uri"><img
src="/sites/default/files/styles/preview/public/@p.uri"
width="480" alt="@p.altText" title="@p.titleText" typeof="foaf:Image" class="image-style-preview"
/></a>
}
}
Twirl comments
Here’s a basic Twirl comment:
@* this is a single-line twirl comment *@
This is a multiline Twirl comment:
@*
- looking at an example photo file
- public://2020-03/colorado-moonset-march-10-2019.jpeg
- files found under the html dir for this image:
- sites/default/files/2020-03/colorado-moonset-march-10-2019.jpeg
- sites/default/files/styles/preview/public/2020-03/colorado-moonset-march-10-2019.jpeg
- sites/default/files/styles/thumbnail/public/2020-03/colorado-moonset-march-10-2019.jpeg
- sites/default/files/styles/thumbnail_160x160/public/2020-03/colorado-moonset-march-10-2019.jpeg
*@
When you want to emit raw HTML in a Twirl template
If you have some HTML in a variable like myHtml
and want to emit that HTML without Twirl interpreting it, use this solution:
@play.twirl.api.HtmlFormat.raw(myHtml)
Conversely, if you just try to display the HTML contents in myHtml
like this:
@myHtml
Twirl will convert all of your <
and >
characters into <
and >
. (It probably munges other characters as well.)
How to call a Twirl template from your Scala code
When you need to call a Twirl template from your Scala code, call it just like you’d call a normal function. The only trick is being familiar with the Twirl data types, such as play.twirl.api.Html:
// this is in my scala “controller” class
def createHtmlForRootIndexPages(
productionMode: Boolean,
pageNumber: Int,
totalNumberOfPages: Int,
nodesForPage: Seq[BlogNode],
photoDetails: Seq[Option[PhotoDetails]]
): String = {
// this is where i use a twirl template named nodeBlogPage.scala.html
val content: play.twirl.api.Html = html.nodeBlogPage(
productionMode,
pageNumber,
totalNumberOfPages,
nodesForPage,
photoDetails
)
content.body
}
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
Summary
Those examples show the most common needs I had when using Twirl templates. One of the biggest problems was when I needed to use a counter inside a for-loop, and using zip
(as shown) or zipWithIndex
seems to be the solution there.
If you need to use Twirl templates in a standalone application or inside the Play Framework, I hope these tips and examples are helpful.