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:
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".
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.
3.145.17.140