View Templates
Learning objectives
- Knows how to use Eta view templates in oak.
We continue to use Eta as the template engine for view templates. When working with vanilla Deno, our applications imported the functions configure
and renderFile
from https://deno.land/x/eta@v2.2.0/mod.ts
. The function configure
was given the path to the view templates as a configuration parameter, while the function renderFile
was given the name of the view template that was to be rendered as well as an object that contained the data that would be injected to the view template.
As an example, the following content would have resided in a file called index.eta
, which would be within the folder called views
.
<h1>
<%= it.title %>
</h1>
Then, the following file -- app.js
-- would be used to handle the requests to the server and respond with the rendered view templates.
import { serve } from "https://deno.land/std@0.222.1/http/server.ts";
import { configure, renderFile } from "https://deno.land/x/eta@v2.2.0/mod.ts";
configure({
views: `${Deno.cwd()}/views/`,
});
const responseDetails = {
headers: { "Content-Type": "text/html;charset=UTF-8" },
};
const data = {
title: "Hello world!",
};
const handleRequest = async (request) => {
return new Response(await renderFile("index.eta", data), responseDetails);
};
serve(handleRequest, { port: 7777 });
Serving rendered content
Almost the same approach works with oak as well. When working with oak, we set value of the body
variable in the response
object to contain the data that we wish to send as a response. Similarly to before, the function configure
is used to set the folder from where the templates are searched from, and the function renderFile
is used to render given data to the view template.
Let's assume that the following contents are in a file called index.eta
that resides in a folder called views
.
<h1>
<%= it.title %>
</h1>
Now, the following oak application would respond to requests with the rendered view template.
import { Application } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { configure, renderFile } from "https://deno.land/x/eta@v2.2.0/mod.ts";
configure({
views: `${Deno.cwd()}/views/`,
});
const app = new Application();
const data = {
title: "Hello world!",
};
const greet = async ({ response }) => {
response.body = await renderFile("index.eta", data);
};
app.use(greet);
app.listen({ port: 7777 });
Note that the function greet
is set as asynchronous above, as the renderFile
function is asynchronous and we have to wait for it to be completed before returning the contents to the user.
Now, when we launch the server and make a request to it, we see the response that we expect.
curl http://localhost:7777
<h1>
Hello world!</h1>
Extending oak
The above example seems somewhat verbose, which leaves us to wonder whether we could improve it a bit. We previously learned about middleware, which are essentially functions that oak uses to process requests. One of the nice things with JavaScript and oak is that extending oak is fairly easy; we can, for example, add a new function to the context
object that can make our lives a bit easier. This is exactly that we will do next.
Let's create a middleware that adds a function called render
to the context
-object. The function render
can be given a view template and an object as a parameter, and it then takes care of rendering the given view template and object and adding the result to the response. The function also adds information about the response to the headers -- it states that the response is a HTML document and that it uses the UTF-8 character encoding.
This is done as follows -- let's call the middleware renderMiddleware
, and place it into a file called renderMiddleware.js
that resides in the middlewares
folder.
import { configure, renderFile } from "https://deno.land/x/eta@v2.2.0/mod.ts";
const renderMiddleware = async (context, next) => {
configure({
views: `${Deno.cwd()}/views/`,
});
context.render = async (file, data) => {
context.response.headers.set("Content-Type", "text/html; charset=utf-8");
context.response.body = await renderFile(file, data);
};
await next();
};
export { renderMiddleware };
Effectively, the middleware adds a new function called render
to the context
object. Now, after the middleware has been taken into use, we can import the renderMiddleware
to our applications, and use it in oak. Every middleware that has been added to oak after the renderMiddleware
can use the function render
.
In the example below, the function greet
now uses the render
function to respond with a view template.
import { Application } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { renderMiddleware } from "./middlewares/renderMiddleware.js";
const app = new Application();
app.use(renderMiddleware);
const data = {
title: "Hello world!",
};
const greet = ({ render }) => {
render("index.eta", data);
};
app.use(greet);
app.listen({ port: 7777 });
curl http://localhost:7777
<h1>
Hello world!</h1>
We will later extend our little renderMiddleware
a bit, but for now, it suffices.
Configuration location
In these materials, the Eta template engine is configured within the render call. Although this is a bit inefficient, this is done to help the automated tests. In practice, one could also configure the views outside the render call as follows.
import { configure, renderFile } from "https://deno.land/x/eta@v2.2.0/mod.ts";
configure({
views: `${Deno.cwd()}/views/`,
});
const renderMiddleware = async (context, next) => {
context.render = async (file, data) => {
context.response.headers.set("Content-Type", "text/html; charset=utf-8");
context.response.body = await renderFile(file, data);
};
await next();
};
export { renderMiddleware };
For now, we include the configuration call within the render function; without this, some of the automated tests fail.
Eta and routes
Using Eta with routes does not differ from the way we used it before. Let's assume that we have a template called count.eta
that resides in the folder views
. The template looks as follows.
<h1>
<%= it.count %>
</h1>
Now, we can take the example from the part '../x-18-routes-and-view-templates'on POST / Redirect / GET, and adjust it to use count.eta
. In the example, we also use the renderMiddleware
that we created above.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { renderMiddleware } from "./middlewares/renderMiddleware.js";
const app = new Application();
app.use(renderMiddleware);
const router = new Router();
const data = {
count: 0,
};
const getCount = ({ render }) => {
render("count.eta", data);
};
const increaseCount = ({ response }) => {
data.count++;
response.redirect("/");
};
router.get("/", getCount)
.post("/", increaseCount);
app.use(router.routes());
app.listen({ port: 7777 });
curl http://localhost:7777
<h1>
0</h1>%
curl -X POST http://localhost:7777
Redirecting to /.%
curl -X POST http://localhost:7777
Redirecting to /.%
curl http://localhost:7777
<h1>
2</h1>%
Render middleware
From now on, all of the assignments come with the render middleware. In your own projects, however, you need to add it separately, should you wish to use it.