From be33f24c9826cf724bd6f31a199f6ffdb0a840fe Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Thu, 4 Jun 2026 13:12:06 -0300
Subject: [PATCH] feat: add LitRenderer.withFunction migration support
Close #33
---
pom.xml | 23 +++++
.../LitRendererAsmPostProcessor.java | 86 +++++++++++++++++++
.../LitRendererMigrationExtension.java | 57 ++++++++++++
.../LitRendererMigrationExtensionTest.java | 70 +++++++++++++++
4 files changed, 236 insertions(+)
create mode 100644 src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java
create mode 100644 src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java
create mode 100644 src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java
diff --git a/pom.xml b/pom.xml
index 17b9e97..b8001b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,6 +71,12 @@
9.8true
+
+ org.ow2.asm
+ asm-commons
+ 9.8
+ true
+ tools.jackson.corejackson-databind
@@ -142,6 +148,7 @@
3.1.0
+ patch-elemental-nodesprocess-classesjava
@@ -158,6 +165,19 @@
+
+ patch-lit-renderer
+ process-classes
+
+ java
+
+
+ com.flowingcode.vaadin.jsonmigration.LitRendererAsmPostProcessor
+
+ ${project.build.outputDirectory}/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.class
+
+
+
@@ -242,6 +262,7 @@
true**/ElementalNodeAsmPostProcessor.java
+ **/LitRendererAsmPostProcessor.java
https://javadoc.io/doc/com.vaadin/vaadin-platform-javadoc/${vaadin.version}
@@ -258,6 +279,8 @@
META-INF/VAADIN/config/flow-build-info.jsoncom/flowingcode/vaadin/jsonmigration/ElementalNodeAsmPostProcessor.classcom/flowingcode/vaadin/jsonmigration/ElementalNodeAsmPostProcessor$*.class
+ com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.class
+ com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor$*.class
diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java
new file mode 100644
index 0000000..38a30ed
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererAsmPostProcessor.java
@@ -0,0 +1,86 @@
+/*-
+ * #%L
+ * Json Migration Helper
+ * %%
+ * Copyright (C) 2025 - 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.jsonmigration;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+/**
+ * Replaces references to the inner LitRenderer stand-in interface with
+ * com.vaadin.flow.data.renderer.LitRenderer, allowing the class to be compiled against Vaadin 14
+ * while referencing the real LitRenderer available in Vaadin 24+.
+ */
+public class LitRendererAsmPostProcessor {
+
+ private static final String SOURCE =
+ "com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension$LitRenderer";
+ private static final String TARGET = "com/vaadin/flow/data/renderer/LitRenderer";
+
+ public static void main(String[] args) throws Exception {
+ for (String arg : args) {
+ Path classPath = Paths.get(arg);
+ byte[] original = Files.readAllBytes(classPath);
+
+ ClassReader cr = new ClassReader(original);
+ ClassWriter cw = new ClassWriter(0);
+
+ Remapper remapper = new Remapper() {
+ @Override
+ public String map(String internalName) {
+ if (internalName.equals(SOURCE)) {
+ return TARGET;
+ }
+ return internalName;
+ }
+ };
+
+ // Drop the InnerClasses entry that ClassRemapper would rewrite to point at the real
+ // LitRenderer, which is not actually an inner class of LitRendererMigrationExtension.
+ ClassVisitor filter = new ClassVisitor(Opcodes.ASM9, cw) {
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ if (!TARGET.equals(name)) {
+ super.visitInnerClass(name, outerName, innerName, access);
+ }
+ }
+ };
+
+ cr.accept(new ClassRemapper(filter, remapper), 0);
+ Files.write(classPath, cw.toByteArray());
+ System.out.println("Successfully patched: " + classPath.getFileName());
+ }
+
+ // Delete the orphaned inner class file produced by the compiler
+ if (args.length > 0) {
+ Path innerClass =
+ Paths.get(args[0]).resolveSibling("LitRendererMigrationExtension$LitRenderer.class");
+ if (Files.deleteIfExists(innerClass)) {
+ System.out.println("Deleted orphaned inner class: " + innerClass.getFileName());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java
new file mode 100644
index 0000000..3506df0
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtension.java
@@ -0,0 +1,57 @@
+package com.flowingcode.vaadin.jsonmigration;
+
+import com.vaadin.flow.function.SerializableBiConsumer;
+import com.vaadin.flow.server.Version;
+import elemental.json.JsonArray;
+import lombok.SneakyThrows;
+
+/**
+ * Provides migration support for {@code LitRenderer} event handler registration across
+ * Vaadin versions.
+ *
+ *
In Vaadin 24, {@code LitRenderer.withFunction} receives a {@code JsonArray} from the
+ * elemental.json API directly. In Vaadin 25+, the argument type changed to a Jackson
+ * {@code ArrayNode}, so this class wraps the handler to perform the necessary conversion
+ * transparently.
+ *
+ *
The {@code LitRenderer} type referenced here is resolved at runtime via a method handle,
+ * allowing this library to compile against Vaadin 14 while supporting Vaadin 24+.
+ *
+ *
Note: This class is only useful when running on Vaadin 24 or later. On earlier
+ * versions, {@code LitRenderer} is not available and any attempt to use this class will fail at
+ * runtime.
+ */
+public class LitRendererMigrationExtension {
+
+ private class LitRenderer {
+ LitRenderer withFunction(String functionName,
+ SerializableBiConsumer handler) {
+ return null;
+ }
+ }
+
+ /**
+ * Registers an event handler function on the given {@code LitRenderer}.
+ *
+ *
On Vaadin 25+, the raw argument passed by the client is a Jackson {@code ArrayNode} rather
+ * than a {@code JsonArray}, so the handler is wrapped to convert it before forwarding.
+ *
+ * @param the bean type of the renderer
+ * @param renderer the {@code LitRenderer} on which to register the function
+ * @param name the client-side function name
+ * @param handler the handler to invoke when the client calls the function; receives the item and
+ * a {@code JsonArray} of arguments
+ * @return the same renderer instance, for chaining
+ */
+ @SneakyThrows
+ public static LitRenderer withFunction(LitRenderer renderer, String name,
+ SerializableBiConsumer handler) {
+ SerializableBiConsumer c = handler;
+ if (Version.getMajorVersion() >= 25) {
+ c = (source, array) -> handler.accept(source,
+ (JsonArray) JsonMigration.convertToJsonValue(array));
+ }
+ return renderer.withFunction(name, c);
+ }
+
+}
diff --git a/src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java b/src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java
new file mode 100644
index 0000000..350f7ab
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/jsonmigration/LitRendererMigrationExtensionTest.java
@@ -0,0 +1,70 @@
+/*-
+ * #%L
+ * Json Migration Helper
+ * %%
+ * Copyright (C) 2025 - 2026 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.jsonmigration;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assume.assumeTrue;
+
+import com.vaadin.flow.function.SerializableBiConsumer;
+import com.vaadin.flow.server.Version;
+import elemental.json.JsonArray;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import org.junit.Test;
+
+public class LitRendererMigrationExtensionTest {
+
+ @Test
+ public void testWithFunctionRegistersHandler() throws Exception {
+ assumeTrue("LitRenderer requires Vaadin 24+", Version.getMajorVersion() >= 24);
+ Class> litRendererClass = Class.forName("com.vaadin.flow.data.renderer.LitRenderer");
+ Method of = litRendererClass.getMethod("of", String.class);
+ Object renderer = of.invoke(null, "");
+
+ SerializableBiConsumer handler = (source, array) -> {};
+
+ Method withFunction = LitRendererMigrationExtension.class.getDeclaredMethod(
+ "withFunction", litRendererClass, String.class, SerializableBiConsumer.class);
+ withFunction.setAccessible(true);
+ Object result = withFunction.invoke(null, renderer, "click", handler);
+
+ assertSame(renderer, result);
+ assertNotNull("Handler for 'click' should be registered on the renderer",
+ findRegisteredHandler(renderer, "click"));
+ }
+
+ private static Object findRegisteredHandler(Object renderer, String functionName)
+ throws Exception {
+ for (Class> c = renderer.getClass(); c != null; c = c.getSuperclass()) {
+ for (Field field : c.getDeclaredFields()) {
+ if (Map.class.isAssignableFrom(field.getType())) {
+ field.setAccessible(true);
+ Map, ?> map = (Map, ?>) field.get(renderer);
+ if (map != null && map.containsKey(functionName)) {
+ return map.get(functionName);
+ }
+ }
+ }
+ }
+ return null;
+ }
+}