Looking to migrate your application to AdonisJS 6? Checkout our migration guide!

Use TSX for your template engine

Romain Lanz

Seven years ago, we decided to build Edge, our templating engine. We created it to ensure longevity, speed, and extensibility.

Edge is one of the best templating engines out there for Node.js. It is not restrictive; any JavaScript expression will work on it; it generates an accurate error stack, helping you debug your templating issue; it provides support for components with props and slots and is easily extendable. In fact, 80% of Edge is built using its public API.

We love Edge and working with it, but it brings a few caveats.

It is a "custom language". We need to have an extension for any IDE to have syntax highlighting. If we want to have type safety, we would need to create a complete LSP (Language Server Protocol) from scratch, which is out of the scope of this project. It also means prettier does not work out of the box with Edge; a custom plugin would be needed.

Those caveats may not be an issue for you, and you love working with Edge, but let me present you another alternative for those who would like better type-safety and IDE support.

JSX

JSX (JavaScript Syntax Extension) is an XML-like syntax extension to ECMAScript without any defined semantics developed by Facebook.

You may have already used it in React, Vue, Solid, or other frontend frameworks. While most of its use cases are inside React, JSX is not tied to it. It has a proper spec for anyone to create and use a parser.

The great part of JSX is its support. All IDE supports JSX; it already has an LSP, a prettier support, and even TypeScript has backed-in support.

It makes it a great candidate for a templating engine.

Examples

For those who have yet to use JSX or TSX (JSX on TypeScript file), let me show you some examples and what it may look like in your code.

First, everything is a component; you define them with a function, and the function returns JSX, which follows an HTML-like syntax.

export function Home() {
return (
<div>
<h1>Hello World</h1>
</div>
)
}

Since this code resides in a tsx file, it can execute any JavaScript function like a standard function.

