Jak uniknąć przekazywania parametrów wszędzie w play2?

125

W play1 zwykle otrzymuję wszystkie dane w akcjach, używam ich bezpośrednio w widokach. Ponieważ nie musimy jawnie deklarować parametrów w widoku, jest to bardzo łatwe.

Ale w play2 stwierdziłem, że musimy zadeklarować wszystkie parametry (w tym request) w nagłówku widoków, bardzo nudne będzie pobranie wszystkich danych w akcjach i przekazanie ich do widoków.

Na przykład, jeśli chcę wyświetlić menu, które są ładowane z bazy danych na stronie głównej, muszę to zdefiniować w 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>

Następnie muszę to zadeklarować na każdej podstronie:

@(menus: Seq[Menu])

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

Następnie muszę zdobyć menu i przekazać je do wyświetlenia w każdej akcji:

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

Na razie to tylko jeden parametr main.scala.html, a jeśli jest ich wiele?

W końcu zdecydowałem się wszystko Menu.findAll()bezpośrednio na widoku:

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

Nie wiem, czy jest to dobre, czy zalecane, czy jest na to lepsze rozwiązanie?

Freewind
źródło
Może play2 powinno dodać coś takiego jak fragmenty windy
Freewind

Odpowiedzi:

229

Moim zdaniem fakt, że szablony są typowane statycznie, jest w rzeczywistości dobrą rzeczą: masz gwarancję, że wywołanie szablonu nie zakończy się niepowodzeniem, jeśli się skompiluje.

Jednak rzeczywiście dodaje pewne standardowe elementy do stron wywołujących. Ale możesz to zmniejszyć (bez utraty zalet pisania statycznego).

W Scali widzę dwa sposoby osiągnięcia tego: poprzez kompozycję akcji lub użycie niejawnych parametrów. W Javie sugeruję użycie Http.Context.argsmapy do przechowywania przydatnych wartości i pobierania ich z szablonów bez konieczności jawnego przekazywania jako parametrów szablonów.

Używanie niejawnych parametrów

Umieść menusparametr na końcu main.scala.htmlparametrów szablonu i oznacz go jako „niejawny”:

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

Teraz, jeśli masz szablony wywołujące ten główny szablon, możesz mieć menusparametr niejawnie przekazany do mainszablonu przez kompilator Scala, jeśli jest on również zadeklarowany jako niejawny parametr w tych szablonach:

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

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

Ale jeśli chcesz, aby była ona niejawnie przekazywana z kontrolera, musisz podać ją jako niejawną wartość, dostępną w zakresie, z którego wywołujesz szablon. Na przykład możesz zadeklarować w kontrolerze następującą metodę:

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

Następnie w swoich działaniach będziesz mógł po prostu napisać:

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

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

Więcej informacji na temat tego podejścia można znaleźć w tym poście na blogu oraz w tym przykładzie kodu .

Aktualizacja : ładny wpis na blogu demonstrujący ten wzór został również napisany tutaj .

Używanie kompozycji akcji

W rzeczywistości często przydatne jest przekazywanie RequestHeaderwartości do szablonów (patrz np. Ten przykład ). Nie dodaje to zbyt wiele schematu do kodu kontrolera, ponieważ można łatwo pisać akcje otrzymujące niejawną wartość żądania:

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

Tak więc, ponieważ szablony często otrzymują przynajmniej ten niejawny parametr, możesz zastąpić go bogatszą wartością zawierającą np. Twoje menu. Możesz to zrobić za pomocą mechanizmu kompozycji akcji Play 2.

Aby to zrobić, musisz zdefiniować swoją Contextklasę, opakowując podstawowe żądanie:

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

Następnie możesz zdefiniować następującą ActionWithMenumetodę:

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

Które można wykorzystać w ten sposób:

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

I możesz wziąć kontekst jako niejawny parametr w swoich szablonach. Np. Dla 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>

Korzystanie z kompozycji akcji umożliwia agregowanie wszystkich niejawnych wartości wymaganych przez szablony w jedną wartość, ale z drugiej strony możesz stracić pewną elastyczność…

Korzystanie z Http.Context (Java)

Ponieważ Java nie ma mechanizmu implicits Scala lub podobnego, jeśli chcesz uniknąć jawnego przekazywania parametrów szablonów, możliwym sposobem jest przechowywanie ich w Http.Contextobiekcie, który istnieje tylko na czas trwania żądania. Ten obiekt zawiera argswartość typu Map<String, Object>.

Dlatego możesz zacząć od napisania przechwytywacza, jak wyjaśniono w dokumentacji :

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

Metoda statyczna to tylko skrót do pobierania menu z bieżącego kontekstu. Następnie dodaj adnotację do kontrolera, który ma zostać zmieszany z Menusprzechwytywaczem akcji:

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

Na koniec pobierz menuswartość ze swoich szablonów w następujący sposób:

