From 3f7b68cbed2ad90f30913021a56961c291aff998 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 26 Jan 2026 08:30:34 +0000 Subject: [PATCH 1/9] docs: improve JavaScript interoperability documentation Add TypeScript coverage, npm package wrapping guidance, and advanced Lit integration patterns to address gaps identified in forum feedback. --- .../web-components/advanced-integration.adoc | 434 ++++++++++++++++++ .../an-in-project-web-component.adoc | 87 +++- .../web-components/index.adoc | 58 ++- .../java-api-for-a-web-component.adoc | 6 + .../acme-widget-wrapper.ts | 160 +++++++ .../demo/component/internals/AcmeWidget.java | 72 +++ 6 files changed, 805 insertions(+), 12 deletions(-) create mode 100644 articles/flow/component-internals/web-components/advanced-integration.adoc create mode 100644 frontend/demo/component-internals/acme-widget-wrapper.ts create mode 100644 src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc new file mode 100644 index 0000000000..35b0573564 --- /dev/null +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -0,0 +1,434 @@ +--- +title: Advanced Integration +page-title: Advanced web component integration with TypeScript and Lit +description: How to wrap npm packages, use advanced Lit features, and handle complex state synchronization. +meta-description: Learn advanced patterns for integrating web components in Vaadin Flow using TypeScript, Lit lifecycle, and npm package wrapping. +order: 25 +--- + + += Advanced Web Component Integration + +This guide covers advanced patterns for creating Web Component wrappers in Vaadin, including wrapping npm packages with TypeScript, using Lit lifecycle hooks and decorators, and managing complex state between the client and server. + +For basic integration, see <> and <>. +For integrating React components, see <<{articles}/flow/integrations/react#,Using React Components in Flow>>. + + +[[typescript-wrappers]] +== TypeScript for Web Component Wrappers + +TypeScript is recommended for non-trivial Web Component wrappers. It provides: + +- Type-safe property definitions that catch mismatches at build time +- Interfaces for configuration objects passed between Java and the client +- Better IDE support with auto-completion when using third-party libraries + +A typical TypeScript wrapper defines interfaces for any complex data structures: + +[source,typescript] +---- +interface ChartConfig { + type: 'bar' | 'line' | 'pie'; + animate: boolean; + colors?: string[]; +} + +interface DataPoint { + label: string; + value: number; +} +---- + +These types ensure that the JSON objects passed from Java via `getElement().setProperty()` conform to the expected shape, and that event data dispatched back to Java is consistent. + + +[[wrapping-npm-packages]] +== Wrapping npm Packages with TypeScript + +Wrapping a third-party npm library involves three parts working together: the `@NpmPackage` annotation declares the dependency, a TypeScript file imports and wraps the library as a Web Component, and a Java class exposes the API to server-side code. + +This section demonstrates the pattern using a fictional `@acme/widget` package. The same approach applies to any npm library. + + +=== Step 1: The TypeScript Wrapper + +The TypeScript file imports the npm package and wraps it in a LitElement-based Web Component: + +[source,typescript] +---- +include::{root}/frontend/demo/component-internals/acme-widget-wrapper.ts[tags=class,indent=0] +---- + +Key points: + +- The `@property` decorator exposes reactive properties that Java can set via `getElement().setProperty()`. When a property changes, Lit automatically re-renders. +- The `@state` decorator marks internal state that triggers re-renders but is not exposed as HTML attributes. +- The `configJson` property uses the `attribute` option to map the HTML attribute `config-json` to the camelCase JavaScript property, since HTML attributes are case-insensitive. +- The `firstUpdated` lifecycle callback initializes the third-party widget after the component's DOM is ready. +- The `disconnectedCallback` lifecycle callback cleans up the widget instance to prevent memory leaks when the component is removed from the DOM. +- A `CustomEvent` is dispatched to communicate changes back to the Java side. + + +=== Step 2: The Java Component Class + +The Java class declares the npm dependency and provides a typed API: + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java[tags=class,indent=0] +---- + +Key points: + +- `@NpmPackage` declares the npm dependency. Vaadin installs it automatically during the build. +- `@JsModule` points to the `.ts` file. Vaadin compiles TypeScript as part of the frontend build. +- Complex configuration is passed as a JSON string. For simple properties, use `setProperty` directly. +- `@DomEvent` and `@EventData` map the client-side `CustomEvent` to a typed Java event class. + + +=== Step 3: Using the Component + +[source,java] +---- +AcmeWidget widget = new AcmeWidget(); +widget.setTitle("Dashboard Widget"); +widget.setConfig("{\"type\": \"bar\", \"animate\": true}"); +widget.addWidgetChangeListener(event -> { + Notification.show(event.getLabel() + ": " + event.getValue()); +}); +add(widget); +---- + + +[[advanced-lit-features]] +== Advanced Lit Features + +When creating Web Component wrappers, Lit provides features beyond basic property binding and rendering. + + +=== Reactive Properties vs. Internal State + +Use `@property` for values that should be settable from Java (via element properties or attributes). Use `@state` for internal values that affect rendering but shouldn't be part of the public API: + +[source,typescript] +---- +import { LitElement, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +@customElement('my-counter') +class MyCounter extends LitElement { + // Public: settable from Java via getElement().setProperty("max", 100) + @property({ type: Number }) + accessor max: number = 10; + + // Internal: only used within this component + @state() + private accessor _count: number = 0; + + render() { + return html` + ${this._count} / ${this.max} + + `; + } + + private _increment() { + this._count++; + this.dispatchEvent( + new CustomEvent('count-changed', { + detail: { count: this._count }, + bubbles: true, + }) + ); + } +} +---- + +Both `@property` and `@state` trigger re-renders when they change. The difference is that `@property` values can be set via HTML attributes and are part of the component's public API. + + +=== Lifecycle Callbacks + +Lit provides several lifecycle callbacks beyond the standard Web Component ones: + +[cols="1,2"] +|=== +|Callback |When to Use + +|`connectedCallback()` +|The element is added to the DOM. Set up event listeners or start periodic tasks. Always call `super.connectedCallback()`. + +|`disconnectedCallback()` +|The element is removed from the DOM. Clean up event listeners, timers, or third-party library instances to prevent memory leaks. Always call `super.disconnectedCallback()`. + +|`firstUpdated(changedProperties)` +|Called once after the component's first render. Use this to initialize third-party libraries that need a DOM element to attach to. + +|`updated(changedProperties)` +|Called after every render. Use this to react to property changes, such as reconfiguring a wrapped library. The `changedProperties` map contains the previous values. + +|`willUpdate(changedProperties)` +|Called before rendering. Use this to compute derived values from properties before they're used in the template. +|=== + +Example using `firstUpdated` to initialize a library and `disconnectedCallback` to clean it up: + +[source,typescript] +---- +@customElement('chart-wrapper') +class ChartWrapper extends LitElement { + private _chart: Chart | null = null; + + @property({ type: String }) + accessor type: string = 'bar'; + + override firstUpdated() { + const canvas = this.renderRoot.querySelector('canvas'); + this._chart = new Chart(canvas, { type: this.type }); + } + + override updated(changed: PropertyValues) { + if (changed.has('type') && this._chart) { + this._chart.config.type = this.type; + this._chart.update(); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this._chart?.destroy(); + this._chart = null; + } + + override render() { + return html``; + } +} +---- + + +=== Shadow DOM vs. Light DOM + +By default, Lit renders into Shadow DOM, which encapsulates styles. Most wrapper components should use Shadow DOM. However, if you need the wrapped content to inherit page styles or participate in form submission, you can render into Light DOM: + +[source,typescript] +---- +import { LitElement, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +@customElement('light-dom-wrapper') +class LightDomWrapper extends LitElement { + override createRenderRoot() { + // Render into Light DOM instead of Shadow DOM + return this; + } + + override render() { + return html`
Content here
`; + } +} +---- + +.When to avoid Light DOM +[NOTE] +Light DOM components don't have style encapsulation. Their styles can leak out and page styles can leak in, which makes them harder to maintain. Prefer Shadow DOM unless you have a specific reason to use Light DOM. + + +[[state-synchronization]] +== State Synchronization Patterns + +Communication between the Java server and the client-side Web Component happens through element properties and events. + + +=== Simple Properties + +For primitive values, set them directly from Java: + +[source,java] +---- +getElement().setProperty("label", "Hello"); +getElement().setProperty("count", 42); +getElement().setProperty("visible", true); +---- + +These are accessible in the TypeScript component as reactive properties. + + +=== Complex Objects via JSON + +For objects and arrays, serialize to JSON on the Java side and parse on the client: + +*Java side:* +[source,java] +---- +// Using a JSON library (e.g., Jackson) +String json = objectMapper.writeValueAsString(config); +getElement().setProperty("configJson", json); +---- + +*TypeScript side:* +[source,typescript] +---- +@property({ type: String, attribute: 'config-json' }) +accessor configJson: string = '{}'; + +private get _config(): MyConfig { + return JSON.parse(this.configJson) as MyConfig; +} +---- + +Alternatively, Vaadin's `Element` API supports `JsonValue` for setting structured data without manual serialization: + +[source,java] +---- +JsonObject config = Json.createObject(); +config.put("type", "bar"); +config.put("animate", true); +getElement().setPropertyJson("config", config); +---- + +On the client, the property receives a JavaScript object directly: + +[source,typescript] +---- +@property({ type: Object }) +accessor config: MyConfig = { type: 'bar', animate: false }; +---- + + +=== Two-Way Binding with Custom Events + +To send state changes from the client back to the server, dispatch a `CustomEvent` and listen for it in Java: + +*TypeScript side:* +[source,typescript] +---- +this.dispatchEvent(new CustomEvent('selection-changed', { + detail: { selectedIds: [1, 2, 3] }, + bubbles: true, + composed: true, +})); +---- + +*Java side using `@DomEvent`:* +[source,java] +---- +@DomEvent("selection-changed") +public static class SelectionChangedEvent + extends ComponentEvent { + + private final JsonArray selectedIds; + + public SelectionChangedEvent(MyComponent source, boolean fromClient, + @EventData("event.detail.selectedIds") JsonArray selectedIds) { + super(source, fromClient); + this.selectedIds = selectedIds; + } + + public JsonArray getSelectedIds() { + return selectedIds; + } +} +---- + +*Java side using `addEventListener`:* +[source,java] +---- +getElement().addEventListener("selection-changed", event -> { + JsonArray ids = event.getEventData().getArray("event.detail.selectedIds"); + // process selection +}).addEventData("event.detail.selectedIds"); +---- + +Set `composed: true` on events that need to cross Shadow DOM boundaries. + + +[[practical-patterns]] +== Practical Patterns + + +=== Loading States and Async Initialization + +When wrapping libraries that require async initialization (e.g., loading data from a remote source), use internal state to track loading: + +[source,typescript] +---- +@state() +private accessor _loading: boolean = true; + +@state() +private accessor _error: string | null = null; + +override async firstUpdated() { + try { + await this._initializeLibrary(); + this._loading = false; + } catch (e) { + this._error = e instanceof Error ? e.message : 'Initialization failed'; + this._loading = false; + } +} + +override render() { + if (this._error) { + return html`
${this._error}
`; + } + if (this._loading) { + return html`
Loading...
`; + } + return html`
`; +} +---- + + +=== Cleanup and Memory Management + +Always clean up when the component is disconnected. This is critical for third-party libraries that create DOM elements, register global event listeners, or start timers: + +[source,typescript] +---- +private _resizeObserver: ResizeObserver | null = null; +private _refreshInterval: number | null = null; + +override connectedCallback() { + super.connectedCallback(); + this._resizeObserver = new ResizeObserver(() => this._handleResize()); + this._resizeObserver.observe(this); + this._refreshInterval = window.setInterval(() => this._refresh(), 30000); +} + +override disconnectedCallback() { + super.disconnectedCallback(); + this._resizeObserver?.disconnect(); + this._resizeObserver = null; + + if (this._refreshInterval !== null) { + clearInterval(this._refreshInterval); + this._refreshInterval = null; + } +} +---- + + +=== Error Handling in Event Dispatch + +When dispatching events with data that might fail to serialize, validate before dispatching: + +[source,typescript] +---- +private _notifyChange(data: unknown): void { + // Only dispatch if there's meaningful data + if (data == null) return; + + this.dispatchEvent(new CustomEvent('data-changed', { + detail: data, + bubbles: true, + composed: true, + })); +} +---- + + +[discussion-id]`4FA5E312-9C1B-4A3E-B7D2-6A8C3F2E1D09` diff --git a/articles/flow/component-internals/web-components/an-in-project-web-component.adoc b/articles/flow/component-internals/web-components/an-in-project-web-component.adoc index 8137bd8d7c..c726bdcaf1 100644 --- a/articles/flow/component-internals/web-components/an-in-project-web-component.adoc +++ b/articles/flow/component-internals/web-components/an-in-project-web-component.adoc @@ -17,21 +17,20 @@ This section demonstrates how to do this using the https://start.vaadin.com. == Creating the Component Template -The first step is to create the JavaScript Lit template in [filename]`frontend/my-test-element/my-test-element.js` +The first step is to create a TypeScript Lit template in [filename]`frontend/my-test-element/my-test-element.ts`. -*Example*: Defining the `my-test-element` JavaScript template. +*Example*: Defining the `my-test-element` TypeScript template. -[source,javascript] +[source,typescript] ---- import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +@customElement('my-test-element') class MyTestElement extends LitElement { - static get properties() { - return { - name: { type: String } - } - } + @property({ type: String }) + accessor name: string = ''; render() { return html` @@ -39,10 +38,14 @@ class MyTestElement extends LitElement { `; } } - -window.customElements.define('my-test-element', MyTestElement); ---- +The `@customElement` decorator registers the class as a custom element, replacing the manual `customElements.define()` call. The `@property` decorator declares `name` as a reactive property with a type hint, so Lit automatically handles attribute-to-property conversion and triggers re-renders when the value changes. + +.JavaScript works, too +[NOTE] +You can also use plain JavaScript (`.js`) files if you prefer. TypeScript is recommended for larger components because it provides type checking and better IDE support, but both work identically with `@JsModule`. + == Creating the Component Java API This works in exactly the same way as described in <>, except that the static files are loaded from your project. @@ -53,7 +56,7 @@ You can modify them while creating the Java API. [source,java] ---- @Tag("my-test-element") -@JsModule("my-test-element/my-test-element.js") +@JsModule("my-test-element/my-test-element.ts") public class MyTest extends Component { public MyTest(String msg) { @@ -62,6 +65,8 @@ public class MyTest extends Component { } ---- +Note the `@JsModule` annotation points to a `.ts` file. Vaadin's build tooling handles TypeScript compilation automatically. + == Using the Web Component You can now use the component in other parts of your code. @@ -77,4 +82,64 @@ public class MainView extends VerticalLayout { ---- +[[using-npm-packages]] +== Using npm Packages in Your Component + +When your Web Component needs a third-party npm package, declare the dependency with `@NpmPackage` on the Java class and import the package in your TypeScript file. + +For example, to use a date formatting library in your component: + +*Step 1*: Import and use the package in TypeScript. + +[source,typescript] +---- +import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { format } from 'date-fns'; + +@customElement('formatted-date') +class FormattedDate extends LitElement { + + @property({ type: String }) + accessor date: string = ''; + + @property({ type: String }) + accessor pattern: string = 'PP'; + + render() { + const formatted = this.date + ? format(new Date(this.date), this.pattern) + : ''; + return html`${formatted}`; + } +} +---- + +*Step 2*: Declare the npm dependency and module in the Java class. + +[source,java] +---- +@Tag("formatted-date") +@NpmPackage(value = "date-fns", version = "4.1.0") +@JsModule("./formatted-date.ts") +public class FormattedDate extends Component { + + public FormattedDate() { + } + + public void setDate(String isoDate) { + getElement().setProperty("date", isoDate); + } + + public void setPattern(String pattern) { + getElement().setProperty("pattern", pattern); + } +} +---- + +The `@NpmPackage` annotation ensures `date-fns` is installed when the application builds. The TypeScript file can then import from it like any other module. + +For more complex integration scenarios, including wrapping full-featured npm libraries and advanced Lit patterns, see <>. + + [discussion-id]`F3B26182-6375-44E5-8B83-09BE00801C2A` diff --git a/articles/flow/component-internals/web-components/index.adoc b/articles/flow/component-internals/web-components/index.adoc index 0982aa5419..e100c7ae4b 100644 --- a/articles/flow/component-internals/web-components/index.adoc +++ b/articles/flow/component-internals/web-components/index.adoc @@ -41,7 +41,7 @@ The `@Tag` annotation here defines the name of the HTML element. The `@JSModule` Your component may require in-project frontend files, such as additional JavaScript modules. In which case, add them to the `src/main/resources/META-INF/frontend` directory so that they're packaged in the component JAR if you choose to make an add-on of your component. -As a example, you might use the `@JsModule` annotation to add a local JavaScript module like so: +As a example, you might use the `@JsModule` annotation to add a local JavaScript or TypeScript module like so: [source,java] ---- @JsModule("./my-local-module.js") @@ -49,6 +49,54 @@ As a example, you might use the `@JsModule` annotation to add a local JavaScript When running `mvn clean install`, the `vaadin-maven-plugin` automatically installs the `npm` package in `node_modules` and imports the JavaScript module file into the document provided to the browser. Additionally, if you run the Jetty web server from Maven (i.e., using `mvn jetty:run`), your project's source code is monitored for changes to these types of annotations. Any change to `@NpmPackage` or `@JsModule` annotations triggers installation of the referenced packages and hot deployment of your application, including the new JS module imports. + +[[typescript-support]] +== TypeScript Support + +Vaadin fully supports TypeScript for client-side files. You can use `.ts` files with the `@JsModule` annotation exactly as you would use `.js` files: + +[source,java] +---- +@JsModule("./my-component.ts") +---- + +TypeScript provides type safety for property definitions, event data, and configuration objects, which is especially valuable for complex components. It also enables better IDE support with auto-completion and inline documentation. + +When using Lit to define Web Components, TypeScript enables typed decorators for properties and events: + +[source,typescript] +---- +import { LitElement, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +@customElement('my-component') +class MyComponent extends LitElement { + @property({ type: String }) + accessor label: string = ''; + + @property({ type: Number }) + accessor count: number = 0; + + render() { + return html`${this.label}: ${this.count}`; + } +} +---- + +See <> for a complete TypeScript example, and <> for guidance on wrapping npm packages with TypeScript and using advanced Lit features. + + +[[npm-package-design]] +== Why `@NpmPackage` Is Declared on Components + +You might wonder why the `@NpmPackage` annotation is placed directly on component classes rather than in a centralized configuration file. This design is intentional: + +*Automatic tree-shaking*:: Only npm packages referenced by components that are actually used in the application are installed. If a component class is never referenced, its npm dependencies are excluded from the bundle. + +*Self-contained add-ons*:: When creating a reusable add-on (e.g., for the Vaadin Directory), the component class carries its own dependency declarations. Consumers don't need to manually configure npm packages -- adding the Java dependency is sufficient. + +*Clear dependency graph*:: Each component explicitly declares what it needs. This makes it straightforward to understand which client-side libraries a component relies on, and avoids hidden or implicit dependencies. + === Understanding the Project Files The project includes the [classname]`AddonView` component class at `src/test/java/…/AddonView.java`: @@ -111,3 +159,11 @@ You can also use the https://github.com/vaadin/addon-template/archive/v24.zip[Ad - Leave the default Web Component URL in the starter form; - Download the project; and then - Delete the `@NpmPackage` and `@JsModule` annotations, and the UI component class. + + +== See Also + +- <> -- creating application-specific components with TypeScript and Lit +- <> -- wrapping npm packages, advanced Lit features, and state synchronization patterns +- <> -- properties, events, functions, and sub-elements +- <<{articles}/flow/integrations/react#,Using React Components in Flow>> -- integrating React components using TypeScript adapters diff --git a/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc b/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc index c82c671156..cecfcc74bd 100644 --- a/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc +++ b/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc @@ -342,4 +342,10 @@ This shows the `CHECK` icon and then changes the icon on every click of the butt You could extend [classname]`Button` directly instead of [classname]`Component`, but you would then also inherit the entire public [classname]`Button` API. +== See Also + +- <> -- TypeScript wrappers, npm package integration, and complex state synchronization +- <> -- building application-specific components with TypeScript + + [discussion-id]`AACDBA11-3ECD-4E3B-9A36-C64E3963C26C` diff --git a/frontend/demo/component-internals/acme-widget-wrapper.ts b/frontend/demo/component-internals/acme-widget-wrapper.ts new file mode 100644 index 0000000000..9ff3f89679 --- /dev/null +++ b/frontend/demo/component-internals/acme-widget-wrapper.ts @@ -0,0 +1,160 @@ +// tag::class[] +import { css, html, LitElement, type PropertyValues } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +// In a real project, you would import from the npm package: +// import { Widget, type WidgetConfig } from '@acme/widget'; +// +// For this example, the types are defined locally to illustrate the pattern. + +interface WidgetConfig { + type?: string; + animate?: boolean; + interactive?: boolean; + onChange?(data: WidgetData): void; +} + +interface WidgetData { + label: string; + value: number; +} + +// Placeholder for the third-party Widget class. +// In a real project, this comes from the npm package. +class Widget { + private _config: WidgetConfig; + + constructor(_container: HTMLElement, config: WidgetConfig) { + this._config = config; + } + + updateConfig(config: WidgetConfig): void { + this._config = { ...this._config, ...config }; + } + + setInteractive(interactive: boolean): void { + this._config.interactive = interactive; + } + + destroy(): void { + // cleanup + } +} + +@customElement('acme-widget-wrapper') +class AcmeWidgetWrapper extends LitElement { + // -- Reactive properties (synced with Java via element properties) -- + + @property({ type: String }) + accessor title: string = ''; + + @property({ type: String, attribute: 'config-json' }) + accessor configJson: string = '{}'; + + @property({ type: Boolean }) + accessor interactive: boolean = true; + + // -- Internal state (not exposed as attributes) -- + + @state() + private accessor _widget: Widget | null = null; + + @state() + private accessor _loading: boolean = true; + + // -- Styles -- + + static override styles = css` + :host { + display: block; + } + .container { + border: 1px solid var(--lumo-contrast-20pct, #ccc); + border-radius: var(--lumo-border-radius-m, 4px); + padding: var(--lumo-space-m, 16px); + } + .loading { + color: var(--lumo-secondary-text-color, #999); + } + `; + + // -- Lifecycle -- + + override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this._initWidget(); + } + + override updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + + // Re-configure when config changes after initial render + if (changedProperties.has('configJson') && this._widget) { + const config = this._parseConfig(); + this._widget.updateConfig(config); + } + + if (changedProperties.has('interactive') && this._widget) { + this._widget.setInteractive(this.interactive); + } + } + + override disconnectedCallback(): void { + super.disconnectedCallback(); + // Clean up to prevent memory leaks + if (this._widget) { + this._widget.destroy(); + this._widget = null; + } + } + + // -- Private methods -- + + private _initWidget(): void { + const container = this.renderRoot.querySelector('#widget-root'); + if (!container) return; + + const config = this._parseConfig(); + + this._widget = new Widget(container as HTMLElement, { + ...config, + interactive: this.interactive, + onChange: (data: WidgetData) => { + // Dispatch event for Java-side listener + this.dispatchEvent( + new CustomEvent('widget-change', { + detail: data, + bubbles: true, + composed: true, + }) + ); + }, + }); + + this._loading = false; + } + + private _parseConfig(): WidgetConfig { + try { + const parsed: WidgetConfig = JSON.parse(this.configJson); + return parsed; + } catch { + return {}; + } + } + + // -- Render -- + + override render() { + return html` +
+ ${this.title ? html`

${this.title}

` : ''} + ${this._loading ? html`
Loading widget...
` : ''} +
+
+ `; + } +} + +export { AcmeWidgetWrapper }; +// end::class[] diff --git a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java b/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java new file mode 100644 index 0000000000..a405531819 --- /dev/null +++ b/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java @@ -0,0 +1,72 @@ +package com.vaadin.demo.component.internals; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.DomEvent; +import com.vaadin.flow.component.EventData; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.dependency.NpmPackage; +import com.vaadin.flow.shared.Registration; + +import elemental.json.JsonObject; + +// tag::class[] +@Tag("acme-widget-wrapper") +@NpmPackage(value = "@acme/widget", version = "2.0.0") +@JsModule("./component-internals/acme-widget-wrapper.ts") +public class AcmeWidget extends Component { + + public AcmeWidget() { + } + + public void setTitle(String title) { + getElement().setProperty("title", title); + } + + public String getTitle() { + return getElement().getProperty("title", ""); + } + + public void setConfig(String configJson) { + getElement().setProperty("configJson", configJson); + } + + public void setInteractive(boolean interactive) { + getElement().setProperty("interactive", interactive); + } + + public boolean isInteractive() { + return getElement().getProperty("interactive", true); + } + + public Registration addWidgetChangeListener( + ComponentEventListener listener) { + return addListener(WidgetChangeEvent.class, listener); + } + + @DomEvent("widget-change") + public static class WidgetChangeEvent extends ComponentEvent { + + private final String label; + private final double value; + + public WidgetChangeEvent(AcmeWidget source, boolean fromClient, + @EventData("event.detail.label") String label, + @EventData("event.detail.value") double value) { + super(source, fromClient); + this.label = label; + this.value = value; + } + + public String getLabel() { + return label; + } + + public double getValue() { + return value; + } + } +} +// end::class[] From a39009ff5d194b75fe30607999b1bd0ac03adf1c Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 26 Jan 2026 09:35:33 +0000 Subject: [PATCH 2/9] docs: use setPropertyBean for typed config instead of raw JSON strings Replace manual JSON string passing with setPropertyBean() and a Java record, matching the idiomatic Vaadin pattern for complex objects. --- .../web-components/advanced-integration.adoc | 46 +++++++++---------- .../acme-widget-wrapper.ts | 22 ++------- .../demo/component/internals/AcmeWidget.java | 9 ++-- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc index 35b0573564..e9fd6aceff 100644 --- a/articles/flow/component-internals/web-components/advanced-integration.adoc +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -40,7 +40,7 @@ interface DataPoint { } ---- -These types ensure that the JSON objects passed from Java via `getElement().setProperty()` conform to the expected shape, and that event data dispatched back to Java is consistent. +These types ensure that the objects passed from Java via `setPropertyBean()` conform to the expected shape, and that event data dispatched back to Java is consistent. [[wrapping-npm-packages]] @@ -62,9 +62,9 @@ include::{root}/frontend/demo/component-internals/acme-widget-wrapper.ts[tags=cl Key points: -- The `@property` decorator exposes reactive properties that Java can set via `getElement().setProperty()`. When a property changes, Lit automatically re-renders. +- The `@property` decorator exposes reactive properties that Java can set via `getElement().setProperty()` or `setPropertyBean()`. When a property changes, Lit automatically re-renders. - The `@state` decorator marks internal state that triggers re-renders but is not exposed as HTML attributes. -- The `configJson` property uses the `attribute` option to map the HTML attribute `config-json` to the camelCase JavaScript property, since HTML attributes are case-insensitive. +- The `config` property uses `type: Object` so that Vaadin's `setPropertyBean()` can pass a Java record directly as a JavaScript object -- no manual JSON serialization needed. - The `firstUpdated` lifecycle callback initializes the third-party widget after the component's DOM is ready. - The `disconnectedCallback` lifecycle callback cleans up the widget instance to prevent memory leaks when the component is removed from the DOM. - A `CustomEvent` is dispatched to communicate changes back to the Java side. @@ -83,7 +83,7 @@ Key points: - `@NpmPackage` declares the npm dependency. Vaadin installs it automatically during the build. - `@JsModule` points to the `.ts` file. Vaadin compiles TypeScript as part of the frontend build. -- Complex configuration is passed as a JSON string. For simple properties, use `setProperty` directly. +- The `WidgetConfig` record is passed to the client via `setPropertyBean()`, which serializes it to a JavaScript object automatically. For simple properties, use `setProperty` directly. - `@DomEvent` and `@EventData` map the client-side `CustomEvent` to a typed Java event class. @@ -93,7 +93,7 @@ Key points: ---- AcmeWidget widget = new AcmeWidget(); widget.setTitle("Dashboard Widget"); -widget.setConfig("{\"type\": \"bar\", \"animate\": true}"); +widget.setConfig(new AcmeWidget.WidgetConfig("bar", true)); widget.addWidgetChangeListener(event -> { Notification.show(event.getLabel() + ": " + event.getValue()); }); @@ -256,30 +256,36 @@ getElement().setProperty("visible", true); These are accessible in the TypeScript component as reactive properties. -=== Complex Objects via JSON +=== Complex Objects with `setPropertyBean` -For objects and arrays, serialize to JSON on the Java side and parse on the client: +For objects and arrays, use `setPropertyBean()` to pass a Java record or bean directly to the client. Vaadin handles the JSON serialization automatically: *Java side:* [source,java] ---- -// Using a JSON library (e.g., Jackson) -String json = objectMapper.writeValueAsString(config); -getElement().setProperty("configJson", json); +record ChartConfig(String type, boolean animate) {} + +ChartConfig config = new ChartConfig("bar", true); +getElement().setPropertyBean("config", config); ---- *TypeScript side:* [source,typescript] ---- -@property({ type: String, attribute: 'config-json' }) -accessor configJson: string = '{}'; +@property({ type: Object }) +accessor config: ChartConfig = { type: 'bar', animate: false }; +---- -private get _config(): MyConfig { - return JSON.parse(this.configJson) as MyConfig; -} +The client receives the bean as a plain JavaScript object. No manual JSON parsing is needed. + +You can also read the bean back on the server using `getPropertyBean()`: + +[source,java] +---- +ChartConfig config = getElement().getPropertyBean("config", ChartConfig.class); ---- -Alternatively, Vaadin's `Element` API supports `JsonValue` for setting structured data without manual serialization: +For cases where you need lower-level control, `setPropertyJson()` accepts an elemental `JsonValue`: [source,java] ---- @@ -289,14 +295,6 @@ config.put("animate", true); getElement().setPropertyJson("config", config); ---- -On the client, the property receives a JavaScript object directly: - -[source,typescript] ----- -@property({ type: Object }) -accessor config: MyConfig = { type: 'bar', animate: false }; ----- - === Two-Way Binding with Custom Events diff --git a/frontend/demo/component-internals/acme-widget-wrapper.ts b/frontend/demo/component-internals/acme-widget-wrapper.ts index 9ff3f89679..25605bd94a 100644 --- a/frontend/demo/component-internals/acme-widget-wrapper.ts +++ b/frontend/demo/component-internals/acme-widget-wrapper.ts @@ -48,8 +48,8 @@ class AcmeWidgetWrapper extends LitElement { @property({ type: String }) accessor title: string = ''; - @property({ type: String, attribute: 'config-json' }) - accessor configJson: string = '{}'; + @property({ type: Object }) + accessor config: WidgetConfig = {}; @property({ type: Boolean }) accessor interactive: boolean = true; @@ -89,9 +89,8 @@ class AcmeWidgetWrapper extends LitElement { super.updated(changedProperties); // Re-configure when config changes after initial render - if (changedProperties.has('configJson') && this._widget) { - const config = this._parseConfig(); - this._widget.updateConfig(config); + if (changedProperties.has('config') && this._widget) { + this._widget.updateConfig(this.config); } if (changedProperties.has('interactive') && this._widget) { @@ -114,10 +113,8 @@ class AcmeWidgetWrapper extends LitElement { const container = this.renderRoot.querySelector('#widget-root'); if (!container) return; - const config = this._parseConfig(); - this._widget = new Widget(container as HTMLElement, { - ...config, + ...this.config, interactive: this.interactive, onChange: (data: WidgetData) => { // Dispatch event for Java-side listener @@ -134,15 +131,6 @@ class AcmeWidgetWrapper extends LitElement { this._loading = false; } - private _parseConfig(): WidgetConfig { - try { - const parsed: WidgetConfig = JSON.parse(this.configJson); - return parsed; - } catch { - return {}; - } - } - // -- Render -- override render() { diff --git a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java b/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java index a405531819..3282991f95 100644 --- a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java +++ b/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java @@ -10,14 +10,15 @@ import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.shared.Registration; -import elemental.json.JsonObject; - // tag::class[] @Tag("acme-widget-wrapper") @NpmPackage(value = "@acme/widget", version = "2.0.0") @JsModule("./component-internals/acme-widget-wrapper.ts") public class AcmeWidget extends Component { + public record WidgetConfig(String type, boolean animate) { + } + public AcmeWidget() { } @@ -29,8 +30,8 @@ public String getTitle() { return getElement().getProperty("title", ""); } - public void setConfig(String configJson) { - getElement().setProperty("configJson", configJson); + public void setConfig(WidgetConfig config) { + getElement().setPropertyBean("config", config); } public void setInteractive(boolean interactive) { From 81ee6cb6e17b9b5d2b04a94839f66db7ddef1be4 Mon Sep 17 00:00:00 2001 From: Jouni Koivuviita Date: Tue, 27 Jan 2026 12:08:32 +0200 Subject: [PATCH 3/9] Update vocabulary list with case variations --- .github/styles/config/vocabularies/Docs/accept.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/styles/config/vocabularies/Docs/accept.txt b/.github/styles/config/vocabularies/Docs/accept.txt index 05e9d1099c..3cacac0093 100644 --- a/.github/styles/config/vocabularies/Docs/accept.txt +++ b/.github/styles/config/vocabularies/Docs/accept.txt @@ -11,9 +11,9 @@ Anthropic # Allow for Spring @Autowired [aA]utowir(e|ed|ing) Arial -async -automount -autorun +[aA]sync +[aA]utomount +[aA]utorun Azure [bB]oldens [bB]oolean @@ -57,7 +57,7 @@ Dragonfly [eE]num Entra [fF]ailsafe -favicon +[fF]avicon FDOs [fF]etch Figma @@ -155,7 +155,7 @@ OpenAPI pageable [pP]asswordless Payara -performant +[pP]erformant [pP]ersister Pivotal [pP]luggable @@ -171,7 +171,7 @@ Postgre(SQL|s) Quarkus [qQ]uantiles? [rR]eact -reactively +[rR]eactively [rR]enderers? Roboto [rR]ollout From cd9160684459bf6dc231f1ea2c5ed79825774a78 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 7 Apr 2026 11:02:14 +0000 Subject: [PATCH 4/9] Move @NpmPackage for fake @acme/widget to inline docs The @NpmPackage annotation referencing the non-existing @acme/widget npm package caused build failures. Move it to inline code in the adoc file so it still appears in the docs but is not compiled. --- .../web-components/advanced-integration.adoc | 4 +++- .../com/vaadin/demo/component/internals/AcmeWidget.java | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc index a0ebb7da99..9b9d25b02b 100644 --- a/articles/flow/component-internals/web-components/advanced-integration.adoc +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -76,7 +76,9 @@ The Java class declares the npm dependency and provides a typed API: [source,java] ---- -include::{root}/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java[tags=class,indent=0] +include::{root}/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java[tags=annotations,indent=0] +@NpmPackage(value = "@acme/widget", version = "2.0.0") +include::{root}/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java[tags=body,indent=0] ---- Key points: diff --git a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java b/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java index 3282991f95..03e0aef2da 100644 --- a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java +++ b/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java @@ -7,12 +7,12 @@ import com.vaadin.flow.component.EventData; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.dependency.JsModule; -import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.shared.Registration; -// tag::class[] +// tag::annotations[] @Tag("acme-widget-wrapper") -@NpmPackage(value = "@acme/widget", version = "2.0.0") +// end::annotations[] +// tag::body[] @JsModule("./component-internals/acme-widget-wrapper.ts") public class AcmeWidget extends Component { @@ -70,4 +70,4 @@ public double getValue() { } } } -// end::class[] +// end::body[] From 7b6d80dda4856aadcccb26624961e8ae96cf95ed Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 29 Jun 2026 13:20:45 +0300 Subject: [PATCH 5/9] docs: update web component integration snippets for Flow 25 Jackson APIs Flow 25 migrated the Element and event APIs from elemental.json to Jackson. Refresh the inline snippets accordingly: - setPropertyJson now takes a Jackson BaseJsonNode; show JacksonUtils.createObjectNode() and add setPropertyList/setPropertyMap - @EventData binds directly to typed values (List) since event data is Jackson-deserialized - low-level listener event data is a Jackson JsonNode, not elemental - callJsFunction accepts all Jackson-serializable types --- .../web-components/advanced-integration.adoc | 30 ++++++++++++++----- .../java-api-for-a-web-component.adoc | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc index 9b9d25b02b..758c6c672b 100644 --- a/articles/flow/component-internals/web-components/advanced-integration.adoc +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -260,7 +260,7 @@ These are accessible in the TypeScript component as reactive properties. === Complex Objects with `setPropertyBean` -For objects and arrays, use `setPropertyBean()` to pass a Java record or bean directly to the client. Vaadin handles the JSON serialization automatically: +For objects, use `setPropertyBean()` to pass a Java record or bean directly to the client. Vaadin handles the JSON serialization automatically: *Java side:* [source,java] @@ -287,11 +287,19 @@ You can also read the bean back on the server using `getPropertyBean()`: ChartConfig config = getElement().getPropertyBean("config", ChartConfig.class); ---- -For cases where you need lower-level control, `setPropertyJson()` accepts an elemental `JsonValue`: +For lists and maps, use `setPropertyList()` and `setPropertyMap()`. Vaadin serializes the elements with Jackson: [source,java] ---- -JsonObject config = Json.createObject(); +getElement().setPropertyList("selectedIds", List.of(1, 2, 3)); +getElement().setPropertyMap("labels", Map.of("x", "Revenue", "y", "Quarter")); +---- + +For cases where you need lower-level control, `setPropertyJson()` accepts a Jackson `BaseJsonNode`: + +[source,java] +---- +ObjectNode config = JacksonUtils.createObjectNode(); config.put("type", "bar"); config.put("animate", true); getElement().setPropertyJson("config", config); @@ -313,32 +321,38 @@ this.dispatchEvent(new CustomEvent('selection-changed', { ---- *Java side using `@DomEvent`:* + +Event data is deserialized with Jackson, so you can bind the expression directly to a typed value such as a `List`: + [source,java] ---- @DomEvent("selection-changed") public static class SelectionChangedEvent extends ComponentEvent { - private final JsonArray selectedIds; + private final List selectedIds; public SelectionChangedEvent(MyComponent source, boolean fromClient, - @EventData("event.detail.selectedIds") JsonArray selectedIds) { + @EventData("event.detail.selectedIds") List selectedIds) { super(source, fromClient); this.selectedIds = selectedIds; } - public JsonArray getSelectedIds() { + public List getSelectedIds() { return selectedIds; } } ---- *Java side using `addEventListener`:* + +The low-level listener receives the raw event data as a Jackson `JsonNode`. Use `getEventData(TypeReference)` to deserialize the whole event payload into a typed object, or navigate the `JsonNode` manually: + [source,java] ---- getElement().addEventListener("selection-changed", event -> { - JsonArray ids = event.getEventData().getArray("event.detail.selectedIds"); - // process selection + JsonNode ids = event.getEventData().get("event.detail.selectedIds"); + // process selection, e.g. ids.get(0).asInt() }).addEventData("event.detail.selectedIds"); ---- diff --git a/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc b/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc index 98d0a2eec4..5ec8a81f69 100644 --- a/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc +++ b/articles/flow/component-internals/web-components/java-api-for-a-web-component.adoc @@ -60,7 +60,7 @@ See <<../events#,Events>> for the full reference, including event data expressio == Calling Element Functions -Use [methodname]`getElement().callJsFunction()` to invoke JavaScript methods on the element. Supported parameter types are `String`, `Boolean`, `Integer`, `Double`, the corresponding primitive types, `JsonValue`, and `Element` and `Component` references. The method returns a server-side promise for the return value. +Use [methodname]`getElement().callJsFunction()` to invoke JavaScript methods on the element. It accepts all types supported by Jackson for JSON serialization, so you can pass primitives, strings, records, beans, collections, and Jackson `JsonNode` values directly. `Element` and `Component` references are a special case: they're sent as DOM element references when attached, or as `null` otherwise. The method returns a server-side promise for the return value. See <<../client-server-rpc#,Remote Procedure Calls>> for the full reference. From 5452326061ee6e4196ff00465291a018f6be606e Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 29 Jun 2026 13:30:07 +0300 Subject: [PATCH 6/9] docs: clarify Light DOM vs Shadow DOM trade-offs for project-specific components --- .../web-components/advanced-integration.adoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc index 758c6c672b..0ee0409085 100644 --- a/articles/flow/component-internals/web-components/advanced-integration.adoc +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -213,7 +213,9 @@ class ChartWrapper extends LitElement { === Shadow DOM vs. Light DOM -By default, Lit renders into Shadow DOM, which encapsulates styles. Most wrapper components should use Shadow DOM. However, if you need the wrapped content to inherit page styles or participate in form submission, you can render into Light DOM: +By default, Lit renders into Shadow DOM, which encapsulates styles. This is usually the right choice for reusable or distributable components, where you don't want the application's styles to affect the component's internals. + +For project-specific components, the opposite is often true: you may want the application's theme and global styles to apply to the component's content. In that case, rendering into Light DOM is a reasonable choice, because it lets page styles reach inside the component. Light DOM also lets the wrapped content participate in form submission. To render into Light DOM, override `createRenderRoot()` to return the element itself: [source,typescript] ---- @@ -233,9 +235,9 @@ class LightDomWrapper extends LitElement { } ---- -.When to avoid Light DOM +.Light DOM trade-offs [NOTE] -Light DOM components don't have style encapsulation. Their styles can leak out and page styles can leak in, which makes them harder to maintain. Prefer Shadow DOM unless you have a specific reason to use Light DOM. +Light DOM components have no style encapsulation: their styles can leak out, and page styles leak in. For a project-specific component meant to match the application theme, that's exactly what you want. For a reusable component intended to look the same everywhere, it makes the component harder to maintain -- prefer Shadow DOM in that case. [[state-synchronization]] From da55c19fe51a7cf7e0a2011a2f97c968682bb887 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 29 Jun 2026 13:30:35 +0300 Subject: [PATCH 7/9] docs: rename @acme/widget example to @example/widget for clarity --- .../web-components/advanced-integration.adoc | 14 +++++++------- ...widget-wrapper.ts => example-widget-wrapper.ts} | 8 ++++---- .../{AcmeWidget.java => ExampleWidget.java} | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) rename frontend/demo/component-internals/{acme-widget-wrapper.ts => example-widget-wrapper.ts} (94%) rename src/main/java/com/vaadin/demo/component/internals/{AcmeWidget.java => ExampleWidget.java} (87%) diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc index 0ee0409085..6e3ff58828 100644 --- a/articles/flow/component-internals/web-components/advanced-integration.adoc +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -48,7 +48,7 @@ These types ensure that the objects passed from Java via `setPropertyBean()` con Wrapping a third-party npm library involves three parts working together: the `@NpmPackage` annotation declares the dependency, a TypeScript file imports and wraps the library as a Web Component, and a Java class exposes the API to server-side code. -This section demonstrates the pattern using a fictional `@acme/widget` package. The same approach applies to any npm library. +This section demonstrates the pattern using a fictional `@example/widget` package. The same approach applies to any npm library. === Step 1: The TypeScript Wrapper @@ -57,7 +57,7 @@ The TypeScript file imports the npm package and wraps it in a LitElement-based W [source,typescript] ---- -include::{root}/frontend/demo/component-internals/acme-widget-wrapper.ts[tags=class,indent=0] +include::{root}/frontend/demo/component-internals/example-widget-wrapper.ts[tags=class,indent=0] ---- Key points: @@ -76,9 +76,9 @@ The Java class declares the npm dependency and provides a typed API: [source,java] ---- -include::{root}/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java[tags=annotations,indent=0] -@NpmPackage(value = "@acme/widget", version = "2.0.0") -include::{root}/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java[tags=body,indent=0] +include::{root}/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java[tags=annotations,indent=0] +@NpmPackage(value = "@example/widget", version = "2.0.0") +include::{root}/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java[tags=body,indent=0] ---- Key points: @@ -93,9 +93,9 @@ Key points: [source,java] ---- -AcmeWidget widget = new AcmeWidget(); +ExampleWidget widget = new ExampleWidget(); widget.setTitle("Dashboard Widget"); -widget.setConfig(new AcmeWidget.WidgetConfig("bar", true)); +widget.setConfig(new ExampleWidget.WidgetConfig("bar", true)); widget.addWidgetChangeListener(event -> { Notification.show(event.getLabel() + ": " + event.getValue()); }); diff --git a/frontend/demo/component-internals/acme-widget-wrapper.ts b/frontend/demo/component-internals/example-widget-wrapper.ts similarity index 94% rename from frontend/demo/component-internals/acme-widget-wrapper.ts rename to frontend/demo/component-internals/example-widget-wrapper.ts index 25605bd94a..f1206b28ac 100644 --- a/frontend/demo/component-internals/acme-widget-wrapper.ts +++ b/frontend/demo/component-internals/example-widget-wrapper.ts @@ -3,7 +3,7 @@ import { css, html, LitElement, type PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; // In a real project, you would import from the npm package: -// import { Widget, type WidgetConfig } from '@acme/widget'; +// import { Widget, type WidgetConfig } from '@example/widget'; // // For this example, the types are defined locally to illustrate the pattern. @@ -41,8 +41,8 @@ class Widget { } } -@customElement('acme-widget-wrapper') -class AcmeWidgetWrapper extends LitElement { +@customElement('example-widget-wrapper') +class ExampleWidgetWrapper extends LitElement { // -- Reactive properties (synced with Java via element properties) -- @property({ type: String }) @@ -144,5 +144,5 @@ class AcmeWidgetWrapper extends LitElement { } } -export { AcmeWidgetWrapper }; +export { ExampleWidgetWrapper }; // end::class[] diff --git a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java b/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java similarity index 87% rename from src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java rename to src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java index 03e0aef2da..9d1bd2b294 100644 --- a/src/main/java/com/vaadin/demo/component/internals/AcmeWidget.java +++ b/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java @@ -10,16 +10,16 @@ import com.vaadin.flow.shared.Registration; // tag::annotations[] -@Tag("acme-widget-wrapper") +@Tag("example-widget-wrapper") // end::annotations[] // tag::body[] -@JsModule("./component-internals/acme-widget-wrapper.ts") -public class AcmeWidget extends Component { +@JsModule("./component-internals/example-widget-wrapper.ts") +public class ExampleWidget extends Component { public record WidgetConfig(String type, boolean animate) { } - public AcmeWidget() { + public ExampleWidget() { } public void setTitle(String title) { @@ -48,12 +48,12 @@ public Registration addWidgetChangeListener( } @DomEvent("widget-change") - public static class WidgetChangeEvent extends ComponentEvent { + public static class WidgetChangeEvent extends ComponentEvent { private final String label; private final double value; - public WidgetChangeEvent(AcmeWidget source, boolean fromClient, + public WidgetChangeEvent(ExampleWidget source, boolean fromClient, @EventData("event.detail.label") String label, @EventData("event.detail.value") double value) { super(source, fromClient); From 24c87a2400b901566e562585b846fe6a9f6ea0e9 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 29 Jun 2026 13:36:28 +0300 Subject: [PATCH 8/9] fix: use legacy decorator syntax in web component examples The project builds with experimentalDecorators/useDefineForClassFields:false, so Lit properties must be declared without the TC39 'accessor' keyword. The dspublisher production build (Rollup) fails to parse 'accessor', which broke build-and-deploy even though Java compile and eslint passed. --- .../web-components/advanced-integration.adoc | 12 ++++++------ .../component-internals/web-components/index.adoc | 4 ++-- .../component-internals/example-widget-wrapper.ts | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/articles/flow/component-internals/web-components/advanced-integration.adoc b/articles/flow/component-internals/web-components/advanced-integration.adoc index 6e3ff58828..4bc2788664 100644 --- a/articles/flow/component-internals/web-components/advanced-integration.adoc +++ b/articles/flow/component-internals/web-components/advanced-integration.adoc @@ -122,11 +122,11 @@ import { customElement, property, state } from 'lit/decorators.js'; class MyCounter extends LitElement { // Public: settable from Java via getElement().setProperty("max", 100) @property({ type: Number }) - accessor max: number = 10; + max: number = 10; // Internal: only used within this component @state() - private accessor _count: number = 0; + private _count: number = 0; render() { return html` @@ -184,7 +184,7 @@ class ChartWrapper extends LitElement { private _chart: Chart | null = null; @property({ type: String }) - accessor type: string = 'bar'; + type: string = 'bar'; override firstUpdated() { const canvas = this.renderRoot.querySelector('canvas'); @@ -277,7 +277,7 @@ getElement().setPropertyBean("config", config); [source,typescript] ---- @property({ type: Object }) -accessor config: ChartConfig = { type: 'bar', animate: false }; +config: ChartConfig = { type: 'bar', animate: false }; ---- The client receives the bean as a plain JavaScript object. No manual JSON parsing is needed. @@ -372,10 +372,10 @@ When wrapping libraries that require async initialization (e.g., loading data fr [source,typescript] ---- @state() -private accessor _loading: boolean = true; +private _loading: boolean = true; @state() -private accessor _error: string | null = null; +private _error: string | null = null; override async firstUpdated() { try { diff --git a/articles/flow/component-internals/web-components/index.adoc b/articles/flow/component-internals/web-components/index.adoc index 0920ca5376..06e8b07c7c 100644 --- a/articles/flow/component-internals/web-components/index.adoc +++ b/articles/flow/component-internals/web-components/index.adoc @@ -84,10 +84,10 @@ import { customElement, property } from 'lit/decorators.js'; @customElement('my-component') class MyComponent extends LitElement { @property({ type: String }) - accessor label: string = ''; + label: string = ''; @property({ type: Number }) - accessor count: number = 0; + count: number = 0; render() { return html`${this.label}: ${this.count}`; diff --git a/frontend/demo/component-internals/example-widget-wrapper.ts b/frontend/demo/component-internals/example-widget-wrapper.ts index f1206b28ac..02702a3482 100644 --- a/frontend/demo/component-internals/example-widget-wrapper.ts +++ b/frontend/demo/component-internals/example-widget-wrapper.ts @@ -46,21 +46,21 @@ class ExampleWidgetWrapper extends LitElement { // -- Reactive properties (synced with Java via element properties) -- @property({ type: String }) - accessor title: string = ''; + title: string = ''; @property({ type: Object }) - accessor config: WidgetConfig = {}; + config: WidgetConfig = {}; @property({ type: Boolean }) - accessor interactive: boolean = true; + interactive: boolean = true; // -- Internal state (not exposed as attributes) -- @state() - private accessor _widget: Widget | null = null; + private _widget: Widget | null = null; @state() - private accessor _loading: boolean = true; + private _loading: boolean = true; // -- Styles -- From 95b0deea9375c75dc98d60beecdefb398e7e6e88 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 29 Jun 2026 15:52:48 +0300 Subject: [PATCH 9/9] format --- .../com/vaadin/demo/component/internals/ExampleWidget.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java b/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java index 9d1bd2b294..551d5d91cf 100644 --- a/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java +++ b/src/main/java/com/vaadin/demo/component/internals/ExampleWidget.java @@ -48,7 +48,8 @@ public Registration addWidgetChangeListener( } @DomEvent("widget-change") - public static class WidgetChangeEvent extends ComponentEvent { + public static class WidgetChangeEvent + extends ComponentEvent { private final String label; private final double value;