简体   繁体   中英

Add <script> to the <head> from scala template tags in Play Framework 2

I would like to add javascript to the <head> of my webpage from within tags.

This is the moreScripts equivalent I'm using on my pages:

main.scala.html

@(title: String, scripts: Html = Html(""))(content: Html)
<!DOCTYPE html>
<html lang="nl">
    <head>
        <title>@title</title>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        @scripts
    </head>
    <body>
        @content
    </body>
</html>

page.scala.html

@scripts = {
    <script type="text/javascript">
        $(document).ready(function() {
            alert(1);
        });
    </script>
}
@main("Title", scripts) {
    <p>page content</p>
}

So far so good! However I want to do the same from within a tag (component) I've written that needs to include some javascript code into the webpage.

My question is how can I pass a <script> element from the tag to the main.scala.html ?

So the page.scala.html would be:

page.scala.html

@import tags._
@main("Title") {
    @mytag("green")
}

mytag.scala.html

@(color: String)
<script type="text/javascript">
    $(document).ready(function() {
        alert('@color');
    });
</script>

<p>Some more content</p>

In this case the <script> tag is rendered halfway the HTML page, I want to pass the <script> tag into the @scripts variable so it can be rendered inside the <head> tag.

Ok, I've come up with a nicer solution IMHO.

I have created the following tags:

script.scala.html

@(content: Html)
@{
    var additionalScripts = ctx().args.get("additionalScripts").asInstanceOf[List[Html]];
    if(additionalScripts == null) {
        additionalScripts = new ArrayList[Html]();
        ctx().args.put("additionalScripts", additionalScripts)
    }

    val added = additionalScripts.add(content);
}

renderscripts.scala.html

@additionalScripts = @{ctx().args.get("additionalScripts").asInstanceOf[List[Html]]}
@if(additionalScripts != null) {
    @for(additionalScript <- additionalScripts) {
        @additionalScript
    }
}

In your main.scala.html you can use:

@(title: String)(content: Html)
@import tags._
<!DOCTYPE html>
<html lang="nl">
    <head>
        <title>@title</title>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        @renderscripts()
    </head>
    <body>
        @content
    </body>
</html>

You can specify additional script(s) in your templates or tags using:

@import tags._
@script {
    <script type="text/javascript">
        $(document).ready(function() {
            alert('This will be in the head!');
        });
    </script>
}

This is nice, right? :)

Maybe someone can clean up or enhance my code using their Scala magic :)

It's doable but I would agree with ajozwik and say that putting your scripts inline is going to be easier and should still work fine.


What you can do is add another parameter group, basically another (content:Html) , to your main template which is what will render the <script> tags generated by using mytag 's.

main.scala.html

@(title: String, scripts: Html = Html(""))(content: Html)(implicit mytagScripts: Html = null)
<!DOCTYPE html>
<html lang="nl">
  <head>
    <title>@title</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    @scripts
    @mytagScripts
  </head>
  <body>
    @content
  </body>
</html>

On any view where you're using mytag you'll need to define the value for mytagScripts . It's implicit so that you don't have to define it on templates where you're not using it, it'll just use the default.

Your tag template isn't going to change much other than you need to generate the <script> and store it for later use. We can use the Context.args map for this.

mytag.scala.html

@(color:String)
@{ctx().args.put("mytagScriptHtml", new Html("<script>alert('" + color + "');</script>"))}
<div>
  <!-- whatever else you're tag is generating -->
</div>

Finally, you're page would look something like this. Note the second set of curly braces which is what is defining mytagScripts on the main template. The closing/opening braces between the groups have to be on the same line or you'll get a compiler error.

page.scala.html

@import tags._
@main("Title") {
  @mytag("green")
} {
  @ctx().args.get("mytagScriptHtml")
}

Simplified example... if you're expecting to use the tag multiple times within a page then you'll need to keep track of the <script> tags in a List or something. Same concept though since you can store any Object in Context.args .

Why don't you just pass in the mytag to the main :

page.scala.html :

@import tags.html.mytag
@main("Title", mytag("green")) {
    <p>page content</p>
}

I did something similar in Play-scala 2.4 but with having scripts going into the footer, right before the body closing tag. I'm not familiar with Play's java api but i believe the concept still works.

Here is the gist: https://gist.github.com/zv3/2dad7cb63813e82f8412

controllers/StackableAction.scala

// Borrowed from: https://github.com/rabitarochan/Play2-ChainAction/blob/master/core/src/main/scala/com/github/rabitarochan/play2/stackableaction/StackableAction.scala
abstract class StackableAction extends ActionBuilder[RequestWithAttributes] with StackableFilter {

  override def filter[A](request: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = {
    f(request)
  }

  def invokeBlock[A](req: Request[A], block: RequestWithAttributes[A] => Future[Result]): Future[Result] = {
    val reqWA = new RequestWithAttributes(req, new TrieMap[AttributeKey[_], Any]())
    filter(reqWA)(block)
  }

}

trait StackableFilter {
  def filter[A](request: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result]
}

trait AttributeKey[A] {
  def ->(value: A): Attribute[A] = Attribute(this, value)
}

case class Attribute[A](key: AttributeKey[A], value: A) {
  def toTuple: (AttributeKey[A], A) = (key, value)
}

class RequestWithAttributes[A](request: Request[A], attributes: TrieMap[AttributeKey[_], Any]) extends WrappedRequest[A](request) {
  def get[B](key: AttributeKey[B]): Option[B] = attributes.get(key).asInstanceOf[Option[B]]
  def set[B](key: AttributeKey[B], value: B): RequestWithAttributes[A] = {
    attributes.put(key, value)
    this
  }
  def getAll[T](implicit classTag: ClassTag[T]) = {
    attributes.filterKeys {
      case p: T => true
      case _    => false
    }
  }
}

views/support/JavascriptPage.scala

object JavascriptPage {
  case class NonBlockingJS(key: String) extends AttributeKey[Html]
  case class BlockingJS(key: String) extends AttributeKey[Html]

  def addNonBlockingJS(div: String)(jscript: Html)(implicit request: Request[_]): Unit = {
    request match {
      case i: RequestWithAttributes[_] =>
        i.set(NonBlockingJS(div), jscript)
      case _ =>
    }
  }

  // scripts that are supposed to go into the <head> tag, thus blocking scripts
  def addBlockingJS(div: String)(jscript: Html)(implicit request: Request[_]): Unit = {
    request match {
      case i: RequestWithAttributes[_] =>
        i.set(BlockingJS(div), jscript)
      case _ =>
    }
  }

  // scripts that are supposed to go before the </body> tag, non blocking scripts that is
  def getNonBlockingJS()(implicit request: Request[_]): Seq[(String, Html)] = {
    request match {
      case i: RequestWithAttributes[_] =>
        i.getAll[NonBlockingJS].toSeq.map {
          case (NonBlockingJS(div), jscript: Html) => (div, jscript)
        }
      case _ => Seq.empty
    }
  }
}

inlineNonBlockingJS.scala.html

@import views.support.JavascriptPage
@(implicit request: Request[_])
<script src="/javascripts/your_javascript_app.js"></script>
<script id="non-blocking" type="text/javascript">
  @defining(JavascriptPage.getNonBlockingJS()) { scripts =>
    @scripts.map { case (_, item) => @item }
  }
</script>

It basically wraps the request (using play's action composition) with a case class that has a TrieMap as one of it's members, which would then serve as a holder of extra attributes tied to the request, and those attributes could be javascript entries and pretty much anything else that you could like to have and share during the life of a request.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM