Template DSL
ZIO HTTP Template2 is a modern, type-safe HTML templating DSL for Scala that allows you to write HTML, CSS, and JavaScript directly in your Scala code with full compile-time checking.
Why Template2?
- Type Safety: Catch HTML errors at compile time
- Composability: Build reusable components as Scala functions
- Pure Scala: No separate template files to maintain, everything is in Scala
Your First HTML Page using Template2
Let's create a simple "Hello World" page:
import zio.http.template2._
val page: Dom =
html(
head(title("Hello World")),
body(
h1("Hello, ZIO HTTP Template2!"),
p("This is my first template.")
)
)
Rendering the above code (page.render(indentation = true)) will produce the following HTML:
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello, ZIO HTTP Template2!</h1>
<p>This is my first template.</p>
</body>
</html>
Serving HTML Page with ZIO HTTP
To serve the HTML page we created using template2, we can set up a simple server as follows:
import utils._
printSource("zio-http-example/src/main/scala/example/template2/HelloWorldExample.scala")
No need to render the HTML manually; Response.html takes care of it for you.
Also you can do the same using Endpoint API:
import utils._
printSource("zio-http-example/src/main/scala/example/template2/EndpointExample.scala")
Attributes
We have three types of attributes: Partial Attributes, Boolean Attributes, and Multi-Value Attributes.
- Partial attributes require a value to become complete, e.g.,
id,href, andname. They use the:=operator orapply()method:
// Using := operator
div(
id := "container",
href := "https://example.com",
name := "username"
)
// Using apply() method
div(
id("container"),
href("https://example.com"),
name("username")
)
There are many predefined partial attributes available for use. However, if you need to create a custom one, you can use the attr helper. For example:
button(
Dom.attr("onclick") := js"handleClick()"
)("Click here!")
Which renders as:
<button onclick="handleClick()">Click here!</button>
- Boolean attributes represent presence/absence states (like
disabled,checked,hidden):
input(
required,
autofocus
)
If we render the above input, it will produce the following HTML:
<input required autofocus/>
- Multi-value attributes handle space-separated or comma-separated values, commonly used for CSS classes:
// CSS classes (space-separated by default)
div(
`class` := ("container", "active", "large")
)
// Alternative using className
div(
className := ("btn", "btn-primary")
)
// With Iterable
div(
className := List("card", "shadow")
)
If we render the first div, it will produce the following HTML:
<div class="container active large"></div>
To control how multi-value attributes are joined, use multiAttr with AttributeSeparator:
// Space-separated (default)
div(Dom.multiAttr("class", List("foo", "bar")))
// Renders as: <div class="foo bar"></div>
// Comma-separated
div(Dom.multiAttr("data-list", AttributeSeparator.Comma, "a", "b", "c"))
// Renders as: <div data-list="a,b,c"></div>
// Semicolon-separated
div(styleAttr := "color: red; font-size: 14px")
// Renders as: <div style="color: red;font-size: 14px"></div>
// Custom separator
div(Dom.multiAttr("custom", AttributeSeparator.Custom("|"), "x", "y"))
// Renders as: <div custom="x|y"></div>
Here is an example of a complete form using various attribute types:
form(
action := "/submit",
method := "POST",
input(
`type` := "text",
name := "email",
placeholder := "Enter email",
required,
maxlength := 100,
),
button(
`type` := "submit",
)("Submit")
)
This will render as:
<form action="/submit" method="POST">
<input name="email" placeholder="Enter email" maxlength="100" type="text" required/>
<button type="submit">Submit</button>
</form>
Dynamic Attribute Management
You may want to manipulate attributes based on runtime conditions. You can use operations like attr, addAttributes, and removeAttr:
val element = div(id := "myDiv", `class` := "container")
// Add or update an attribute
val updated = element.attr("title", "Updated title")
// Add multiple attributes
val updatedWithData =
div(name("shopping-card")).addAttributes(
data("item-id") := "98765",
data("category") := "electronics",
)
// Remove an attribute
val removed = updated.removeAttr("title")
Conditional Attributes
Use when and whenSome for conditional attribute application:
div(
id := "container"
).when(isActive)(
`class` := "active",
ariaExpanded := true
)
div(
id := "user"
).whenSome(maybeEmail) { email =>
Seq(
data("email") := email,
titleAttr := s"User: $email"
)
}
Elements
Before we dive into creating elements, here's a simple example of an HTML page using various elements:
import zio.http.template2.{main => mainTag, _}
html(lang := "en")(
head(
meta(charset := "UTF-8"),
title("My Page"),
style.inlineResource("styles/main.css"),
script.externalJs("https://cdn.example.com/lib.js")
),
body(
header(
nav(
ul(
li(a(href := "/")("Home")),
li(a(href := "/about")("About")),
),
),
),
mainTag(
article(
h1("Article Title"),
p("Article content..."),
),
),
footer(
p("© 2024"),
),
),
)
Generic Elements
Most HTML elements are predefined for you to use directly by their names (e.g., div, span, p) without any extra parentheses or arguments:
br
// Renders as: <br/>
div
// Renders as: <div></div>
img
// Renders as: <img/>
To give you an overview, here's a categorized list of some of the predefined elements:
| Category | Elements |
|---|---|
| Text Content | p, span, div, h1, h2, h3, h4, h5, h6, blockquote, pre, code |
| Forms | form, input, button, select, option, textarea, label, fieldset, legend |
| Lists | ul, ol, li, dl, dt, dd |
| Tables | table, thead, tbody, tfoot, tr, th, td, caption, colgroup, col |
| Media | img, video, audio, source, track, canvas, svg |
| Semantic | header, footer, main, nav, article, section, aside |
| Interactive | details, summary, dialog |
| Metadata | head, title, meta, link, base, style, script |
They can accept attributes and children via the apply method. The apply method takes a variable number of Modifier arguments (which can be attributes or children):
div(id := "main") // Attribute
// Renders as: <div id="main"></div>
div("Hello World") // Child
// Renders as: <div>Hello World</div>
div(p("Paragraph")) // nested Child
// Renders as: <div><p>Paragraph</p></div>
// Mixing attributes and children
div(
id := "main", // Attribute
`class` := "container", // Attribute
p("First paragraph"), // Child
data("section") := "intro", // Attribute
p("Second paragraph") // Child
)
// Renders as:
// <div id="main" class="container" data-section="intro">
// <p>First paragraph</p>
// <p>Second paragraph</p>
// </div>
// The same as above but with grouped children in a separate block
div(
id := "main", // Attribute
`class` := "container", // Attribute
data("section") := "intro", // Attribute
)(
p("First paragraph"), // Child
p("Second paragraph"), // Child
)
// Renders as:
// <div id="main" class="container" data-section="intro">
// <p>First paragraph</p>
// <p>Second paragraph</p>
// </div>
Please note that void elements (e.g., br, hr, input, ...) are self-closing and cannot have children.
We can map over collections to generate lists of elements:
val items = List("Apple", "Banana", "Cherry")
ul(
items.map(item => li(item))
)
// With indices
ol(
items.zipWithIndex.map { case (item, idx) =>
li(id := s"item-$idx")(item)
}
)
We can conditionally include children based on parameters:
def userCard(user: User, showEmail: Boolean): Dom.Element = {
div(`class` := "user-card")(
h3(user.name),
if (showEmail) p(user.email) else Dom.empty,
user.avatar.map(url => img(src := url))
)
}
Please note that in the above example, the user.avatar.map(url => img(src := url)) is an Option[Dom]. If the avatar is None, it will not render anything in that place.
If we need to create a custom element that is not predefined, we can use the Dom.element method:
Dom.element("custom-tag")(Dom.attr("x-property") := "value")("Content here")
Script Elements
To include JavaScript in your HTML, you can use the script element with various methods for inline and external scripts:
// Inline JavaScript with Js type
script.inlineJs(js"console.log('Hello from inline JavaScript!');")
// Renders as:
// <script type="text/javascript">console.log('Hello from inline JavaScript!');</script>
// External JavaScript
script.externalJs("https://cdn.example.com/lib.js")
// Renders as:
// <script src="https://cdn.example.com/lib.js" type="text/javascript"></script>
// ES6 Module
script.externalModule("/js/app.js")
// Renders as:
// <script src="/js/app.js" type="module"></script>
// Inline module
script.inlineJs(
"""
|import { helper } from './utils.js';
|helper.init();
|""".stripMargin,
)
// Render as:
// <script type="text/javascript">
// import { helper } from './utils.js';
// helper.init();
// </script>
// With attributes
script
.externalJs("/app.js")
.async
.integrity("sha384-...")
.crossOrigin("anonymous")
// Renders as:
// <script async src="/app.js" type="text/javascript" integrity="sha384-..." crossorigin="anonymous"></script>
Style Elements
Style elements are specialized for CSS content.
To include inline CSS, use the inlineCss method:
// Inline CSS
style.inlineCss(
css"""
|body {
| margin: 0;
| font-family: sans-serif;
|}
|""".stripMargin,
)
To inline a CSS file from the resource directory, use inlineResource:
// Loading from resources
style.inlineResource("styles/main.css")
To point to an external CSS file, you can use the link element:
link(rel := "stylesheet", href := "https://example.com/styles/main.css")
Finding, Filtering and Collecting
Using find, filter, and collect methods, we can traverse the Dom to locate, select, or extract specific elements based on defined criteria:
val element = div(
p(`class` := "important")("Important"),
p("Normal"),
span("Other")
)
// Find specific elements
val firstP: Option[Dom] = element.find {
case el: Dom.Element => el.tag == "p"
case _ => false
}
// Filter elements
val filtered = element.filter {
case el: Dom.Element => el.attributes.contains("class")
case _ => true
}
// Collect all matching elements
val allParagraphs: List[Dom] = element.collect {
case el: Dom.Element if el.tag == "p" => el
}
Building Reusable Components
We can also build reusable components using functions. Here's an example of a card component:
def card(
title: Option[String] = None,
footer: Option[Dom] = None,
)(content: Dom*): Dom.Element = {
div(`class` := "card")(
title.map(t => div(`class` := "card-header")(h5(t))),
div(`class` := "card-body")(content),
footer.map(f => div(`class` := "card-footer")(f)),
)
}
def customButton(
text: String,
variant: String = "primary",
size: String = "md",
): Dom.Element =
button(
`class` := (s"btn-$variant", s"btn-$size"),
role := "button"
)(text)
// Usage
card(
title = Some("User Profile"),
footer = Some(customButton("Save")),
)(
p("Name: John Doe"),
p("Email: john@example.com"),
)
Using Third-Party Template Libraries
If you're running your project with other template libraries like Twirl or Scalate and want to migrate to ZIO HTTP, you can integrate them without rewriting all your templates. After migrating and integrating with ZIO HTTP, you can gradually replace your old templates with ZIO HTTP Template2 templates.
Here's an example of how to integrate Twirl templates with ZIO HTTP. Assume you have written a Twirl template named greetUser.scala.html inside the mytwirltemplate package:
@import models.User
@(param: User)
<html>
<head>
<title>Greet User Twirl Template</title>
</head>
<body>
<h1>Hello, @param.name!</h1>
<p>Your email is: @param.email</p>
</body>
</html>
And you have a case class User defined as follows:
package models
import zio.schema.DeriveSchema
case class User(name: String, email: String)
object User {
implicit val schema = DeriveSchema.gen[User]
}
Now you can use this Twirl template inside a ZIO HTTP route as follows:
import zio.http._
import zio.schema.codec.JsonCodec.schemaBasedBinaryCodec
val greetRoute =
Method.GET / "greet" -> handler { (req: Request) =>
for {
user <- req.body.to[User]
response = mytwirltemplate.greetUser.render(user)
} yield Response(
body = Body.fromString(response),
headers = Headers(Header.ContentType(MediaType.text.html)),
)
}
If you want to use Endpoint API, you can use RawHtml to convert your rendered Twirl template into a Dom:
import zio.http._
import zio.schema.codec.JsonCodec.schemaBasedBinaryCodec
val endpoint: Endpoint[Unit, User, ZNothing, Dom, None] =
Endpoint(Method.GET / Root)
.in[User]
.out[Dom](MediaType.text.`html`)
val route =
endpoint.implementHandler{
handler{ user: User =>
RawHtml(mytwirltemplate.greetUser.render(user))
}
}
You can follow a similar approach to integrate other template libraries with ZIO HTTP.