1

My intention: I'd like to use a HTML-/JavaScript component in a Vaadin (23.1.4) application. This component is hidden initially (because its parent wrapper component is hidden). The component should get (JavaScript-)updates from server-side.

Problem: As this component initially is hidden, it is not part of the DOM yet. And therefore it is not available to get updates with JavaScript commands from server-side.

Question: Is there an event for a component when it gets visible / un-hidden (because a parent components gets visible / un-hidden)?

What I tried / what does not seem to be a solution:

  • the method setVisible(boolean) in MyCustomField is not called (because it is wrapped in a parent component). So I can't listen to that.
  • an AttachListener of MyCustomField is called when the UI loads. It's not called when MyCustomField is getting un-hidden.
  • method setEnabled(boolean) in MyCustomField is not called. So I can't listen to that.

Possible workarounds:

  • Show all components in the form and hide them later. This is not the intended approach for me because there are some components that need some time to load. All of them loaded initially would need a lot of time.
  • Execute the JavaScript update commands within an interval that checks the visibility of MyCustomField and executes all collected commands when MyCustomField is visible.

Code to reproduce:

This is a very small custom field that updates some of its content by JavaScript:

private static class MyCustomField extends CustomField<String> {
    
    public MyCustomField() {
        this.add(new Html("<div id=\"myField\">Initial value</div>"));
    }

    protected void setPresentationValue(String newPresentationValue) {
        UI.getCurrent().getPage().executeJs("document.getElementById('myField').innerHTML='"+newPresentationValue+"';");
    }

    protected String generateModelValue() {return null;}
}

And this is my View:

@Route("myview")
public class MyView extends VerticalLayout {

public MyView() {
    MyCustomField myCustomField = new MyCustomField();
    
    Div wrapperComponent = new Div();
    wrapperComponent.setId("wrapper");
    wrapperComponent.setVisible(false);
    wrapperComponent.add(myCustomField);

    Button toggleVisibilityButton = new Button("Toggle visibility");
    toggleVisibilityButton.addClickListener(clickEvent -> {
        wrapperComponent.setVisible(!wrapperComponent.isVisible());
    });
    
    Button setNewValueButton = new Button("Set new value");
    setNewValueButton.addClickListener(clickEvent -> {
        myCustomField.setPresentationValue(LocalDateTime.now().toString());
    });
    
    this.add(wrapperComponent, toggleVisibilityButton, setNewValueButton);
}

When the view loads, there is only this empty hidden wrapper element: <div hidden="true"></div> and two buttons.

When the user clicks the "Set new value" button, the browser shows an error: document.getElementById(...) is null (because the element 'myField' does not exist in the DOM yet). So here some JavaScript-actions get lost.

When the user first clicks the "Toggle visibility" button and then the "Set new value" button, then everything works fine.

S. Doe
  • 685
  • 1
  • 6
  • 25

1 Answers1

-1

In the meantime I found a working approach.

  1. Server creates a client-side array in the current page (that is not part of MyCustomField and therefore not hidden).
  2. Server pushes commands into that array whenever it likes.
  3. MyCustomField gets visible at an arbitrary time and executes executeDelayed(),
  4. which executes every collected command.

Necessary adjustment to instantiate multiple MyCustomField instances: extend the name of the commandsToExecuteLater variable by an identifier so that there is one command array/queue for every instance of MyCustomField. I didn't paste it here to keep the code simple/readable.

Code:

private static class MyCustomField extends CustomField<String> {
    
    public MyCustomField() {
        // (1) Initialize an array immediately (explicitly not as part of the component).
        UI.getCurrent().getPage().executeJs("window.commandsToExecuteLater=new Array();");
        
        this.add(new Html("<div id=\"myField\">Initial value</div>"));
        //@formatter:off
        this.add(new Html("<script>"
                // (4) Function to execute the collected commands delayed
                + "function executeDelayed() {"
                    + "if(document.getElementById('myField')!=null){"
                        + "var command=window.commandsToExecuteLater.shift();"
                        + "while(command!=null) {"
                            + "eval(command);"
                            + "command=window.commandsToExecuteLater.shift();"
                        + "}"
                    + "}"
                + "}"
                // (3) this command is executed then MyCustomField gets visible
                + "executeDelayed();</script>"));
        //@formatter:on
    }

    protected void setPresentationValue(String newPresentationValue) {
        // (2) Add a command to the collection of commands to execute delayed / later
        UI.getCurrent().getPage().executeJs("window.commandsToExecuteLater.push(\"document.getElementById('myField').innerHTML='"+newPresentationValue+"'\");");
        // If MyCustomField is visible then the function is visible. Execute the delayed commands then.
        UI.getCurrent().getPage().executeJs("if (typeof executeDelayed === \"function\") {executeDelayed();}");
    }

    protected String generateModelValue() {
        return null;
    }
    
}
S. Doe
  • 685
  • 1
  • 6
  • 25
  • 1
    With so much custom JavaScript, I would highly recommend to take a look at a custom client side Implementation based on lit and use the created lit component on the server side. – Knoobie Aug 13 '22 at 18:48