Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use html tag / content in a stream template #25

Open
Driox opened this issue Oct 23, 2015 · 9 comments
Open

Use html tag / content in a stream template #25

Driox opened this issue Oct 23, 2015 · 9 comments

Comments

@Driox
Copy link

Driox commented Oct 23, 2015

In my app I have pages that use BigPipe and pages that don't.

I don't want to duplicate basic template tag in both format. e.g. analytics.scala.stream / analytics.scala.html or footer.scala.stream / footer.scala.html

In a classic html template I use the standard syntaxe :

in index.scala.html : @footer()
in build.sbt : TwirlKeys.templateImports ++= Seq("tags._")

If I do the same in detail.scala.stream I get the following error :

not found: value footer

I can use something like that

@{
  HtmlStream.fromHtml(views.html.tags.footer())
}

but it's a bit ugly and I have the feeling we can do better.

Next level to the question : I have a template with header/menu/footer. I want to use it for stream and html pages.

@(bigPipe   : BigPipe
, enterprise: Pagelet
, investors : Pagelet
, comments  : Pagelet
, documents : Pagelet
)(implicit request: RequestHeader, lang:Lang)

@scripts = {
    <script src="/assets/com/ybrikman/ping/big-pipe.js"></script>
}


@main("meta.application.index.title"
    , "meta.application.index.description"
    , "meta.application.index.keyword"
    , scripts) {


    <h1>@{m("meta.project.detail.title")}</h1>

        @bigPipe.render { pagelets =>
          <table class="wrapper">
            <tr>
              <td>@pagelets(enterprise.id)</td>
              <td>@pagelets(investors.id)</td>
            </tr>
            <tr>
              <td>@pagelets(comments.id)</td>
              <td>@pagelets(documents.id)</td>
            </tr>
          </table>
        }
}

The above doesn't work for the same reason : not found: value main
If I try to use the following I get : expected start of definition

@HtmlStream.fromHtml(views.html.main("meta.application.index.title"
    , "meta.application.index.description"
    , "meta.application.index.keyword"
    , scripts) {


    <h1>@{m("meta.project.detail.title")}</h1>

        @bigPipe.render { pagelets =>
          <table class="wrapper"> <!-- expected start of definition here -->
            <tr>
              <td>@pagelets(enterprise.id)</td>
              <td>@pagelets(investors.id)</td>
            </tr>
            <tr>
              <td>@pagelets(comments.id)</td>
              <td>@pagelets(documents.id)</td>
            </tr>
          </table>
        }
})
@brikis98
Copy link
Owner

Let's start with the simpler case: @footer(). My guess at why you're getting a "not found" error is because you are missing the import. The .scala.html templates get compiled into the view.html package, so other classes in that package see them automatically. However, .scala.stream templates get compiled into the view.stream package, so you'd need to explicitly import the view.html package.

Try the following in a streaming template:

@import views.html.tags._

@footer()

@Driox
Copy link
Author

Driox commented Oct 23, 2015

The import works for the "not found" issue.

however when I just use @footer() I get the content escaped

&lt;!-- FOOTER --&gt;
&lt;footer&gt;
    &lt;!-- footer content --&gt;
    &lt;div class=&quot;footer-content&quot;&gt;

instead of

 <!-- FOOTER --> <footer> <!-- footer content --> <div class="footer-content"> 

I can use

@HtmlStream.fromHtml(footer())

which is better. I also try an implicit converter from Html to HtmlStream without success

@implicitHtmlToStream(html:Html) = @{
  HtmlStream.fromHtml(html)
}

// this works 
@implicitHtmlToStream(footer())

// this doesn't work
@footer()

@brikis98
Copy link
Owner

OK, glad to hear the import thing works. You may want to add those imports to TwirlKeys.templateImports in your build.sbt file.

The escaping issue is something that would be good to fix. I suspect your approach with implicits should work. Let me experiment a bit.

@brikis98
Copy link
Owner

Ah, actually, Scala won't bother to look for an implicit in this case. That's because the template gets compiled into a series of calls to a _display_ method that takes Any as an argument and returns an HtmlStream. We need some other way to inject this behavior...

@brikis98
Copy link
Owner

Unfortunately, I don't see any way to fix this without a change to Play's Twirl template compiler. The core of the issue is that templates get compiled into a sequence of calls to the _display_ method in BaseScalaTemplate:

  def _display_(o: Any)(implicit m: Manifest[T]): T = {
    o match {
      case escaped if escaped != null && escaped.getClass == m.runtimeClass => escaped.asInstanceOf[T]
      case () => format.empty
      case None => format.empty
      case Some(v) => _display_(v)
      case xml: scala.xml.NodeSeq => format.raw(xml.toString())
      case escapeds: immutable.Seq[_] => format.fill(escapeds.map(_display_))
      case escapeds: TraversableOnce[_] => format.fill(escapeds.map(_display_).to[immutable.Seq])
      case escapeds: Array[_] => format.fill(escapeds.view.map(_display_).to[immutable.Seq])
      case string: String => format.escape(string)
      case v if v != null => format.escape(v.toString)
      case _ => format.empty
    }
  }

If the type of the item in your template is HtmlStream, it gets passed through unchanged. If it's Html, it skips down to the before-last case statement and calls format.escape(v.toString) on it, which is what leads to your HTML being escaped.

