Theme
Version
GitHub PyPI Discord
On this page

Replace part of the page from Python

The form currently reports success by changing a line of text in the browser. For the final step, Python will render a new Confirmation component into the result area.

The form itself will stay where it is. Only the chosen part of the page will change.

Continue from Handle and validate a form. Keep citry_setup.py and app.py unchanged.

Return a rendered update

Replace components.py with the finished version:

The New in this step comments mark the complete change: the new Confirmation component, the form's new success action and result area, and the two places where the page collects component CSS and JavaScript.

"""The completed Getting started server application."""

from citry_setup import citry_app

from citry import Component
from citry.ext.events import EventError, actions

CHOICE_BATCHES = (
    ("Ocean", "Forest"),
    ("History", "Science"),
)


def load_choices_from_database(batch: int) -> list[str]:
    choices = CHOICE_BATCHES[batch % len(CHOICE_BATCHES)]
    return list(choices)


class ChoiceButton(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <button class="choice-button" type="button">
        Choose
        <span
          class="choice-button__label"
          x-text="clientProps.label"
        ></span>
      </button>
    """

    js = """
      $component({
        props: {
          label: { type: String, required: true },
        },
        init: ({ props, scope }) => {
          scope.clientProps = props;
        },
      });
    """


class ChoicePicker(Component):
    citry = citry_app

    class Kwargs:
        batches_loaded: int = 0

    class Slots:
        pass

    class State(Kwargs):
        pass

    class Events:
        def load_choices(self, state):
            choices = load_choices_from_database(state.batches_loaded)
            state.batches_loaded += 1
            return actions.Dispatch(
                "choice-picker:loaded",
                {
                    "choices": choices,
                    "batches_loaded": state.batches_loaded,
                },
            )

    template = """
      <section
        class="choice-picker"
        c-x-data="{
          'choices': [],
          'choice': '',
          'batchesLoaded': batches_loaded,
        }"
        @choice-picker:loaded="
          choices = $event.detail.choices;
          choice = choices[0];
          batchesLoaded = $event.detail.batches_loaded;
        "
      >
        <button
          type="button"
          :disabled="$loading('load_choices')"
          @c-click="load_choices"
        >
          Load choices
        </button>
        <span x-show="$loading('load_choices')">Loading...</span>
        <p>
          Sets loaded:
          <output x-text="batchesLoaded">
            {{ batches_loaded }}
          </output>
        </p>

        <p x-show="choices.length === 0">
          No choices loaded yet.
        </p>
        <div x-show="choices.length > 0">
          <p>
            Current choice:
            <output
              class="choice-picker__value"
              x-text="choice"
            ></output>
          </p>

          <c-ChoiceButton
            $c-props="{ label: choice }"
            @click="
              choice = choices[
                (choices.indexOf(choice) + 1) % choices.length
              ]
            "
          />
        </div>
      </section>
    """


# New in this step: render this component after a valid form.
class Confirmation(Component):
    citry = citry_app

    class Kwargs:
        email: str

    class Slots:
        pass

    class JsData:
        email: str

    def js_data(self, kwargs: Kwargs, slots: Slots) -> JsData:
        return self.JsData(email=kwargs.email)

    template = """
      <section class="confirmation">
        <strong>Request received</strong>
        <p>We will write to {{ email }}.</p>
        <p class="confirmation__status">
          Preparing confirmation...
        </p>
      </section>
    """

    js = """
      $component(({ els, data }) => {
        els[0].querySelector(".confirmation__status").textContent =
          `Confirmation ready for ${data.email}`;
      });
    """

    css = """
      .confirmation {
        border: 2px solid #2f855a;
        border-radius: 0.5rem;
        padding: 1rem;
      }
    """


class SignupIn:
    email: str


class SignupForm(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    class Events:
        def submit(self, data: SignupIn):
            email = data.email.strip()
            if not email.endswith("@example.com"):
                raise EventError(
                    "Please fix the email address.",
                    fields={"email": "Use an @example.com address."},
                )
            # New in this step: replace the result area's contents.
            return actions.Render(
                Confirmation(email=email),
                target="#signup-result",
                swap="inner",
            )

    template = """
      <section class="signup-form">
        <form @c-submit.prevent="submit">
          <label>
            Work email
            <input
              name="email"
              type="email"
              autocomplete="email"
              required
            />
            <span
              class="signup-form__error"
              role="alert"
              x-show="$error?.fieldErrors?.email"
              x-text="$error?.fieldErrors?.email || ''"
            ></span>
          </label>
          <button
            type="submit"
            :disabled="$loading('submit')"
            x-text="$loading('submit') ? 'Sending' : 'Send request'"
          >
            Send request
          </button>
        </form>
        {# New in this step: give Render a stable target. #}
        <div id="signup-result" aria-live="polite">
          <p>Your confirmation will appear here.</p>
        </div>
      </section>
    """


class TutorialPage(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <title>Reading room</title>
          {# New in this step: place collected component CSS. #}
          <c-css />
        </head>
        <body>
          <main>
            <h1>Reading room</h1>
            <c-ChoicePicker />
            <c-SignupForm />
          </main>
          {# New in this step: place collected component JS. #}
          <c-js />
        </body>
      </html>
    """

Open http://127.0.0.1:8000/. Try ada@elsewhere.test once more to confirm that the field error still works. Then submit ada@example.com.

The placeholder inside the result area becomes a bordered confirmation. Its status changes to “Confirmation ready for ada@example.com.” The email form remains above it.

Build the component Python will return

Confirmation is an ordinary component. Focusing only on its input and visible HTML, it looks like this:

class Confirmation(Component):
    class Kwargs:
        email: str

    template = """
      <section class="confirmation">
        <strong>Request received</strong>
        <p>We will write to {{ email }}.</p>
        <p class="confirmation__status">
          Preparing confirmation...
        </p>
      </section>
    """

The complete class above also includes its Citry instance, Slots, browser data, JavaScript, and CSS. This smaller excerpt shows the first connection: Confirmation(email=email) supplies the value that {{ email }} prints.

Return the component from the handler

The successful handler now returns an actions.Render action:

return actions.Render(
    Confirmation(email=email),
    target="#signup-result",
    swap="inner",
)

Confirmation(email=email) builds a fresh component for this response. Citry renders it as a browser update, including the information needed for that component's CSS and JavaScript. The validation path still raises the same EventError; only a successful submission reaches this return statement.

Choose the part of the page to update

The form contains a stable result area:

<div id="signup-result" aria-live="polite">
  <p>Your confirmation will appear here.</p>
</div>

target="#signup-result" points to that element with a CSS selector. swap="inner" replaces its contents while keeping the <div id="signup-result"> itself. The form, input, and button remain where they are. aria-live="polite" also lets assistive technology announce the new confirmation without moving keyboard focus.

Use a selector that identifies the intended area without matching unrelated elements.

Give the new component browser data

The confirmation sends its email address to browser code through js_data():

class JsData:
    email: str


def js_data(self, kwargs: Kwargs, slots: Slots) -> JsData:
    return self.JsData(email=kwargs.email)

The $component callback receives that value as data.email:

$component(({ els, data }) => {
  els[0].querySelector(".confirmation__status").textContent =
    `Confirmation ready for ${data.email}`;
});

When the new component starts, this changes “Preparing confirmation...” to the finished status. As with the browser data from the earlier lesson, the value returned by js_data() must be JSON-serializable.

Bring the component's styles and script

Confirmation also owns the border that makes the new result visible:

.confirmation {
  border: 2px solid #2f855a;
  border-radius: 0.5rem;
  padding: 1rem;
}

The full page provides places for component CSS and JavaScript:

<head>
  ...
  <c-css />
</head>
<body>
  ...
  <c-js />
</body>

<c-css /> and <c-js /> place the assets needed by components in the first response. Earlier pages worked without these tags because Citry can choose the usual document positions automatically. Adding the tags makes the positions explicit.

Confirmation is created later, so its assets arrive with the rendered update, not through the two tags in the first response. Citry loads them before the new component starts. That is why the green border appears and the status changes after the confirmation enters the page.

See Event actions for the other results a handler can return, and HTML fragments for a deeper look at rendering and inserting partial HTML.

Keep building

You have now used Citry from both sides of the connection. Python rendered the first page, browser data handled instant changes, server events called typed Python handlers, State continued across calls, form errors came back beside their fields, and a final handler replaced one part of the page with a new component.

From here:

  • use Examples when you want working code for a specific task;
  • read Docs when you want a concept or guided workflow; or
  • open Reference when you need the exact API for a class, method, or return action.