¿Cómo evitar pasar parámetros a todas partes en play2?

125

En play1, generalmente obtengo todos los datos en acciones, los uso directamente en las vistas. Como no necesitamos declarar explícitamente los parámetros a la vista, esto es muy fácil.

Pero en play2, descubrí que tenemos que declarar todos los parámetros (incluidos request) en el encabezado de las vistas, será muy aburrido obtener todos los datos en acciones y pasarlos a las vistas.

Por ejemplo, si necesito mostrar menús cargados desde la base de datos en la página principal, tengo que definirlo en 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>

Luego tengo que declararlo en cada subpágina:

@(menus: Seq[Menu])

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

Luego tengo que obtener los menús y pasarlos para verlos en cada acción:

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))
}

Por ahora es solo un parámetro main.scala.html, ¿y si hay muchos?

Entonces, por fin, decidí a todos Menu.findAll()directamente a la vista:

@(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>

No sé si es bueno o recomendado, ¿hay alguna solución mejor para esto?

Viento libre
fuente
Quizás play2 debería agregar algo como fragmentos de ascensor
Freewind

Respuestas:

229

En mi opinión, el hecho de que las plantillas estén estáticamente escritas es realmente bueno : tiene la garantía de que llamar a su plantilla no fallará si se compila.

Sin embargo, de hecho agrega algo repetitivo en los sitios de llamadas. Pero puede reducirlo (sin perder las ventajas de la escritura estática).

En Scala, veo dos formas de lograrlo: a través de la composición de acciones o mediante el uso de parámetros implícitos. En Java sugiero usar el Http.Context.argsmapa para almacenar valores útiles y recuperarlos de las plantillas sin tener que pasarlos explícitamente como parámetros de plantillas.

Usando parámetros implícitos

Coloque el menusparámetro al final de los main.scala.htmlparámetros de su plantilla y márquelo como "implícito":

@(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>

Ahora, si tiene plantillas que llaman a esta plantilla principal, puede hacer que el compilador Scala menuspase el parámetro implícitamente a la mainplantilla si también se declara como un parámetro implícito en estas plantillas:

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

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

Pero si desea que se pase implícitamente desde su controlador, debe proporcionarlo como un valor implícito, disponible en el ámbito desde donde llama a la plantilla. Por ejemplo, puede declarar el siguiente método en su controlador:

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

Luego, en sus acciones, podrá escribir lo siguiente:

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

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

Puede encontrar más información sobre este enfoque en esta publicación de blog y en este ejemplo de código .

Actualización : Aquí también se ha escrito una buena publicación de blog que demuestra este patrón .

Usar composición de acciones

En realidad, a menudo es útil pasar el RequestHeadervalor a las plantillas (ver, por ejemplo, esta muestra ). Esto no agrega tanto repetitivo a su código de controlador porque puede escribir fácilmente acciones que reciben un valor de solicitud implícito:

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

Entonces, dado que las plantillas a menudo reciben al menos este parámetro implícito, puede reemplazarlo con un valor más rico que contenga, por ejemplo, sus menús. Puedes hacerlo utilizando el mecanismo de composición de acciones de Play 2.

Para hacerlo, debe definir su Contextclase, ajustando una solicitud subyacente:

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

Luego puede definir el siguiente ActionWithMenumétodo:

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

Que se puede usar así:

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

Y puede tomar el contexto como un parámetro implícito en sus plantillas. Por ejemplo para 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>

El uso de la composición de acciones le permite agregar todos los valores implícitos que requieren sus plantillas en un solo valor, pero por otro lado puede perder algo de flexibilidad ...

Usando Http.Context (Java)

Dado que Java no tiene el mecanismo de implicaciones de Scala o similar, si desea evitar pasar parámetros de plantillas explícitamente, una posible forma es almacenarlos en el Http.Contextobjeto que vive solo durante la duración de una solicitud. Este objeto contiene un argsvalor de tipo Map<String, Object>.

Por lo tanto, puede comenzar escribiendo un interceptor, como se explica en la documentación :

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");
    }
}

El método estático es solo una abreviatura para recuperar los menús del contexto actual. Luego anote su controlador para mezclarlo con el Menusinterceptor de acción:

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

Finalmente, recupere el menusvalor de sus plantillas de la siguiente manera:

