Components
A component is a reusable template snippet that works like a function. It can take arguments, render content, and be composed with other components to build complex UIs.
Think of components as the building blocks of your interface; buttons, cards, forms, layouts; anything you use more than once or want to keep organized.
Creating a Component¶
Components are Jinja template files with a .jinja extension:
{#def text #}
<button class="btn">{{ text }}</button>
Anatomy of a Component¶
A complete component can have these parts:
{#import "./header.jinja" as Header #}
{#css card.css #}
{#js card.js #}
{#def title, subtitle="" #}
<div class="card">
<Header title={{ title }} subtitle={{ subtitle }} />
<div class="card-body">
{{ content }}
</div>
</div>
From top to bottom:
- Imports - Other components this one uses
- Assets - CSS and JS files
- Arguments - Data the component accepts
- Template - The HTML to render
All parts are optional except the template.
Using Components¶
Import a component, then use it like an HTML tag:
{#import "button.jinja" as Button #}
{#import "card.jinja" as Card #}
<Card title="Welcome">
<p>Hello world!</p>
<Button text="Click me" />
</Card>
Block syntax for components with content:
<Card title="Hello">
<p>Content goes here</p>
</Card>
Self-closing syntax for components without content:
<Button text="Click me" />
Imports¶
Jx requires explicit imports before using components.
Basic Syntax¶
{#import "path/to/component.jinja" as Name #}
File Naming¶
Component files and folders can use any naming convention: button.jinja, user-card.jinja, form_input.jinja, etc. The import alias, however, must be PascalCase to distinguish components from HTML:
{#import "user-card.jinja" as UserCard #}
{#import "form_input.jinja" as FormInput #}
<UserCard /> {# Component #}
<div /> {# HTML #}
Absolute Imports¶
Paths relative to a catalog folder:
{#import "components/button.jinja" as Button #}
{#import "layouts/base.jinja" as Base #}
Use for shared components used across your project.
Relative Imports¶
Paths relative to the current file:
{#import "./sibling.jinja" as Sibling #}
{#import "../parent/component.jinja" as Component #}
{#import "./subfolder/child.jinja" as Child #}
Use for tightly related components. Move an entire folder and internal imports still work.
Example structure:
components/
modal/
modal.jinja {#import "./header.jinja" as Header #}
header.jinja {#import "./close-btn.jinja" as CloseBtn #}
close-btn.jinja
Prefixed Imports¶
For components from prefixed catalog folders:
catalog.add_folder("vendor/ui-lib", prefix="ui")
{#import "@ui/button.jinja" as Button #}
{#import "@ui/modal.jinja" as Modal #}
Use for third-party component libraries.
Arguments¶
Components accept arguments to customize their behavior.
Declaring Arguments¶
Use {#def ... #} at the top of your component:
{#def title, count=0 #}
<h2>{{ title }}: {{ count }}</h2>
title- Required (no default)count- Optional (defaults to0)
Default Values¶
Defaults can be any python value: strings, numbers, booleans, lists, dicts, etc.
{#def
name,
count=0,
active=true,
items=[],
config={}
#}
Passing Arguments¶
Strings use quotes:
<Button text="Click me" />
Expressions use {{ }}:
<Card
user={{ current_user }}
count={{ items | length }}
active={{ true }}
items={{ [1, 2, 3] }}
/>
Booleans can use HTML-style shorthand for true:
<Input required /> {# Same as required={{ true }} #}
<Input disabled={{ false }} />
Dash to Underscore¶
Dashes in attribute names convert to underscores:
{#def aria_label, data_id #}
<Button aria-label="Close" data-id="123" />
Both aria-label and aria_label match aria_label in the def.
Content & Slots¶
Components can wrap content passed between their tags.
The content Variable¶
Everything between a component's tags is available as content:
{#def title #}
<div class="card">
<h3>{{ title }}</h3>
<div class="body">{{ content }}</div>
</div>
<Card title="Hello">
<p>This becomes the content!</p>
</Card>
Fallback Content¶
Provide defaults when no content is passed:
<div class="body">
{{ content or "No content provided" }}
</div>
Named Slots¶
For multiple content areas, use named slots.
Define slots in the component with {% slot %}:
<div class="modal">
<div class="modal-header">
{% slot header %}
<h3>Default Header</h3>
{% endslot %}
</div>
<div class="modal-body">
{{ content }}
</div>
<div class="modal-footer">
{% slot footer %}
<button>Close</button>
{% endslot %}
</div>
</div>
Fill slots when using the component with {% fill %}:
<Modal>
{% fill header %}
<h3>Confirm</h3>
{% endfill %}
<p>Are you sure?</p>
{% fill footer %}
<button>Yes</button>
<button>No</button>
{% endfill %}
</Modal>
Unfilled slots use their default content.
When to Use Slots vs Props¶
Use Props When:¶
- The content is a single value
- You want validation
{#def title, count #}
<h2>{{ title }}: {{ count }}</h2>
Use Content When:¶
- The content is HTML
- There's one main content area
- You want flexibility in what gets passed
{#def title #}
<div class="card">
<h2>{{ title }}</h2>
{{ content }}
</div>
Use Named Slots When:¶
- You need multiple content areas
- Each area has a specific purpose
- You might want to provide defaults for each area
<div class="panel">
<header>{% slot header %}Default{% endslot %}</header>
<main>{{ content }}</main>
<footer>{% slot footer %}Default{% endslot %}</footer>
</div>