Docs

Documentation versions (currently viewingVaadin 14)

You are viewing documentation for an older Vaadin version. View latest documentation

Demonstration: Address Book

How to build two Vaadin Portlets that communicate with each other as a multi-module project

This demonstration brings together all the topics covered in the previous chapters. The focus of this chapter is on using inter-portlet communication between two Vaadin Portlets. Using this document along with the demonstration code, you will be able to understand the demonstration code and build similar applications yourself.

The learning goals are:

  • Why events are needed

  • How to send events from one Vaadin Portlet to another

  • How to listen for events from other Vaadin Portlets

  • How to update the portlet’s state based on the event

The source code with a complete project for this example is available in

Address Book Project Structure

The address book demonstration shows how to build a simple address book application using Vaadin Portlets. The address book consists of two portlets. One portlet, Contact List, displays all the available contacts the user has. The other portlet, Contact Form, shows information about the contact selected via the contact list. The Contact Form portlet also allows the user to edit an existing contact.

The project is structured as a multi-module project and has the following four modules:

  • addressbook-backend
    Holds the shared data layer for both portlets.

  • addressbook-bundle
    Builds the frontend resource bundle for both portlets.

  • addressbook-form
    Contact information portlet.

  • addressbook-grid
    Contact list portlet.

  • portal
    Configuration for automatically setting up and running the Pluto portal using the Cargo plugin.

In this chapter, we focus on portlet implementations. To get more familiar with setting up and maintaining a multi-module Vaadin Portlet project, take a look at the next chapter, Creating a Multi-Module Portlet Project.

Structure

The address book consists of two portlets and their view components. The classes ending in Portlet extend VaadinPortlet/VaadinLiferayPortlet, and classes ending in View are the view component classes for the portlets.

  • Contact List: ContactListPortlet and ContactListView (in the addressbook-grid module)

    Responsible for displaying the available contacts in the address book. Allows the user to select a contact to be displayed in the Contact Form.

  • Contact Form: ContactFormPortlet and ContactFormView (in the addressbook-form module)

    Responsible for displaying and modifying the contact information in the address book.

The other classes are there to provide business logic and mock implementations for data services

Functionality Requirements as Portlets

Contact List

The portlet operates in only one portlet mode – view – and the user cannot change the mode. When the user selects a contact from the list, that contact’s details are displayed on the Contact Form portlet. When the contact’s information is changed in the Contact Form portlet, the relevant contact information is updated on the list. The list shows a limited number of contact attributes, including first name, last name, and phone number. When the portlet is maximized (its window state is changed to MAXIMIZED), more attributes are shown, including email and birthday.

In order to provide this functionality, the Contact List's view class needs to be able to

  • receive events

  • send events

  • react to window state changes

These requirements can be fulfilled by implementing the PortletView interface.

Contact Form

The portlet supports two portlet modes: view and edit. When the portlet is in view mode, the contact information is only displayed and cannot be changed. When user changes the portlet to edit mode, the information can be updated. After editing the information, the user can either save or discard the changes.

In order to provide this functionality, the Contact List view class needs to be able to

  • react to portlet mode changes

  • receive events

  • send events

These requirements can be fulfilled by implementing the PortletView interface.

Note
We use PortletView in both view classes, as it provides a single entry point to the portlet state. We can both register event listeners and mutate the portlet’s state through the PortletViewContext object. However, if we were interested in only very particular types of updates and did not need to change the state, we could simply implement the PortletModeHandler, WindowStateHandler, or EventHandler interfaces

Implementation Details

In this section, we have a look at selected parts of the view classes ListPortletView and FormPortletView. The classes contain code that is not directly related to the portlet implementation and we will skip those parts.

public class ContactListView extends VerticalLayout implements PortletView {
    private ListDataProvider<Contact> dataProvider;

    private Grid<Contact> grid = new Grid<>(Contact.class);

    private PortletViewContext portletViewContext;

    @Override
    public void onPortletViewContextInit(PortletViewContext context) {
        // save context for sending events
        portletViewContext = context;

        // add event listeners for both "contact-updated" custom event
        // and window state change event
        context.addEventChangeListener("contact-updated",
                this::onContactUpdated);
        context.addWindowStateChangeListener(
                event -> handleWindowStateChanged(event.getWindowState()));
        init();
    }

    private void onContactUpdated(PortletEvent event) {
        int contactId = Integer
                .parseInt(event.getParameters().get("contactId")[0]);
        // retrieve the contact information from contact service
        Optional<Contact> contact = getService()
                .findById(contactId);
        // update grid's data provider with the updated contact
        contact.ifPresent(value -> dataProvider.refreshItem(value));
    }

    private ContactService getService() {
        // returns ContactService instance
    }

    private void handleWindowStateChanged(WindowState windowState) {
        if (WindowState.MAXIMIZED.equals(windowState)) {
            grid.setColumns("firstName", "lastName", "phoneNumber", "email",
                    "birthDate");
            grid.setMinWidth("700px");
            // ... rest of the configuration
        } else if (WindowState.NORMAL.equals(windowState)) {
            grid.setColumns("firstName", "lastName", "phoneNumber");
            grid.setMinWidth("450px");
            // ... rest of the configuration
        }
    }

