Creating an Embedded Vaadin Application Tutorial

In this section, we demonstrate how to create a basic embedded application: that is, an application that can be embedded in another page.

Note
The technology you use to create the host page is irrelevant: you can use Vaadin or non-Vaadin server-side Java technologies, like JSP, Thymeleaf, or servlet, or even a basic static HTML page.

In order to be able to embed a Vaadin application, you need to create:

  • An entry point. This is achieved by creating a WebComponentExporter for the Vaadin Component you want to embed, and

  • A VaadinServlet to handle requests to your web component. The servlet can be declared in a non-Vaadin application, or deployed separately as a standalone WAR file.

Our scenario uses:

  • A single custom servlet to handle the main application logic.

    • The servlet displays primarily static content.

    • The content differs depending on whether a user is logged in or not.

  • An embeddable Vaadin Component to implement a login form.

Note
VaadinServlet forms part of the application, but its mapping differs from the main servlet mapping.

You can start with a clean project, or start building on top of the Vaadin Base Starter (download the "Plain Java Servlet" version).

  1. Create the MainAppServlet servlet class.

    This servlet serves the static page which embeds our login form as a web component.

    @WebServlet(urlPatterns = {"/example"})
    public class MainAppServlet extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse response)
                throws ServletException, IOException {
            response.setContentType("text/html;charset=UTF-8");
    
            Object authToken = req.getSession().getAttribute("auth_token");
            boolean isAuthenticated = authToken != null;
    
            try (PrintWriter out = response.getWriter()) {
                out.println("<!DOCTYPE html>");
                out.println("<html><head>");
                out.println("<meta http-equiv='Content-Type' content='text/html; "
                        + "charset=UTF-8'>");
    
                if (!isAuthenticated) {
                    out.println("<script type='text/javascript' "
                            + "src='/vaadin/VAADIN/build/webcomponentsjs/"
                            + "webcomponents-loader.js'></script>");
                    out.println("<script type='module' src='/vaadin/web-component"
                            + "/login-form.js'></script>");
                    out.println("<script type='text/javascript' "
                            + "src='/log-in.js' defer></script>");
                }
                out.println("</head><body>");
                if (isAuthenticated) {
                    out.println("<h1>Welcome "
                            + UserService.getInstance().getName(authToken)
                            + "</h1>");
                } else {
                    out.println("<login-form userlbl='Username' pwdlbl='Password'>"
                            + "</login-form>");
                }
                out.println("</body>");
                out.println("</html>");
            }
        }
    }

    Let’s analyze the HTML content generated by MainAppServlet:

    • This line loads the web component polyfill:

      <script type='text/javascript'
              src='/vaadin/VAADIN/build/webcomponentsjs/webcomponents-loader.js'>
      </script>
    • This line loads the web component:

      <script type='module'
              src='/vaadin/web-component/login-form.js'>
      </script>
      • Both script src attributes start with /vaadin/. This is the URI to use to map to the Vaadin servlet (see step 3 below).

      • The second part the link URI,/web-component/login-form.js, is the standard URI to use to import the web component. It consists of the hard-coded web-component part, followed by login-form.js, which is the web component file. The web component file is generated by Vaadin, based on the configuration set in the exporter.

    • The name of the web component in our example must be "login-form". This name must be used in both the super constructor of the exporter (see LoginFormExporter in step 5 below) and the HTML code where the web component is inserted. In our example this is right under the <body> tag:

      <login-form userlbl='Username' pwdlbl='Password'>
      </login-form>
      • The "login-form" web component has two properties, userlbl and pwdlbl. These values are passed from the HTML to a web component instance.

        Note
        If embedding applications is targeted towards very specific browsers, the polyfill is not needed. For example Chrome and Firefox do not need the polyfill while Edge does.
