Scala templating in Play

Play supports the use of Scala code within views and also provides a couple of helper methods to ease the process of defining a view.

We've created different views till now. Let's see how they are actually rendered. Consider the view for the Task Tracker app we saw in Chapter 1, Getting Started with Play.

@(tasks: List[Task], taskForm: Form[String])

@import helper._

@main("Task Tracker") {
 
    <h2>Task Tracker</h2>

    <div>
    @form(routes.TaskController.newTask) {

        @taskForm.globalError.map { error =>
            <p class="error">
                @error.message
            </p>
        }
        <form>
            <input type="text" name="taskName" placeholder="Add a new Task" required>

            <input type="submit" value="Add">
        </form>
    }
    </div>
    <div>
        <ul>
        @tasks.map { task =>
            <li>
                @form(routes.TaskController.deleteTask(task.id)) {
                  @task.name <input type="submit" value="Remove">
                }
            </li>
        }
        </ul>
    </div>

}

The view has Scala code along with HTML, so how is it rendered correctly?

Open the Task Tracker view in a browser without running the Play application. The browser renders the page as follows:

Scala templating in Play

Now have a look at how differently it is rendered when you run the Play application!

When a Play application is compiled, the route-related files (routes_reverseRouting.scala and routes_routing.scala, controllers/routes.java) and Scala views are generated. The routes-related files are generated through the routes compiler, while the Scala views are generated by the template compiler. The Scala template engine of Play has been extracted to facilitate its use in projects independent of Play. The Play Scala template engine is now available as Twirl. According to https://github.com/spray/twirl, the reason for choosing Twirl as the name is:

As a replacement for the rather unwieldy name "Play framework Scala template engine" we were looking for something shorter with a bit of "punch" and liked Twirl as a reference to the template languages "magic" character @, which is sometimes also called "twirl".

Understanding the working of Twirl

Play's plugin is defined with a dependency on SbtTwirl; we can see this in the plugin definition:

object Play
  extends AutoPlugin
  with PlayExceptions
  with PlayReloader
  with PlayCommands
  with PlayRun
  with play.PlaySettings
  with PlayPositionMapper
  with PlaySourceGenerators {

  override def requires = SbtTwirl && SbtJsTask && SbtWebDriver

  val autoImport = play.PlayImport


  override def projectSettings =
    packageArchetype.java_server ++
      defaultSettings ++
      intellijCommandSettings ++
      Seq(testListeners += testListener) ++
      Seq(
        scalacOptions ++= Seq("-deprecation", "-unchecked", "-encoding", "utf8"),
        javacOptions in Compile ++= Seq("-encoding", "utf8", "-g")
      )
}

In addition to this, there are some SBT keys defined in defaultSettings using TwirlKeys. TwirlKeys exposes some keys, which can be used to customize Twirl as per our requirement. The keys that are exposed using TwirlKeys are:

  • twirlVersion: This is the Twirl version used for twirl-api dependency (SettingKey[String]).
  • templateFormats: This defines Twirl template formats (SettingKey[Map[String, String]]). The default formats available are html, txt, xml, and js.
  • templateImports: This includes the extra imports used for twirl templates (SettingKey[Seq[String]]). By default, its value is an empty sequence.
  • useOldParser: This uses the original Play template parser (SettingKey[Boolean]); the value is false by default.
  • sourceEncoding: This includes the source encoding for template files and generated Scala files (TaskKey[String]). If no encoding is specified in Scala compiler options, it uses the UTF-8 encoding.
  • compileTemplates: This compiles twirl templates into Scala source files (TaskKey[Seq[File]]).

To understand this task, let's see how twirlSettings are defined in the Twirl plugin:

  def twirlSettings: Seq[Setting[_]] = Seq(
    includeFilter in compileTemplates := "*.scala.*",
    excludeFilter in compileTemplates := HiddenFileFilter,
    sourceDirectories in compileTemplates := Seq(sourceDirectory.value / "twirl"),

    sources in compileTemplates <<= Defaults.collectFiles(
      sourceDirectories in compileTemplates,
      includeFilter in compileTemplates,
      excludeFilter in compileTemplates
    ),

    watchSources in Defaults.ConfigGlobal <++= sources in compileTemplates,

    target in compileTemplates := crossTarget.value / "twirl" / Defaults.nameForSrc(configuration.value.name),

    compileTemplates := compileTemplatesTask.value,

    sourceGenerators <+= compileTemplates,
    managedSourceDirectories <+= target in compileTemplates
  )

