How to avoid passing parameters everywhere in play2?
Asked Answered
E

5

126

In play1, I usually get all data in actions, use them directly in views. Since we don't need to explicitly declare parameters in view, this is very easy.

But in play2, I found we have to declare all the parameters(including request) in the head of views, it will be very boring to get all data in actions and pass them into views.

For example, if I need to display menus which are loaded from database in the front page, I have to define it in main.scala.html:

@(title: String, menus: Seq[Menu])(content: Html)    

<html><head><title>@title</title></head>
<body>
    <div>
    @for(menu<-menus) {
       <a href="#">@menu.name</a>
    }
    </div>
    @content
</body></html>

Then I have to declare it in every sub page:

@(menus: Seq[Menu])

@main("SubPage", menus) {
   ...
}

Then I have to get the menus and pass it to view in every action:

def index = Action {
   val menus = Menu.findAll()
   Ok(views.html.index(menus))
}

def index2 = Action {
   val menus = Menu.findAll()
   Ok(views.html.index2(menus))
}

def index3 = Action {
   val menus = Menu.findAll()
   Ok(views.html.index(menus3))
}

For now it's only one parameter in main.scala.html, what if there are many?

So at last, I decided to all Menu.findAll() directly in view:

@(title: String)(content: Html)    

<html><head><title>@title</title></head>
<body>
    <div>
    @for(menu<-Menu.findAll()) {
       <a href="#">@menu.name</a>
    }
    </div>
    @content
</body></html>

I don't know if it is good or recommended, is there any better solution for this?

Enrichetta answered 9/3, 2012 at 5:14 Comment(1)
Maybe play2 should add something like lift's snippetsEnrichetta
S
229

In my opinion, the fact that templates are statically typed is actually a good thing: you’re guaranteed that calling your template will not fail if it compiles.

However, it indeed adds some boilerplate on the calling sites. But you can reduce it (without losing static typing advantages).

In Scala, I see two ways to achieve it: through actions composition or by using implicit parameters. In Java I suggest using the Http.Context.args map to store useful values and retrieve them from the templates without having to explicitly pass as templates parameters.

Using implicit parameters

Place the menus parameter at the end of your main.scala.html template parameters and mark it as “implicit”:

@(title: String)(content: Html)(implicit menus: Seq[Menu])    

<html>
  <head><title>@title</title></head>
  <body>
    <div>
      @for(menu<-menus) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

Now if you have templates calling this main template, you can have the menus parameter implicitly passed for you to the main template by the Scala compiler if it is declared as an implicit parameter in these templates as well:

@()(implicit menus: Seq[Menu])

@main("SubPage") {
  ...
}

But if you want to have it implicitly passed from your controller you need to provide it as an implicit value, available in the scope from where you call the template. For instance, you can declare the following method in your controller:

implicit val menu: Seq[Menu] = Menu.findAll

Then in your actions you’ll be able to just write the following:

def index = Action {
  Ok(views.html.index())
}

def index2 = Action {
  Ok(views.html.index2())
}

You can find more information about this approach in this blog post and in this code sample.

Update: A nice blog post demonstrating this pattern has also been written here.

Using actions composition

Actually, it’s often useful to pass the RequestHeader value to the templates (see e.g. this sample). This does not add so much boilerplate to your controller code because you can easily write actions receiving an implicit request value:

def index = Action { implicit request =>
  Ok(views.html.index()) // The `request` value is implicitly passed by the compiler
}

So, since templates often receive at least this implicit parameter, you could replace it with a richer value containing e.g. your menus. You can do that by using the actions composition mechanism of Play 2.

To do that you have to define your Context class, wrapping an underlying request:

case class Context(menus: Seq[Menu], request: Request[AnyContent])
        extends WrappedRequest(request)

Then you can define the following ActionWithMenu method:

def ActionWithMenu(f: Context => Result) = {
  Action { request =>
    f(Context(Menu.findAll, request))
  }
}

Which can be used like this:

def index = ActionWithMenu { implicit context =>
  Ok(views.html.index())
}

And you can take the context as an implicit parameter in your templates. E.g. for main.scala.html:

@(title: String)(content: Html)(implicit context: Context)

