npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

fraglates

v0.4.0

Published

An open source templating engine for generating server-side hypertext templates and fragments.

Downloads

28

Readme


An open source templating engine built on top of Nunjucks for server-side rendering (SSR) hypertext templates and fragments. Includes support for precompiling, automatic caching, asynchronous filters and tags, and progressive rendering of templates with Suspense-like fallbacks using a single http request.


npm npm

These docs are a work in progress. Not all features are documented and are subject to change.

Installation and Usage

npm install fraglates

Import into your app, initialize with your template and/or precompiled template directory, and use the render method to render full templates or partial fragments.

import Fraglates from "fraglates";

// Create a new instance of Fraglates
const fraglates = new Fraglates({
  templates: "./templates", // templates directory
  precompiled: "./precompiled", // precompile template directory (optional)
});

// Render the whole template (template, context)
const fullpage = await fraglates.render("my-template.html", {
  title: "My Dynamic Title",
  items: ["one", "two", "three"],
});

// Render just the #header fragment (template#fragment, context)
const header = await fraglates.render("my-template.html#header", {
  title: "My Dynamic Title",
});

Note: Fraglates uses a caching mechanism on the returned instance. DO NOT destructure the render method or the caching will break (i.e. const { render } = new Fraglates(...);).

Progressive Rendering with ReadableStream

Fraglates supports progressive rendering by generating a ReadableStream that can be streamed to the browser. The stream() method takes three arguments:

  • template: The template to render.
  • context: The data/variables passed to the template.
  • blocks: An object with block names for keys and asynchronous callbacks for values.

The blocks object references any {% block someBlock %} ... {% endblock %} blocks in the supplied template. These blocks will be rendered based on your template definition and streamed as soon as the initial template is rendered. Their output will be progressively replaced by the result of the async callback functions as they are resolved and added to the stream.

// Render the whole template
const stream = await fraglates.stream(
  "my-template.html", // Template name
  { title: "My Dynamic Title" }, // Context/variables
  {
    // Blocks
    header: async () => {
      // do some async stuff
      return { headerText: "Resolved header" }; // return an object
    },
    someBlock: async () => {
      // do some async stuff
      return "This is a string"; // or return a string
    },
  }
);

If the async callback function returns an Object, Fraglates will render the same fragment with the object merged into the main context.

If the async callback returns a string, the block will be replaced by the string. You can use this to optional render different fragments to replace a block.

Note that when blocks/fragments are rendered in the main template, they will contain an added __fallback variable set to a boolean value of true. This can be used to conditionally render content within the blocks. The __fallback value is scoped to the block, so any nested blocks will not inherit this value.

Streaming to the browser

The stream() method returns a ReadableStream that can be sent to the browser using any modern framework. Here is an example using Hono:

import { Hono } from "hono";
const app = new Hono();

app.get("/stream-fraglates", (c) => {
  // Render the whole template
  const stream = await fraglates.stream(
    "my-template.html", // Template name
    { title: "My Dynamic Title" }, // Context/variables
    {
      // Blocks
      header: async () => {
        // do some async stuff
        return { headerText: "Resolved header" }; // return an object
      },
      someBlock: async () => {
        // do some async stuff
        return "This is a string"; // or return a string
      },
    }
  );

  return c.body(stream, {
    headers: {
      "Content-Type": "text/html; charset=UTF-8",
      "Transfer-Encoding": "chunked",
    },
  });
});

app.fire();

Templating

Fraglates uses Nunjucks as the core templating engine. Currently Nunjucks Environment methods such as addFilter() and addGlobal() are supported.

The templating syntax is the same as Nunjucks, with one important addition: content wrapped in {% block blockName %} and {% endblock %} tags are accessible as fragments. Fragments can be rendered independently, giving you the ability to collocate HTML code within the same template for better readability. See this HTMX essay on fragments for more information and motivation.

In the template below, you can either render the entire template, or just the header fragment using the render method.

<!-- my-template.html -->
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Simple Template</title>
  </head>

  <body>
    {% block header -%}
    <header>{{ headerText }}</header>
    {%- endblock %}

    <div>{{ content }}</div>
  </body>
</html>

Note that blocks can be nested and include any Nunjucks templating logic like conditionals, filters, includes, etc.

Fraglates supports advanced Nunjucks features such as template inheritance, includes, macros/imports, custom filters, and more.

Precompiling Templates

Fraglates provides the fraglates cli command to precompile templates into JavaScript files that can be lazy loaded with dynamic imports. This is significantly faster than reading directly from the file system.

From your project directory, run the following:

fraglates '**/*.{html,njk}' -p path/to/templates -o path/to/precompiled

This will compile all html and njk files in your path/to/templates directory and write them to the path/to/compiled directory as .js files. You should add this to your npm build script so that templates are compiled at build time as well.

If you want to compile templates while developing, you can add the --watch or -w flag to the above command to watch the template files for changes and automatically recompile.

Note: Compiled templates are referenced using the name of the template, e.g. my-template.html. If the precompiled template doesn't exist, Fraglates will fall back to the filesystem if you provide a templates directory on initialization.