@(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>
Julien Richard-Foy
fuente
¿Quiso decir menús en lugar de menú? "menús de valores implícitos: Seq [Menú] = Menú.findAll"
Ben McCann
1
Además, dado que mi proyecto está escrito solo en Java en este momento, ¿sería posible seguir la ruta de composición de acciones y tener solo mi interceptor escrito en Scala, pero dejar todas mis acciones escritas en Java?
Ben McCann
"menú" o "menús", no importa :), lo que importa es el tipo: Seq [Menú]. Edité mi respuesta y agregué un patrón Java para manejar este problema.
Julien Richard-Foy
3
En el último bloque de código, llama @for(menu <- Menus.current()) {pero Menusnunca está definido (coloca menús (minúsculas):) ctx.args.put("menus", Menu.find.all());. ¿Hay una razón? ¿Te gusta jugar que lo transforma en mayúsculas o algo así?
Cyril N.
1
@ cx42net Hay una Menusclase definida (el interceptor Java). @adis Sí, pero puedes guardarlos en otro lugar, incluso en el caché.
Julien Richard-Foy
19

La forma en que lo hago es crear un nuevo controlador para mi navegación / menú y llamarlo desde la vista

Para que pueda definir su 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>
}

Entonces, en mi vista principal, puedo llamar a eso NavController:

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
     @NavController.nav
     @content
  </body>
</html>
Darko
fuente
¿Cómo se supone que se ve NavController en Java? No puedo encontrar una manera de hacer que el controlador devuelva el html.
Mika
Y sucede que encuentras la solución justo después de pedir ayuda :) El método del controlador debería verse así. public static play.api.templates.Html sidebar () {return (play.api.templates.Html) sidebar.render ("mensaje"); }
Mika
1
¿Es una buena práctica llamar al controlador desde una vista? No quiero ser riguroso, así que pregunta por curiosidad genuina.
0fnt
Además, no puede hacer cosas basadas en solicitudes de esta manera, ¿puede usted, por ejemplo, la configuración específica del usuario?
0fnt
14

Apoyo la respuesta de stian. Esta es una forma muy rápida de obtener resultados.

Acabo de migrar de Java + Play1.0 a Java + Play2.0 y las plantillas son la parte más difícil hasta ahora, y la mejor manera que encontré para implementar una plantilla base (por título, encabezado, etc.) es usando el Http .Contexto.

Hay una sintaxis muy buena que puedes lograr con las etiquetas.

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

donde get.scala.html es:

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

y set.scala.html es:

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

significa que puede escribir lo siguiente en cualquier plantilla

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

Por lo tanto, es muy legible y agradable.

Esta es la forma en que elegí ir. stian - buenos consejos. Demuestra que es importante desplazarse hacia abajo para ver todas las respuestas. :)

Pasando variables HTML

Todavía no he descubierto cómo pasar variables HTML.

@ (título: cadena, contenido: HTML)

Sin embargo, sé cómo pasarlos como bloque.

@ (título: Cadena) (contenido: HTML)

por lo que es posible que desee reemplazar set.scala.html con

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

de esta manera puedes pasar bloques Html así

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

EDITAR: efecto secundario con mi implementación "Set"

Un caso de uso común es la herencia de plantillas en Play.

Tiene una base_template.html y luego tiene page_template.html que extiende base_template.html.

base_template.html podría verse algo así

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

mientras que la plantilla de la página podría verse algo así

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

y luego tienes una página (supongamos que login_page.html) se parece a

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

@page_template()

Lo importante a tener en cuenta aquí es que configura "cuerpo" dos veces. Una vez en "login_page.html" y luego en "page_template.html".

Parece que esto desencadena un efecto secundario, siempre y cuando implemente set.scala.html como sugerí anteriormente.

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

ya que la página mostraría "cosas de inicio de sesión ..." dos veces porque put devuelve el valor que aparece la segunda vez que ponemos la misma clave. (ver poner firma en java docs).

Scala proporciona una mejor manera de modificar el mapa

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

que no causa este efecto secundario.

chico mograbi
fuente
En el controlador scala, intento hacer que no haya un método put en play.mvc.Htt.Context.current (). ¿Me estoy perdiendo de algo?
0fnt
intente poner el argscontexto posterior a la llamada actual.
chico mograbi
13

Si está utilizando Java y solo quiere la forma más simple posible sin tener que escribir un interceptor y utilizando la anotación @With, también puede acceder al contexto HTTP directamente desde la plantilla.

Por ejemplo, si necesita una variable disponible de una plantilla, puede agregarla al contexto HTTP con:

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

Luego puede acceder desde la plantilla con:

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

Obviamente, si ensucia sus métodos con Http.Context.current (). Args.put ("", "") es mejor que use un interceptor, pero para casos simples puede ser útil.

stian
fuente
Hola, Stian, mira mi última edición en mi respuesta. Me acabo de enterar que si usas "poner" en args dos veces con la misma tecla, obtienes un efecto secundario desagradable. Debería usar ... args (clave) = valor en su lugar.
chico mograbi
6

Por la respuesta de Stian, probé un enfoque diferente. Esto funciona para mi.

EN EL CÓDIGO JAVA

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

EN CABEZAL DE PLANTILLA HTML

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

Y UTILIZAR COMO

@if(isOk) {
   <div>OK</div>
}
angelokh
fuente