3.5.15.3.4. Создание GWT компонента

В данном примере мы рассмотрим создание простого GWT-компонента - поля рейтинга в виде 5 звезд, а также использование его в экранах приложения.

rating field component

Создадим новый проект в CUBA Studio. Имя проекта - ratingsample.

Создайте модуль web-toolkit. Его удобно создать с помощью CUBA Studio: В главном меню нажмите CUBA > Advanced > Manage modules > Create 'web-toolkit' Module.

Чтобы создать GWT компонент, необходимо создать следующие файлы:

  • RatingFieldWidget.java - виджет GWT в модуле web-toolkit.

  • RatingFieldServerComponent.java - класс компонента Vaadin.

  • RatingFieldState.java - класс состояния компонента.

  • RatingFieldConnector.java - коннектор, связывающий клиентский код с серверным компонентом.

  • RatingFieldServerRpc.java - класс, определяющий API сервера для клиентской части.

Создадим требуемые файлы и внесем в них необходимые изменения.

  • Создайте в модуле web-toolkit класс RatingFieldWidget. Замените содержимое файла на следующий код:

    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");
            }
        }
    }

    Виджет представляет собой клиентский класс, отвечающий за отображение компонента в веб-браузере и реакцию на события. Он определяет интерфейсы для работы с серверной частью. В нашем случае это метод setValue() и интерфейс StarClickListener.

  • Класс компонента Vaadin RatingFieldServerComponent. Он определяет API для серверного кода, различные get/set методы, слушатели событий и подключение источников данных. Прикладные разработчики используют в своём коде методы этого класса.

    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);
        }
    }
  • Класс состояния RatingFieldState отвечает за то, какие данные будут пересылаться между клиентом и сервером. В нём определяются публичные поля, которые будут автоматически сериализованы на сервере и десериализованы на клиенте.

    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;
    }
  • Интерфейс RatingFieldServerRpc — определяет API сервера для клиентской части, его методы могут вызываться с клиента при помощи механизма удалённого вызова процедур, встроенного в Vaadin. Этот интерфейс мы реализуем в самом компоненте, в данном случае просто вызываем метод setValue() нашего поля.

    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);
    }
  • Создайте в модуле web-toolkit класс RatingFieldConnector. Коннектор связывает клиентский код с серверной частью.

    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);
            }
        }
    }

Код виджета RatingFieldWidget не определяет внешний вид компонента, кроме назначения имён стилей ключевым элементам. Для того, чтобы определить внешний вид нашего компонента, создадим файлы стилей. Это удобно сделать с помощью CUBA Studio: В главном меню нажмите CUBA > Advanced > Manage themes > Create theme extension. Другой способ - использовать команду extend-theme в CUBA CLI. В списке тем для расширения выберем hover и нажмем кнопку Create. Эта тема использует вместо значков глифы шрифта FontAwesome, чем мы и воспользуемся.

Стили каждого компонента принято выделять в отдельный файл componentname.scss в каталоге components/componentname в формате примеси SCSS. В каталоге themes/hover/com.company.ratingsample модуля web создадим структуру вложенных каталогов: components/ratingfield. Затем внутри ratingfield создадим файл ratingfield.scss:

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;
  }
}

Подключим этот файл в главном файле темы hover-ext.scss:

@import "components/ratingfield/ratingfield";

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

Для демонстрации работы компонента создадим новый экран в модуле web.

Назовите экран rating-screen.

Перейдем к редактированию экрана rating-screen.xml в IDE. Нам понадобится контейнер для нашего компонента, объявим его в 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>

Откроем класс контроллера экрана RatingScreen.java и добавим код размещения нашего компонента на экране:

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);
    }
}

Запускаем сервер приложения и смотрим на результат.

rating screen result