Repository navigation
Conversation
An Ajax response could carry components, header contributions and scripts, but no plain data. Client-side code that needed a value from the server had to receive it through an evaluated script, for example by appending JavaScript that stores JSON somewhere for later pickup. That mixes data with code, depends on where the script happens to run, and offers no ordering guarantee relative to the rest of the response. IPartialPageRequestHandler (and with it AjaxRequestTarget and the websocket request handler) now has addData(name, value). Each entry is written as a <data name="..."> element in the <ajax-response>. Both Ajax libraries publish it on the new topic '/ajax/call/data' with the call's attributes, the name and the value, after the components have been replaced and the scripts evaluated, and before the success handlers run. Wicket delivers the value as text and never interprets it. This adds an abstract method to IPartialPageRequestHandler, implemented in AbstractPartialPageRequestHandler, and an abstract writeData to PartialPageUpdate, implemented in XmlPartialPageUpdate. Applications that use these classes see no change. A custom implementation that does not extend those base classes has to implement the new method. The data part is the foundation for embedding JavaScript framework components, which need a result channel for server calls that is separate from scripts. Migration guide: "IPartialPageRequestHandler#addData(String, CharSequence) and PartialPageUpdate#writeData(Response, String, CharSequence) are new abstract methods. Implementations that do not extend AbstractPartialPageRequestHandler or XmlPartialPageUpdate must implement them." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omponents Wicket has no supported way to embed components written for a client-side framework such as React, Vue or Angular. Applications glue them in by hand: state passed through data attributes or inline scripts, server calls through custom behaviors, results returned through evaluated JavaScript. The new experimental module wicket-islands makes such a component the client-side peer of a Wicket component. An Island renders a host element with its type, a key that identifies it across re-renders, and its state: the props from its model as JSON, escaped for the script element they are written into, and the Ajax attributes for calling back. Calls registered with onCall run in the context of the component as Wicket Ajax requests, so a handler can update other components through the request target; the result, or a structured failure (invalid, forbidden, error), is returned as a data entry of the Ajax response. Payloads are deserialized by a pluggable serializer, Jackson by default, which rejects properties the request type does not declare. The client-side modules come from a Vite build: in production from its manifest on the classpath, with the built files served by a resource that serves only files listed in the manifest; in development optionally from a Vite dev server. The module is new and changes no existing API. The client-side runtime, the React adapter and an example follow separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Islands The wicket-islands module rendered island hosts and answered their calls, but nothing in the browser mounted a framework component into a host or made the calls. An npm workspace in wicket-islands/npm now holds three packages. The runtime finds island hosts, mounts the module registered for their type, keeps a mounted island alive when Wicket re-renders it or an ancestor (moving its root element into the new host and passing the new props), unmounts islands whose host is gone, and sends calls as Wicket Ajax requests whose promise settles after the response's other updates have been applied. The React adapter turns a component into an island module that receives the props as React props and keeps its state across updates. The Vite plugin builds one entry per island type, named after it, so the Java side finds it in the manifest, and serves the entries to a Wicket application in development. The packages are type-checked and tested with Vitest under the js-test profile; the runtime tests run against the real wicket-ajax.js. The build of the module is unchanged without the profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h and in dev mode Running Wicket Islands in a real application, the Treasure Hunt example, showed several defects. An island call that could not reach its island was answered the way Wicket answers any listener invocation in that situation: a redirect to an error page or to a re-rendered page. The island's promise never settled. The client now sends the island's key with each call, and the endpoint answers 'expired' when it reaches a different island instance, as on a page recreated after expiry; Island allows its listener to be called after expiry so the check can run. A request cycle listener, installed with the settings, answers island calls that fail earlier: an expired, stale or removed page as 'expired', a call that is not allowed or not authorized as 'forbidden'. The URLs of island modules were rendered relative to the servlet context, which misses the Wicket filter path; they are now generated from the mounted resource reference. In development, the dev-server loader renders Vite's csp-nonce meta tag, so the styles Vite injects pass a nonce-based Content Security Policy. The Vite plugin no longer imports vite at runtime, which failed when the plugin was installed as a link. It gains an adapter option: island modules export a plain component and the generated entry wraps it, which keeps them eligible for React Fast Refresh. wicket-islands-react provides the options as the reactIslands preset in wicket-islands-react/vite (replacing wicket-islands-react/preamble), and takes wicket-islands as a peer dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game board built with React, mounted as a Wicket island in an otherwise plain Wicket page: probing a cell is an island call whose response also updates Wicket labels, "new game" re-renders the panel around the board while the board keeps running, and changing the board size replaces the island. The board's props show the treasures only once they are found or the game is over, as props are written into the page. The frontend lives in src/main/frontend and is built with Vite under the js-test profile into a generated resource directory. Without the profile the example shows that it was built without its JavaScript. In development mode it can load the board from a Vite dev server (-Dwicket.islands.devServer) for hot reload. WellFormedXmlTestCase now skips node and node_modules directories, which the frontend build creates under the examples' source tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Islands Islands could render props and receive page-bound calls, but could not share state with each other, take part in a Wicket form, or fetch data without the page lock. The client runtime keeps named client stores per page, shared by its islands without involving the server. FormIsland is a form component whose client-side peer edits its value: the value travels as JSON in a hidden input, is converted with the island serializer (JSON that does not fit the value type is a conversion error) and passes a sanitize hook, and the peer receives the form state, including the component's feedback messages; after an Ajax validation the island adds itself to the target. Ajax behaviors on a form island serialize the hidden input. Page-less endpoints are registered with the application together with a required authorization, made callable by an island with usePageless, and served outside the page lock to POST requests carrying the islands header from the same origin. The runtime keeps a TanStack Query client per page and applies the query keys the server invalidates, also from classic Ajax requests through IslandTargets; the React adapter adds useStore, useFormValue, useFormState, useIslandQuery and useIslandMutation. Island and FormIsland share their island support; IslandCall#getIsland() is replaced by getComponent(), as a call can come from either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scores The probe history is a second island that shares the highlighted cell with the board through a client store. When a game is over, a plain Wicket form with a form island for the player's name and colour saves the score; the high-score list is an island fetching the scores through a page-less endpoint, refreshed when a score is saved. Under the js-test profile, jetty:run also reads the generated Vite output, so the example works even when target/classes is rebuilt without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Treasure Hunt example was only checked by hand in a browser, so nothing in the build would notice the example breaking while the islands module changes. TreasureHuntSeleniumTest plays the game in a headless Chrome with either Ajax engine: a probe updating the board, the history and the Wicket labels; the hover shared between two islands; a new game keeping the islands mounted and a new board size mounting a new board; a probe from a stale tab answered as expired; and a score validated, saved with markup in the name and listed through the page-less endpoint. Each test also fails on browser errors, CSP violations included, and on an island that outlived its host. Like the other Selenium tests it only runs with -Dwicket.selenium=true, and it is skipped when the examples were built without -Pjs-test. The status line and the probe count get a class to find them by, because markup ids are generated in deployment mode. The page explains which parts are islands and what each demonstrates, and how to work on the React components with hot reload. The source viewer lists the island modules and vite.config.js next to the Java source, so both halves of the example can be read in one place. The Vite build writes THIRD-PARTY-LICENSES.md next to the manifest, listing the npm packages bundled into the war with their licences. It is not served. The release build does not use -Pjs-test, so the released war bundles none of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ules the CSP nonce A page-less endpoint could not know which record the page shows unless the client told it, and anything the client sends can be changed: the members of one board could be asked for another by editing an id. An island now passes a context with usePageless(name, endpointId, model). When the island renders, the context goes into the application's IIslandHandleStore and the island state carries only an opaque handle, which the runtime sends with each call in the X-Wicket-Islands-Handle header. The endpoint reads the context with PagelessCall#getHandle. The default SessionHandleStore keeps up to 1000 handles per session in the session's metadata, each 24 random bytes from the application's secure random supplier and bound to the endpoint it was issued for; issuing one binds a temporary session. A handle that does not resolve (unknown, from another session, for another endpoint, or pushed out by newer ones) is answered as expired before the endpoint runs, and getHandle on a call without a handle rejects it as invalid. An island keeps its handle while the context stays equal. A handle is not an authorization: the endpoint still checks access to the context, as the Javadoc says where the methods are. The "pageless" entries of the island state change from a URL to an object with the URL and an optional handle; the runtime reads the new form. Nothing has been released, so no application is affected. Component libraries that inject style elements, Mantine and TipTap among them, need the page's CSP nonce, as Wicket's strict policy allows inline styles only with it. Every island now renders the csp-nonce meta tag that only the dev-server loader rendered before, and the runtime exports cspNonce() to read it; the React adapter re-exports it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second worked use case: a team board whose state lives in an application-wide repository rather than in the page, with React islands next to a plain Wicket summary, dialog and form. - Cards are dragged between columns (dnd-kit). The board shows a move at once (React's useOptimistic) and calls the server, which checks the column's limit and the board's version. A refused move rolls back; a refusal because the board changed also carries the current board. "Let a teammate move a card" changes the repository behind the page's back to show that. - A double click calls the island, which opens a plain Wicket ModalDialog. Its form mixes classic fields with two form islands, a Mantine rich-text editor and date picker, validated and submitted together. The description's HTML comes from the browser, so it goes through an allowlist sanitizer (OWASP Java HTML Sanitizer, added to the root pom's dependency management) before it reaches the card; a preview renders it unescaped only because of that. - The filter writes a client store that the board reads. The assignee suggestions and the cards-per-week chart come from page-less endpoints that get the board as a handle and check that the session may see it. A move invalidates the chart's data. Every session gets a board of its own, so a public examples site cannot be defaced; two tabs of one browser share it. Mantine is used without its CSS reset, which would restyle the Wicket page around the islands, and gets the page's CSP nonce for the styles it injects. The npm packages are pinned to versions published at least two weeks ago; all that are bundled are MIT, ISC or 0BSD and listed in the Vite build's THIRD-PARTY-LICENSES.md. TaskBoardPageTest covers the calls and the form with WicketTester; TaskBoardSeleniumTest uses the board in Chrome with either Ajax engine. The browser plumbing it shares with TreasureHuntSeleniumTest moved into IslandSeleniumTestCase. The js-test profile no longer configures an extra classpath for jetty:run. MavenWebAppContext has setExtraClasspath(String) and setExtraClasspath(List<Resource>), and the configurator picked either, so jetty:run failed now and then with "Cannot create instance of class Resource". The Vite output reaches target/classes as a resource anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
papegaaij
left a comment
There was a problem hiding this comment.
Review of the branch as of 5be988d: the core <data> part of the Ajax response, the islands module (Java, client runtime, React adapter, Vite plugin) and both examples. Line-level findings are inline; this comment has the overview and what doesn't belong to a single line.
Worth fixing before anyone builds on it, in rough order:
- Islands that use the same query name share one cached result (
react/src/index.tsx): the second island shows the first island's data. canCallListenerAfterExpiry()returnstruefor the whole component, so every Ajax behavior on an island or form island runs on a page Wicket recreated after expiry (Island.java,FormIsland.java).- Any global Ajax call listener makes the island state invalid JSON, and then no island on the page mounts (
IslandSupport.java). - The Vite plugin leaves
baseat/, so imported assets, CSSurl()and the CSS of lazily imported chunks point outside Wicket's mount (vite-plugin/src/index.js). - A
FormIslandre-renders itself after every Ajax validation; with an Ajax behavior on it, keystrokes typed during the round trip are lost and focus drops (FormIsland.java). - One broken island host stops the islands after it from mounting (
runtime.ts). - A result that fails to serialize is answered as
invalidand logged at DEBUG (IslandEndpointBehavior.java,PagelessEndpointResource.java). sanitize()receivesnull, contrary to its Javadoc (FormIsland.java).
Items 1, 2, 4 and 8 were reproduced with throwaway tests, which are not committed. Item 5 follows from the code and was not reproduced in a browser. A JSON null does not get past required: FormComponent#validate re-checks it after conversion.
Security. No exploitable vulnerability found. What holds:
- The props JSON is escaped for its
<script>element, and the runtime never evaluates or inserts what it receives. - Page-less endpoints accept only a POST with the custom header, and refuse a
Sec-Fetch-Siteother thansame-origin. - Every page-less endpoint must declare its authorization.
- Handles are 24 bytes from the secure random supplier, bound to the session and the endpoint.
- Unknown JSON properties are rejected and no type information from the client is honoured.
- Error detail is shown only in development mode.
- Only files listed in the manifest are served.
Two points that don't sit on one line:
- npm becomes part of the trusted base. Island bundles run with the page's privileges: a compromised dependency can invoke any listener on the page as the user and read the handles. The design notes treat island modules as trusted like markup, and that is the right model, but the documentation should spell out that this now includes the application's npm dependency tree. See also the inline comment on
npm ci. - Jackson serializes every getter. Handing an entity to an island as props publishes all its fields and can trigger lazy loading of its relations. The
IslandJavadoc warns about it; a strict default (e.g. records only, or a mapper without auto-detected getters) would make the safe path the easy one.
What else will bite applications using islands, beyond the inline comments:
- Inputs rendered inside an island that sits in a Wicket
<form>take part in it: a named input is submitted with the form and can collide with a Wicket input name, Enter in an island input submits the form, and a<button>without atypeis a submit button. - Client state is not part of page versions, and calls from a back-navigated tab reach that older page version, as for any Ajax behavior.
- Handles live in the session, so clusters need sticky sessions or session replication.
Conventions. The Java follows Wicket's format, public API has Javadoc with @since, and the commit messages follow AGENTS.md. The React code is idiomatic (useSyncExternalStore, useOptimistic, default-exported components so Fast Refresh works). A few nits inline.
🤖 Generated with Claude Code
The review of pull request #1834 found these, now fixed: - Islands using the same query name shared one cache entry, so the second island showed the first island's data and never made its own call. Queries are now keyed [name, scope, payload]: the island for a page-bound call, the handle or the endpoint for a page-less one, so page-less results are still shared where they cannot differ. The server's invalidate(name) still reaches all of them, as a prefix. - canCallListenerAfterExpiry() returned true for the whole island, so every Ajax behavior on an island or form island ran on a page Wicket recreated after expiry. It now returns true only while the island's endpoint behavior is being invoked; the key check still answers those calls as expired. - The island state embedded the behavior's full Ajax attributes. Global call listeners, preconditions and dynamic parameters - the usual way to add a busy indicator - are rendered as functions, which made the state invalid JSON and stopped every island on the page from mounting. The state now carries the URL, method and channel only. Listeners added to every behavior therefore no longer apply to island calls; listeners subscribed through Wicket.Event still fire. An island without page-bound calls renders no Ajax attributes, and its endpoint behavior no longer makes the page stateful. - The Vite plugin left base at "/", so imported assets, CSS url() and the CSS of lazily imported chunks pointed outside Wicket's mount. It now sets base "./" for builds unless the application sets one. - A form island re-rendered itself after every Ajax validation. With an Ajax behavior on it, the re-render replaced the value with the request's, dropped the focus and lost what was typed during the round trip. It now sends its form state as a data entry, which the runtime applies without touching the value; a re-render the application asks for still replaces both. - One broken island host stopped the hosts after it from mounting, and a module that failed to mount stayed registered. Each host is now mounted on its own, and a failure is logged with the host's id. - A result that could not be serialized - a server bug - was answered as invalid and logged at DEBUG. It is now answered as error and logged at ERROR, for page-bound and page-less calls. - sanitize() received null for the JSON text null, contrary to its Javadoc. It is skipped for null; required still applies. And the smaller points: - The island key now comes from the application's secure random supplier instead of ThreadLocalRandom: where no resource isolation listener is installed, it is what stops a cross-site POST from reaching a handler. - A page-less call is aborted when its island unmounts, as page-bound calls are. - An HTTP error from Wicket Ajax is reported as error (or forbidden for 403), not as a network failure, and an answer that cannot be parsed as error. - The page's QueryClient no longer refetches page-bound queries when the window gets the focus, and retries only network failures. - IslandSettings.install() returns the installed settings when called again, instead of mounting and registering everything twice. - IslandCallRejectedException drops null messages, so rejecting with the message of an exception that has none no longer throws. - usePageless() fails at once for an endpoint id that is not registered. - In development mode the Vite manifest is read again when it changes. - The default JacksonIslandSerializer writes only records, collections, maps, arrays, enums and single values. It used to write every getter, so an entity handed to an island published all its fields and could load its relations; that now fails at render time. defaultMapperBuilder() keeps the rule for applications adding modules, and a mapper of their own opts out. Props that were plain classes need to become records. - The Javadoc of Island now says that island modules, and with them the application's npm dependencies, are trusted like markup; what moving an island into a re-rendered host costs; that islands in repeaters recreating their items are remounted; how islands relate to page versions and to an enclosing form. SessionHandleStore says that handles need sticky sessions or session replication in a cluster. - The conversion error of FormIsland has a default bundle entry, "The value of '${label}' is not valid." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ripts The frontend-maven-plugin version and the Node and npm versions it installs were repeated in the poms of wicket-js-tests, wicket-islands and wicket-examples, where they would drift apart. The root pom now manages the plugin, with the node.version and npm.version properties, and the modules only say what to run. The islands module and the examples ran npm ci with the install scripts of every dependency, which on a CI or release machine is a supply-chain exposure for little gain. Both now run npm ci --ignore-scripts; the builds and tests need none of those scripts (esbuild and rollup ship their binaries as optional dependencies). wicket-js-tests is unchanged. Raised in the review of pull request #1834. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of pull request #1834 noted that examples get copied, and found these in the task board: - EditCardPanel captured a Board snapshot in its choice renderer and submit handler, so it was serialized with the page and its member list stayed as it was when the dialog opened. It now reads the board through the page's model and keeps only the card's id. - The rich-text editor offered more than the sanitizer keeps (any heading level, horizontal rules, links with a target), which was dropped silently on save, and never showed a value the server changed. StarterKit now matches the allowlist, and the editor follows the value the server sends back. - Opening a card took a double click; dnd-kit takes Enter and Space to pick a card up, so there was no keyboard path. Each card has an Edit button, whose keys do not reach the card's keyboard sensor. - The assignee search sent a request per keystroke; it is debounced. The description is also validated while the user types (at most 1000 characters of HTML), through an Ajax behavior on the form island. That shows the form state reaching the editor without a re-render, and a new Selenium test types during the round trips and checks that nothing is lost and the editor keeps the focus. The taller cards put the bottom of a column below the fold, where the Selenium drags dropped, and dnd-kit's default collision detection picked the target with the largest overlap rather than the one under the pointer. The board now drops under the pointer, falling back to the overlap for the keyboard, and the tests drop on the column title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # pom.xml
# Conflicts: # pom.xml
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1834 +/- ##
=========================================
Coverage 62.71% 62.72%
- Complexity 11512 11520 +8
=========================================
Files 1254 1254
Lines 48665 48690 +25
Branches 6845 6851 +6
=========================================
+ Hits 30521 30541 +20
- Misses 15468 15469 +1
- Partials 2676 2680 +4 🚀 New features to boost your workflow:
|
Draft for review: Wicket Islands, an experimental module (
wicket-experimental/wicket-islands) for embedding JavaScript framework components in Wicket pages, React first. Wicket keeps owning pages, navigation and layout; an island is a component with a client-side peer.What is in it
<ajax-response>handled by both Ajax engines.Island<P>(props rendered as escaped JSON, mounted by the runtime), page-bound endpoints whose calls answer with the result plus Wicket's partial updates, failed calls answered as invalid / expired / forbidden, islands preserved across an ancestor's re-render,FormIsland<T,P>as a realFormComponent(conversion, validation, feedback).-Pjs-test:/treasurehunt): page-bound calls, preserve vs. remount, a form island, page-less high scores./taskboard): optimistic drag-and-drop that rolls back on a stale board or a WIP limit, a WicketModalDialogwith rich-text and date-picker form islands, sanitized HTML (OWASP Java HTML Sanitizer), a client-only filter, a chart refreshed after a move.Running the examples
mvn install -DskipTests -Pfast cd wicket-examples mvn -Pjs-test jetty:runThen http://localhost:8080/treasurehunt and http://localhost:8080/taskboard.
Tests
-Pjs-test -Dwicket.selenium=true. LikeVeilPageSeleniumTestthey do not run in CI.mvn clean verify -Pjs-testis green.Open questions for the committers
@IslandCallableannotations as well; this shapes the planned TypeScript contract generation.-Pjs-test, so the war carries no JavaScript. Bundling it would need npm registry access in the release build and the third-party licence list in the war'sLICENSE.Not built yet
Typed contracts (TypeScript generated from the Java declarations), an
IslandTester, store seeding, the JAX-RS integration, Vue and Angular adapters, websocket push.🤖 Generated with Claude Code