Theme
Version
GitHub PyPI Discord
On this page

Simple components

A page made of many small components, such as a status label in every table row, can spend more time setting up each component than producing its HTML. For every ordinary component, Citry creates a Python object for it (the component instance that self refers to), runs its lifecycle hooks, and creates a separate Vue component for it in the browser.

Declare simple = True on a component that only turns its inputs into HTML. Citry then renders its template as part of the component around it and skips that setup. The component gives up the features that need an instance, such as hooks and its own JavaScript.

Set simple = True

Set the flag on the class:

from citry import Component

class StatusLabel(Component):
    simple = True

    class Kwargs:
        text: str

    template = """
      <span class="status">{{ text }}</span>
    """

Use it like any other component: as a <c-StatusLabel> tag, as a StatusLabel(text="Ready") value inserted from Python, or rendered on its own. Its inputs are validated and read again on every call.

simple = True does not store output. Each call renders the template again. To reuse output, see Pure components and Cache rendered output.

A simple = True component cannot have:

  • lifecycle hooks or instance methods;
  • transparent = True;
  • its own js, css, js_data, css_data, or translation messages;
  • nested configuration such as State, Events, Cache, Dependencies, or I18n;
  • named slots or slot fallback content.

Citry raises an error when the class uses one of these, so a simple = True component never silently renders the slow way. The component's template can still use other components, and those keep all their features.

Use a static method

By default the template receives the component's inputs. To compute other values, write template_data as a static method that takes kwargs and slots:

from citry import Component

class UpperLabel(Component):
    simple = True

    class Kwargs:
        text: str

    @staticmethod
    def template_data(kwargs, slots):
        return {"text": kwargs.text.upper()}

    template = """
      <span>{{ text }}</span>
    """

Citry runs it each time the component renders. With a Kwargs or Slots class it receives a new instance of that class; without one, it receives a plain mapping. Citry validates the inputs and the returned data as it does for an ordinary component.

When you convert an existing component, the usual mistake is to keep an instance method. A simple component has no instance, so there is no self to pass:

# Fails with simple = True: the method expects self.
def template_data(self, kwargs, slots):
    return {"text": kwargs.text.upper()}

# Works: a static method with the two input parameters.
@staticmethod
def template_data(kwargs, slots):
    return {"text": kwargs.text.upper()}

The method must be a plain synchronous function. Async methods, generators, and other callable objects are rejected. Any other helper method on the class must be static too; instance methods, class methods, and properties are not supported.

Accept slot content

A simple = True template can show content the caller passes in, through one plain <c-slot />:

from citry import Component, Slot

class Inset(Component):
    simple = True

    class Slots:
        default: Slot | None = None

    template = """
      <section class="inset"><c-slot /></section>
    """

The caller passes the content as the tag's body:

<c-Inset>
  <strong>{{ user_name }}</strong>
</c-Inset>

user_name comes from the caller's data, as it would with an ordinary component. From Python, pass Inset(slots={"default": "Ready"}). A template_data method receives the content as a Slot that has not rendered yet, so it can render it or pass it to the template.

The slot rules are strict:

  • The template can contain only <c-slot />: no name, fallback content, slot data, or required flag.
  • The caller cannot use <c-fill>, not even for the default slot.
  • A Slots class, if you declare one, can contain only default, with a default of None, and no custom constructor or factory.

Uses the caller's scope

A simple = True call has no component instance, Vue component, or hooks of its own, and no render ID (the value that identifies a component in the browser). Its HTML becomes part of the nearest ordinary component above it. That has three effects:

  • Vue bindings in its template run in that ordinary component's scope.
  • Component tags in its template get that ordinary component as their parent.
  • Content passed into its slot keeps the caller's Vue scope.

Its template can still use HTML attributes, c-bind spreads, expressions, c-if, c-for, and Vue bindings.

Add Vue state

simple = True gives the component no browser state of its own. Any Vue binding in its template, such as @click, runs in the scope of the ordinary component around it.

Use simple = "vue" when the component needs its own Vue state and JavaScript but still no Python instance. Citry skips the Python setup and creates a separate Vue component in the browser:

from citry import Component

class Toggle(Component):
    simple = "vue"

    class Kwargs:
        label: str

    template = """
      <button
        :aria-expanded="open"
        @click="open = !open"
      >
        {{ label }}
      </button>
    """

    js = """
      $component({
        data() {
          return { open: false };
        },
      });
    """

Each Toggle on the page keeps its own open value and updates aria-expanded on each click.

A simple = "vue" component can use:

  • text, c-if, c-for, fixed c-* attributes, and c-bind objects;
  • Vue bindings written in the template;
  • its own js and css, and static js_data and css_data methods;
  • calls to other components, as described in the next section.

It cannot use:

  • slots or transparent = True;
  • provide and inject, lifecycle hooks, or translation messages;
  • nested configuration such as State, Events, Cache, or I18n, or js and css entries in Dependencies.