Note
The generated "login-form" element is a web component which wraps the original LoginForm HTML representation element ("div" , see below). This element is inside wrapper’s shadow root and it’s not available outside it. So its styles don’t leak to the page and global page styles don’t affect the styles of the embedded component.
  1. Create the UserService class. This class contains the authentication logic and is shared between the MainAppServlet and the web component class (created in step 5 below). You can use any interface and implementation you like. This example is a custom stub implementation and is provided as a reference.

    public final class UserService {
    
        private static final UserService INSTANCE =
                new UserService();
    
        private UserService(){
        }
    
        public static UserService getInstance() {
            return INSTANCE;
        }
    
        public String getName(Object authToken) {
            return "Joe";
        }
    
        public Optional<Object> authenticate(String user,
                String passwd) {
            if ("admin".equals(user) &&
                    "admin".equals(passwd)) {
                return Optional.of(new Object());
            } else {
                return Optional.empty();
            }
        }
    }
  2. Register the VaadinServlet.

    @WebServlet(urlPatterns = { "/vaadin/*" })
    public class WebComponentVaadinServlet extends VaadinServlet {
    }
    • As mentioned above, the /vaadin/* mapping allows the VaadinServlet to handle web component requests. You can use any URI, but be sure to use the same URI in the mapping and in the import declaration.

      Note
      If you deploy your web component exporter(s) as a standalone WAR application, an explicit servlet registration is unnecessary. A servlet instance is registered automatically with the "/*" mapping.
  3. Create the LoginForm component class.

    public class LoginForm extends Div {
        private TextField userName = new TextField();
        private PasswordField password =
                new PasswordField();
        private Div errorMsg = new Div();
        private String userLabel;
        private String pwdLabel;
        private FormLayout layout = new FormLayout();
        private List<SerializableRunnable> loginListeners =
                new CopyOnWriteArrayList<>();
    
        public LoginForm() {
            updateForm();
    
            add(layout);
    
           Button login = new Button("Login",
                    event -> login());
            add(login, errorMsg);
        }
    
         public void setUserNameLabel(
                String userNameLabelString) {
            userLabel = userNameLabelString;
            updateForm();
        }
    
        public void setPasswordLabel(String pwd) {
            pwdLabel = pwd;
            updateForm();
        }
    
        public void updateForm() {
            layout.removeAll();
    
            layout.addFormItem(userName, userLabel);
            layout.addFormItem(password, pwdLabel);
        }
    
        public void addLoginListener(
                SerializableRunnable loginListener) {
            loginListeners.add(loginListener);
        }
    
        private void login() {
            Optional<Object> authToken = UserService
                    .getInstance()
                    .authenticate(userName.getValue(),
                        password.getValue());
            if (authToken.isPresent()) {
                VaadinRequest.getCurrent()
                        .getWrappedSession()
                        .setAttribute("auth_token",
                                authToken.get());
                fireLoginEvent();
            } else {
                errorMsg.setText("Authentication failure");
            }
        }
    
        private void fireLoginEvent() {
            loginListeners.forEach(
                    SerializableRunnable::run);
        }
    }
    • The example uses several Vaadin components: FormLayout, TextField, PasswordField and Button.

    • The code takes care of authentication and sets an authentication token in the HttpSession, which makes it available while the session is live.

    • Because the main application servlet uses the same HttpSession instance, it changes behavior and redirects authenticated users to the main servlet that now shows content specific to authenticated users. There are various ways to do this:

      • Execute JavaScript directly from your Java code and set the location to "/example" : getUI().get().getPage().executeJs("window.location.href='/example'");.

      • Use a solution similar to this example: design the component code so that its logic is isolated and it does not need to know anything about the embedding context. This method allows you to completely decouple the embedded component logic from the application that uses it. In this example, the addLoginListener method allows you to register a listener which is called in the fireLoginEvent method.

  4. The final step is to export the LoginForm component as an embeddable web component using the web component exporter.

    public class LoginFormExporter
            extends WebComponentExporter<LoginForm> {
        public LoginFormExporter() {
            super("login-form");
            addProperty("userlbl", "")
                    .onChange(LoginForm::setUserNameLabel);
            addProperty("pwdlbl", "")
                    .onChange(LoginForm::setPasswordLabel);
        }
    
        @Override
        protected void configureInstance(
                WebComponent<LoginForm> webComponent,
                LoginForm form) {
            form.addLoginListener(() ->
                    webComponent.fireEvent("logged-in"));
        }
    }
    • The exporter defines its tag name as "login-form" by calling the super constructor super("login-form");.

    • The addProperty method defines the component properties (userlbl='Username' and `pwdlbl='Password') to receive values from the HTML element to the web component instance. In this example we declare the labels for user name field and password field via HTML, instead of hard-coding them in the LoginForm component class.

    • LoginFormExporter class implements the abstract method, configureInstance, which registers a login listener.

    • The login listener fires a client-side "logged-in" event, using the webcomponent.fireEvent() method. The main application needs to handle this event somehow.

    • The custom event is handled by the JavaScript file declared via the line <script type='text/javascript' src='log-in.js'></script> in MainAppServlet. This is the log-in.js file content:

      • Place the log-in.js under ./src/main/webapp/

        var editor = document.querySelector("login-form");
        editor.addEventListener("logged-in", function(event) {
            window.location.href='/example';
        });
        
    • The embedding servlet uses the API provided by LoginForm via a custom event and adds an event listener for the event. The listener simply redirects the page to the "/example" location.

  5. Run the application with mvn jetty:run. Once jetty has started, navigate to http://localhost:8080/example.

    • Type in login information and click on "Login" button

      • Username: admin

      • Password: admin