<dependency>
<groupId>com.github.andriykuba</groupId>
<artifactId>play-handlebars</artifactId>
<version>2.5.1</version>
</dependency>
sbt:
libraryDependencies += "com.github.andriykuba" % "play-handlebars" % "2.5.1"
<dependency>
<groupId>com.github.andriykuba</groupId>
<artifactId>play-handlebars</artifactId>
<version>2.5.1</version>
</dependency>
sbt:
libraryDependencies += "com.github.andriykuba" % "play-handlebars" % "2.5.1"
public class HomeController extends Controller {
@Inject
private HandlebarsApi handlebarsApi;
public Result index() {
// Data.
final Map data = new HashMap<>();
data.put("title", "Page Title");
data.put("header", "Header");
data.put("main", ImmutableMap.of("article", "Main Article"));
data.put("footer", "Footer");
// Fill it with the data.
final Content page = handlebarsApi.html("page", data, Context.current().lang().code());
// Return the page to the client.
return ok(page);
}
}
Or
class HomeController @Inject() (val handlebarsApi: HandlebarsApi)extends Controller with HandlebarsSupport{
def index = Action { implicit request =>{
val jsonData =
Json.obj("users" -> Json.arr(
Json.obj(
"name" -> "Jhon",
"age" -> 4,
"role" -> "Worker"
),
Json.obj(
"name" -> "Duck",
"age" -> 6,
"role" -> "Administrator"
)))
val page = render("page", jsonData)
Ok(page)
}}
}
...
<form action="{{route "controllers.HomeController.loginSubmit"}}" method="POST>
...
{{route "controllers.HomeController.myAction()"}}
{{route "controllers.HomeController.myActionName(\"name\")"}}
{{route "controllers.HomeController.myActionAge(33)"}}
The core method of realization is the reflection of the correspond routes class:
private static String reverseUrl(
final String controllerPackage,
final String controllerClass,
final String methodName,
final RouteMethodArguments methodArguments) throws Exception {
// Get the play class loader.
final ClassLoader classLoader = Play.classloader(Play.current());
// Load the auto generated class "routes".
final Class routerClass = classLoader.loadClass(controllerPackage + ".routes");
// Get the reverse router object of the controller.
final Field declaredField = routerClass.getDeclaredField(controllerClass);
// It's static field.
final Object object = declaredField.get(null);
final Class type = declaredField.getType();
// Get the action of the reverse controller.
final Method routerMethod = type.getMethod(methodName, methodArguments.types);
final Call invoke = (Call) routerMethod.invoke(object, methodArguments.values);
// Get the URL of the action.
final String actionUrl = invoke.url();
return actionUrl;
}
There are also some trivial code for the parsing helper parameter and for the caching. For now, I support only the String and Integer parameters for the actions. The cashe system is the guava cache.
private static class RouteMethodArguments {
final Class<?>[] types;
final Object[] values;
RouteMethodArguments(Class<?>[] types, Object[] values) {
this.types = types;
this.values = values;
}
}
@Inject
private HandlebarsApi handlebarsApi;
Content page = handlebarsApi.html("page", data);
# configure Handlebars API
handlebars{
include "handlebars.conf"
}
directory: "/templates"
extension: ".hbs"
configuration.getString("handlebars.directory")
package handlebars;
@Singleton
public class HandlebarsApi {
...
}
package handlebars;
import play.api.Configuration;
import play.api.Environment;
import play.api.inject.Binding;
import scala.collection.Seq;
public class Module extends play.api.inject.Module {
@Override
public Seq<Binding<?>> bindings(final Environment environment, final Configuration configuration) {
return seq(bind(HandlebarsApi.class).toSelf());
}
}
# Bind Handlebars API
play.modules.enabled += "handlebars.Module"
libraryDependencies += "com.github.jknack" % "handlebars-guava-cache" % "4.0.4"
...
import java.util.concurrent.TimeUnit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
...
// Initialize the cache. Could be builded from configuration as well
// For example: CacheBuilder.from(config.getString("hbs.cache")).build()
final Cache cache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
// Initialize the engine with the cache
handlebars = new Handlebars(loader)
.with(new GuavaTemplateCache(cache));
...
public final class Helpers {
final MessagesApi messagesApi;
public Helpers(final MessagesApi messagesApi){
this.messagesApi = messagesApi;
}
...
public CharSequence message(final String key, final Options options) {
// Get the current language.
final Lang lang = Context.current().lang();
// Retrieve the message, internally formatted by MessageFormat.
return messagesApi.get(lang, key, options.params);
}
}
...
@Inject
public HandlebarsApi(... final MessagesApi messagesApi) {
...
// Add helpers. MessagesApi is a singleton so we can use it in the helpers.
Helpers helpers = new Helpers(messagesApi);
handlebars.registerHelpers(helpers);
...
}
...
package handlebars;
import play.twirl.api.Content;
class HtmlContent implements Content {
private String body;
HtmlContent(final String body){
this.body = body;
}
@Override
public String body() {
return body;
}
@Override
public String contentType() {
return "text/html";
}
}
public String render(final String templateName, final Object data) throws Exception {
return handlebars
.compile(templateName)
.apply(data);
}
public Content html(final String templateName, final Object data) throws Exception {
return new HtmlContent(render(templateName, data));
}
java.lang.RuntimeException: There is no HTTP Context available from here.
So do not forget to mockup the Http.Context. It's easy to do.
// Initialize application
Application application = new GuiceApplicationBuilder().build();
// Setup an HTTP Context
Http.Context context = mock(Http.Context.class);
// Setup the language and messages
Lang langRequest = Lang.forCode(requestLang);
Lang langSession = Lang.forCode(sessionLang);
MessagesApi messagesApi = application.injector().instanceOf(MessagesApi.class);
Messages messages = new Messages(langRequest, messagesApi);
// Train the Context
when(context.lang()).thenReturn(langSession);
when(context.messages()).thenReturn(messages);
//Http.Context.current.set(context);
// Get the handlebars API
HandlebarsApi handlebarsApi = application.injector().instanceOf(HandlebarsApi.class);
import java.text.MessageFormat;
import com.github.jknack.handlebars.Options;
import play.i18n.Messages;
...
private final Messages messages;
...
/**
* Creates Helpers with the given message pack.
*
* @param messages
* The Play message pack.
*/
public Helpers(final Messages messages) {
this.messages = messages;
}
...
/**
* Do the same as "@Message(key)" in Twirl. It use MessageFormat for the
* formatting as well as "@Message(key)".
*
* @param key
* message key in the messages.** files.
* @return message
*/
public CharSequence message(final String key, Options options){
String message = messages.at(key);
String messageFormatted = MessageFormat.format(message, options.params);
return messageFormatted;
}
import play.i18n.Messages;
import play.i18n.MessagesApi;
...
@Inject
private MessagesApi messagesApi;
...
Messages messages = new Messages(ctx().lang(), messagesApi);
Helpers helpers = new Helpers(messages);
handlebars.registerHelpers(helpers);
...
{{message "page.header.sub" "name"}}
page.header.sub=Page Sub Header {0}
I used the new Messages(ctx().lang(), messagesApi) opposite to the ctx().messages() because I want full support of the ctx().changeLang() and the ctx().setTransientLang().<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<script src="@routes.Assets.versioned("javascripts/hello.js")" type="text/javascript"></script>
We need to be able to do @routes.Assets.versioned("...") with the handlebars. Let's create the helper class that will hold all our handlebars helpers. From the start, we will add only one helper, the assets helper. This one could be done even in static method:
package handlebars;
public class Helpers {
/**
* Do the same as "@routes.Assets.versioned" in Twirl.
*
* @param url relative path to the asset
* @return actual path to the asset
*/
public static CharSequence asset(String url) {
return controllers.routes.Assets.versioned(new controllers.Assets.Asset(url)).toString();
}
}
Now we need to register it. Jut one line into the code from my previous post:...
// Initialize the engine
Handlebars handlebars = new Handlebars(loader);
// Add helpers
handlebars.registerHelpers(Helpers.class);
// Compile the "templates/page.hbs" template
Template template = handlebars.compile("page");
...
Now we can add assets to the handlebars template:<link rel="stylesheet" media="screen" href="{{asset "stylesheets/main.css"}}">
<link rel="shortcut icon" type="image/png" href="{{asset "images/favicon.png"}}">
<script src="{{asset "javascripts/hello.js"}}" type="text/javascript"></script>
And the result, the same as with Twirl:<link rel="stylesheet" media="screen" href="/assets/stylesheets/main.css">
<link rel="shortcut icon" type="image/png" href="/assets/images/favicon.png">
<script src="/assets/javascripts/hello.js" type="text/javascript&qu
// Add the handlebars library
libraryDependencies += "com.github.jknack" % "handlebars" % "4.0.3"
// Copy handlebars templates to the production
mappings in Universal ++=
(baseDirectory.value / "templates" * "*" get) map
(x => x -> ("templates/" + x.getName))
The last thing just to use the handlebars engine. I extend trivial controller class from the play-java framework.package controllers;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Template;
import com.github.jknack.handlebars.io.FileTemplateLoader;
import com.github.jknack.handlebars.io.TemplateLoader;
import com.google.common.collect.ImmutableMap;
import play.Environment;
import play.mvc.Controller;
import play.mvc.Result;
public class Application extends Controller {
// We need an environment to get the template folder
@Inject
private Environment environment;
public Result index() throws Exception {
// The data
Map data = new HashMap<>();
data.put("title", "Page Title");
data.put("header", "Header");
data.put("main", ImmutableMap.of("article", "Main Article"));
data.put("footer", "Footer");
// Get the template folder
File rootFolder = environment.getFile("/templates");
// Put the ".hbs" as a template extension.
TemplateLoader loader = new FileTemplateLoader(rootFolder, ".hbs");
// Initialize the engine
Handlebars handlebars = new Handlebars(loader);
// Compile the "templates/page.hbs" template
Template template = handlebars.compile("page");
// Fill it with data
String page = template.apply(data);
// Return the page to the client
return ok(page).as("text/html");
}
}
Yea, here is the template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<meta name="description" content="{{description}}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header>
{{header}}
</header>
<main>
<article>
{{main.article}}
</article>
</main>
<footer>
{{footer}}
</footer>
</body>
</html>
And the result:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Title</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header>
Header
</header>
<main>
<article>
Main Article
</article>
</main>
<footer>
Footer
</footer>
</body>
</html>