The compileTemplates setting gets its value from compileTemplatesTask.value. The compileTemplatesTask in turn returns the result from the TemplateCompiler.compile method, as shown here:

  def compileTemplatesTask = Def.task {
    TemplateCompiler.compile(
      (sourceDirectories in compileTemplates).value,
      (target in compileTemplates).value,
      templateFormats.value,
      templateImports.value,
      (includeFilter in compileTemplates).value,
      (excludeFilter in compileTemplates).value,
      Codec(sourceEncoding.value),
      useOldParser.value,
      streams.value.log
    )
  }

...
}

TemplateCompiler.compile is defined as follows:

  def compile(
    sourceDirectories: Seq[File],
    targetDirectory: File,
    templateFormats: Map[String, String],
    templateImports: Seq[String],
    includeFilter: FileFilter,
    excludeFilter: FileFilter,
    codec: Codec,
    useOldParser: Boolean,
    log: Logger) = {

    try {
      syncGenerated(targetDirectory, codec)
      val templates = collectTemplates(sourceDirectories, templateFormats, includeFilter, excludeFilter)
      for ((template, sourceDirectory, extension, format) <- templates) {
        val imports = formatImports(templateImports, extension)
        TwirlCompiler.compile(template, sourceDirectory, targetDirectory, format, imports, codec, inclusiveDot = false, useOldParser = useOldParser)
      }
      generatedFiles(targetDirectory).map(_.getAbsoluteFile)
    } catch handleError(log, codec)
  }

The compile method creates the target/scala-scalaVersion/src_managed directory within the project if it does not already exist. If it exists, then it deletes all the files that match the "*.template.scala" pattern through the cleanUp method. After this, the collectTemplates method gets Seq[(File, String, TemplateType)] by searching for files whose names match the "*.scala.*" pattern and end with a supported extension.

Each object from the result of collectTemplates is then passed as an argument for TwirlCompiler.compile.

TwirlCompiler.compile is responsible for parsing and generating Scala templates and is defined as follows:

def compile(source: File, sourceDirectory: File, generatedDirectory: File,
formatterType: String, additionalImports: String = "", logRecompilation: (File, File) => Unit = (_, _) => ()) = {
    val resultType = formatterType + ".Appendable"
    val (templateName, generatedSource) = generatedFile(source, sourceDirectory, generatedDirectory)
    if (generatedSource.needRecompilation(additionalImports)) {
      logRecompilation(source, generatedSource.file)
      val generated = parseAndGenerateCode(templateName, Path(source).byteArray, source.getAbsolutePath, resultType, formatterType, additionalImports)

      Path(generatedSource.file).write(generated.toString)

      Some(generatedSource.file)
    } else {
      None
    }
  }

The parseAndGenerateCode method gets the parser and parses the file. The resulting parsed Template (internal object) is passed on to the generateFinalCode method. The generateFinalCode method is responsible for generating the code. Internally, it uses the generateCode method, which is defined as follows:

def generateCode(packageName: String, name: String, root: Template, resultType: String, formatterType: String, additionalImports: String) = {
  val extra = TemplateAsFunctionCompiler.getFunctionMapping(
    root.params.str,
    resultType)

    val generated = {
      Nil :+ """
package """ :+ packageName :+ """

import twirl.api._
import TemplateMagic._

  """ :+ additionalImports :+ """
/*""" :+ root.comment.map(_.msg).getOrElse("") :+ """*/
object """ :+ name :+ """ extends BaseScalaTemplate[""" :+ resultType :+ """,Format[""" :+ resultType :+ """]](""" :+ formatterType :+ """) with """ :+ extra._3 :+ """ {

    /*""" :+ root.comment.map(_.msg).getOrElse("") :+ """*/
    def apply""" :+ Source(root.params.str, root.params.pos) :+ """:""" :+ resultType :+ """ = {
        _display_ {""" :+ templateCode(root, resultType) :+ """}
    }

  """ :+ extra._1 :+ """

  """ :+ extra._2 :+ """

    def ref: this.type = this

}"""
    }
    generated
  }