<html><head><title>@title</title></head>
  <body>
    <div>
      @for(menu <- context.menus) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

Using actions composition allows you to aggregate all the implicit values your templates require into a single value, but on the other hand you can lose some flexibility…

Using Http.Context (Java)

Since Java does not have Scala’s implicits mechanism or similar, if you want to avoid to explicitly pass templates parameters a possible way is to store them in the Http.Context object which lives only for the duration of a request. This object contains an args value of type Map<String, Object>.

Thus, you can start by writing an interceptor, as explained in the documentation:

public class Menus extends Action.Simple {

    public Result call(Http.Context ctx) throws Throwable {
        ctx.args.put("menus", Menu.find.all());
        return delegate.call(ctx);
    }

    public static List<Menu> current() {
        return (List<Menu>)Http.Context.current().args.get("menus");
    }
}

The static method is just a shorthand to retrieve the menus from the current context. Then annotate your controller to be mixed with the Menus action interceptor:

@With(Menus.class)
public class Application extends Controller {
    // …
}

Finally, retrieve the menus value from your templates as follows:

@(title: String)(content: Html)
<html>
  <head><title>@title</title></head>
  <body>
    <div>
      @for(menu <- Menus.current()) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>
Sultana answered 9/3, 2012 at 9:58 Comment(16)
Did you mean menus instead of menu? "implicit val menus: Seq[Menu] = Menu.findAll"Starflower
Also, since my project is written only in Java right now, would it be possible to go the action composition route and have just my interceptor written in Scala, but leave all my actions written in Java?Starflower
"menu" or "menus", it doesn’t matter :), what matters is the type: Seq[Menu]. I edited my answer and added a Java pattern to handle this problem.Sultana
In the last code block, you call @for(menu <- Menus.current()) { but Menus is never defined (you put menus (lower case) : ctx.args.put("menus", Menu.find.all());). Is there a reason? Like Play that transforms it in uppercase or something?Goral
Aren't these interceptors not expensive, since we read the database on each request?Segmentation
@cx42net There is a Menus class defined (the Java interceptor). @Segmentation Yes but you’re free to store them in another place, even in the cache.Sultana
Why my template cannot find action? I have error "not found: value Menus"Doting
"Using implicit parameters" will not be that useful if you re tring to pass paramters with same types...Symptom
I don't believe that Http.Context.current() will actually work. It will throw an error "There is no HTTP Context available from here", similar to aviks problemUnalloyed
It should work. The problem you mention appears only on 2.0.x when you try to access the HTTP Context after having called map or flatMap on an async result (it has been fixed in 2.1.0).Sultana
I don't see this helps at all, I acknowledge the idea that type safe template is good, but the cost of convenience is not a good compromise.Flashing
When I try the Java aproach I have compilation error that there are no variable called Menus in template... I am using play in version 2.2.1 and mine call(Http.Context ctx) returns Promise<SimpleResult>.Leeth
That’s probably just a matter of using a fully qualified name. In which package is your Menus class defined?Sultana
Great answer, however one thing I didn't understand is, in he Scala action composition part, what loss of flexibility are you talking about? 'lose some flexibility'Climactic
With the given action composition approach, you aggregate all your parameters into a single one. With the implicit parameters approach you can have templates that use just one parameter. This point can matter if the code required to retrieve a parameter value involves some heavy computationSultana
@JulienRichard-Foy Got it, Thanks. Newbie question, scala related: but how does one use the ActionWithMenu without classifying it by object, across all controllers?Climactic
N
19

The way I do it, is to just create a new controller for my navigation/menu and call it from the view

So you can define your NavController:

object NavController extends Controller {

  private val navList = "Home" :: "About" :: "Contact" :: Nil

  def nav = views.html.nav(navList)

}

nav.scala.html

@(navLinks: Seq[String])

@for(nav <- navLinks) {
  <a href="#">@nav</a>
}

Then in my main view I can call that NavController:

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
     @NavController.nav
     @content
  </body>
</html>
Ninos answered 4/4, 2012 at 16:42 Comment(4)
How the NavController is supposed look in Java? I can't find a way to make the controller to return the html.Pinder
And so it happens that you find the solution right after asking for some help :) The controller method should look like this. public static play.api.templates.Html sidebar() { return (play.api.templates.Html) sidebar.render("message"); }Pinder
Is this a good practise to call controller from a view? I don't want to be a stickler, so asking out of genuine curiosity.Climactic
Also, you can't do stuff based on requests in this way, can you.. , for example user specific settings..Climactic
A
14

