Search code examples
scalaplayframework-2.0template-engine

How to deal with multiple tier templates with scripts in play framework 2?


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!


Solution

  • 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.