3.5.17.3.4. Creating a GWT component

In this section, we will cover the creation of a simple GWT component (a rating field consisting of 5 stars) and its usage in application screens.

rating field component

Create a new project in CUBA Studio and name it ratingsample.

Create the web-toolkit module. A convenient way to do this is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage modules > Create 'web-toolkit' Module.

In order to create a GTW component, we need to create the following files:

  • RatingFieldWidget.java - a GWT widget in the web-toolkit module.

  • RatingFieldServerComponent.java - a Vaadin component class.

  • RatingFieldState.java - a component state class.

  • RatingFieldConnector.java - a connector that links the client code with the server component.

  • RatingFieldServerRpc.java - a class that defines a server API for the client.

Let’s create required files and make necessary changes in them.

  • Create the RatingFieldWidget class in the web-toolkit module. Replace its content with the following code:

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.google.gwt.dom.client.DivElement;
    import com.google.gwt.dom.client.SpanElement;
    import com.google.gwt.dom.client.Style.Display;
    import com.google.gwt.user.client.DOM;
    import com.google.gwt.user.client.Event;
    import com.google.gwt.user.client.ui.FocusWidget;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class RatingFieldWidget extends FocusWidget {
    
        private static final String CLASSNAME = "ratingfield";
    
        // API for handle clicks
        public interface StarClickListener {
            void starClicked(int value);
        }
    
        protected List<SpanElement> stars = new ArrayList<>(5);
        protected StarClickListener listener;
        protected int value = 0;
    
        public RatingFieldWidget() {
            DivElement container = DOM.createDiv().cast();
            container.getStyle().setDisplay(Display.INLINE_BLOCK);
            for (int i = 0; i < 5; i++) {
                SpanElement star = DOM.createSpan().cast();
    
                // add star element to the container
                DOM.insertChild(container, star, i);
                // subscribe on ONCLICK event
                DOM.sinkEvents(star, Event.ONCLICK);
    
                stars.add(star);
            }
            setElement(container);
    
            setStylePrimaryName(CLASSNAME);
        }
    
        // main method for handling events in GWT widgets
        @Override
        public void onBrowserEvent(Event event) {
            super.onBrowserEvent(event);
    
            switch (event.getTypeInt()) {
                // react on ONCLICK event
                case Event.ONCLICK:
                    SpanElement element = event.getEventTarget().cast();
                    // if click was on the star
                    int index = stars.indexOf(element);
                    if (index >= 0) {
                        int value = index + 1;
                        // set internal value
                        setValue(value);
    
                        // notify listeners
                        if (listener != null) {
                            listener.starClicked(value);
                        }
                    }
                    break;
            }
        }
    
        @Override
        public void setStylePrimaryName(String style) {
            super.setStylePrimaryName(style);
    
            for (SpanElement star : stars) {
                star.setClassName(style + "-star");
            }
    
            updateStarsStyle(this.value);
        }
    
        // let application code change the state
        public void setValue(int value) {
            this.value = value;
            updateStarsStyle(value);
        }
    
        // refresh visual representation
        private void updateStarsStyle(int value) {
            for (SpanElement star : stars) {
                star.removeClassName(getStylePrimaryName() + "-star-selected");
            }
    
            for (int i = 0; i < value; i++) {
                stars.get(i).addClassName(getStylePrimaryName() + "-star-selected");
            }
        }
    }

    A widget is a client-side class responsible for displaying the component in the web browser and handling events. It defines interfaces for working with the server side. In our case these are the setValue() method and the StarClickListener interface.

  • RatingFieldServerComponent is a Vaadin component class. It defines an API for the server code, accessor methods, event listeners and data sources connection. Developers use the methods of this class in the application code.

    package com.company.ratingsample.web.toolkit.ui;
    
    import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc;
    import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState;
    import com.vaadin.ui.AbstractField;
    
    // the field will have a value with integer type
    public class RatingFieldServerComponent extends AbstractField<Integer> {
    
        public RatingFieldServerComponent() {
            // register an interface implementation that will be invoked on a request from the client
            registerRpc((RatingFieldServerRpc) value -> setValue(value, true));
        }
    
        @Override
        protected void doSetValue(Integer value) {
            if (value == null) {
                value = 0;
            }
            getState().value = value;
        }
    
        @Override
        public Integer getValue() {
            return getState().value;
        }
    
        // define own state class
        @Override
        protected RatingFieldState getState() {
            return (RatingFieldState) super.getState();
        }
    
        @Override
        protected RatingFieldState getState(boolean markAsDirty) {
            return (RatingFieldState) super.getState(markAsDirty);
        }
    }
  • The RatingFieldState state class defines what data are sent between the client and the server. It contains public fields that are automatically serialized on server side and deserialized on the client.

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.vaadin.shared.AbstractFieldState;
    
    public class RatingFieldState extends AbstractFieldState {
        {   // change the main style name of the component
            primaryStyleName = "ratingfield";
        }
        // define a field for the value
        public int value = 0;
    }
  • The RatingFieldServerRpc interface defines a server API that is used from the client-side. Its methods may be invoked by the RPC mechanism built into Vaadin. We will implement this interface in the component.

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.vaadin.shared.communication.ServerRpc;
    
    public interface RatingFieldServerRpc extends ServerRpc {
        //method will be invoked in the client code
        void starClicked(int value);
    }
  • Create the RatingFieldConnector class in the web-toolkit module. Connector links client code with the server.

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
    import com.vaadin.client.communication.StateChangeEvent;
    import com.vaadin.client.ui.AbstractFieldConnector;
    import com.vaadin.shared.ui.Connect;
    
    // link the connector with the server implementation of RatingField
    // extend AbstractField connector
    @Connect(RatingFieldServerComponent.class)
    public class RatingFieldConnector extends AbstractFieldConnector {
    
        // we will use a RatingFieldWidget widget
        @Override
        public RatingFieldWidget getWidget() {
            RatingFieldWidget widget = (RatingFieldWidget) super.getWidget();
    
            if (widget.listener == null) {
                widget.listener = value ->
                        getRpcProxy(RatingFieldServerRpc.class).starClicked(value);
            }
            return widget;
        }
    
        // our state class is RatingFieldState
        @Override
        public RatingFieldState getState() {
            return (RatingFieldState) super.getState();
        }
    
        // react on server state change
        @Override
        public void onStateChanged(StateChangeEvent stateChangeEvent) {
            super.onStateChanged(stateChangeEvent);
    
            // refresh the widget if the value on server has changed
            if (stateChangeEvent.hasPropertyChanged("value")) {
                getWidget().setValue(getState().value);
            }
        }
    }