@(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
źródło
Czy chodziło Ci o menu zamiast menu? "implicit val menus: Seq [Menu] = Menu.findAll"
Ben McCann
1
Ponadto, skoro mój projekt jest teraz napisany tylko w Javie, czy byłoby możliwe, aby przejść do trasy kompozycji akcji i napisać tylko mój przechwytywacz w Scali, ale zostawić wszystkie moje akcje napisane w Javie?
Ben McCann,
„menu” czy „menu”, to nie ma znaczenia :), liczy się typ: Seq [Menu]. Zredagowałem moją odpowiedź i dodałem wzorzec Java, aby rozwiązać ten problem.
Julien Richard-Foy
3
W ostatnim bloku kodu wywołujesz, @for(menu <- Menus.current()) {ale Menusnigdy nie jest zdefiniowany (umieszczasz menu (małe litery) ctx.args.put("menus", Menu.find.all());:). Czy jest powód? Lubisz Play, który przekształca to w wielkie litery, czy coś?
Cyril N.
1
@ cx42net Istnieje Menuszdefiniowana klasa (przechwytywacz Java). @adis Tak, ale możesz przechowywać je w innym miejscu, nawet w pamięci podręcznej.
Julien Richard-Foy
19

Sposób, w jaki to robię, polega po prostu na utworzeniu nowego kontrolera dla mojej nawigacji / menu i wywołaniu go z widoku

Możesz więc zdefiniować swoje 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>
}

W moim głównym widoku mogę to nazwać NavController:

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
  <head>
    <title>@title</title>
  </head>
  <body>
     @NavController.nav
     @content
  </body>
</html>
Darko
źródło
Jak powinien wyglądać NavController w Javie? Nie mogę znaleźć sposobu, aby kontroler zwrócił kod HTML.
Mika
I tak się składa, że ​​rozwiązanie znajduje się zaraz po poproszeniu o pomoc :) Sposób kontrolera powinien wyglądać tak. public static play.api.templates.Html sidebar () {return (play.api.templates.Html) sidebar.render ("wiadomość"); }
Mika
1
Czy to dobra praktyka wywoływania kontrolera z widoku? Nie chcę być pedałem, więc pytam z prawdziwej ciekawości.
0fnt
Poza tym nie możesz w ten sposób robić rzeczy na podstawie żądań, prawda ..., na przykład ustawienia użytkownika ...
0fnt
14

Popieram odpowiedź Stiana. To bardzo szybki sposób na uzyskanie wyników.

Właśnie przeprowadziłem migrację z Java + Play1.0 do Java + Play2.0, a szablony są jak dotąd najtrudniejszą częścią, a najlepszym sposobem, w jaki znalazłem zaimplementowanie szablonu podstawowego (dla tytułu, nagłówka itp.), Jest użycie Http .Kontekst.

Istnieje bardzo ładna składnia, którą można uzyskać za pomocą tagów.

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

gdzie get.scala.html to:

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

a set.scala.html to:

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

oznacza, że ​​możesz napisać w dowolnym szablonie

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

Jest więc bardzo czytelny i ładny.

To jest droga, którą wybrałem. stian - dobra rada. Dowodzi, że przewijanie w dół, aby zobaczyć wszystkie odpowiedzi, jest ważne. :)

Przekazywanie zmiennych HTML

Nie wymyśliłem jeszcze, jak przekazywać zmienne HTML.

@ (tytuł: String, treść: Html)

jednak wiem, jak przekazać je jako blok.

@ (tytuł: String) (zawartość: Html)

więc możesz zamienić set.scala.html na

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

w ten sposób możesz przekazywać bloki HTML w ten sposób

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

EDYCJA: Efekt uboczny mojej implementacji „zestawu”

Typowe dziedziczenie szablonów IT w Play.

Masz plik base_template.html, a następnie page_template.html, który rozszerza base_template.html.

base_template.html może wyglądać mniej więcej tak

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

podczas gdy szablon strony może wyglądać jak

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

a następnie masz stronę (załóżmy login_page.html), która wygląda jak

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

@page_template()

Ważną rzeczą do zapamiętania jest dwukrotne ustawienie „body”. Raz w „login_page.html”, a następnie w „page_template.html”.

Wygląda na to, że wywołuje to efekt uboczny, o ile implementujesz set.scala.html, jak zasugerowałem powyżej.

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

tak jak strona pokaże dwukrotnie "login stuff ...", ponieważ polecenie put zwraca wartość, która pojawia się przy drugim umieszczeniu tego samego klucza. (zobacz umieszczanie podpisu w dokumentach java).

scala zapewnia lepszy sposób modyfikowania mapy

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

który nie powoduje tego efektu ubocznego.

facet mograbi
źródło
W kontrolerze scala próbuję zrobić, że w play.mvc.Htt.Context.current () nie ma metody put. Czy coś mi brakuje?
0fnt
spróbuj umieścić argspo wywołaniu kontekstu jako bieżący.
facet mograbi
13

Jeśli używasz Javy i chcesz po prostu jak najprostszy sposób bez konieczności pisania przechwytywacza i używania adnotacji @With, możesz również uzyskać dostęp do kontekstu HTTP bezpośrednio z szablonu.

Np. Jeśli potrzebujesz zmiennej dostępnej z szablonu, możesz dodać ją do kontekstu HTTP za pomocą:

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

Następnie możesz uzyskać do niego dostęp z szablonu za pomocą:

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

Oczywiście, jeśli zaśmiecasz swoje metody za pomocą Http.Context.current (). Args.put ("", ""), lepiej jest użyć przechwytywacza, ale w prostych przypadkach może to załatwić sprawę.

stian
źródło
Cześć stian, spójrz na moją ostatnią zmianę w mojej odpowiedzi. Właśnie się dowiedziałem, że jeśli użyjesz "put" na argumentach dwa razy z tym samym kluczem, otrzymasz nieprzyjemny efekt uboczny. Zamiast tego należy użyć ... args (klucz) = wartość.
facet mograbi
6

Z odpowiedzi Stiana, spróbowałem innego podejścia. To działa dla mnie.

W KODZIE JAVA

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

W nagłówku szablonu HTML

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

I UŻYWAJ JAK

@if(isOk) {
   <div>OK</div>
}
angelokh
źródło