If you are using Java and just want the simplest possible way without having to write an interceptor and using the @With annotation, you can also access the HTTP context directly from the template.

E.g. if you need a variable available from a template you can add it to the HTTP context with:

Http.Context.current().args.put("menus", menus)

You can then access it from the template with:

@Http.Context.current().args.get("menus").asInstanceOf[List<Menu>]

Obviously if you litter your methods with Http.Context.current().args.put("","") you're better of using an interceptor, but for simple cases it may do the trick.

Avocet answered 12/12, 2012 at 10:14 Comment(1)
Hi stian, please have a look at my last edit in my answer. I just found out that if you use "put" in on args twice with same key, you get a nasty side-effect. You should use ...args(key)=value instead.Apo
A
14

I support stian's answer. This is a very quick way to get results.

I just migrated from Java+Play1.0 to Java+Play2.0 and the templates are the hardest part so far, and the best way I found to implement a base template (for title, head etc..) is by using the Http.Context.

There is a very nice syntax you can achieve with tags.

views
  |
  \--- tags
         |
         \------context
                  |
                  \-----get.scala.html
                  \-----set.scala.html

where get.scala.html is :

@(key:String)
@{play.mvc.Http.Context.current().args.get(key)}

and set.scala.html is:

@(key:String,value:AnyRef)
@{play.mvc.Http.Context.current().args.put(key,value)}

means you can write the following in any template

@import tags._
@context.set("myKey","myValue")
@context.get("myKey")

So it is very readable and nice.

This is the way I chose to go. stian - good advice. Proves it is important to scroll down to see all answers. :)

Passing HTML variables

I haven't figured out yet how to pass Html variables.

@(title:String,content:Html)

however, I know how to pass them as block.

@(title:String)(content:Html)

so you might want to replace set.scala.html with

@(key:String)(value:AnyRef)
@{play.mvc.Http.Context.current().args.put(key,value)}

this way you can pass Html blocks like so

@context.set("head"){ 
     <meta description="something here"/> 
     @callSomeFunction(withParameter)
}

EDIT: Side-Effect With My "Set" Implementation

A common use-case it template inheritance in Play.

You have a base_template.html and then you have page_template.html that extends base_template.html.

base_template.html might look something like

<html> 
    <head>
        <title> @context.get("title")</title>
    </head>
    <body>
       @context.get("body")
    </body>
</html>

while page template might look something like

@context.set("body){
    some page common context here.. 
    @context.get("body")
}
@base_template()

and then you have a page (lets assume login_page.html) that looks like

@context.set("title"){login}
@context.set("body"){
    login stuff..
}

@page_template()

The important thing to note here is that you set "body" twice. Once in "login_page.html" and then in "page_template.html".

It seems that this triggers a side-effect, as long as you implement set.scala.html like I suggested above.

@{play.mvc.Http.Context.current().put(key,value)}

as the page would show "login stuff..." twice because put returns the value that pops out the second time we put same key. (see put signature in java docs).

scala provides a better way to modify the map

@{play.mvc.Http.Context.current().args(key)=value}

which does not cause this side effect.

Apo answered 17/12, 2012 at 7:2 Comment(2)
In scala controller, I try to do there is no put method in play.mvc.Htt.Context.current(). Am I missing something?Climactic
try putting the args after calling context current.Apo
S
6

From Stian's answer, I tried a different approach. This works for me.

IN JAVA CODE

import play.mvc.Http.Context;
Context.current().args.put("isRegisterDone", isRegisterDone);

IN HTML TEMPLATE HEAD

@import Http.Context
@isOk = @{ Option(Context.current().args.get("isOk")).getOrElse(false).asInstanceOf[Boolean] } 

AND USE LIKE

@if(isOk) {
   <div>OK</div>
}
Stonecutter answered 24/3, 2013 at 0:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.