Skip to content

Server components

JoltUI renders on the client, but a component's template does not have to come from the client. A component can ask a server for its markup; the server decides what is shown, and the client class provides behaviour.

Setting up

Configure the endpoint on the App and opt components in with useServer:

const app = new MyApp({
    appName: "Portal",
    serverComponentsUrl: "/api/components",
});
@component({
    tagName: "account-summary",
    groupName: "account",
    useServer: true,
})
export default class AccountSummary extends CustomElement<MyApp> {
    refresh = async (): Promise<void> => {
        await this.rerender();
    };
}
<account-summary :currency="EUR"></account-summary>

The request

To render, the component sends a POST request to serverComponentsUrl?<current query string> with credentials included and a JSON body:

{
    "tagName": "account-summary",
    "groupName": "account",
    "routeParameters": { "accountId": 42 },
    "queryParameters": { "tab": "overview" },
    "attributes": { "currency": "EUR" }
}

attributes contains the component's arguments (this.args, from its data-jolt-* attributes). groupName defaults to "default". The server identifies the component by tagName (and groupName, if it organises templates in groups) and answers with HTML.

Patera

The Patera Python framework implements this endpoint in its patera-joltui extension, with typed group and component handlers.

The response is a template

The returned HTML is treated exactly like the output of markup(). doT expressions are evaluated with the client component as this, @event and :argument shorthands are expanded, and event handlers are bound to the client class's methods:

<!-- returned by the server -->
<section class="summary">
    <h2>Balance: 1,204.50 EUR</h2>
    <button @click="refresh">Refresh</button>
    <p data-bind="app.user">Signed in as {{! this.app.user?.name }}</p>
</section>

Server components can contain bindings and child components like any other template.

Failures

Response Error
network failure NetworkError
400 ServerComponentBadRequestResponse
401 AuthenticationError
403 AuthorizationError
404 ServerComponentNotFound
5xx InternalServerError
any other non-2xx HttpRequestError
2xx with an empty or unreadable body MarkupFetchError

The error fails the component's initialization (or rejects rerender()) and reaches the error handlers as usual. A missing serverComponentsUrl is reported as a configuration error.

Customising the request

fetchHTML(request, options) performs the request. It is an instance property rather than a prototype method, so super.fetchHTML is not available. To extend it, capture the inherited function in the constructor and wrap it:

@component({ tagName: "price-list", useServer: true })
export default class PriceList extends CustomElement<MyApp> {
    constructor() {
        super();
        const baseFetchHTML = this.fetchHTML;
        this.fetchHTML = async (request, options = {}) =>
            await baseFetchHTML(
                {
                    ...request,
                    attributes: {
                        ...request.attributes,
                        locale: this.app.getData("locale"),
                    },
                },
                options,   // always forward: carries the signal and cache settings
            );
    }
}

Always forward options: they contain the abort signal and the cache settings. The cache key is computed from the request before your override changes it; if the added value affects the response, include it in templateCacheKey() too.

Prefetching

prefetchHTML(request, options?) loads a server template into the cache ahead of time. It uses an independent request that is not tied to any component's lifetime:

@lifecycle(Lifecycle.AFTER_INIT)
warmUpDetails(): void {
    void this.prefetchHTML({ tagName: "order-details", groupName: "orders" });
}

When an <order-details> element with matching parameters renders later, its template comes from the cache.