Values that its template expressions produce must be JSON-like: strings, numbers, booleans, lists, and dictionaries. The class must stay registered with its Citry instance. Citry raises an error for an unsupported class or value instead of rendering the component the ordinary way.

Some app-wide features need a component instance. While any of them applies, Citry renders simple = "vue" components the ordinary way:

  • translation (i18n) settings passed to Citry;
  • translation messages declared on any other registered component;
  • an extension that handles component data or lifecycle hooks;
  • an extension that changes element attributes through the on_attrs_resolved hook, used by this template.

The page shows the same HTML and the data method still runs once per call; the component only loses its speed advantage.

Call other components

A simple = "vue" template can call other components. Each child renders the way its own class says: an ordinary child still gets its instance, hooks, and assets, and a simple = "vue" child still skips its instance.

from citry import Component

class Row(Component):
    simple = "vue"

    class Kwargs:
        row: dict

    template = """
      <li>
        <button @click="open = !open">
          {{ row['title'] }}
        </button>
        <div v-show="open">
          <c-Details c-row="row" />
        </div>
      </li>
    """

    js = """
      $component({
        data() {
          return { open: false };
        },
      });
    """

The page shows the same HTML whether Details is ordinary or simple = "vue", and the same HTML as if Row were ordinary.

A call from a simple = "vue" template passes only inputs:

  • Each input is a static attribute or a c-* expression.
  • #c-key is allowed, and required when c-for repeats the call.
  • The call cannot pass content (a tag body or <c-fill>), a c-bind spread, or Vue bindings and events on the component tag (@click, :prop, v-show). Put those on an element inside the child, or call the child from an ordinary component.

The child cannot be a simple = True component, a transparent component, or a <c-component> that chooses its target with c-is. Each of these puts its HTML into its caller's template, which a simple = "vue" template cannot hold. Rendering the parent raises an error that names the child:

<!-- Fails when Row is simple = "vue"
     and Label is simple = True -->
<c-Label c-text="row['title']" />

Make Label an ordinary or simple = "vue" component, or call it from an ordinary component.

Calls outside c-if and c-for are the fastest. Citry builds the browser template (the template Citry sends to Vue) once and reuses it for every row, unless the parent defines css_data. Calls inside c-if or c-for work too, but Citry builds the browser template for each row from that row's values, so turning the page into HTML takes longer.

An ordinary child has no Python parent in the simple = "vue" component. Its self.parent is the nearest ordinary component above, and it receives the values provided above that component.

Error messages still show the whole path, for example Page > Row > Details.

Find the failing check

Citry checks a simple component at three points and raises an error at the first problem:

Checked whenExamples of what fails
The class is definedA simple value other than False, True, or "vue". With simple = True, also instance data methods, lifecycle hooks, js, css, messages, and State, Events, Cache, Dependencies, or I18n.
The template is loadedWith simple = True, named or fallback slots and unsupported custom tags, even inside a c-if branch that never runs. With simple = "vue", a child call that passes content, a c-bind spread, Vue bindings, or #c-ignore. Both modes reject $c-tr translation bindings.
Each callWith simple = True, <c-fill>, Vue bindings or #c-key/#c-ignore on the component tag, and unsupported slot names from Python. With simple = "vue", instance data methods or lifecycle hooks (the class is created without error, and rendering it fails), a call to a simple = True component, a transparent component, or a <c-component> that uses c-is. Both modes reject invalid inputs, invalid returned data, and $c-tr keys passed through a c-bind spread.

Edge cases

Empty declarations

The restrictions apply to everything the class inherits. An empty string still counts as a declaration, so css = "" on a simple = True class raises an error.

Subclasses

simple is inherited. Citry checks each subclass on its own, and a subclass can set simple = False to become an ordinary component. LibraryComponent accepts the flag too; Citry checks the class when the library is loaded into an app.

Later class changes

You cannot reassign simple or the nested Kwargs and Slots classes after the class is defined. A simple = True class also rejects changes to its other declarations. A simple = "vue" class allows them, and Citry checks the class again on its next render. When hot reload or a template reset reloads the template, Citry checks the new template too.

Data method timing

For a component tag, the data method runs at the same point in the render as an ordinary component's. For a value inserted from Python, it runs when the expression that inserts it runs.

Rendering it alone

Rendering a simple = True component as the whole page still costs the setup that every page render has. The saving comes from many calls inside a larger page.

With <c-component>

<c-component> can choose a simple class. The <c-component> tag itself keeps its own instance and hooks, and the chosen class still follows its simple rules.

Translations

A simple template cannot use $c-tr, even when its caller has a translation catalog active.

Combining with pure

A component can declare both simple = True and pure = True when both promises hold. Its data method still runs on every call. A simple = True body that contains <c-slot /> renders again on every call, even with pure = True.

A simple = "vue" component can declare pure = True too. It has no effect while Citry renders the component without an instance. It reuses HTML only while one of the app-wide features listed under Add Vue state makes Citry render the component the ordinary way, for example when translation settings are passed to Citry.