    private void fireSelectionEvent(
            ItemClickEvent<Contact> contactItemClickEvent) {
        // get contact id
        Integer contactId = contactItemClickEvent.getItem().getId();

        // save the id into a string-to-string map
        Map<String, String> param = Collections.singletonMap(
                "contactId", contactId.toString());

        // send the event with name "contact-selected"
        portletViewContext.fireEvent("contact-selected", param);
    }

    private void init() {
        // ... grid initialization

        // add item click listener which fires our contact-selected event
        grid.addItemClickListener(this::fireSelectionEvent);

        // ... rest of the configuration
    }
}

The ContactListView view implements the PortletView interface. The onPortletViewContextInit(PortletViewContext) method in the PortletView interface gives the implementing class a reference to a PortletViewContext object, which allows us to register listeners and change the portlet’s state. Apart from onPortletViewContextInit(), the ContactListView has three important methods from the portlet perspective: fireSelectionEvent(), handleWindowStateChanged(), and contactUpdated(). Firing the selection event is triggered when user selects a contact in the list. The method creates a parameter map that contains the id of the selected contact. We then use our portletViewContext instance to send the event under the name contact-selected. Other Vaadin Portlet views that have registered listeners for this event name will be notified about the event.

handleWindowStateChanged() is registered as a listener for the WindowStateChange event. It is called when, for example, the portlet view is maximized or normalized. In this method, when the window state is changed to maximized, the minimum width of the grid increased and more grid columns are shown.

The other method, contactUpdated(), is registered as an event listener for the contact-updated event via a PortletViewContext instance. The contact-updated event has the same parameters as the contact-selected event. We use the contact id to update the correct contact information in the list.

public class ContactFormView extends VerticalLayout implements PortletView {
    private static final String ACTION_EDIT = "Edit";
    private static final String ACTION_CREATE = "Create new";
    private static final String ACTION_SAVE = "Save";

    private PortletViewContext portletViewContext;

    private Binder<Contact> binder;
    private Contact contact;

    private Button action;
    // ... other components

    @Override
    public void onPortletViewContextInit(PortletViewContext context) {
        // save context for sending events
        this.portletViewContext = context;
        // add event listeners for both "contact-selected" custom event
        // and portlet mode change event
        context.addEventChangeListener("contact-selected",
                this::onContactSelected);
        context.addPortletModeChangeListener(this::handlePortletModeChange);
        init();
    }

    // handles "contact-selected" event from PortletListView.
    // we check that the contact ID parameter is correct and that the contact exists.
    // then we display the contact information on the form.
    private void onContactSelected(PortletEvent event) {
        int contactId = Integer
                .parseInt(event.getParameters().get("contactId")[0]);
        Optional<Contact> contact = getService().findById(contactId);
        if (contact.isPresent()) {
            // ... set active contact
            this.contact = contact.get();
            // ... update the form
        } else {
            // ... empty the form
            clear();
        }
    }

    // called when the portlet mode changes
    // FormPortlet supports two modes: 'view' and 'edit'
    private void handlePortletModeChange(PortletModeEvent event) {
        // set fields to read-only mode when portlet mode is 'view'
        binder.setReadOnly(event.isViewMode());

        // set the button's text based on the portlet mode
        if (event.isViewMode()) {
            action.setText(ACTION_EDIT);
        } else {
            action.setText(ACTION_SAVE);
        }
    }

    private void fireUpdateEvent(Contact contact) {
        Map<String, String> param = Collections
                .singletonMap("contactId", contact.getId().toString());

        portletViewContext.fireEvent("contact-updated", param);
    }

    private PortletMode getPortletMode() {
        return portletViewContext.getPortletMode();
    }

    private void init() {
        // ... create the form layout
        setupButtons();

        // ... add components to form
    }

    private ContactService getService() {
        // returns ContactService instance
    }

    private void setupButtons() {
        action = new Button("action", event -> {
            if (PortletMode.EDIT.equals(getPortletMode())) {
                save();
            } else {
                portletViewContext.setPortletMode(PortletMode.EDIT);
            }
        });

        // ... setup rest of the buttons
    }

    private void clear() {
        // ... reset contact and clear form
    }

    private void save() {
        if (contact != null) {
            // ... save contact
        } else {
            // ... create new contact
        }
        // send custom portlet event
        fireUpdateEvent(contact);

        // ... update form

        // sent portlet mode back to view
        portletViewContext.setPortletMode(PortletMode.VIEW);
    }
}

ContactFormView uses the PortletViewContext received via the onPortletViewContextInit(PortletViewContext) method to register an event listener and portlet mode listener. The important methods for the portlet operation are handlePortletMode() and onContactSelected(). The ContactFormView supports two portlet modes – view and edit – which are declared in portlet.xml. In the handlePortletMode() method, depending on the portlet mode, we either enable or disable editing on the form fields. We also change the name of the action button to correspond to the correct mode.

The onContactSelected() method is called when the contact-selected event is sent by the Contact List portlet. When the event arrives, the contact id is used to display information for the selected Contact.

E34EF2E1-1EDE-40F7-ABD4-4494C83FAC5F