The result from parseAndGenerateCode is written into its corresponding file.

Let's check out where we are going to use the file we generated!

Consider the view defined in Chapter 1, Getting Started with Play; the generated Scala template is similar to the following:

package views.html

import play.templates._
import play.templates.TemplateMagic._

import play.api.templates._
import play.api.templates.PlayMagic._
import models._
import controllers._
import play.api.i18n._
import play.api.mvc._
import play.api.data._
import views.html._
/**/
object index extends BaseScalaTemplate[play.api.templates.HtmlFormat.Appendable,Format[play.api.templates.HtmlFormat.Appendable]](play.api.templates.HtmlFormat) with play.api.templates.Template2[List[Task],Form[String],play.api.templates.HtmlFormat.Appendable] {

    /**/
    def apply/*1.2*/(tasks: List[Task], taskForm: Form[String]):play.api.templates.HtmlFormat.Appendable = {
      _display_ {import helper._


Seq[Any](format.raw/*1.45*/("""

"""),format.raw/*4.1*/("""
"""),_display_(Seq[Any](/*5.2*/main("Task Tracker")/*5.22*/ {_display_(Seq[Any](format.raw/*5.24*/("""

    <h2>Task Tracker</h2>

    <div>
    """),_display_(Seq[Any](/*10.6*/form(routes.TaskController.newTask)/*10.41*/ {_display_(Seq[Any](format.raw/*10.43*/("""

        """),_display_(Seq[Any](/*12.10*/taskForm/*12.18*/.globalError.map/*12.34*/ { error =>_display_(Seq[Any](format.raw/*12.45*/("""
            <p class="error">
          """),_display_(Seq[Any](/*14.18*/error/*14.23*/.message)),format.raw/*14.31*/("""
            </p>
        """)))})),format.raw/*16.10*/("""
        <form>
            <input type="text" name="taskName" placeholder="Add a new Task" required>

            <input type="submit" value="Add">
        </form>
        """)))})),format.raw/*22.6*/("""
    </div>
    <div>
        <ul>
        """),_display_(Seq[Any](/*26.10*/tasks/*26.15*/.map/*26.19*/ { task =>_display_(Seq[Any](format.raw/*26.29*/("""
            <li>
        """),_display_(Seq[Any](/*28.18*/form(routes.TaskController.deleteTask(task.id))/*28.65*/ {_display_(Seq[Any](format.raw/*28.67*/("""
        """),_display_(Seq[Any](/*29.22*/task/*29.26*/.name)),format.raw/*29.31*/(""" <input type="submit" value="Remove">
                """)))})),format.raw/*30.18*/("""
            </li>
        """)))})),format.raw/*32.10*/("""
        </ul>
    </div>

""")))})))}
    }
    
    def render(tasks:List[Task],taskForm:Form[String]): play.api.templates.HtmlFormat.Appendable = apply(tasks,taskForm)
    
    def f:((List[Task],Form[String]) => play.api.templates.HtmlFormat.Appendable) = (tasks,taskForm) => apply(tasks,taskForm)
    
    def ref: this.type = this

}
                /*
                    -- GENERATED --
                    DATE: Timestamp
                    SOURCE: /TaskTracker/app/views/index.scala.html
                    HASH: ff7c2a525ebc63755f098d4ef80a8c0147eb7778
                    MATRIX: 573->1|726->44|754->63|790->65|818->85|857->87|936->131|980->166|1020->168|1067->179|1084->187|1109->203|1158->214|1242->262|1256->267|1286->275|1345->302|1546->472|1626->516|1640->521|1653->525|1701->535|1772->570|1828->617|1868->619|1926->641|1939->645|1966->650|2053->705|2113->733
                    LINES: 19->1|23->1|25->4|26->5|26->5|26->5|31->10|31->10|31->10|33->12|33->12|33->12|33->12|35->14|35->14|35->14|37->16|43->22|47->26|47->26|47->26|47->26|49->28|49->28|49->28|50->29|50->29|50->29|51->30|53->32
                    -- GENERATED --
*/

So, when we refer to this view in a controller as views.html.index(Task.all, taskForm), we are calling the apply method of the generated template object index.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.179.59