Unreleased
Citry 0.6.0 replaces Alpine with Vue. Most Python components, templates, c-* attributes, slots, and Events handlers keep working, but browser code moves to Vue and some Python code changes too, such as lint settings, js_data() key names, and extension hooks. Upgrade to Citry 0.6.0 walks through every step.
Highlights
- Write Vue directives, bindings, props, events, and slots in component templates, and Vue Options or Vue's Composition API (
Citry.vue) in component JavaScript. - Citry compiles each component to Vue on the server and sends one pinned Vue runtime only to the pages that need it, so you need no Node.js or frontend build step.
- Interactive pages send their content in the served HTML, so search engines and readers without JavaScript see it, and Vue takes over that HTML instead of building the page again.
- An Events handler can render one region of the page again: wrap it in
<c-mark name="cart-badge">and returnactions.Render(CartBadge(count=n), target="mark:cart-badge"), or target one rendered component withrender:<id>. citry check --typestype-checks component JavaScript and Vue expressions with TypeScript, Python template expressions with ty, and eachc-*input against the child'sKwargs, reporting findings as the editor does; it needscitry-lsp, Node.js, and TypeScript'stsc.simple = "vue"gives a component its own Vue state and assets without a Python component instance.
Upgrading from 0.5.x
- Upgrade the packages together: install
citry-ui0.3.0 andcitry-lsp0.2.0 with Citry 0.6.0, because older versions fail to start or lose their browser behavior (guide). - Alpine syntax becomes Vue: rewrite
x-*attributes, Alpine-only modifiers,$dispatch, and$c-propswith their Vue forms;citry checklists thex-*attributes you missed (guide). - Component tag attributes become Vue bindings:
:name,v-*, andrefon a component tag no longer reach the child as kwargs,#c-ignoreworks only on plain HTML elements, not on component tags or<c-element>, and Vue slot syntax such as<template #title>inside a component tag fails when the template loads; fill the slot with<c-fill name="title">(guide). - Vue names from Python data stop the render: a
c-bindkey, ac-*attribute such asc-@click, or anon_attrs_resolvedhook that sets an attribute name starting withv-,:,.,^,@, or#now raises the newcitry.VueCodeError, aTypeError, when the template renders, on every page. If a script you load yourself, such as Alpine, read these names on an HTML element, spread the long form, such asx-on:click(guide). - Component JavaScript moves to Vue Options: the
$componentcallback gets a smaller context, andjs_data()keys that start with$or_must be renamed (guide). - Events code: render targets are
mark:<name>orrender:<id>instead of CSS selectors, custom transports must forwardrequest.headers, and astateaction you build, forCitry.events.applyActionsor anon_event_resulthook, needs apublicStateobject..enterand.escapeon a@c-*binding work only onkeydown,keyup, orkeypress, and on a:c-*binding only with.on:keydown,.on:keyup, or.on:keypress. A:c-*binding that names two.on:events, a binding with two key filters, and a second@c-*binding for the same event on one element now fail (guide). - Handlers that return a different component: a handler can no longer replace the outermost component of a page or HTML fragment with a different component; move the part that changes into a child component or a
<c-mark>region. Replacing any other calling component still works, and the old component's browser state is still lost (docs). - Page scripts and selectors:
Citry.alpine,Citry.manager, andCitry.i18nare removed, and Vue-rendered elements carry nodata-cid-*ordata-citry-keyattributes (globals, selectors). - Deployment and security: several worker processes need a shared cache backend, and a proxy or CDN may need to pass Citry's CORS header through (cache, proxy).
- Lint settings and diagnostic codes: the Alpine names become Vue names, such as
rule_unknown_vue_variableandcitry.vue.unknown-variable, andcitry.csp.incompatible-browser-codeis removed: Vue expressions compile on the server and never needunsafe-eval(guide). - Extensions and Python tools: dependency scripts on interactive pages must be classic JavaScript,
OnSerializeContextandOnDependenciesContextgainselected_render,ctx.before_manifestbecomesctx.early_scripts,URLRoutechecksmethods,citry.analysisresults leaveselfout ofparameters,citry.analysis.lint_csp_compatibilityandCspCompatibilityFindingare removed, and theTemplateNodeclass is removed (guide). - Subclasses name the parent class to add inputs: a nested
Kwargs,Slots,State,TemplateData,JsData, orCssDataon a subclass now replaces the parent's class, like any nested Python class, so writeclass Kwargs(Parent.Kwargs):to keep the parent's fields. Citry warns withNestedSchemaReplacedWarningwhen a subclass leaves out a parent's fields. A replacingStatealso drops the parent's_public,_model, and_max_age, and Citry warns. A component whose parents declare differentKwargs, or that replaces aStatewith_storage = "server"without setting_storage, raisesValueErrorwhen defined.Events,Dependencies, and other settings classes still add to the parent's (guide). - HTML returned from
on_rendershows as text:on_render()now escapes a plainstrit returns or yields, as{{ }}does, so user input in it cannot add a script. Wrap HTML inMarkupor return a component. The extension hookson_component_rendered()andon_slot_rendered()follow the same rule; wrap their HTML inMarkup(guide). Markupin an attribute is escaped: aMarkupvalue in an HTML attribute is now escaped like any other value, so a"in it can no longer end the attribute and add another one. An entity such as&still reads as its character (guide).
Other additions
- A component tag accepts
v-if,v-else-if,v-else,v-model,v-show, and custom directives. onServerRender({ component, revision, onEvent })runs after mount and after each server render of the component, and it may beasync.Citry.vue.use(plugin)installs a Vue plugin on every Vue app Citry creates.- A text input,
<textarea>, or<select>keeps what the user typed when its component renders again, until the server sends a different value. - Interactive pages served through Citry's routes link their stylesheets and preload their scripts in
<head>, so they are styled from the first paint, and browser assets are minified. - Interactive fragments load their own assets, so HTMX pages no longer need the
citry-htmx.jshelper orhx-ext="citry-fragments". citry checkand the editor report misspelled component members, Vue bindings that read Python loop variables, undeclared emitted events, leftoverx-*attributes, invalid HTML attribute values (rule_invalid_attribute_value), andjs_data()values Citry cannot prove it can send as JSON.- A translation catalog may leave messages out:
citry checknow warns, instead of failing, when atr()call or a message inI18n.client_messageswould show text from a fallback language. SetLintSettings(rule_i18n_cross_language_fallback="error")to require complete translations; a<c-trans>message still needs every translation. citry check --typeschecksc-class,c-style, and bound attributes such as:styleagainst their expected types, which can report existing bindings such as:aria-expanded="String(open)".- Interactive pages start inside an iframe sandboxed without
allow-same-origin, because Citry's asset routes sendAccess-Control-Allow-Origin: *. - A Content Security Policy needs no nonce or hash for a page's app data, which the browser reads as JSON without running it, and
csp_script_hasheslists only the scripts the browser runs. Citry(vue_asset_max_bytes=...)caps the memory a process without a configured cache spends on compiled component code (default 64 MiB).- Render-cache entries saved by 0.5.x count as misses and are saved again, so you do not need to clear the cache.
Fixes
class Kwargs(Parent.Kwargs):on a subclass now keeps the parent's field defaults, and a nested data class such asKwargscan list a required field after one with a default.$stateand State bindings now show the State an Events handler changed when the handler returns nothing, returns data, or renders only a<c-mark>region, instead of keeping the old values until the component renders again.- On Python 3.10, an i18n catalog package without
__init__.pynow loads instead of failing with aTypeError. - Events handlers declared with
PUT,DELETE, or another non-POST method now work, and each still rejects other methods and checks CSRF. GETEvents calls reject arguments with unpaired surrogates instead of silently replacing characters.citry checkno longer reports every template variable as undeclared whentemplate_data(),js_data(), orcss_data()runs severalifchecks before it returns.citry checkno longer reports the keys of atemplate_data()written as a@staticmethodor@classmethodas undeclared, and checks the value types of ajs_data()written that way.citry checkno longer reports browser names such asDocumentorbtoaas unknown in component JavaScript.citry checkno longer reportscitry.js-data.unsupported-typefor ajs_data()value that reads aKwargsfield annotated with a type alias of aLiteral.- In an app without i18n settings,
citry checkno longer reports "Unknown i18n format profile" for a formatter call in component JavaScript or one guarded byself.i18n.configured, matching the editor; an unguarded Python call is still reported. citry checkno longer crashes with a traceback when a component'smessagesfail to compile, for example a selector variable that is not declared as a number with@param. It reportscitry.i18n.catalog-invalidinstead, and the JSON output gives the line and column inside themessagesblock.Fallback text that runs in the other direction, such as English on an Arabic page, no longer reorders the surrounding sentence when the browser translates it with
$i18n.tr(),$c-tr,i18n.bind(), orresolve(). The browser now adds the same direction marks the server adds, andresolve().directionmatches the server.citry createnow rejects a name whose file would be a Python keyword, such ascitry create class, instead of writing a module nobody can import.- A Flask mount prefix written with a trailing slash, such as
prefix="/citry/", now serves Citry's routes instead of sending every Citry request to your app. A FastAPI or Flask prefix without a leading/raises Citry'sValueErrorwithout changing the app. - Server-side State is now stored under
citry:state:cache keys, so a State token can only refer to State that Citry stored. After the upgrade, events from a page opened earlier fail with astale_stateerror until the page is reloaded. - A component that declares
Dependencieswith an emptycsslist no longer makes a fragment raiseRuntimeErrorwhen no web integration is mounted. - An
on_renderoron_component_renderedhook that returnsstr(result)with extra HTML no longer writes the component'sdata-cid-*attribute twice on one tag. - An extension hook that returns a value of the wrong type, such as an
on_component_rendered()that returns a number, anon_serialize()that returns something other than astr, or anon_event_result()that returns something other than a list of actions, now raises aTypeErrorthat names the hook and the extension, before the next extension runs. Before, the error blamed the next extension or appeared later without naming the extension, andserialize()returned whateveron_serialize()gave. - An attribute that an
on_dependencies()hook adds to a script or stylesheet in one render no longer appears in later renders of the same component. - mypy and pyright now accept
self.events.url(), and it is listed in the API reference. enable_hot_reload(engine, mode="restart")fromcitry.contrib.djangonow restarts the Django dev server when a component file changes.enable_hot_reloadnow notices component files underCitry(dirs=...)outside Django's template folders. Pointdirsat your component folders, since the reloader checks every file under them.- A component that keeps rendering itself, such as a tree node whose data lists the node among its own children, now fails quickly with a
RecursionErrornaming the component instead of running until the process runs out of memory. PassCitry(max_component_depth=...)(default 2000) if a page really nests deeper. - An element with an empty
classorstyle, such as<p class c-class="name">or<p style="" c-style="styles">, no longer raisesTypeError: Invalid class value: True(orInvalid style value: True) when it also has ac-class,c-style,c-bind, or otherc-*attribute. The empty attribute adds nothing to the value from Python. With no Python value, the element keeps the empty attribute.