Theme
Version
GitHub PyPI Discord
On this page

HTML fragments

Sometimes one request should update only part of a page that is already open: search results below a search box, the contents of a modal, or an htmx-style swap. Render a component as an HTML fragment, send it, and insert it into the page. The rest of the page stays as it is, and the fragment's styles and browser behavior work once it is inserted.

If the update comes from a Python event handler, you do not need this page: return the component from the handler and Citry updates the page for you. See Event actions.

Render a fragment

Render the component, then serialize it with deps_strategy="fragment":

card = Card(title="Welcome")
html = card.render().serialize(
    deps_strategy="fragment",
)

The result is the component's HTML plus what the browser needs to load its JavaScript and CSS and start its Vue behavior. A component with no JavaScript, CSS, or browser behavior comes back as plain HTML.

DepsStrategy lists the other ways to place JavaScript and CSS, and Rendering explains the render() and serialize() steps.

Insert into a Citry page

Load Citry's runtime once in the page, then insert the response wherever you like. Get the runtime's URL from citry.runtime_url() in the route that renders the page, and pass it to the page's template:

from citry import citry

context = {"citry_runtime_url": citry.runtime_url()}

This example is a Jinja or Django template:

<script src="{{ citry_runtime_url }}"></script>
<div id="results"></div>
<script>
  fetch('/search-fragment')
    .then((response) => response.text())
    .then((html) => {
      document.getElementById('results').innerHTML = html;
    });
</script>

The runtime notices the new fragment, loads the scripts and stylesheets it is missing, and starts the fragment's components as their own Vue app. A component script the page already loaded is not loaded or run again. Removing the fragment's elements from the page stops its Vue app.

The URL looks like /citry/citry.<hash>.js. It names one version of the file, so the browser keeps it between page loads and downloads it again only after Citry changes the file. In a Citry template, return the URL from template_data() as runtime_url and write <script c-src="runtime_url"></script>. The c- prefix sets an attribute from a template value; see c-* Python attrs.

If the runtime does not load, for example because the page names an old runtime after a Citry upgrade, inserted fragments do not start, and the browser console shows the failed request. When a fragment's own loader adds the runtime (see Insert into other pages) and the file fails, the console also shows Citry could not load its runtime.

A static HTML file that you write once cannot call runtime_url(), so link the stable /citry/citry.js from it instead. The browser downloads that file on every page load. See Link the runtime by hand.

Each fragment adds its own stylesheet link, so its styles stay in place even if the page's own Vue app is removed. The browser usually serves the repeated link from its cache.

Insert the whole response in one step. Do not split it into separate swaps, because the fragment's HTML and the data block that describes its components must arrive together.

When the page uses a Content Security Policy (CSP) nonce, put the nonce on the runtime's <script> tag and serialize the fragment with the same nonce. Otherwise the fragment does not start, and the browser console shows an error. See Nonces in fragments.

The browser does not check the content of a <script> tag you write yourself. If your page has a CSP, add base-uri 'self' to it, so that injected markup cannot point this tag at another site. See Limit the <base> tag.

Insert into other pages

When the page has not loaded Citry, the fragment brings a small script that loads the runtime. That script must run, and a <script> added through innerHTML does not run. The example above works only because Citry was already on the page.

Use a swap library that runs scripts in the response, such as htmx, or parse the response and create its <script> elements again as new DOM nodes. The script then loads Citry, which starts the fragment.

Serve Citry's files

A fragment with JavaScript or CSS loads it by URL, and an interactive fragment loads Citry's runtime by URL too. Add one of Citry's web framework integrations to your app so those URLs work.

This applies when the fragment has any of these:

  • a component's js or css, or a Dependencies entry, even an inline Script;
  • Vue bindings or $component options;
  • browser or server event handlers, or Events State;
  • browser data from Python, such as js_data().

Serializing such a fragment raises RuntimeError when no integration is added and no prefix is recorded (below), so it never returns broken URLs.

A worker that renders fragments but does not serve Citry's routes can record the URL prefix the serving process uses, with Citry.set_mounted_prefix:

from citry import Citry, Component

app = Citry()
app.set_mounted_prefix("/citry")

class Notice(Component):
    citry = app

    def js_data(self, kwargs, slots):
        return {"message": "Ready"}

    template = """
      <p class="notice" v-text="message"></p>
    """

notice = Notice()
html = notice.render().serialize(
    deps_strategy="fragment",
)

Call it before serializing, because the URLs are written into the HTML at that point.

Render fresh each time

If a second insertion of the same fragment loses its styling or browser behavior, check whether your endpoint returns HTML it serialized earlier. An interactive fragment can be inserted only once per page, and Citry rejects a second copy. Render a new fragment for each response, even when the content has not changed:

# Wrong: every request returns the same rendered fragment.
card = Card(title="Welcome")
saved_html = card.render().serialize(
    deps_strategy="fragment",
)

def card_fragment():
    return saved_html
def card_fragment():
    # Right: render a new fragment for each request.
    card = Card(title="Welcome")
    return card.render().serialize(
        deps_strategy="fragment",
    )

A static demo that ships one pre-rendered fragment can insert it once, then reload the page to let the reader try again.

Keep fragments intact

HTML optimizers and sanitizers must not remove or change the data block a fragment carries, or the Citry runtime on the page that reads it. See Preserve page HTML.

Use several workers

The browser's request for a fragment's JavaScript or CSS may reach a different worker from the one that rendered the fragment. Configure a shared cache backend so that every worker can serve those files. See Cache backends.

Use the same URL prefix and cache configuration in the processes that render and the processes that serve.

How assets load

A fragment includes each dependency according to how you declared it:

  • A URL stays a URL, and the browser loads it.
  • A local file is included in the fragment by default.
  • With Dependencies.local_files = "serve", a local file becomes a URL served by the web framework integration.
  • An object that provides only finished HTML through __html__ raises TypeError. Declare a Script, Style, or URL instead.

A fragment ignores deps_position, which applies only to the document and simple strategies.

See also