vaadin / flow

Vaadin Flow is a Java framework binding Vaadin web components to Java. This is part of Vaadin 10+.
Apache License 2.0
617 stars 167 forks source link

[DX] Element api to call / execute javascript should support arrays/iterables of Serializable as argument #12595

Open stefanuebe opened 2 years ago

stefanuebe commented 2 years ago

Describe your motivation

Simple example: I want to call a javascript method and pass some simple parameters plus an array of components. I tried several different approaches to get this working, but found none. I tried converting the components to elements and then to a json array, but that does not work. Using the JsonUtils also threw exceptions. Not sure how such an use case can be realized atM.

protected void doSomething(String foo, Double bar, Component... components) {
    getElement().callJsFunction("doSomething", foo, bar, components); // the components array leads to the exception
}

Describe the solution you'd like

The JsonCodec, which is used to serialize JS parameters, should be extended to support different types of common iterables. Those iterables would be converted to a JsonArray.

Supported types should be:

Potential alternatives

I tried several:

This for instance did not work, it just threw another exception:

protected void doSomething(String foo, Double bar, Component... components) {
        Element[] elements = Stream.of(pComponents).map(Component::getElement).toArray(Element[]::new);
        getElement().callJsFunction("doSomething", foo, bar, pPosition.getClientSideName(), toJsonArray(pElements));
}

private static JsonArray toJsonArray(Element[] pElements) {
    JsonArray array = Json.createArray();
    for (int i = 0; i < pElements.length; i++) {
        array.set(i, JsonCodec.encodeWithTypeInfo(pElements[i]));
    }

    return array;
}

Same, when trying to use the JsonUtils for conversion:

JsonUtils.listToJson(Arrays.asList(pElements))

The only workaround that I found is to split the calls into multiple calls:

getElement().callJsFunction("doSomething1", "foo", "bar");
getElement().callJsFunction("doSomething2", elements);

Sample exception

Part of an exception stack thrown, when an array is passed (in this case an array of Element)

Caused by: java.lang.IllegalArgumentException: Can't encode class [Lcom.vaadin.flow.dom.Element; to json
    at com.vaadin.flow.internal.JsonCodec.encodeWithoutTypeInfo(JsonCodec.java:208)
    at com.vaadin.flow.internal.JsonCodec.encodeWithTypeInfo(JsonCodec.java:95)
    at com.vaadin.flow.component.internal.UIInternals$JavaScriptvocation.<init>(UIInternals.java:118)
    at com.vaadin.flow.dom.Element.scheduleJavaScriptvocation(Element.java:1488)
    at com.vaadin.flow.dom.Element.callJsFunction(Element.java:1392)
stefanuebe commented 2 years ago

Not sure if enhancement is the correct tag, might also be counted as bug.

stefanuebe commented 2 years ago

Samples to reproduce the issue - simply comment out different use cases

@Route(value = "sample")
public class SampleView extends VerticalLayout {

    public SampleView() {
        Button button1 = new Button();
        Button button2 = new Button();
        Button[] buttonArray = Stream.of(button1, button2).toArray(Button[]::new);
        Element[] elements = Stream.of(buttonArray).map(Component::getElement).toArray(Element[]::new);

        // using components directly - exception
        getElement().executeJs("console.warn($0, $1, $2)", "foo", "bar", buttonArray);

        // using elements - exception
        getElement().executeJs("console.warn($0, $1, $2)", "foo", "bar", elements);

        // trying different flow internal approaches
        // encode - shows an array on the client side, but all null
        getElement().executeJs("console.warn($0, $1, $2)", "foo", "bar", toJsonArray(elements));

        // append and then encode - the elements are displayed in the ui, but the console output still shows an array of null values
        getElement().appendChild(elements);
        getElement().executeJs("console.warn($0, $1, $2)", "foo", "bar", toJsonArray(elements));

        // json utils - exception
        getElement().executeJs("console.warn($0, $1, $2)", "foo", "bar", JsonUtils.listToJson(Arrays.asList(elements)));

        // append and use multiple calls - this works
        getElement().executeJs("console.warn($0, $1)", "foo", "bar");
        getElement().appendChild(elements);
        getElement().executeJs("console.warn($0)", elements);
    }

   private static JsonArray toJsonArray(Element[] pElements) {
        JsonArray array = Json.createArray();
        for (int i = 0; i < pElements.length; i++) {
            array.set(i, JsonCodec.encodeWithTypeInfo(pElements[i]));
        }

        return array;
    }
}
stefanuebe commented 2 weeks ago

A simple workaround to allow the call of a single method with some automatic parsing of nested arrays.

public static void callFunctionWithDynamicParams(Element pElement, String pMethod, Serializable... pParameters) {
    int i = 0;
    String paramVars = "";

    List<Serializable> expandedParams = new LinkedList<>();

    for (Serializable parameter : pParameters) {
        if (parameter instanceof Serializable[] a) {
            paramVars += "[";
            for (Serializable o : a) {
                paramVars += "$" + i++ + ",";
                expandedParams.add(o);
            }

            paramVars = paramVars.substring(0, paramVars.length() - 1) + "]";

        } else {
            paramVars += "$" + i++ + ",";
            expandedParams.add(parameter);
        }
    }

    if(paramVars.endsWith(","))
        paramVars = paramVars.substring(0, paramVars.length() - 1);

    pElement.executeJs("this." + pMethod + "(" + paramVars + ")", expandedParams.toArray(new Serializable[0]));
}