Use TSX for your template engine

adonisframework

AdonisJS

Posted on March 26, 2024

Use TSX for your template engine

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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

// ...
Enter fullscreen mode Exit fullscreen mode

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

{
  // ...
  "compilerOptions": {
   // ...
   // insert-start
   "jsx": "react",
   "jsxFactory": "Html.createElement",
   "jsxFragmentFactory": "Html.Fragment",
   "plugins": [{ "name": "@kitajs/ts-html-plugin" }]
    // insert-end
  }
}
Enter fullscreen mode Exit fullscreen mode

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 files directly.

import router from '@adonisjs/core/services/router'
import { Home } from 'path/to/your/tsx/file'

router.get('/', () => {
  return <Home />
})
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

xss-scan --help
Enter fullscreen mode Exit fullscreen mode

Updating Vite & RC file

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

// title: adonisrc.ts
export default defineConfig({
  // ...

  metaFiles: [
    {
      // delete-start
      pattern: 'resources/views/**/*.edge',
      // delete-end
      // insert-start
      pattern: 'resources/views/**/*.tsx',
      // insert-end
      reloadServer: false,
    },
    {
      pattern: 'public/**',
      reloadServer: false,
    },
  ],

  //...
})
Enter fullscreen mode Exit fullscreen mode
// title: vite.config.ts
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
       */
      // delete-start
      reload: ['resources/views/**/*.edge'],
      // delete-end
      // insert-start
      reload: ['resources/views/**/*.tsx'],
      // insert-end
    }),
  ],
})
Enter fullscreen mode Exit fullscreen mode

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".

// title: resources/views/layouts/app.tsx
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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
import { route } from '#start/view'

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

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)
}
Enter fullscreen mode Exit fullscreen mode

CSRF Field

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

:::note
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' })
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode
import { Vite } from '#start/view'

<Vite.Entrypoint entrypoints={['resources/css/app.scss', 'resources/js/app.js']} />
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
<div my-custom-attribute="hello world" />
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
adonisframework
AdonisJS

Posted on March 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Use TSX for your template engine
adonisjs Use TSX for your template engine

March 26, 2024