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-lsp0.1.7 fails to start withImportError: cannot import name 'BROWSER_UNKNOWN_COMPONENT_PROP', and the editor shows no Citry diagnostics.citry-lsp0.2.0 is the release for Citry 0.6.0.citry-ui0.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-show | v-text, v-html, v-show |
x-model | v-model |
x-for | v-for with a :key |
x-if | v-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 $refs | ref and this.$refs |
x-init | mounted() or onServerRender |
x-effect | watch, or Citry.vue.watchEffect in setup() |
x-cloak | Delete 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 value | 0.6.0 replacement |
|---|---|
data, scope, props | The Vue component: component.name or this.name |
effect(fn) | Citry.vue.watchEffect(fn) inside the callback |
reactive(value) | Citry.vue.reactive(value) |
provide, inject | The Vue provide and inject options |
unprovide, graph | No 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 keycitryId, stops the render with aValueErrorthat names the component and the key. Rename it, for example_counttocount. - A component class named
Mark, or one registered asmark, fails when the class is defined, with anAlreadyRegisterederror that says the built-in<c-mark>reserves the name. Rename the class or give it anothername. 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, customv-*directives, andrefare Vue bindings. 0.5.1 passed them to the child as Python kwargs. A child that readkwargs[":class"]or arefkwarg no longer receives it. Pass a plain kwarg or ac-attrsmapping instead.@eventlistens 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 inemitsand calls$emit. A native event such as@clickthat the child does not declare goes to the child's single root element; a child with several root elements must place it withv-bind="$attrs". See Listen to child events.x-on:eventstops the render withAlpine binding 'x-on:click' was removed; use native Vue v-on or @event syntax.Write@clickinstead.- A
.debounceor.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 & 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 theisandc-isforms. 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.
| Removed | What 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.provider | component.$i18n or this.$i18n; see Browser i18n. |
window.Alpine, alpine:init | The citry:ready event on document. |
/ext/events/runtime-csp.js route | Nothing. 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 code | 0.6.0 code |
|---|---|
citry.alpine.unknown-variable | citry.vue.unknown-variable |
citry.component-js.unknown-data-member | citry.component-js.unknown-member |
citry.browser.unknown-component-prop | Removed |
citry.csp.incompatible-browser-code | Removed; Vue expressions never need unsafe-eval |
citry.i18n.cross-language-fallback on a <c-trans> tag | citry.i18n.rich-message-fallback |
citry.i18n.client-message-invalid for a missing translation | citry.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=...)andEvents._methodsaccept only GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS. Any other method raisesValueErrorwhen 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 raisesTypeError. An empty tuple raisesValueError, 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.
TemplateNodeis removed. Compiled templates never contained it, so delete any import of it orisinstancecheck against it. An extension that built one inon_template_compiledshould instead add the nested template's markup to the template source inon_template_loaded, so it compiles with the rest of the template.- The ownership APIs are removed: the
citry.ownershipandcitry.ownership_manifestmodules, and theownershipparameters ofCitryContextandCitryElement. Delete the imports and those arguments. lint_csp_compatibilityandCspCompatibilityFindingare removed fromcitry.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.jsandhx-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 Vuerefin component JavaScript.data-citry-keyselectors:#c-keyon an element no longer writes adata-citry-keyattribute, so selectors for it stop matching.#c-keystill 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
ssrandssr_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 checkand the editor report a misspelledjs_data()member and a Vue binding that reads a Python loop variable; see Template linting.
Upgrade checklist
- Install
citry0.6.0,citry-ui0.3.0, andcitry-lsp0.2.0 in the same environment. - Run
citry checkto find leftoverx-*attributes, and replace each with its Vue form. - Move
x-datastate intodata()orjs_data(). - Rewrite
.outside,.window,.document,.debounce, and.throttlemodifiers on plain@eventlisteners. End a statement listener such asif (ok) save()with;. - Replace
$dispatchwith$emit, and declareemitsin the child. - Replace
$c-propswith:nameprops, declared in the child'sprops. - Replace
data,scope,props,graph,effect,reactive,provide,inject, andunprovidein$componentcallbacks. - Replace
$provide,$inject, and$unprovidewith Vueprovideandinject. - Rename
js_data()keys that start with$or_, and a component class namedMarkor registered asmark. - Change CSS-selector render targets to
mark:<name>orrender:<id>, and move a replaced outermost component's changing part into a child or a<c-mark>region. - Forward
request.headersin custom transports. - Replace nested
$statewrites with whole-field assignments, removewait: falseand unknown call options, and wait forcitry:readybefore callingCitry.events.send. Move.enterand.escapefrom a@c-*binding on any other event to akeydownorkeyupbinding, and give a:c-*binding that uses them.on:keydownin place of.lazyor its other.on:event. Keep one.on:event and one key filter on each binding, and one@c-*binding per event on each element. - Set
security_cspandsecurity_javascripton theCitryinstance for pages with Events. - Update action lists passed to
Citry.events.applyActions, includingpublicStatein anystateaction you build,$onEventlisteners for page DOM events,citry:events:stalelisteners that check forcancelledortimeout, and code that readsdata-citry-eventsscript tags. - Check component tags for
:x,v-*, andrefattributes the child read as kwargs, and forx-on:, timed@c-*bindings, and@c-poll. - Move
c-:attr,c-@event, and Vue keys inc-bindinto the template, or spell an Alpine key in its long form (x-on:click). Set an attribute such astitlewithc-titleor:titlebut not both, and write Vue-bound group content inside the group's tag. Set several attributes with ac-bindmapping, not with quotes inside aMarkupvalue. - Keep
#c-ignoreonly on HTML elements whose contents a library manages, and move it off component tags, table row elements, and<c-element>. - In subclasses that add fields to a parent's
Kwargs,Slots,State,TemplateData,JsData, orCssData, name the parent's class as a base, such asclass Kwargs(Parent.Kwargs):. On a component whose parents declare one of these classes differently, declare it on the component itself. - Wrap HTML in
Markupwhenon_render()returns or yields it, or when an extension'son_component_rendered()oron_slot_rendered()returns it. Keep a string that contains user input plain, so Citry escapes it. - Close every tag in
<c-raw>andMarkupinside interactive components, and put an interactive page's content inside one<body>. - Remove uses of
Citry.alpine,Citry.manager,Citry.i18n,window.Alpine, andalpine:init. - On a deployment with several workers, configure a shared cache backend, and let a proxy or CDN pass
Access-Control-Allow-Originthrough for Citry's files. - Rename the Alpine lint settings and diagnostic codes, and fix any output that
security_csp="strict"now rejects. - Check that dependency scripts on interactive pages are classic JavaScript, rename
ctx.before_manifesttoctx.early_scripts, and buildOnSerializeContextandOnDependenciesContextwith keyword arguments. - Pass
URLRoute(methods=...)as a tuple of uppercase names, readparametersfromcitry.analysisresults starting at index 0, and removeTemplateNode,citry.ownershipimports,lint_csp_compatibility, andownership=arguments. - Remove
citry-htmx.js,hx-ext="citry-fragments", anddata-cid-*ordata-citry-keyselectors. - Open each interactive page, reloading pages opened before the upgrade, and check the browser console for
[Citry]errors.