Создание веб-компонента
Web Components можно создавать с нуля на основе библиотек Polymer 3 или Lit. После реализации веб-компонента на JavaScript, для него можно создать Java API.
Пример ниже показывает, как построить переключатель светлой/темной темы на основе Lit.
Создание JavaScript веб-компонента
Создайте файл theme-toggle.js в папке frontend/src/component/theme-toggle. Он содержит веб-компонент, реализующий переключатель светлой/темной темы.
import {html} from 'lit';
import {Button} from '@vaadin/button';
import {defineCustomElement} from '@vaadin/component-base/src/define.js';
export class ThemeToggle extends Button { (1)
static get is() {
return 'theme-toggle'; (2)
}
static get template() { (3)
return html`
<div class="vaadin-button-container">
<span part="prefix" aria-hidden="true">
<slot name="prefix"></slot>
</span>
<span part="label">
<slot></slot>
</span>
</div>
<slot name="tooltip"></slot>
`;
}
static get properties() { (4)
return {
ariaLabel: {
type: String,
value: 'Theme toggle',
reflectToAttribute: true,
},
storageKey: {
type: String,
value: 'jmix.flowui.theme',
observer: '_onStorageKeyChanged'
}
};
}
constructor() {
super();
this.addEventListener('click', () => this.toggleTheme());
this.addEventListener('click', () => {
const customEvent = new CustomEvent('theme-changed', {detail: {value: this.getCurrentTheme()}});
this.dispatchEvent(customEvent); (5)
});
}
/** @protected */
ready() {
super.ready();
this.applyStorageTheme();
}
applyStorageTheme() {
let storageTheme = this.getStorageTheme();
let currentTheme = this.getCurrentTheme();
if (storageTheme && currentTheme !== storageTheme) {
this.applyTheme(storageTheme);
}
}
getStorageTheme() {
return localStorage.getItem(this.storageKey);
}
getCurrentTheme() {
return document.documentElement.getAttribute('theme');
}
toggleTheme() {
const theme = this.getCurrentTheme();
this.applyTheme(theme === 'dark' ? '' : 'dark');
}
applyTheme(theme) {
document.documentElement.setAttribute('theme', theme);
localStorage.setItem(this.storageKey, theme);
}
/** @protected */
_onStorageKeyChanged(storageKey, oldStorageKey) {
const theme = localStorage.getItem(oldStorageKey);
localStorage.removeItem(oldStorageKey);
if (theme) {
localStorage.setItem(storageKey, theme);
}
}
}
defineCustomElement(ThemeToggle); (6)
| 1 | Расширяет Vaadin Button, чтобы переиспользовать поведение и стилизацию кнопки. |
| 2 | Определяет имя HTML-элемента. |
| 3 | Определяет шаблон Shadow DOM, включая слоты для иконки, текста и всплывающей подсказки. |
| 4 | Определяет клиентские свойства, включая ключ local storage, используемый для сохранения выбранной темы. |
| 5 | Отправляет событие theme-changed после переключения темы компонентом. |
| 6 | Экспортирует пользовательский HTML-элемент с именем, определённым в методе static get is(). |
Создание Java API для веб-компонента
Создайте файл ThemeToggle.java, который является классом UI-компонента. Он определяет API для серверного кода, методы доступа, обработчики событий и подключение источников данных.
import com.vaadin.flow.component.*;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.shared.HasTooltip;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.shared.Registration;
import io.jmix.flowui.kit.component.HasTitle;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Tag("theme-toggle") (1)
@JsModule("./src/component/theme-toggle/theme-toggle.js") (2)
public class ThemeToggle extends Component implements ClickNotifier<ThemeToggle>,
Focusable<ThemeToggle>, HasTheme, HasEnabled, HasSize, HasStyle,
HasText, HasTooltip, HasTitle, HasAriaLabel { (3)
public static final String STORAGE_KEY_PROPERTY = "storageKey";
public static final String THEME_CHANGED_EVENT = "theme-changed";
protected Component iconComponent;
public ThemeToggle() {
setIcon(createDefaultIcon());
}
public ThemeToggle(Component icon) {
setIcon(icon);
}
public ThemeToggle(String text, Component icon) {
setIcon(icon);
setText(text);
}
protected Icon createDefaultIcon() {
Icon icon = VaadinIcon.ADJUST.create();
icon.getElement().getStyle().set("rotate", "180deg");
return icon;
}
@Override
public void setText(String text) {
removeAll(getNonTextNodes());
if (text != null && !text.isEmpty()) {
getElement().appendChild(Element.createText(text));
}
updateThemeAttribute();
}
public Component getIcon() {
return iconComponent;
}
public void setIcon(Component icon) {
if (icon != null && icon.getElement().isTextNode()) {
throw new IllegalArgumentException("Text node can't be used as an icon.");
}
if (iconComponent != null) {
remove(iconComponent);
}
iconComponent = icon;
if (icon != null) {
add(icon);
updateIconSlot();
}
updateThemeAttribute();
}
protected void updateIconSlot() {
iconComponent.getElement().setAttribute("slot", "prefix");
}
protected void add(Component... components) {
for (Component component : components) {
getElement().appendChild(component.getElement());
}
}
protected void remove(Component... components) {
for (Component component : components) {
if (getElement().equals(component.getElement().getParent())) {
component.getElement().removeAttribute("slot");
getElement().removeChild(component.getElement());
} else {
throw new IllegalArgumentException(
"The given component (" + component + ") is not a child of this component");
}
}
}
public boolean isAutofocus() {
return getElement().getProperty("autofocus", false);
}
public void setAutofocus(boolean autofocus) {
getElement().setProperty("autofocus", autofocus);
}
public String getStorageKey() {
return getElement().getProperty(STORAGE_KEY_PROPERTY);
}
public void setStorageKey(String storageKey) {
getElement().setProperty(STORAGE_KEY_PROPERTY, storageKey);
}
protected void removeAll(Element... exclusion) {
Set<Element> toExclude = Stream.of(exclusion)
.collect(Collectors.toSet());
Predicate<Element> filter = toExclude::contains;
getElement().getChildren()
.filter(filter.negate())
.forEach(child -> child.removeAttribute("slot"));
getElement().removeAllChildren();
getElement().appendChild(exclusion);
}
protected Element[] getNonTextNodes() {
return getElement().getChildren()
.filter(element -> !element.isTextNode())
.toArray(Element[]::new);
}
protected void updateThemeAttribute() {
long childCount = getElement().getChildren()
.filter(element ->
element.isTextNode() || !"vaadin-tooltip".equals(element.getTag()))
.count();
if (childCount == 1 && iconComponent != null) {
getThemeNames().add("icon");
} else {
getThemeNames().remove("icon");
}
}
public Registration addThemeChangeListener(ComponentEventListener<ThemeToggleThemeChangedEvent> listener) {
return addListener(ThemeToggleThemeChangedEvent.class, listener);
}
@DomEvent(THEME_CHANGED_EVENT) (4)
public static class ThemeToggleThemeChangedEvent extends ComponentEvent<ThemeToggle> {
protected String value;
public ThemeToggleThemeChangedEvent(ThemeToggle source, boolean fromClient,
@EventData("event.detail.value") String value) { (5)
super(source, fromClient);
this.value = value;
}
public String getValue() {
return value;
}
}
}
| 1 | Определяет корневой элемент, который автоматически создается классом Component и к которому можно обращаться с помощью метода getElement(). Должен совпадать с экспортом веб-компонента. |
| 2 | Аннотация @JsModule определяет импорт JavaScript-модуля. |
| 3 | Использование Vaadin Mixin Interfaces для предоставления общих API и поведения по умолчанию для наборов функциональности, встречающихся в большинстве веб-компонентов. |
| 4 | Использование аннотации @DomEvent для подключения компонента ThemeToggle к событию DOM theme-changed. |
| 5 | Использование аннотации @EventData для определения дополнительных данных события, в данном случае значения темы. |
| Более подробную информацию о создании пользовательских компонентов можно найти в документации Vaadin: Creating Components, Using Vaadin Mixin Interfaces, Using Events with Components. |
Пример использования
После реализации компонента его можно использовать в экранах, например:
XML Дескриптор
<view xmlns="http://jmix.io/schema/flowui/view"
xmlns:app="http://company.com/schema/app-ui-components"
title="msg://themeToggleView.title"> (1)
<layout>
<app:themeToggle text="Click to switch theme"/> (2)
</layout>
</view>
| 1 | Определяет пространство имен со значением, совпадающим со значением атрибутов xmlns и targetNamespace XSD компонента. |
| 2 | Добавляет элемент themeToggle с префиксом пространства имен. |
Программное использование
@Subscribe
public void onInit(final InitEvent event) {
ThemeToggle themeToggle = new ThemeToggle();
themeToggle.setText("Click to switch theme");
themeToggle.addThemeChangeListener(changedEvent ->
notifications.create("Theme switched: " + getThemeValue(changedEvent))
.withPosition(Notification.Position.TOP_CENTER)
.show());
getContent().add(themeToggle);
}