How to use presenters in AdonisJs Edge templating engine

Reading time ~7 minutes

Introduction

Being a very passionate Laravel fan, I decided to step up and finally try properly AdonisJs. I already tried some small bits of it a couple of years ago, but the generator syntax did not really appeal to me. Finally with async/await we have a battle-tested MVC framework (the current version is 4.1) with modern syntax that brings Rails/Laravel concepts to NodeJs.

The value proposition of the framework is the same that fuels modern MVC frameworks: reduce the time required to be productive, by providing an opinionated structure to get you up and running quickly, without the hassle of installing and configuring different packages.

Basically, AdonisJs takes care of handling:

and other different aspects of modern web applications, so you can just focus on your app logic.

If you are familiar with Laravel, you will immediately find yourself at home with AdonisJs.

AdonisJs Homepage

Documentation

The documentation is good, but in my opinion it can be further improved. Sometimes you need to reach out for the community to know how to implement specific things, but as you can imagine AdonisJs community is really smaller compared to Rails or Laravel ones.

Just to make an example, the documentation of the Edge templating engine, which is the official template engine for AdonisJs, it is not so detailed and also it lacks a few sections. In addition to this, a few concepts (for example runtime debugging) are explained only inside YouTube videos, taking away the most valuable asset of developers (time) and their most powerful tool (copy and paste).

The Problem

While working on a small side-project I’m building with AdonisJs, I got stuck trying to figure out how to display more complex logic on a view. I was trying to display a list of bookmarks, and for each bookmark I wanted to display the names of the categories to which the bookmark belongs. Pretty basic stuff, but it got me blocked for a couple of hours while trying to figure out the optimal way to do this.

Suppose that the bookmarks variable coming from the controller to the view is like the following:

{
    bookmarks: [
        {
            name: "AdonisJs",
            created_at: "2019-01-25 09:04:15",
            url: "https://adonisjs.com/",
            categories: [
                {
                    name: "JavaScript"
                },
                {
                    name: "MVC"
                }
            ]
        },
        [...]
    ]
}

As you can see it’s just a simple JS object which we can traverse using the @each directive inside the template:

<table>
    <tbody>
        @each(bookmark in bookmarks)
            <tr>
                <td>
                    <a href="{{ bookmark.url }}">{{ bookmark.name }}</a>
                </td>
                <td>{{ bookmark.created_at }}</td>
                <td>
                    <a href="{{ route('bookmarks.edit', { id: bookmark.id }) }}">Edit</a>
                </td>
            </tr>
            <tr>
                <td colspan="3">{{ bookmark.categories }}</td>
            </tr>
        @endeach
    </tbody>
</table>

We can display correctly the attributes of each bookmark, but we would like to display also all the names of the categories, not a generic [object Object].

Sure, we can do the following and achieve the result with the minimum effort:

<td colspan="3">
    @each(category in bookmark.categories)
        {{ category.name }}
    @endeach
</td>

It works for smaller and basic cases like this one, but what about when we need more complex logic? For example I would like to be able to do the following:

<td colspan="3">
    {{ bookmark.categories.map(category => category.name).join(', ') }}
</td>

But unfortunately this code does not work.

My second attempt was to look for a specific tag to inject raw JavaScript code in the template, following the idea of the @php directive of Laravel, but as you can imagine there’s no such tag.

View Presenters

View Presenter is a concept that belongs more to the Model View Presenter (MVP) pattern rather than to the old Model Viw Controller (MVC).

MVP is an evolution of MVC which aims to further separate concerns in order to build efficient decoupled systems. The Presenter is responsible for updating the view with the new data generated by the model, but in addition to this it can receive the UI events from the view and respond to them as needed. The popularity of MVP comes from the .NET world and the overall enterprise software.

In the context of MVC patterns, a View Presenter is just a way to encapsulate complex (display) logic inside a separate class, instead of writing it inside your template files.

AdonisJs offers View Presenters along with its templating engine, but the documentation is really poor on this aspect. That’s why I decided to write this post.

Presenters Registration

The first thing to do in order to create a View Presenter is to make a new presenters folder inside the resources directory. The YouTube video available on the documentation refers to the manual registration of the folder for the Edge templating enging, but this is not required anymore, as you can see from the source code. Those lines are already there for you:

edge.registerPresenters(Helpers.resourcesPath('presenters'))

Create the first Presenter

Creating a new presenter is as simple as creating new ES6 Class inside the presenters folder. I’m calling this new class CategoryPresenter.

const { BasePresenter } = require('edge.js')

class CategoryPresenter extends BasePresenter {

}

module.exports = CategoryPresenter

Now that we have a basic skeleton for our presenter we can add an ES6 getter and insert some dummy string to test that if it gets actually printed on the page:

class CategoryPresenter extends BasePresenter {

    get foo()
    {
        return 'Calling foo from presenter'
    }
}

Okay, now it gets tricky. How are we supposed to know how to actually use this presenter inside our template? The documentation is not really helping us. Luckily for us there’s the community.

You should define the presenter before rendering the view, inside the controller:

class BookmarkController {

    async index ({ view, params }) {
        const bookmarks = await Bookmark.query()
            .with('categories')
            .fetch()

        return view.presenter('CategoryPresenter')
            .render('bookmarks.index', {
                bookmarks: bookmarks.toJSON()
            })
    }
}

Warning: Unfortunately (as far as I know) you cannot pass more than one presenter to a view. Passing an array of presenters or chaining presenter calls breaks the framework. Therefore it’s better to make view-specific presenters instead of entity-specific presenters (as I did in this example).

Now you can finally use your presenter inside the view, using the foo getter just like if it was a normal variable:

<td colspan="3">
    {{ foo }}
</td>

and of course it will display Calling foo from presenter.

Use template data in Presenter

Good we have something that works. Now let’s move to the next step, how can we use existing data from the template inside our presenter?

Each presenter is defined inside the context of the template, this means that using the keyword this we can actually access Edge’s template instance. The actual variables passed by the controller are stored by Edge inside the $data object:

class CategoryPresenter extends BasePresenter {

    get foo()
    {
        return this.$data.bookmarks.map(bookmark => bookmark.name).join(', ')
    }
}

Okay this looks good as well. But remember that we want to display the joined names of the categories of each bookmark, therefore we need to be able to access the categories object inside the @each cycle of bookmarks.

Well the answer is right there, we have an entire JS class at our disposal. We don’t really need to access the $data object, we can just use a function of the presenter:

class CategoryPresenter extends BasePresenter {

    joinNames (categories, separator = ', ') {
        return categories.map(category => category.name).join(separator)
    }
}

The final code looks like the following, cleaner and powerful:

<table>
    <tbody>
        @each(bookmark in bookmarks)
            <tr>
                <td>
                    <a href="{{ bookmark.url }}">{{ bookmark.name }}</a>
                </td>
                <td>{{ bookmark.created_at }}</td>
                <td>
                    <a href="{{ route('bookmarks.edit', { id: bookmark.id }) }}">Edit</a>
                </td>
            </tr>
            <tr>
                <td colspan="3">{{ joinNames(bookmark.categories) }}</td>
            </tr>
        @endeach
    </tbody>
</table>

Notes:

comments powered by Disqus