Asynchronous Support

IMPORTANT: Fraglates is asynchronous by default because of how it lazy loads compiled templates with dynamic imports. Calls to the render function must be awaited.

Custom filters are automatically converted to asynchronous filters. They can return a synchronous result, a resolved promise, or a promise. See Add a custom filter for more information.

Any additional context/variables passed to the Fraglates render function must be resolved first.

const foo = await someAsyncCall();

await fraglates.render("my-template.html", {
  foo: foo, // foo is already resolved
  // or you can just await the async call here
  bar: await someAsyncFunction(),
});

Extending with Nunjucks

An instance of Fraglates creates a private Nunjucks Environment behind the scenes. This is to ensure any manipulate of the underlying environment would not compromise the Fraglates instance.

Fraglates does supports Nunjucks methods such as addFilter() and addGlobal(). Using these methods will affect all templates and fragments.

Add a custom filter

// Create a new instance of Fraglates
const fraglates = new Fraglates({
  templates: "./templates", // template directory
});

// Add an 'upper' filter
fraglates.addFilter("upper", (str) => str.toUpperCase());

// Add an async filter
fraglates.addFilter("getUser", async (id) => {
  let user = await data.get(id);
  return user;
});

Add a custom global

// Create a new instance of Fraglates
const fraglates = new Fraglates({
  templates: "./templates", // template directory
});

// Add global variable
fraglates.addGlobal("someGlobalVar", "This is Global");

// Add a global function
fraglates.addGlobal("rand", (x, y) => {
  return Math.floor(Math.random() * (y - x + 1) + x);
});

NOTE: Globals cannot be asynchronous. Defining async globals will throw an error.

Custom Tags

You can extend Fraglates even more by using custom tags. Custom tags are heavily inspired by Eleventy's Paired Shortcodes that allow you to create new "block" types within your templates.

Custom tags can be made asynchronous by passing an async function as the second parameter. The function signature is as follows:

  • content: Anything wrapped inside the custom tag block
  • keywords: Auto-parsed keywords using Nunjucks' keyword arguments support
  • arg0...argn: Any positional arguments passed in to the custom tag in the template. Argument names can be specified directly, e.g. (contents, keywords, x, y, z) => {} or captured using the spread operator, i.e. (contents, keywords, ...args) => {}.

Create a synchronous custom tag:

fraglates.addTag("customTag", (content, keywords, ...args) => {
  return `<div style="color:${keywords.color};">${content}</div>`;
});

Create an asynchronous custom tag:

fraglates.addTag("customAsyncTag", async (content, keywords, userId) => {
  const user = await data.get(userId);
  return `<div>
    <h3>${user.name}</h3>
    ${content}
  </div>`;
});

Custom tags can be used in templates like this:

<h1>My template with custom tags!</h1>

{% customTag color="blue" %}
This will change my color to blue.
And this {{ variable }} will render before it is passed into the custom tag
{% endcustomTag %}

{% customAsyncTag 1234 %}
<p>This is some test content in a custom tag</p>
{% endcustomAsyncTag %}

Functional Components with JSX

If you're using a modern web framework like Hono, you may want to use JSX in order to build hypertext server side responses. Fraglates includes a component function that "componentizes" your template for you.

The HTML returned from a template needs to be HtmlEscaped in order to be usable in JSX. Hono includes a raw helper function that can be passed into the Fraglates constructor.

import { Hono } from "hono";
import { raw } from "hono/html";
import { FC } from "hono/jsx";
import Fraglates from "fraglates";

// Create a new instance of Fraglates
const fraglates = new Fraglates({
  templates: "./templates", // template directory
  precompiled: "./precompiled", // precompile template folder (optional)
  raw, // use Hono's HTML helper to escape templates
});

// Create the Hono app
const app = new Hono();

// Define a route
app.get("/header", async (c) => {
  // Componentize the header fragment (type as an FC - Functional Component)
  const Header: FC = await fraglates.component("my-template.html#header");

  // Return Header component with JSX
  return c.html(<Header headerText="My Header Text" />);
});

Note that c.html will automatically await the asynchronous components.

Functional components automatically pass attributes as data into the template and anything wrapped in the tag as a children prop. This allows you to nest templates, fragments, other components, and JSX to build more complex responses.

Define a template with children:

<!-- simple-div.html -->
<div class="{{ class }}">{{ children | safe }}</div>

Wrap additional JSX with a componentized version of the template:

// Define a route
app.get("/div", async (c) => {
  // Componentize the template
  const SimpleDiv: FC = await fraglates.component("simple-div.html");

  const text = "Sample text";

  // Return Header component with JSX
  return c.html(
    <SimpleDiv class="red">
      <h1>Hello Fraglates!</h1>
      <p>{text}</p>
    </SimpleDiv>
  );
});

Known issues

Fragments cannot make super() calls when rendered independently. This has to do with the way Nunjucks processes template inheritance. A fix is actively being explored.

Contributions & Feedback

Issues and PRs are welcome! 🙌