vaadin / flow

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

Use custom Flow components in Hilla views #18401

Closed mshabarov closed 9 months ago

mshabarov commented 10 months ago

Describe your motivation

Hilla framework gives you an opportunity to develop UI using Lit or React libraries using corresponding Vaadin components: React or Lit-based.

For unification reasons, Vaadin should also allow to use custom Flow components on the Hilla views, like follows (example for Lit):

@customElement('hilla-view')
export class HillaView extends LitElement {

  render() {
    return html`
      <vaadin-vertical-layout>
          <h1>Hilla view</h1>
          <login-form userlbl='Username' pwdlbl='Password'>Login Form</login-form>
      </vaadin-vertical-layout>
    `;
  }
}

Where the login-form is a Flow server-side component:

public class LoginForm extends Div {
    private TextField userName = new TextField();
    private PasswordField password = new PasswordField();
    // implementation omitted
}

Describe the solution you'd like

The Flow component can be exported using WebComponentExporter API:

public class LoginFormExporter
        extends WebComponentExporter<LoginForm> {
    public LoginFormExporter() {
        super("login-form");
        addProperty("userlbl", "")
                .onChange(LoginForm::setUserNameLabel);
        addProperty("pwdlbl", "")
                .onChange(LoginForm::setPasswordLabel);
    }
}

and adding the web-component's script into index.ts:

const script = document.createElement('script');
script.src = '/web-component/login-form.js';
document.head.appendChild(script);

However, this doesn't work out-of-the-box with both Lit and React, the following errors are shown up in the browser console preventing Hilla to render the component:

VM475:1 Uncaught TypeError: window.Vaadin.Flow.initApplication is not a function
    at <anonymous>:1:8053
    at <anonymous>:1:8098
    at web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:6:8583
    at web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:10:3
(anonymous) @ VM475:1
(anonymous) @ VM475:1
(anonymous) @ web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:6
(anonymous) @ web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:10

VM477:1 Uncaught SyntaxError: Unexpected identifier 'RefreshRuntime'
    at web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:6:9152
    at web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:10:3
(anonymous) @ web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:6
(anonymous) @ web-component-bootstrap.js?url=http://localhost:8080/web-component/web-component-bootstrap.js:10

FlowClient.js:628 Uncaught TypeError: $wnd.Vaadin.Flow.registerWidgetset is not a function
    at Wo (FlowClient.js:628:46)
    at To (FlowClient.js:993:263)
    at Array.Ui (FlowClient.js:200:20)
    at g (FlowClient.js:893:114)
    at Eb (FlowClient.js:458:29)
    at Hb (FlowClient.js:918:157)
    at FlowClient.js:644:46
    at Ki (FlowClient.js:894:46)
    at yb (FlowClient.js:3:26)
    at Ib (FlowClient.js:14:707)
    at FlowClient.js:15:144

Apparently, the Flow client part isn't completely initialised and web-component cannot be initialised in turn. This should be properly handled by WebComponentBootstrapHandler/ WebComponentProvider/WebComponentExporter implementation.

However, if I open the Flow view and navigate back to the Hilla view with my component, it's rendered fine, which proves my assumption of incomplete Flow initialisation.

This error can be reproduced with Hilla-React or Hilla-Lit examples:

  1. Start the app, open http://localhost:8080/hilla in the browser
  2. Notice that no login form is rendered on the page and check that JS errors are shown in the console.
  3. Navigate to the Flow view using side menu.
  4. Refresh the page.
  5. Navigate back to Hilla view using browser's back button.
  6. See the component.

You can also check that window.Vaadin.Flow.registerWidgetset is defined when component is rendered, and undefined when not.

Describe alternatives you've considered

Apart from the essential problem described above, the example projects show the following inconveniences:

Additional context

When I navigate to the Flow view and do a refresh in step 4 (see above), I got the following error on server that prevents the page to render:

2024-01-05T16:16:56.438+02:00 ERROR 9512 --- [nio-8080-exec-9] .v.f.s.c.r.AttachTemplateChildRpcHandler : Attach existing element has failed because the client-side element is not found
2024-01-05T16:16:56.440+02:00 ERROR 9512 --- [nio-8080-exec-9] c.v.flow.server.DefaultErrorHandler      : 

java.lang.IllegalStateException: The element with the tag name 'flow-container-wc-root-2521314' and id 'wc-ROOT-2521314' was not found in the parent with id='1'
        at com.vaadin.flow.server.communication.rpc.AttachTemplateChildRpcHandler.handleNode(AttachTemplateChildRpcHandler.java:85) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.communication.rpc.AbstractRpcInvocationHandler.handle(AbstractRpcInvocationHandler.java:74) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocationData(ServerRpcHandler.java:466) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.communication.ServerRpcHandler.lambda$handleInvocations$4(ServerRpcHandler.java:447) ~[flow-server-24.3.2.jar:24.3.2]
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na]
        at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:447) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:324) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:114) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:40) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1573) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:398) ~[flow-server-24.3.2.jar:24.3.2]
        at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:106) ~[vaadin-spring-24.3.2.jar:na]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.16.jar:6.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:642) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:408) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:313) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:277) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.springframework.web.servlet.mvc.ServletForwardingController.handleRequestInternal(ServletForwardingController.java:141) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at org.springframework.web.servlet.mvc.AbstractController.handleRequest(AbstractController.java:178) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:51) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.16.jar:6.0]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.2.jar:6.1.2]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.16.jar:6.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.2.jar:6.1.2]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.2.jar:6.1.2]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.2.jar:6.1.2]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.2.jar:6.1.2]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.2.jar:6.1.2]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.2.jar:6.1.2]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
        at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

