Theme
Version
GitHub PyPI Discord
On this page

Upgrade to Citry 0.6.0

Use this guide to move a project from Citry 0.5.x to 0.6.0. In 0.6.0, components run in the browser with Vue instead of Alpine. Your Python components, templates, c-* attributes, slots, and Events handlers mostly keep working. Most of the work is in browser code: x-* attributes, Alpine expressions, and component JavaScript move to Vue.

Work from the top. The first sections affect almost every project with browser behavior. Later sections apply only if you use that feature, and each starts with what you see when you skip it. The checklist at the end lists every step.

A project that only renders static HTML from Python, with no browser behavior, usually needs only these steps: upgrade the packages, check component tag attributes, check #c-ignore, rename a component named Mark, name the parent class in subclasses that add inputs, wrap HTML that on_render() returns, and work through Settings and extensions.

Upgrade the packages

Upgrade Citry, its UI library, and the language server in one step:

python -m pip install -U "citry==0.6.0" "citry-ui==0.3.0" \
  "citry-lsp==0.2.0"

Citry 0.6.0 installs citry-core 1.8.0 for you.

pip lets older versions stay installed next to Citry 0.6.0, but they break:

  • citry-lsp 0.1.7 fails to start with ImportError: cannot import name 'BROWSER_UNKNOWN_COMPONENT_PROP', and the editor shows no Citry diagnostics. citry-lsp 0.2.0 is the release for Citry 0.6.0.
  • citry-ui 0.2.x components render, but their browser behavior does not work.

Rewrite Alpine syntax

What you see: the page looks right but does nothing. A menu does not open, a counter does not count, and there is no error in Python or in the browser console. Citry still writes x-* attributes into the HTML, but nothing reads them.

Move local browser state from x-data into the component's JavaScript, and write each directive in its Vue form:

class Faq(Component):
    # 0.5.1
    template = """
      <div x-data="{ open: false }">
        <button x-on:click="open = !open">Toggle</button>
        <p x-show="open" x-cloak>Ships in two days.</p>
      </div>
    """
class Faq(Component):
    # 0.6.0: the state lives in the Vue component.
    template = """
      <div>
        <button @click="open = !open">Toggle</button>
        <p v-show="open">Ships in two days.</p>
      </div>
    """

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

State that Python computes still comes from js_data(). Each key it returns is now a member of the Vue component, so this.count in JavaScript and count in a template both read it.

0.5.1 (Alpine)0.6.0 (Vue)
x-data="{...}"$component({ data() {...} }) or js_data()
x-text, x-html, x-showv-text, v-html, v-show
x-modelv-model
x-forv-for with a :key
x-ifv-if, v-else-if, v-else
x-on:click, @click@click or v-on:click
x-bind:class, :class:class or v-bind:class
x-ref and $refsref and this.$refs
x-initmounted() or onServerRender
x-effectwatch, or Citry.vue.watchEffect in setup()
x-cloakDelete it (see below)

Delete x-cloak and its [x-cloak] { display: none } CSS rule. Nothing removes the attribute any more, so the rule would hide the element for good. You do not need it: the served HTML already shows the page's content; see what a page shows before Vue starts.

Vue in templates lists the directives Citry accepts and the few it rejects, such as <Transition> and <Teleport>.

Find leftover x-*

Run citry check before you test the pages. It reports each x-* attribute left on an HTML element, and so does the editor. A leftover x-cloak is an error; other x-* attributes are warnings.

citry --app myproject.app:app check

If another library on the page reads x-* attributes, turn the warning off with rule_alpine_attribute="ignore" in LintSettings or in the component's Lint class.

Rewrite Alpine modifiers

What you see: the template fails to load, and the error names the Vue way to write the listener.

Vue has no .outside, .window, .document, .debounce, or .throttle modifiers:

{# Fails: Vue has no .outside modifier #}
<div @click.outside="open = false;">...</div>

Write that behavior in a method instead. For example, add a mousedown listener to document in mounted() and remove it in unmounted().

To slow down calls to the server, the @c-* Events bindings still accept .debounce and .throttle; see Bind events in templates.

A key modifier such as .enter works only on keydown, keyup, and keypress listeners. On another event, such as @click.enter or @input.enter, the template fails to load, because that event has no key: Vue would ignore the modifier and run the listener on every click or input event. Remove the modifier, or listen to keydown if you meant the key.

End statements with ;

What you see: the render stops with a Vue compile error, "Error parsing JavaScript expression".

Alpine ran a listener such as @click="if (ok) save()" as written. Vue reads a listener as an expression unless it contains a ;. End a statement with ;, or move it into a method:

{# Fails: Vue reads an `if` statement as an expression #}
<button @click="if (ok) save()">Save</button>

{# Works #}
<button @click="if (ok) save();">Save</button>

Use $emit

What you see: citry check reports $dispatch as an unknown variable, and clicking throws an error in the browser console, because Alpine's $dispatch does not exist in Vue.

Emit the event with Vue's $emit, and declare it in the child's emits:

{# 0.5.1 #}
<button @click="$dispatch('select', 'red')">Red</button>

{# 0.6.0 #}
<button @click="$emit('select', 'red')">Red</button>
$component({
  emits: ["select"],
});

The parent listens on the component tag with @select="...". A Vue event reaches only that listener; it does not bubble up through the page. When page code outside the parent must hear it, dispatch a DOM CustomEvent from an element you hold in a ref.

Replace $c-props

What you see: the template stops loading with '$c-props' was removed; use native Vue ':prop' or 'v-bind' syntax.

Pass each value as a Vue prop, and declare the props in the child:

{# 0.5.1 #}
<c-Dialog $c-props="{ open }" />

{# 0.6.0 #}
<c-Dialog :open="open" />
$component({
  props: {
    open: Boolean,
  },
});

The same applies to Citry UI 0.3.0 components: write :open="open" on <c-CDialog>. A callback that 0.5.1 passed as a prop usually becomes an emitted event; see Use $emit.

Update $component

$component(callback) and $component({ init }) still work, but they run at a different time and receive less.

When it runs. Citry calls the function after the component mounts, children before their parent, and again after each server render that updates this component. A change to local Vue state does not call it. Code that needs a value before the first render belongs in data() or setup().

What it receives. The context still has id, els, state, i18n, sendEvent, onEvent, loading, and error, and adds component, the live Vue component, and revision. Reading any other 0.5.1 value gives undefined, so a callback that reads props.name fails with a TypeError. Replace those values:

0.5.1 context value0.6.0 replacement
data, scope, propsThe Vue component: component.name or this.name
effect(fn)Citry.vue.watchEffect(fn) inside the callback
reactive(value)Citry.vue.reactive(value)
provide, injectThe Vue provide and inject options
unprovide, graphNo replacement
// 0.5.1
$component(({ data, scope, effect }) => {
  scope.doubled = data.count * 2;
  effect(() => console.log(scope.doubled));
});
// 0.6.0
$component({
  computed: {
    // `count` is a js_data() key, so it is a component member.
    doubled() {
      return this.count * 2;
    },
  },
  onServerRender({ component }) {
    // Citry stops this effect before the next call.
    Citry.vue.watchEffect(() => console.log(component.doubled));
  },
});

The function may be async. Citry does not wait for it, still runs a cleanup function it resolves to, and logs a rejection instead of stopping the page. The Browser APIs reference lists every option $component accepts.

Replace $provide

What you see: code that calls $provide, $inject, or $unprovide fails, because these template helpers do not exist in 0.6.0.

Provide a value from a component with Vue's provide option, and read it in a descendant with inject. See Vue provide/inject.

Rename reserved names

  • A js_data() key that starts with $ or _, or the key citryId, stops the render with a ValueError that names the component and the key. Rename it, for example _count to count.
  • A component class named Mark, or one registered as mark, fails when the class is defined, with an AlreadyRegistered error that says the built-in <c-mark> reserves the name. Rename the class or give it another name. See Built-in tags.

Reserved names lists the rest.

Update Events code

Change render targets

What you see: actions.Render with a CSS selector as its target raises ValueError, saying that the target must be render:<id>, mark:<name>, or None.

A render target now names one place: mark:<name> for a region you wrap in <c-mark>, or render:<id> for one rendered component. Wrap the region and target it by name:

<c-mark name="cart-badge">
  <c-CartBadge c-count="cart.count" />
</c-mark>
badge = CartBadge(count=cart.count)
return actions.Render(badge, target="mark:cart-badge")

A mark: or render: target accepts only swap="morph". A 0.5.1 selector that matched several elements becomes one Render action per target. One response can return them all, one after another, as long as no target sits inside another. Citry.events.applyActions follows the same rule for Render and Event actions. See Event actions.

Keep the root component

What you see: the browser reports an error that names both components, and the page stays as it was.

A handler can still return a different component to replace the calling one, and the old component's browser state is still lost. But it cannot replace the outermost component of a page or HTML fragment. Move the part that changes into a child component, or into a <c-mark> region. Props, listeners, and a ref that the parent wrote on the old component's tag do not reach the new one. See Render another component.

Nested $state writes

What you see: writing to a nested value, such as $state.tags.push("new"), throws "Nested $state values are read-only".

Assign the whole field instead: $state.tags = [...$state.tags, "new"].

Remove wait: false

What you see: $sendEvent or Citry.events.send rejects.

Calls from one app are now sent in order, and an unknown option, or wait: false, makes the call reject. For live search, where only the newest call matters, declare the handler with @event(latest_wins=True).

Wait for citry:ready

What you see: a call made while the page loads rejects.

A call made before the page's Vue app mounts now rejects instead of waiting. Listen for the citry:ready event on document first.

Forward request headers

What you see: every handler that returns a render fails with "Vue Events requires current app, occurrence, and revision headers."

Citry now calls a custom transport as send(envelope, request). Send request.headers with the request. See custom event transports.

Set security on Citry

What you see: on a page with Events, serialize() raises ValueError when you pass it a different security_csp or security_javascript.

Pass security_csp and security_javascript to Citry(...) instead. A later server render of the same page must use the same policy, so serialize() cannot choose a different one.

Clean up applyActions

What you see: applyActions throws a ProtocolValueError (a TypeError) and applies none of the actions.

It now rejects an action with a field it does not know, a value that is not plain JSON such as undefined, or an array with gaps such as [a, , b]. Remove the extra fields from actions your page code builds or forwards. A state action you build, for applyActions or in an on_event_result hook, must now include publicState, the component's public State values.

Listen for DOM events

What you see: a $onEvent listener no longer fires for an event that page code dispatches inside the component.

$onEvent, and the callback's onEvent, now hear only the events that this component's server handlers dispatch. For a DOM event that page code fires, call addEventListener on an element you hold in a ref.

Key filters need a keyboard event

What you see: a @c-* binding such as @c-click.enter, or a :c-* binding such as :c-query.enter, fails when the template loads, saying that only keyboard events have a key.

.enter and .escape on a @c-* binding now need keydown, keyup, or keypress, as they do on a Vue listener. Citry 0.5.1 accepted them on any event name, where a click never matched and a custom event matched only when its object had a key. Listen to the keyboard event instead:

<input @c-keydown.enter="search" />

A :c-* binding checks the key of its update event: by default input or change, or the event named in .on:. Citry 0.5.1 accepted .enter on any of them, and on the default events the binding never sent. Name a keyboard update event:

<input :c-query.on:keydown.enter="search" />

One .on: per binding

What you see: a :c-* binding that names two update events, such as :c-query.on:keyup.on:change, fails when the template loads, and the error names both events.

Citry 0.5.1 used the last one. Keep the event you want:

<input :c-query.on:change="search" />

One key per binding

What you see: a binding with two key filters, such as @c-keydown.enter.escape, or an element with two @c-* bindings for one event, such as @c-keydown.enter and @c-keydown.escape, fails when the template loads, and the error names both.

Citry 0.5.1 used the last key filter of a binding and ran every binding for an event. To react to either key with one handler, call it from one Vue listener. To call a different handler for each key, write one Vue listener per key:

<input @keydown.enter.escape="$sendEvent('search')" />

<input
  @keydown.enter="$sendEvent('search')"
  @keydown.escape="$sendEvent('clear')"
/>

Update stale listeners

What you see: a listener that checks for cancelled or timeout never matches.

The event now reports superseded, retired, epoch, disposed, or version.

Stop reading Events tags

What you see: page code that parsed these tags finds nothing.

Pages no longer carry them. Inside the component, read Events values through $state, $loading, and $error.

Reload open pages

What you see: events from a page that was open during the upgrade fail with a stale_state error.

Citry 0.6.0 cannot read the server-side State that 0.5.x saved. Reload the page; pages opened after the upgrade are not affected.

Component tag attributes

What you see: a child component no longer receives a kwarg it read, a listener on a component tag stops firing, or the render stops with an error that names the attribute.

The attributes on a <c-Child> tag now split into Python inputs and Vue bindings by how they are spelled:

  • :name, v-bind, v-if, v-else-if, v-else, v-model, v-show, custom v-* directives, and ref are Vue bindings. 0.5.1 passed them to the child as Python kwargs. A child that read kwargs[":class"] or a ref kwarg no longer receives it. Pass a plain kwarg or a c-attrs mapping instead.
  • @event listens for an event the child emits. In 0.5.1 it added a DOM listener to every root element of the child. Now the child declares the event in emits and calls $emit. A native event such as @click that the child does not declare goes to the child's single root element; a child with several root elements must place it with v-bind="$attrs". See Listen to child events.
  • x-on:event stops the render with Alpine binding 'x-on:click' was removed; use native Vue v-on or @event syntax. Write @click instead.
  • A .debounce or .throttle @c-* binding, or @c-poll, stops the render. Put it on an element inside the child's template.

Move Python bindings

What you see: rendering the template raises citry.VueCodeError, with a message that names the attribute and says Vue would read it as code.

In 0.5.1, Python could set an attribute such as @click or :class through c-bind, c-:attr, or c-@event, and Alpine read it in the browser. In 0.6.0, an attribute name that comes from Python data must not start with v-, :, ., ^, @, or #, in any letter case. The render fails on every page, including a static page that never loads Vue, and even when the value is None:

{# Stops the render: Python writes browser code #}
<div c-:class="'{ active: selected }'">...</div>

{# Works: the binding is in the template #}
<div :class="{ active: selected }">...</div>

Pass the values the binding needs through js_data() or props.

If a script you load yourself, such as Alpine, reads these attributes on an HTML element, spread its long-form names instead. Citry writes them as plain attributes on HTML elements; on a component tag, an x-on: name still stops the render:

# Stops the render: '@click' and ':class' are Vue syntax
attrs = {"@click": "open = true", ":class": "classes"}

# Works: your own Alpine script reads the long form
attrs = {"x-on:click": "open = true", "x-bind:class": "classes"}

Set attributes once

What you see: the template fails to load, with a message that c-title and :title set the same attribute.

One element can no longer set an attribute both ways, such as c-title together with :title. Keep one of them. :class and :style are the exception: they combine with class and c-class, or style and c-style, as in Vue.

For the same reason, an object v-bind="..." or a dynamic :[name] cannot sit on an element that has any c-* attribute, and :key cannot sit next to #c-key. See c-* with : bindings.

Markup attribute values

What you see: a " inside a Markup attribute value now shows as part of the value. In 0.5.x it ended the attribute, and the text after it became more attributes.

Citry now escapes every attribute value, Markup included. Markup only says a string is safe to insert between tags. An entity in it still reads as its character, so Markup("Tom &amp; Jerry") still sets the attribute to Tom & Jerry. To set several attributes from Python, pass a mapping to c-bind:

# 0.5.x: with c-title="label", the quote added data-id.
return {"label": Markup('Saved" data-id="1')}

# 0.6.0: with c-bind="attrs", one key per attribute.
return {"attrs": {"title": "Saved", "data-id": 1}}

Inline group content

What you see: the render stops with an error that names the component that wrote the tab, the line, the Vue code it uses, such as v-text="label" on <span>, and CTabs as the component that moves the content.

Citry UI's CTabs collects the <c-CTab> and <c-CTabPanel> tags inside it and moves their content into the CTabs template. Other Citry UI components that collect item tags, such as CStepper and CTimeline, work the same way. Content that uses Vue data, handlers, v-model, a ref, or a @c-* binding must therefore be written in the component that holds <c-CTabs>. The error appears when a separate component placed inside <c-CTabs> writes that content. Move the tags into the component that holds <c-CTabs>, or set transparent = True on the component that writes them and define their Vue data in the component that holds <c-CTabs>. See Vue data inside CTabs.

Check #c-ignore

What you see: the template fails to load, with a message that says what #c-ignore cannot hold or where it cannot go.

#c-ignore on an HTML element keeps its contents as the server first rendered them, so a chart or map library can own them. In 0.6.0, the contents can hold only HTML, {{ }} expressions, <c-if>, <c-for>, and <c-raw>. Move any component, slot, Vue binding, or ref out of them. A ref can go on the #c-ignore element itself:

<div class="chart" ref="chart" #c-ignore>
  <canvas></canvas>
</div>

#c-ignore also fails on these tags:

  • A component tag. Move it onto the element inside the component's template that the library manages.
  • <table>, <tbody>, <tr>, and the other table row elements. Wrap the table in a <div #c-ignore> instead.
  • <c-element>, in both the is and c-is forms. Write the element as a plain tag, such as <section #c-ignore>.

See #c-ignore.

Extend a parent's Kwargs

What you see: rendering a subclass fails with unexpected keyword argument naming an input its parent accepts, or an inherited method fails with AttributeError when it reads that input, or defining the class raises ValueError. Defining the class also prints a NestedSchemaReplacedWarning that names the missing fields.

Since 0.3.0, a nested Kwargs on a subclass added its fields to its parent's. In 0.6.0 it replaces them, like any nested Python class. Name the parent's class as a base to keep its fields:

# 0.5.1: SignedMessage takes `text` and `author`.
class SignedMessage(Message):
    class Kwargs:
        author: str

# 0.6.0: name Message.Kwargs to keep `text`.
class SignedMessage(Message):
    class Kwargs(Message.Kwargs):
        author: str

The same applies to Slots, State, TemplateData, JsData, and CssData. A subclass that declares none of them still uses its parent's. class Kwargs(Message.Kwargs): also keeps the parent's defaults now, so a parent field with a default stays optional in the subclass.

If a parent's State sets _storage = "server", a subclass with its own plain class State: must set _storage too. Otherwise defining it raises ValueError, because the values would move into the page. Write class State(Parent.State):, or set _storage in the new class.

A plain class State: also starts from the default settings, so browser code can read and change all of its fields. Name the parent's class to keep its _public, _model, and _max_age.

A component with two parent components that declare different Kwargs raises ValueError when it is defined. Pick one with Kwargs = Parent.Kwargs, or list the fields of both in module-level classes and write class Kwargs(FirstFields, SecondFields):.

Events, Dependencies, Lint, Cache, and other settings classes still add to the parent's settings, so they need no change. See Subclass components.

Wrap on_render HTML

What you see: HTML that a component's on_render() returns shows on the page as text, with its tags visible, such as <p>No data</p>.

In 0.6.0, a plain str that on_render() returns or yields is text, the same as a value in {{ }}, and Citry escapes it. In 0.5.x, Citry inserted it as HTML. Wrap HTML in Markup, or return a component:

from citry import Markup

# 0.5.x: the string was inserted as HTML.
def on_render(self):
    return "<p>No data</p>"

# 0.6.0: Markup marks the string as HTML.
def on_render(self):
    return Markup("<p>No data</p>")

If a hook returns str(result) after a yield, that is a plain str too, so wrap it in Markup. For a component that runs in the browser, put added HTML in the template instead; see Avoid str(result).

Before you wrap a string, check where its text comes from. If it contains user input, such as a name or a comment, keep the string plain: escaping stops that input from adding a script to the page. If it needs markup around the user input, write the markup in the template and pass the value in as a component input.

The same applies to a plain str that an extension's on_component_rendered() or on_slot_rendered() returns: 0.5.x inserted it as HTML, and 0.6.0 shows it as text. Wrap HTML there in Markup too.

Fix rejected HTML

These rules apply only to components that run in the browser. Static pages are not affected.

Close <c-raw> tags

What you see: the render stops with an error such as "The <c-raw> block at line 2, column 10 is not a complete HTML fragment".

Inside an interactive component, <c-raw> content and Markup values must each be a complete HTML fragment: every tag closed, and no stray <. The error names the input and explains the rule.

One <body> per page

What you see: serialization stops with an error such as "Interactive Vue document serialization requires one well-ordered body element."

An interactive page template that writes <!doctype html> or <html> must put its content inside one <body> element.

Removed browser globals

What you see: page code throws because Citry.alpine or another removed global is undefined, an alpine:init listener never runs, or a request for runtime-csp.js returns 404.

RemovedWhat to use
Citry.alpine.beforeStart(fn)Citry.vue.use(plugin), called from a defer script in <head> or a script an extension adds to ctx.early_scripts. An Alpine plugin has no direct replacement.
Citry.manager.* (loadJs, loadCss, callComponent, ...)Declare assets on the component with Component.js, Component.css, or Dependencies. Interactive fragments load their own assets.
Citry.i18n.providercomponent.$i18n or this.$i18n; see Browser i18n.
window.Alpine, alpine:initThe citry:ready event on document.
/ext/events/runtime-csp.js routeNothing. Citry serves one runtime for every CSP mode.

Share the cache

What you see: with several worker processes, an interactive page shows its HTML but never starts, and the browser console shows 404 responses for component code or stylesheets.

The worker that rendered the page stored those files in the Citry cache, and another worker, with its own cache in memory, answered the request. Point every worker at one shared cache backend, such as Redis or DiskCache.

Without a configured cache, a single process keeps this code in memory, up to vue_asset_max_bytes (64 MiB by default). A page always finds its own files right after it loads. A page left open long enough to ask for a file that was dropped gets the same 404, so raise the limit or configure a cache. See Share the cache.

Proxies and CDNs

What you see: the page shows its server-rendered HTML, but components never start, and the browser console reports a CORS or Subresource Integrity error.

Citry's asset routes now send Access-Control-Allow-Origin: *. When a proxy or CDN serves Citry's files from another host, or the page runs in a sandboxed iframe, make sure the proxy or CDN passes that header through. See Keep the CORS header.

Settings and extensions

Rename lint settings

What you see: LintSettings raises TypeError, or defining a component whose Lint class uses the old names raises ValueError.

Rename the settings in LintSettings and in any Component.Lint:

# 0.5.1
LintSettings(
    rule_unknown_alpine_variable="warning",
    alpine_variables={"theme": str},
)

# 0.6.0
LintSettings(
    rule_unknown_vue_variable="warning",
    vue_variables={"theme": str},
)

A tool that builds TemplateLintInfo uses the same new names. Keep only the names your Vue expressions still read; an Alpine helper such as $dispatch means nothing to Vue.

Rename diagnostic codes

What you see: a script that filters citry check --format json output by code stops matching.

0.5.1 code0.6.0 code
citry.alpine.unknown-variablecitry.vue.unknown-variable
citry.component-js.unknown-data-membercitry.component-js.unknown-member
citry.browser.unknown-component-propRemoved
citry.csp.incompatible-browser-codeRemoved; Vue expressions never need unsafe-eval
citry.i18n.cross-language-fallback on a <c-trans> tagcitry.i18n.rich-message-fallback
citry.i18n.client-message-invalid for a missing translationcitry.i18n.cross-language-fallback

Untranslated text warns

What you see: citry check passes for a tr() call whose message is not translated into every locale, and prints a citry.i18n.cross-language-fallback warning where 0.5.1 reported an error.

A locale may now stay partly translated; the untranslated text falls back to another language. To keep failing the check on such text, set LintSettings(rule_i18n_cross_language_fallback="error"). A <c-trans> message still needs every translation. See Change fallback warnings.

Strict CSP mode

What you see: with security_csp="strict", rendering raises ValueError: "Citry found N strict-CSP incompatibility issue(s)", followed by a list of the problems.

security_csp="strict" used to select Citry's Alpine CSP build and limit the expressions you could write. It now checks the final HTML for raw <script> and <style> elements, on* attributes, and javascript: URLs. Any Vue expression works, because Citry compiles templates on the server. See Security.

Classic scripts only

What you see: serializing a page or an Events response raises ValueError about a script.

A component's Dependencies scripts, and an extension's ctx.scripts and ctx.early_scripts, must be classic JavaScript on an interactive page. Citry loads these scripts itself, one after another in list order, so it rejects a type="module" or JSON data script, and a script marked async, defer, or nomodule. Scripts in ctx.early_scripts load before ctx.scripts.

ctx.early_scripts

What you see: an on_dependencies() hook raises AttributeError, naming early_scripts.

Rename ctx.before_manifest to ctx.early_scripts. The scripts it holds still run before ctx.scripts. The old name has no alias.

Keyword hook contexts

What you see: code that builds OnSerializeContext or OnDependenciesContext with positional arguments, such as an extension test, fails or passes values to the wrong fields.

Both gain a selected_render field, the render being serialized. Build them with keyword arguments.

Check HTTP methods

What you see: defining an Events class or building a URLRoute raises an error.

  • @event(methods=...) and Events._methods accept only GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS. Any other method raises ValueError when the class is defined.
  • URLRoute(methods=...) is checked when you build the route. Pass a non-empty tuple of uppercase method names, such as ("GET", "POST"). A list raises TypeError. An empty tuple raises ValueError, and so does a lowercase name such as "get", which no framework adapter would match.

citry.analysis results

What you see: a tool that reads citry.analysis results skips the kwargs parameter.

In the results of analyze_template_data_source, analyze_js_data_source, and analyze_css_data_source, parameters now leaves out self or cls. parameters[0], when present, is the kwargs parameter, so a tool that skipped the first entry should read from index 0.

The Alpine helpers in citry.analysis are removed. Use VueLintConsumer, VueLintFinding, lint_unknown_vue_variables, and VUE_AMBIENT_NAMES in place of AlpineLintConsumer, AlpineLintFinding, lint_unknown_alpine_variables, and ALPINE_AMBIENT_NAMES.

Removed Python APIs

What you see: an ImportError, or a TypeError for an unexpected argument.

  • TemplateNode is removed. Compiled templates never contained it, so delete any import of it or isinstance check against it. An extension that built one in on_template_compiled should instead add the nested template's markup to the template source in on_template_loaded, so it compiles with the rest of the template.
  • The ownership APIs are removed: the citry.ownership and citry.ownership_manifest modules, and the ownership parameters of CitryContext and CitryElement. Delete the imports and those arguments.
  • lint_csp_compatibility and CspCompatibilityFinding are removed from citry.analysis. They reported nothing for Vue templates, so delete the call. Citry reports CSP problems when it serializes a page.

Clean up integrations

  • HTMX: delete citry-htmx.js and hx-ext="citry-fragments". Citry no longer writes the markers that helper kept, and inserted fragments start on their own. See Use Citry with HTMX.
  • data-cid-* selectors: only static output carries these attributes. Elements that Vue renders have none, so CSS or scripts that select [data-cid-...] stop matching. Use a Vue ref in component JavaScript.
  • data-citry-key selectors: #c-key on an element no longer writes a data-citry-key attribute, so selectors for it stop matching. #c-key still keeps rows matched across renders.
  • Render cache: entries saved by Citry 0.5.x count as misses and are saved again on the next render. You do not need to clear the cache.

New features to try

  • Interactive pages send their content in the served HTML, so search engines and readers without JavaScript see it. Tune this with ssr and ssr_element_threshold; see What the server sends.
  • simple = "vue" gives a component its own Vue state and assets without a Python component instance; see Simple components.
  • A component tag accepts v-if, v-model, v-show, and custom directives; see Vue on component tags.
  • <c-mark> names a region that an Events handler can render again; see Built-in tags.
  • A text field keeps what the user typed when its component renders again, until the server sends a different value; see Keep typed input.
  • citry check and the editor report a misspelled js_data() member and a Vue binding that reads a Python loop variable; see Template linting.

Upgrade checklist

  1. Install citry 0.6.0, citry-ui 0.3.0, and citry-lsp 0.2.0 in the same environment.
  2. Run citry check to find leftover x-* attributes, and replace each with its Vue form.
  3. Move x-data state into data() or js_data().
  4. Rewrite .outside, .window, .document, .debounce, and .throttle modifiers on plain @event listeners. End a statement listener such as if (ok) save() with ;.
  5. Replace $dispatch with $emit, and declare emits in the child.
  6. Replace $c-props with :name props, declared in the child's props.
  7. Replace data, scope, props, graph, effect, reactive, provide, inject, and unprovide in $component callbacks.
  8. Replace $provide, $inject, and $unprovide with Vue provide and inject.
  9. Rename js_data() keys that start with $ or _, and a component class named Mark or registered as mark.
  10. Change CSS-selector render targets to mark:<name> or render:<id>, and move a replaced outermost component's changing part into a child or a <c-mark> region.
  11. Forward request.headers in custom transports.
  12. Replace nested $state writes with whole-field assignments, remove wait: false and unknown call options, and wait for citry:ready before calling Citry.events.send. Move .enter and .escape from a @c-* binding on any other event to a keydown or keyup binding, and give a :c-* binding that uses them .on:keydown in place of .lazy or its other .on: event. Keep one .on: event and one key filter on each binding, and one @c-* binding per event on each element.
  13. Set security_csp and security_javascript on the Citry instance for pages with Events.
  14. Update action lists passed to Citry.events.applyActions, including publicState in any state action you build, $onEvent listeners for page DOM events, citry:events:stale listeners that check for cancelled or timeout, and code that reads data-citry-events script tags.
  15. Check component tags for :x, v-*, and ref attributes the child read as kwargs, and for x-on:, timed @c-* bindings, and @c-poll.
  16. Move c-:attr, c-@event, and Vue keys in c-bind into the template, or spell an Alpine key in its long form (x-on:click). Set an attribute such as title with c-title or :title but not both, and write Vue-bound group content inside the group's tag. Set several attributes with a c-bind mapping, not with quotes inside a Markup value.
  17. Keep #c-ignore only on HTML elements whose contents a library manages, and move it off component tags, table row elements, and <c-element>.
  18. In subclasses that add fields to a parent's Kwargs, Slots, State, TemplateData, JsData, or CssData, name the parent's class as a base, such as class Kwargs(Parent.Kwargs):. On a component whose parents declare one of these classes differently, declare it on the component itself.
  19. Wrap HTML in Markup when on_render() returns or yields it, or when an extension's on_component_rendered() or on_slot_rendered() returns it. Keep a string that contains user input plain, so Citry escapes it.
  20. Close every tag in <c-raw> and Markup inside interactive components, and put an interactive page's content inside one <body>.
  21. Remove uses of Citry.alpine, Citry.manager, Citry.i18n, window.Alpine, and alpine:init.
  22. On a deployment with several workers, configure a shared cache backend, and let a proxy or CDN pass Access-Control-Allow-Origin through for Citry's files.
  23. Rename the Alpine lint settings and diagnostic codes, and fix any output that security_csp="strict" now rejects.
  24. Check that dependency scripts on interactive pages are classic JavaScript, rename ctx.before_manifest to ctx.early_scripts, and build OnSerializeContext and OnDependenciesContext with keyword arguments.
  25. Pass URLRoute(methods=...) as a tuple of uppercase names, read parameters from citry.analysis results starting at index 0, and remove TemplateNode, citry.ownership imports, lint_csp_compatibility, and ownership= arguments.
  26. Remove citry-htmx.js, hx-ext="citry-fragments", and data-cid-* or data-citry-key selectors.
  27. Open each interactive page, reloading pages opened before the upgrade, and check the browser console for [Citry] errors.