import { cva } from 'class-variance-authority'
interface ButtonProps { /* ... */ }
export function Button(props: ButtonProps) {
const { color, children, size = 'medium', ...extraProps } = props
const buttonStyle = cva('button', { /* ... */ }
return (
<button class={button({ color, size })} {...extraProps}>
{children}
</button>
)
}

We use cva from the class-variance-authority package in this example.

We can leverage the Async Local Storage of AdonisJS to access the HttpContext anywhere in your template.

We recommend doing props drilling since using ALS will create a performance hit for your application.

export function Header() {
const { auth } = HttpContext.getOrFail()
if (auth.user) {
return <header>Authenticated!</header>
}
return <header>Please connect!</header>
}

Setting up TSX

I have tried different packages to use TSX as a templating engine. For this tutorial, we will use @kitajs/html, but feel free to use the one you prefer.

First, you have to install the package. At the time of writing, this package is at version 3.1.1.

We will also install their plugin to enable editor intellisense.

npm install @kitajs/html @kitajs/ts-html-plugin

Once done, we will edit the bin/server.ts file to register Kita.

import 'reflect-metadata'
import '@kitajs/html/register.js'
import { Ignitor, prettyPrintError } from '@adonisjs/core'
// ...

We must also change our tsconfig.json file to add JSX support.

{
// ...
"compilerOptions": {
// ...
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment",
"plugins": [{ "name": "@kitajs/ts-html-plugin" }]
}
}

From now on, you can change the files' extension containing JSX from .ts to .tsx. For example, your route file may become routes.tsx.

Doing so will allow you to use JSX inside those file directly.

import router from '@adonisjs/core/services/router'
import { Home } from 'path/to/your/tsx/file'
router.get('/', () => {
return <Home />
})

Preventing XSS Injection

When using JSX, you must be careful about XSS injection. You should always escape user input and never trust it.

Always use the safe attribute when rendering uncontrolled HTML.

export function Home(props) {
const { username } = props
return (
<div>
<h1>Hello World</h1>
<p safe>{username}</p>
</div>
)
}

The @kitajs/ts-html-plugin package provides a script (xss-scan) to scan your code for potential XSS injection.

xss-scan --help

Updating Vite & RC file

You must update your adonisrc.ts and vite.config.ts files to change .edge references to .tsx.

export default defineConfig({
// ...
metaFiles: [
{
pattern: 'resources/views/**/*.edge',
pattern: 'resources/views/**/*.tsx',
reloadServer: false,
},
{
pattern: 'public/**',
reloadServer: false,
},
],
//...
})
export default defineConfig({
plugins: [
adonisjs({
/**
* Entrypoints of your application. Each entrypoint will
* result in a separate bundle.
*/
entrypoints: ['resources/css/app.scss', 'resources/js/app.js'],
/**
* Paths to watch and reload the browser on file change
*/
reload: ['resources/views/**/*.edge'],
reload: ['resources/views/**/*.tsx'],
}),
],
})

Sending HTML doctype

The <!doctype html> preamble is not added by default when using JSX. You can add it by creating a layout file and using a small "hack".

import { Vite } from '#start/view'
import type { Children } from '@kitajs/html'
interface LayoutProps {
children: Children
}
export function Layout(props: LayoutProps) {
const { children } = props
return (
<>
{'<!DOCTYPE html>'}
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BoringMoney</title>
<Vite.Entrypoint entrypoints={['resources/css/app.scss', 'resources/js/app.js']} />
</head>
<body>{children}</body>
</html>
</>
)
}

Adding global helpers

In Edge, some helpers are added by AdonisJS to make your life easier. For example, you may use the route helper to generate routes.

<a href="{{ route('posts.show', [post.id]) }}">
View post
</a>

Again, since TSX files are JS files, you can simply define those functions anywhere in your codebase and then import them.

import router from '@adonisjs/core/services/router'
export function route(...args: Parameters<typeof router.makeUrl>) {
return router.makeUrl(...args)
}
import { route } from '#start/view'
<a href={route('posts.show', [post.id])}>
View post
</a>

Examples of some helpers

Here are some helpers you may want to add to your project.

Route Helper

This helper will allow you to generate URLs for your routes.

import router from '@adonisjs/core/services/router'
export function route(...args: Parameters<typeof router.makeUrl>) {
return router.makeUrl(...args)
}

CSRF Field

This helper will generate a hidden input with the CSRF token.

We are using ALS in this example, but you can use any other way to access the HttpContext.

import { HttpContext } from '@adonisjs/core/http'
export function csrfField() {
// Note the usage of ALS here.
const { request } = HttpContext.getOrFail()
return Html.createElement('input', { type: 'hidden', value: request.csrfToken, name: '_csrf' })
}

Asset Path

Those helpers will generate the path to your assets. If you are in production, it will also add a hash to the file name.

import vite from '@adonisjs/vite/services/main'
function Image(props: { src: string; alt?: string; class?: string }) {
const url = vite.assetPath(props.src)
return Html.createElement('img', { src: url, alt: props.alt, class: props.class })
}
function Entrypoint(props: { entrypoints: string[] }) {
const assets = vite.generateEntryPointsTags(props.entrypoints)
const elements = assets.map((asset) => {
if (asset.tag === 'script') {
return Html.createElement('script', {
...asset.attributes,
})
}
return Html.createElement('link', {
...asset.attributes,
})
})
return Html.createElement(Html.Fragment, {}, elements)
}
export const Vite = {
Entrypoint,
Image,
}
import { Vite } from '#start/view'
<Vite.Entrypoint entrypoints={['resources/css/app.scss', 'resources/js/app.js']} />

Extending the typings

TSX will not allow you to use any non-standard HTML attributes. For example, if you are using unpoly or htmx, the compiler will complain about the up-* or hx-* attributes.

KitaJS comes with some typings for those attributes (htmx, Hotwire Turbo), but you may want to add your own.

To do so, you need to extend the JSX namespace.

declare global {
namespace JSX {
// Adds support for `my-custom-attribute` on any HTML tag.
interface HtmlTag {
['my-custom-attribute']?: string
}
}
}
<div my-custom-attribute="hello world" />

Learn more about this in the KitaJS documentation.

Conclusion

I hope this article will help you decide if you want to use TSX as your templating engine. If you have any questions, feel free to ask them on our Discord Server or GitHub Discussion.