可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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?
回答1:
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>
回答2:
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>
回答3:
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.
回答4:
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.
回答5:
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>
}