Not sure if this is a part of the same issue or a separate one. A new ticket can be created for it later, if it has a different root cause.

caalador commented 9 months ago

A clean sample for adding the flow component could be:

import {VerticalLayout} from "@vaadin/react-components/VerticalLayout.js";
import React, {useEffect} from "react";

const LOGIN_COMPONENT = "my-flow-component";

function MyLoginView() {
    loadComponentScript(LOGIN_COMPONENT);
    return React.createElement(LOGIN_COMPONENT);
}

const loadComponentScript = (name: String) => {
    useEffect(() => {
        const script = document.createElement('script');
        script.src = `/web-component/${name}.js`;
        document.head.appendChild(script);

        return () => {
            document.head.removeChild(script);
        }
    }, []);
}

export default function HillaView() {
    return (
        <>
            <VerticalLayout className={'centered-content'}>
                <h3>Hilla View</h3>
                <MyLoginView/>
            </VerticalLayout>
        </>
    );
}

from where loadComponentScript could be moved to Flow.tsx for instance making the sample:

import {VerticalLayout} from "@vaadin/react-components/VerticalLayout.js";
import React, {useEffect} from "react";
import { loadComponentScript } from "Frontend/generated/flow/Flow";

function MyLoginView() {
    const LOGIN_COMPONENT = "my-flow-component";
    loadComponentScript(LOGIN_COMPONENT);
    return React.createElement(LOGIN_COMPONENT);
}

export default function HillaView() {
    return (
        <>
            <VerticalLayout className={'centered-content'}>
                <h3>Hilla View</h3>
                <MyLoginView/>
            </VerticalLayout>
        </>
    );
}
caalador commented 9 months ago

Documentation start:

---
title: Flow components in Hilla view
order: 10
---

= Use Custom Flow Components In Hilla

Custom Flow components can be embedded into Hilla views by implementing a [classname]`WebComponentExporter` and using it in the view.

A [classname]`WebComponentExporter` can target any Flow Component, for instance the following component:

[source,java]
----
public class CustomComponent extends Div {

    public CustomComponent(@Autowired GreetService service) {
        Button button = new Button("Say hello", e -> {
            Notification.show("Hello!");
        });

        add(button);
    }
}
----

would be made into a [classname]`WebComponent` like:

[source,java]
----
public class MyFlowComponentExporter
        extends WebComponentExporter<CustomComponent> {

    public static final String TAG = "my-flow-component";

    public MyFlowComponentExporter() {
        super(TAG);
    }

    @Override
    protected void configureInstance(WebComponent<CustomComponent> webComponent,
                                     CustomComponent component) {
    }
}
----

For more information see <<{articles}/flow/integrations/embedding/exporter,Creating an Embedded Vaadin Application>>

To add the exported [classname]`WebComponent` to a hilla view import `createWebComponent` from Flow and create a React.DOMElement for the [classname]`WebComponent` `TAG` and then use the element inside the view layout.

[source,typescriptjsx]
----
import { VerticalLayout } from "@vaadin/react-components/VerticalLayout";
import { createWebComponent } from "Frontend/generated/flow/Flow";

function MyLoginView() {
  return createWebComponent("my-flow-component");
}

export default function HillaView() {
    return (
        <>
            <VerticalLayout className={'centered-content'}>
                <h3>Hilla View</h3>
                <MyLoginView/>
            </VerticalLayout>
        </>
    );
}
----

If the component needs attributes or other custom parts in `React.createElement` then instead of importing `createWebComponent` import `loadComponentScript` and create the element in the function.
[source,typescriptjsx]
----
import { VerticalLayout } from "@vaadin/react-components/VerticalLayout";
import { loadComponentScript } from "Frontend/generated/flow/Flow";
import React from "react";

function MyLoginView() {
  loadComponentScript("my-flow-component");
  // Use the wanted createElement here!
  return React.createElement("my-flow-component");
}

export default function HillaView() {
    return (
        <>
            <VerticalLayout className={'centered-content'}>
                <h3>Hilla View</h3>
                <MyLoginView/>
            </VerticalLayout>
        </>
    );
}
----

[discussion-id]`920dc03d-5eb4-4826-8934-4416b58a9a3e`

++++
<style>
[class^=PageHeader-module--descriptionContainer] {display: none;}
</style>
++++
mshabarov commented 9 months ago

The documentation part is not merged yet, but I propose to handle it separately via normal review.