Views and Templates
AdonisJS comes with an official templating library to create server rendered apps. The template engine (Edge) offers a convenient way to generate dynamic HTML using runtime data and also write logic within your views.
By the end of this guide, you will know:
- How to setup the template engine?
- How the templating syntax works.
- Using partials and components to create reusable HTML fragments.
- Using Presenters to encapsulate templates logic for better testing experience.
Setup
The default application created using npx
or yarn create
is pre-configured to use views and templates. Open .adonisrc.json
file and check if @adonisjs/view
is registered under the providers array or not.
{
"providers": [
"@adonisjs/core",
"@adonisjs/view"
]
}
If not, then follow the upcoming steps to install the package and invoke post install instructions to set it up.
Install the Package
Install the @adonisjs/view
package from npm registry using the following command.
Invoke Generator
AdonisJS packages can configure themselves by running the post install instructions. Run the following command to setup @adonisjs/view
package.
node ace invoke @adonisjs/view
Working with Views
The views are stored inside resources/views
directory with files ending in .edge
extension. The edge syntax can be written along with any other markup language like HTML, Markdown, XML and so on. However, we will be using HTML throughout this guide.
Let's start by creating a view to render a list of posts on the blog homepage.
node ace make:view posts/index
# ✔ create resources/views/posts/index.edge
The make:view
command creates an empty view file. You can start writing HTML inside it and render it to the browser using view.render
method.
Route.get('/posts', async ({ view }) => {
return view.render('posts/index')
})
Make sure to start the HTTP server using node ace serve --watch
command.
If you visit http://localhost:3333/posts, you will be greeted with Hello world
.
Passing data to the views
You can pass data from your route handlers to the views during view.render
method call, as shown below.
import Route from '@ioc:Adonis/Core/Route'
Route.get('/posts', async ({ view }) => {
const posts = [
{
id: 1,
title: 'Getting Started with AdonisJS',
body: '',
},
{
id: 2,
title: 'Covering Basics of Lucid ORM',
body: '',
},
{
id: 3,
title: 'Understanding Build Process',
body: '',
}
]
return view.render('posts/index', { posts })
})
If you visit the registered route /posts
, you must see the following output on the browser screen.
What just happened?
- The route handler renders the
posts/index.edge
template and passes along an array ofposts
to it. - There is no need to type the
.edge
extension when usingview.render
method. - The template loops over the array of posts using the
@each
tag. The@each
tag is part of the Edge templating syntax.
As you can see, Edge syntax doesn't interfere with your HTML. You can use all of your creativity to create beautiful looking webpages with dynamic data inside them. Infact, Edge gives you all the necessary tooling to structure and re-use your HTML using Layouts, Partials and Components.
Using Layouts
As the name suggests, the layouts let you define the overall structure of a web page with placeholders to replace markup that is different for every single page.
Continuing with the blog posts listing page, let's create a master layout for our blog.
node ace make:view layouts/master
# Output
# ✔ create resources/views/layouts/master.edge
Open the newly created file (resources/views/layouts/master.edge)
and paste the following contents inside it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> AdonisJS Blog </title>
</head>
<body>
<section>
<!-- HEADER -->
<header>
<div>
<a href="/posts">
AdonisJS Blog
</a>
</div>
<navbar>
<a href=""> Home </a>
<a href=""> Archives </a>
</navbar>
</header>
</section>
<section>
<!-- ANY TEMPLATE CAN INJECT CONTENT HERE 👇 -->
@section('main')
@endsection
</section>
</body>
</html>
Along with the standard HTML markup, there is also a @section
block. The sections are placeholders with unique names in which other templates can inject content.
Let's open the resources/views/posts/index.edge
file and make it use the master
layout.
@layout('layouts/master')
@section('main')
<main>
@each(post in posts)
<article>
<h2>
<a href="/"> {{ post.title }} </a>
</h2>
</article>
@endeach
</main>
@endsection
If you re-visit the /posts
URL, you must see the following output on the browser screen.
How does layout and sections work?
- The
@layout
tag takes the path to the layout file. The path is relative from theviews
directory. - The
@section
tag inside the layout defines a named placeholder. All section names has to be unique. - Template using a layout uses the same
@section
name to define the content for that given section.
Using Partials
Partials are one of the best ways to have fragments of re-usable markup. The great thing about partials is, they have access to all the data from the parent template and hence no extra work needs to be done when creating partials.
When to use partials?
Partials have no technical advantage over keeping all the markup inside a single file. However, they do help in organizing the code in a better way, as you can keep dedicated pieces of markup in their own files over having a single giant HTML file.
Extract header to it's own partial
Continuing with the blog example. Let's move the <header>
tag to it's own partial and include it inside the master
layout.
node ace make:view partials/header
# Output
# ✔ create resources/views/partials/header.edge
Open the resources/views/partials/header.edge
and paste the following contents inside it.
<header>
<div>
<a href="/posts">
AdonisJS Blog
</a>
</div>
<navbar>
<a href=""> Home </a>
<a href=""> Archives </a>
</navbar>
</header>
Finally, edit the resources/views/layouts/master.edge
and make it include the header partial.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> AdonisJS Blog </title>
</head>
<body>
<section>
<!-- HEADER 👇 -->
@include('partials/header')
</section>
<section>
<!-- ANY VIEW CAN INJECT CONTENT HERE 👇 -->
@section('main')
@endsection
</section>
</body>
</html>
How does partials work?
- The
@include
tag accepts only a single argument, the path of the template to include. - All the state of the parent template is shared with the partial.
Using Components
Components are just like partials, but with their own state. Since partials share the state of the parent template, they are limited to the use cases they can serve. On the other hand, components can accept data and have isolated state of their own.
Using Components to Create a Form
Let's create a signup form using components. Assuming that you are now familiar with the ace commands and view.render
method, we will do the initial setup without explaining every step.
Create
button
andinput
components.node ace make:view components/button node ace make:view components/input
Create a parent template to show the signup form.
node ace make:view signup
Render the view from the signup route
(start/routes.ts)
.Route.get('/signup', async ({ view }) => { return view.render('signup') })
Add markup to
components/button.edge
file.<button type="{{ type }}"> {{ text }} </button>
Add markup to
components/input.edge
file.<div> <label for="{{ name }}">{{ text }}</label> <input type="{{ type }}" name="{{ name }}" /> </div>
Finally use the
@component
tag to include the components inside the signup form.<form action=""> @!component('components/input', { name: 'username', text: 'Enter username', type: 'input' }) @!component('components/input', { name: 'password', text: 'Enter password', type: 'password' }) @!component('components/button', { text: 'Signup', type: 'submit' }) </form>
If you visit http://localhost:3333/signup, you must a form similar to the following screenshot.
How does components work?
- The
@component
tag accepts path to a given template, similar to the@include
tag. But it also accepts an object of values called the component state. - Components do not have access to the parent template state, except the globals created using
view.global
orview.share
. We will talk about globals shortly.
Conditionals and Loops
Templates won't be fun, if you cannot conditionally render HTML or loop over an array or object. Let's start with conditionals.
Conditionals
Just like JavaScript, you can write if
, elseif
and else
statements in Edge using the dedicated conditional tags.
@if(username)
<h1> Hello {{ username }}! </h1>
@else
<h1> Hello Guest! </h1>
@endif
Using elseif
@if(user.role === 'admin')
<h1> You can delete users </h1>
@elseif (user.role === 'staff')
<h1> You can view users </h1>
@else
<h1> You can view only your own profile </h1>
@endif
Important thing to note. The @elseif
and @else
are always inside the @endif
tag.
As you can experience, the syntax of conditionals is very close to JavaScript. Infact, one of the goals of Edge is to stay closer to the JavaScript syntax for smaller learning curve.
Edge also has @unless
tag, which is opossite of @if
tag. Sometimes writing unless
feels more natural than writing a negative if
.
<!-- Show the login button unless user property exists -->
@unless(user)
<a href="/login"> Login </a>
@endunless
Loops
The @each
tag of Edge allows you to loop over Arrays
and Objects
both.
<!-- Setting users array inline. This can come from controller too -->
@set('users', [
{
username: 'virk'
},
{
username: 'romain',
},
{
username: 'tobi'
}
])
@each(user in users)
<li>{{ user.username }}</li>
@endeach
Loop over object
@set('food', {
'ketchup': '5 tbsp',
'mustard': '1 tbsp',
'pickle': '0 tbsp'
})
@each((amount, ingredient) in food)
<li>Use {{ amount }} of {{ ingredient }}</li>
@endeach
What's next?
We have just scratched the surface with templates and there is more to learn. We recommend reading the following guides for better understanding.