I have a template structure like this:
modal.scala.view
@()
... HTML code to display a modal in my app ...
foo.scala.view
@()
@scripts {
... The scripts required by foo.scala.view components ...
... The scripts required by modal.scala.view ... I want to avoid this!
}
@main(scripts){
... HTML code of foo ...
@modal
}
main.scala.view
@(scripts: Html)
... Main HTML code ...
@scripts
I would like to keep the scripts of modal in the modal.scala.view but I cant find a way to pass the scripts from the sub-template to the parent in order to render them in the correct place of the main template. Anyideas? Thanks in advance!
I don't think there's a canonical answer to your question that the Play team has blessed, but I can think of a couple of approaches: a monadic approach and an imperative approach.
Wrap views in sub-controllers; encapsulate scripts in the output
A large project I'm working on uses this strategy. We created a MultipartHtml
type that contains Html
output that should be contained in the document body and a type we created called Resources
which contain stuff that should go elsewhere. We treat this type like a monad, so that we can map
and flatMap
it to manipulate the Html
document content, while accumulating and deduplicating the Resources
.
All of our controllers return MultipartHtml
. They construct an instance from the results of the views and then simply :+
Resource
tags to the result. Our page-level controllers composite these pieces together. The core of what we do looks something like this:
/**
* Output type for components to render body, head and end-of-body content
* @param bodyMarkup the visual part of the component output
* @param resources tags for content to include in the head or footer
*/
case class MultipartHtml(bodyMarkup: Html, resources: Seq[MultipartHtml.Resource] = Nil) {
import com.huffpost.hyperion.lib.MultipartHtml._
/**
* Apply a transformation to the body content of this object
* @param bodyMapper transformation function
* @return a new object with transformed body content
*/
def map(bodyMapper: Html => Html): MultipartHtml = MultipartHtml(bodyMapper(bodyMarkup), resources)
/**
* @param bodyMapper transformation function
* @return the bodyMapper result combined with the component resource list
*/
def flatMap(bodyMapper: Html => MultipartHtml): MultipartHtml = bodyMapper(bodyMarkup) ++ resources
/**
* Add a head and/or footer content to this object
* @param resources the resources to add
* @return a new object with the resource added
*/
def ++(resources: GenTraversableOnce[Resource]): MultipartHtml = resources.foldLeft(this)(_ :+ _)
/**
* Add a head or footer content to this object
* @param resource the resource to add
* @return a new object with the resource added
*/
def :+(resource: Resource): MultipartHtml = MultipartHtml(bodyMarkup, (resources :+ resource).distinct)
/**
* Prepend a head or footer content to this object
* @param resource the resource to add
* @return a new object with the resource added
*/
def +:(resource: Resource): MultipartHtml = MultipartHtml(bodyMarkup, (resource +: resources).distinct)
/** Get tags by resource type for injection into a template */
def renderResourcesByType(resourceType: ResourceType): Html = Html(resources.filter(_.resourceType == resourceType).mkString("\n"))
}
/** Utility methods for MultipartHtml type */
object MultipartHtml {
/** Empty MultipartHtml */
def empty = MultipartHtml(Html(""))
/** A resource that can be imported in the HTML head or footer*/
trait ResourceType
trait Resource {
def resourceType: ResourceType
}
object HeadTag extends ResourceType
object FooterTag extends ResourceType
/** A style tag */
case class StyleTag(styleUrl: String) extends Resource {
val resourceType = HeadTag
override def toString = {
val assetUrl = routes.Assets.at(styleUrl).url
s"""<link rel="stylesheet" type="text/css" media="screen" href="$assetUrl">"""
}
}
/** A script tag */
case class ScriptTag(scriptUrl: String) extends Resource {
val resourceType = FooterTag
override def toString = {
val assetUrl = routes.Assets.at(s"javascript/$scriptUrl").url
s"""<script type="text/javascript" src="$assetUrl"></script>"""
}
}
}
There's a whole architecture built on top of this, but I probably shouldn't share too much, as it's an unreleased product.
Create a helper that stores scripts in a mutable collection
Another strategy might be to have your top level controller create an object that stores up the tags in a mutable data structure. I haven't tried this myself, but you could do something like:
import play.twirl.api.Html
case class TagStore(id: String) {
val tags = scala.collection.mutable.Set[Html]()
def addTag(tag: Html): Unit = {
store += tag
}
}
Then in your template, you could do:
@(addTag: Html => Unit)
@addTag {
<script src="myscript.js"></script>
}
@* Generate HTML *@
Downside here is that you have to forward this object down the line somehow, which could be a pain if your hierarchy of partial views can get deep.