Catalog

The Catalog is the central manager for your components. It handles loading, caching, and rendering components from one or more folders.

Basic Setup

from jx import Catalog

catalog = Catalog("components/")

This creates a catalog that loads components from the components/ folder.

Constructor Options

catalog = Catalog(
    folder="components/",       # Optional initial folder
    jinja_env=None,             # Custom Jinja2 environment
    extensions=None,            # Extra Jinja2 extensions
    filters=None,               # Custom template filters
    tests=None,                 # Custom template tests
    auto_reload=True,           # Auto-detect file changes
    asset_resolver=None,        # Asset URL resolver callback
    **globals                   # Global template variables
)

folder

Optional path to a component folder. Shortcut for calling add_folder():

# These are equivalent:
catalog = Catalog("components/")

catalog = Catalog()
catalog.add_folder("components/")

auto_reload

When True (default), Jx checks if component files have changed and reloads them automatically. Great for development.

For production, set to False to skip file modification checks:

catalog = Catalog("components/", auto_reload=False)

globals

Variables available to all components:

catalog = Catalog(
    "components/",
    site_name="My App",
    current_year=2026,
    debug=True,
)
components/footer.jinja
<footer>
  © {{ current_year }} {{ site_name }}
</footer>

filters

Custom Jinja2 filters:

def format_price(value):
    return f"${value:,.2f}"

def pluralize(count, singular, plural=None):
    plural = plural or f"{singular}s"
    return singular if count == 1 else plural

catalog = Catalog(
    "components/",
    filters={
        "price": format_price,
        "pluralize": pluralize,
    }
)
<span>{{ product.price | price }}</span>
<span>{{ count }} {{ count | pluralize("item") }}</span>

tests

Custom Jinja2 tests:

def is_admin(user):
    return user.role == "admin"

catalog = Catalog(
    "components/",
    tests={"admin": is_admin}
)
{% if user is admin %}
  <a href="/admin">Admin Panel</a>
{% endif %}

extensions

Extra Jinja2 extensions to load:

catalog = Catalog(
    "components/",
    extensions=["jinja2.ext.i18n", "jinja2.ext.loopcontrols"]
)

Note: The jinja2.ext.do extension is always enabled (required for attrs manipulation).

jinja_env

Use an existing Jinja2 environment instead of creating a new one:

from jinja2 import Environment

env = Environment()
env.globals["my_func"] = my_function
env.filters["my_filter"] = my_filter

catalog = Catalog("components/", jinja_env=env)

This is useful when integrating with frameworks that provide their own Jinja environment.

asset_resolver

Optional callback for transforming component asset URLs. Receives (url, prefix) and returns the resolved URL string. Only invoked for components whose prefix has a registered assets folder (see add_folder).

def my_resolver(url, prefix):
    return f"/static/{prefix}/{url}"

catalog = Catalog("components/", asset_resolver=my_resolver)

Adding Folders

add_folder(path, prefix="", assets=None)

Add a folder of components to the catalog:

catalog = Catalog()
catalog.add_folder("components/")
catalog.add_folder("layouts/")

The optional assets parameter specifies a folder containing CSS/JS files for components in this folder. When set, the asset_resolver callback is used to transform asset URLs at render time (see Installable Packages for details).

Components are imported by their path relative to the folder:

{#import "button.jinja" as Button #}
{#import "forms/input.jinja" as Input #}

Using Prefixes

Prefixes namespace components, useful for third-party libraries:

catalog.add_folder("components/")
catalog.add_folder("vendor/ui-kit/", prefix="ui")
catalog.add_folder("vendor/icons/", prefix="icons")

Import prefixed components with @prefix/:

{#import "button.jinja" as Button #}
{#import "@ui/modal.jinja" as Modal #}
{#import "@icons/check.jinja" as CheckIcon #}

Multiple Folders, Same Prefix

If you add multiple folders with the same prefix (or no prefix), they're treated as one namespace. If both contain a component with the same path, the first one added wins:

catalog.add_folder("my-components/")      # Has button.jinja
catalog.add_folder("fallback-components/") # Also has button.jinja

# "button.jinja" resolves to my-components/button.jinja

Rendering

render(relpath, globals=None, **kwargs)

Render a component by its path:

html = catalog.render("page.jinja", title="Hello", user=current_user)

Arguments:

  • relpath - Path to the component (e.g., "pages/home.jinja")
  • globals - Dict of variables available to this component and all its imports
  • **kwargs - Arguments passed directly to the component
# Pass data as keyword arguments
html = catalog.render(
    "user-profile.jinja",
    user=user,
    posts=posts,
    show_email=True,
)

# Or use globals for values needed by child components too
html = catalog.render(
    "page.jinja",
    globals={"request": request, "csrf_token": token},
    title="Dashboard",
)

render_string(source, globals=None, **kwargs)

Render a component from a string (not a file):

source = """
{#def name #}
<h1>Hello, {{ name }}!</h1>
"""

html = catalog.render_string(source, name="World")
# <h1>Hello, World!</h1>

Useful for:

  • Testing components
  • Dynamic templates from a database
  • Simple one-off renders

Note: String components can use absolute imports but not relative imports (no file path to resolve from).


Introspection

list_components()

Returns a list of all registered component paths:

paths = catalog.list_components()
# ["button.jinja", "card.jinja", "forms/input.jinja"]

get_signature(relpath)

Returns a component's signature, including its arguments and metadata:

sig = catalog.get_signature("button.jinja")

Returns a dictionary with:

  • required - dict of required argument names mapped to their type (or None)
  • optional - dict of optional arguments mapped to (default_value, type or None)
  • slots - tuple of slot names
  • css - tuple of CSS file URLs
  • js - tuple of JS file URLs
sig = catalog.get_signature("modal.jinja")
# {
#     "required": {"title": str},
#     "optional": {"size": ("md", str)},
#     "slots": ("header", "footer"),
#     "css": ("modal.css",),
#     "js": ("modal.js",),
# }

collect_assets(output)

Copies all registered package assets to an output folder. For each prefix that has a registered assets folder (see add_folder), files are copied to <output>/<prefix>/:

copied = catalog.collect_assets("static/vendor")
# [("ui", Path("button.css")), ("ui", Path("button.js")), ...]

Returns a list of (prefix, relative_path) tuples for every file copied.


Framework Integration

Flask

from flask import Flask, url_for
from jx import Catalog

app = Flask(__name__)
catalog = Catalog(
    "components/",
    auto_reload=app.debug,
    url_for=url_for,  # Make url_for available in components
)

@app.route("/")
def home():
    return catalog.render("pages/home.jinja", products=get_products())

FastAPI

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from jx import Catalog

app = FastAPI()
catalog = Catalog("components/", auto_reload=True)

@app.get("/", response_class=HTMLResponse)
def home(request: Request):
    return catalog.render(
        "pages/home.jinja",
        globals={"request": request},
        products=get_products(),
    )

Sharing Jinja Environment

If your framework has its own Jinja environment with filters, globals, etc., pass it to the catalog:

# Flask example
from flask import Flask

app = Flask(__name__)
app.jinja_env.filters["my_filter"] = my_filter
app.jinja_env.globals["my_global"] = my_global

catalog = Catalog("components/", jinja_env=app.jinja_env)

Now your components have access to everything registered in Flask's environment.


Production Settings

For production, disable auto-reload.

import os

catalog = Catalog(
    "components/",
    auto_reload=os.environ.get("DEBUG", "false").lower() == "true",
)

Or based on your framework's debug setting:

# Flask
catalog = Catalog("components/", auto_reload=app.debug)

# FastAPI
catalog = Catalog("components/", auto_reload=settings.debug)

Built-in Template Globals

In addition to any globals you pass to the constructor, Jx automatically provides these functions to all components:

_get_random_id(prefix="id")

Generates a unique string suitable for HTML element IDs. Useful for form elements, popovers, and other components that require unique IDs to function correctly:

components/popover.jinja
{#def label, content #}

{% set popover_id = _get_random_id("popover") %}

<button popovertarget="{{ popover_id }}">{{ label }}</button>
<div id="{{ popover_id }}" popover>{{ content }}</div>

Each call returns a different ID like popover-a1b2c3d4e5f6..., so you can use it as a default without requiring the caller to pass an explicit ID:

components/input.jinja
{#def name, label="", id="" #}

{% set input_id = id or _get_random_id(name) %}

{% if label %}
  <label for="{{ input_id }}">{{ label }}</label>
{% endif %}
<input id="{{ input_id }}" name="{{ name }}" {{ attrs.render() }} />