The RatingFieldWidget class does not define the component appearance, it only assigns style names to key elements. To define an appearance of the component, we’ll create stylesheet files. A convenient way to do this is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage themes > Create theme extension. Select the hover theme in the popup window. Another way is to use the extend-theme command in CUBA CLI. The hover theme uses FontAwesome font glyphs instead of icons. We’ll use this fact.

It is recommended to put component styles into a separate file componentname.scss in the components/componentname directory in the form of SCSS mixture. Create the components/ratingfield directories structure in the themes/hover/com.company.ratingsample directory of the web module. Then create the ratingfield.scss file inside the ratingfield directory:

gwt theme ext structure
@mixin ratingfield($primary-stylename: ratingfield) {
  .#{$primary-stylename}-star {
    font-family: FontAwesome;
    font-size: $v-font-size--h2;
    padding-right: round($v-unit-size/4);
    cursor: pointer;

    &:after {
          content: '\f006'; // 'fa-star-o'
    }
  }

  .#{$primary-stylename}-star-selected {
    &:after {
          content: '\f005'; // 'fa-star'
    }
  }

  .#{$primary-stylename} .#{$primary-stylename}-star:last-child {
    padding-right: 0;
  }

  .#{$primary-stylename}.v-disabled .#{$primary-stylename}-star {
    cursor: default;
  }
}

Include this file in the hover-ext.scss main theme file:

@import "components/ratingfield/ratingfield";

@mixin com_company_ratingsample-hover-ext {
  @include ratingfield;
}

To demonstrate how the component works let’s create a new screen in the web module.

Name the screen rating-screen.

Open the rating-screen.xml file in the IDE. We need a container for our component. Declare it in the screen XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="msg://caption"
        messagesPack="com.company.ratingsample.web.screens.rating">
    <layout expand="container">
        <vbox id="container">
            <!-- we'll add vaadin component here-->
        </vbox>
    </layout>
</window>

Open the RatingScreen.java screen controller and add the code that puts the component to the screen.

package com.company.ratingsample.web.screens.rating;

import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;
import com.vaadin.ui.Layout;

import javax.inject.Inject;

@UiController("ratingsample_RatingScreen")
@UiDescriptor("rating-screen.xml")
public class RatingScreen extends Screen {
    @Inject
    private VBoxLayout container;

    @Subscribe
    protected void onInit(InitEvent event) {
        RatingFieldServerComponent field = new RatingFieldServerComponent();
        field.setCaption("Rate this!");
        container.unwrap(Layout.class).addComponent(field);
    }
}

Start the application server and see the result.

rating screen result