I can't find a way to modify this behavior. Implicits won't do it because the compiler has no reason to look for them. I can't override the _display_ method because I don't directly control the class that gets generated for my template. The twirl compiler handles that and, as far as I can see, offers no way to override methods or specify the base class.

The closest I've been able to come is to override _display_ directly in the body of my template:

@_display_(x: Any) = @{
  x match {
    case html: Html => HtmlStream.fromHtml(html)
    case _ => super._display_(x)
  }
}

@footer() // This will no longer be escaped

Defining this at the top of every template is a chore, and you can't import such a method from another class, or you get a "reference to display is ambiguous" error.

If anyone has any suggestions, please let me know. In the meantime, the workaround is to wrap calls to .scala.html templates (e.g. @footer()) with HtmlStream.fromHtml() (e.g. @HtmlStream.fromHtml(footer())).

@Driox
Copy link
Author

Driox commented Oct 23, 2015

Thanks for the quick answer. It looks like we can't do really better than @HtmlStream.fromHtml(footer()) so I'll stick we it

I try to reuse an html template (aka menu/footer) in a stream page and can't figure it out.

//detail.scala.stream
@(bigPipe   : BigPipe
, enterprise: Pagelet
, investors : Pagelet
, comments  : Pagelet
, documents : Pagelet
)(implicit request: RequestHeader, lang:Lang)

@main("meta.application.index.title"
    , "meta.application.index.description"
    , "meta.application.index.keyword") {
    <h1>Coucou</h1>
    //I try also @Html("<h1>coucou</h1>") without success
}

//main.scala.html
@(title      : String
, description: String = ""
, keywords   : String = ""
, scripts    : Html   = Html("")
, styles     : Html   = Html("")
)(content: Html)(implicit request: RequestHeader, lang:Lang)

<!DOCTYPE html>

<html lang="fr">
<head>
    <title>Title</title>
</head>
<body>
@content
</body>
</html>


I always get

type mismatch; found : com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamFormat.Appendable (which expands to) com.ybrikman.ping.scalaapi.bigpipe.HtmlStream required: play.twirl.api.Html

@brikis98
Copy link
Owner

In a .scala.stream template, all markup will be wrapped in an HtmlStream. For example, the following template:

@main(...) {
  <div>Some markup</div>
}

Gets roughly compiled to:

main(...)(format.raw("""<div>Some markup</div>"""))

The format is defined by the template type, and in this case, it's HtmlStreamFormat, who's raw method returns an HtmlStream. Since your main template is looking for Html, this leads to a compile error.

There are two possibilities:

  1. The content you want to pass into the main template contains streaming content (e.g., pagelets). In this case, there is no way to reuse the main template directly, since it returns Html and the only way to use streaming is to return HtmlStream. You'll have to create a new streaming main template. If you want to reuse parts of the non-streaming one, break those into separate helper templates such as head.scala.html, nav.scala.html, footer.scala.html, etc (this is a good practice anyway).
  2. The content you want to pass into the main template is completely static HTML. In that case, put it into a separate .scala.html template and pass it to main:
<!-- staticMarkupForMain.scala.html -->

<div>Some markup</div>
<!-- index.scala.html -->

@main(...)(staticMarkupForMain())

@Driox
Copy link
Author

Driox commented Oct 24, 2015

Thanks for your response. I think I understand way better how template works now

I try to reverse the process : define template in stream format and use it from html page

@content() = {
    <h1>coucou</h1>
}
@views.stream.main("...")(HtmlStream.fromHtml(content()))

But it only display a toString version of the HtmlStream

com.ybrikman.ping.scalaapi.bigpipe.HtmlStream@718e5a8a

With the complexity of mixing 2 types of templates, is going full stream template a good option ? Do you see any drawbacks ?
The first I can see is when using external libs (e.g. play form helper) we need to wrap it.

@brikis98
Copy link
Owner

A .scala.stream tempate will return an HtmlStream object, which is a wrapper for an Enumerator[Html]. There is no way to include those inside of a .scala.html template, since those return an Html object, which is a wrapper for a plain old String.

In other words, if the base template you're rendering is a .scala.stream template, you can include .scala.html templates within it by wrapping them in an HtmlStream.fromHtml(...) call:

// Controller
def index = Action {
  Ok.chunked(views.stream.main(...))
}
<!-- main.scala.stream -->
<div>
  <!-- Including other streaming templates is easy: -->
  @someStreamingTemplate()

  <!-- Including HTML templates requires a little wrapping: -->
  @HtmlStream.fromHtml(someHtmlTemplate())
</div>

If the base template you're rendering is a .scala.html template, you can only include other .scala.html templates within it:

// Controller
def index = Action {
  Ok(views.html.main(...))
}
<!-- main.scala.html -->
<div>
  <!-- You CANNOT include a streaming template -->
  <!-- This won't work: @someStreamingTemplate() -->

  <!-- Including other HTML templates is easy: -->
  someHtmlTemplate()
</div>

Therefore, it's more flexible to use .scala.stream as the base template in your app. I would still build static HTML blocks using .scala.html templates, as it communicates the intent better and makes them more reusable elsewhere (e.g. in tests). And you can reuse static HTML templates from external libs by wrapping them in HtmlStream.fromHtml(...) calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants