Templates#
jimble's templates are jte. Rather than interpreting strings at runtime, it compiles them to Java.
get("/", context -> {
context.response().putData("title", "ブログ");
context.response().putData("posts", listPosts());
context.response().view("blog/posts.jte");
});
Whatever you put in with putData() arrives as Data, and view() names the template.
The template side#
src/main/jte/blog/posts.jte:
@import db.blog_example.table.post.Post
@import io.jimble.util.data.Data
@import java.util.List
@param Data data
!{List<Data> posts = data.getDataList("posts");}
<h1>${data.getString("title")}</h1>
@for(Data post : posts)
<b>${post.getString(Post.title)}</b>
<p>${post.getString(Post.body)}</p>
@endfor
${ }is HTML-escaped. Use$unsafe{ }to emit raw output- Because of
@paramand@import, your types hold inside the template too - You can look values up by
Column, as inPost.title, so you never scatter string keys through your templates
Sharing the frame#
Writing <html> on every page means changing the header means changing every page.
Keep the frame in one layout, and let pages write only their own content.
src/main/jte/layout/page.jte:
@import gg.jte.Content
@import io.jimble.util.data.Data
@param Data data
@param Content content
<!doctype html>
<html lang="en">
<head>
<title>${data.getString("title")}</title>
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
@template.tag.nav(current = data.getString("nav"))
<main>${content}</main>
</body>
</html>
src/main/jte/pages/home.jte:
@import io.jimble.util.data.Data
@param Data data
@template.layout.page(data = data, content = @`
<p>The content goes here.</p>
`)
Content is "something to render later". Pass a block wrapped in @ and backticks,
and it lands where the layout writes ${content}.
The call name mirrors the directory layout.
src/main/jte/layout/page.jte is @template.layout.page(...),
src/main/jte/tag/nav.jte is @template.tag.nav(...).
Reusable parts#
The same mechanism gives you reusable parts.
src/main/jte/tag/card.jte:
@import gg.jte.Content
@param String title
@param Content body
@param String note = ""
<section class="card">
<h2>${title}</h2>
${body}
@if(!note.isEmpty())
<p class="note">${note}</p>
@endif
</section>
@paramcan carry a default (= ""). Callers may then omit it- A part travels better when it takes what it needs rather than a whole
Data. Hand itDataand the caller has to read the template to learn which keys matter
examples/approval-pages has all of this running.
Tip
Share the layout with your error pages too. Otherwise your 404 is the one page that looks like a different site.
Comments#
A jte comment is <%-- … --%>.
<%-- this does not reach the output --%>
[!WARNING]
@*…*@is not a comment. It is written out verbatim, and any@template.inside it is executed. Put a layout's own usage example inside one, inside that layout, and you get infinite recursion and aStackOverflowError(we walked into this one).Comments do not nest. Write the closing marker inside your prose and the comment ends there.
HTML comments (
<!-- -->) are also stripped by default. Markers you want the browser or an outside tool to see belong in a static file, not a template.
Building#
Add the io.jimble.jte plugin and src/main/jte is converted to Java
before compileJava.
plugins {
application
id("io.jimble.jte")
}
A mistake in a template breaks the build. You never discover it by hitting a 404 at runtime.
What you can configure is in the Gradle plugins.
The compiler (gg.jte:jte) is used at build time only;
the application's runtime classpath carries nothing but gg.jte:jte-runtime.
Hot reload#
If you started with ./gradlew jimbleRun, the moment you edit a .jte
the conversion and the build run, and the next request serves it.