Vaadin TODO за 30 минут

Fullstack веб-приложение за 30 минут? Звучит интересно. Учитывая что при этом без использования css и js можно получить годный дизайн и нормальную адаптивную верстку!

Vaadin 8

Вообще с самим Vaadin я уже работал, он отлично подходит для написания админок, корпоративных не сильно нагруженных АРМов, позволяя создать фронт на native-java.

Относительно недавно вышедшая новая 8 версия внесла много крутых изменений (21 глобальное, если точно), писать кода можно сильно меньше, скорость работы прилично выросла.

Поддерживает хорошую интеграцию с spring-boot, что позволяет избежать настроек томката и реально за 5 минут получить работающий скелет.

TODO application

Предлагаю рассмотреть создание маленького веб-приложения — TODO (списка дел).

Для начала очень удобно и быстро собрать скелет помогает Spring Initializr

На скриншоте я показал нужные нам компоненты для нашего проекта: Web, Vaadin, JPA и Н2 (в качестве БД).

Нажимаем Generate и скачиваем готовый maven проект с нужными нам зависимостями. Остается только импортировать его в любимую IDE (у меня это IDEA).

После начинается магия :)

Главное UI

Создадим класс TodoUI на который повесим аннотацию @SpringUI, этим дадим знать spring`у что это наш главный UI приложения.

@SpringUI
public class TodoUI extends UI {

    private VerticalLayout root;
    
    @Autowired
    private TodoListLayout todoLayout;

    @Override
    protected void init(VaadinRequest vaadinRequest) {
        setupLayout();
        addHeader();
        addForm();
        addTodoList();
        addDeleteButton();
    }
}

В перегруженный метод init() добавим методы инициализации наших нашего интерфейса, root — это главный layout, он может быть только один и задается методом setContent()

Теперь по порядку:

Создадим root, зададим выравнивание

private void setupLayout() {
         root = new VerticalLayout();
         root.setDefaultComponentAlignment(Alignment.MIDDLE_CENTER);
         setContent(root);
}

Добавим хидер. Сразу зададим стиль

private void addHeader() {
         Label header = new Label("TODO");
         header.addStyleName(ValoTheme.LABEL_H1);
         root.addComponent(header);
}

Создадим форму

private void addForm() {
    HorizontalLayout formLayout = new HorizontalLayout();
    formLayout.setWidth("80%");             // Поле для ввода, и кнопочку добавления
    TextField task = new TextField();
    Button add = new Button("");
    add.addStyleName(ValoTheme.BUTTON_PRIMARY);
    add.setIcon(VaadinIcons.PLUS);         // Поле ввода займет всю ширину за минусом минимальной для кнопки
    formLayout.addComponentsAndExpand(task);
    formLayout.addComponents(add);         // Лисенер для кнопки Добавить
    add.addClickListener(click -> {
        todoLayout.add(new Todo(task.getValue()));
        task.clear();
        task.focus();
    });
    task.focus();         // Кнопка плюс будет работать по нажатию ENTER

    add.setClickShortcut(ShortcutAction.KeyCode.ENTER);
    root.addComponent(formLayout);
}

Добавим список самих задач. Он подключается через @Autowired спрингом

private void addTodoList() {
    todoLayout.setWidth("80%");
    root.addComponent(todoLayout);
}

Кнопку удаления выполненных задач. С логикой удаления и проверки на «ничего не выбрано»

private void addDeleteButton() {
    root.addComponent(new Button("Delete completed", click -> {
        if (todoLayout.countCompleted() == 0) {
            Notification.show("No completed task", Notification.Type.HUMANIZED_MESSAGE);
        }
        todoLayout.deleteCompleted();
    }));
}

Entry & JPA

Сама сущность весьма проста. Опускаю конструкторы и set/get методы

@Entity
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String text;
    private boolean done;
}

И настоящая магия Jpa и Spring-data — репозиторий!

public interface TodoRepository extends JpaRepository<Todo, Long> {

    @Transactional
    void deleteByDone(boolean done);

    int countByDone(boolean done);
}

Да, достаточно просто создать интерфейс, написать метод и все дальше произойдет само. По имени метода будет сгенерирован запрос к базе. Черт, это реально круто! Никакого SQL :)

Layout для отдельной задачи

В простейшим случае нам нужно иметь CheckBox + TextField. Добавим эти 2 объекта на layout, зададим стиль.

public class TodoItemLayout extends HorizontalLayout {

    private final TextField text;
    private final CheckBox done;

    public TodoItemLayout(Todo todo, TodoChangeListener changeListener) {
        setWidth("100%");
        setDefaultComponentAlignment(Alignment.MIDDLE_LEFT);

        text = new TextField();
        done = new CheckBox();

        text.addStyleName(ValoTheme.TEXTFIELD_BORDERLESS);
        text.setValueChangeMode(ValueChangeMode.BLUR);

        addComponents(done);
        addComponentsAndExpand(text);
    }
}

Самое интересное тут — Vaadin Binder. Он позволяет связать jpa сущность с UI. Для этого нам нужно соответствие полей entity и layout класса.

// Создадим Binder        
Binder<Todo> binder = new Binder<>(Todo.class);
binder.bindInstanceFields(this);
binder.setBean(todo);
// Добавим лисенер для обновления информации с чекбокса
binder.addValueChangeListener(event -> changeListener.todoChanged(todo));

Список всех задач

И последний класс — список всех задач. Он будет содержать все TodoItemLayout и работать с TodoRepository.

Тут важно не забыть обновлять компонент при изменениях и инициализации

@SpringComponent
public class TodoListLayout extends VerticalLayout implements TodoChangeListener {

    @Autowired
    TodoRepository repository;

    @PostConstruct
    void init() {
        update();
    }

    private void update() {
        setTodos(repository.findAll());
    }

    private void setTodos(List<Todo> all) {
        removeAllComponents();
        all.forEach(todo -> addComponent(new TodoItemLayout(todo, this)));
    }

    public void add(Todo todo) {
        repository.save(todo);
        update();
    }

    public int countCompleted() {
        return repository.countByDone(true);
    }

    public void deleteCompleted() {
        repository.deleteByDone(true);
        update();
    }

    @Override
    public void todoChanged(Todo todo) {
        add(todo);
    }
}

Не забудем про интерфейс

public interface TodoChangeListener {
    void todoChanged(Todo todo);
}

Наполнение БД

В resources создадим файл data.sql, в котором напишем команды для создания и наполнения таблицы

CREATE TABLE IF NOT EXISTS Todo(
  id IDENTITY PRIMARY KEY,
  done BOOLEAN,
  TEXT VARCHAR
);
DELETE FROM Todo;
INSERT INTO Todo VALUES(1, TRUE, 'Do something');
INSERT INTO Todo VALUES(2, FALSE, 'Do something else');
INSERT INTO Todo VALUES(3, TRUE, 'Test application');

Запуск

Все готово. Собираем и запускаем проект:

mvn package spring-boot:run

После можно проверить работу по адресу localhost:8080, если порт уже занят, открываем application.properties и прописываем

server.port=8081

Готово!

На подлесок идеи того, что еще можно прикрутить:

  • Заменить удаление на «сохранение в выполненные»
  • Добавить приоритет
  • Настроить персистентность БД
  • Добавить поддержку пользователей
  • Сделать конкурента Wunderlist :)

Github: https://github.com/sboychenko/vaadin-todo


10 Комментариев

Добавьте свой →

  1. Скажите, уважаемый!
    Подскажите, а как можно подцепить сторонний REST API, который реализован например на Ratcpack?

  2. У вас шаринг plus.google.com не работает

  3. Сергей, огромное спасибо за материал. Пытаюсь сейчас переделать Ваш код. Мне нужно, чтобы страничка выводила таблицу, в которую какой-то метод какого-то класса…. не важной какой, пусть это будет просто набор заданных строк… в общем чтобы этот метод выкидывал в табличку данные и чтобы я мог этот метод вызвать по нажатию кнопки Update на странице. То есть проще говоря мне нужна табличка с кнопкой ее обновления. Как это реализовать? может вам попадались примеры кода?

    • У ваадина есть sampler, где много примеров с кодом.
      https://demo.vaadin.com/sampler/#ui/grids-and-trees/grid/features — вот таблица
      Так же есть хорошая документация на сайте https://vaadin.com/docs/v8/framework/components/components-grid.html вот про таблицы.

      По сути, нужно просто добавить таблицу и кнопку в UI, на кнопку повесить listener, который будет обновлять данные. Если нужно автоматическая перерисовка таблицы по событию в бекенде смотрите в сторону vaadin push.

      • Сергей, огромное Вам спасибо. В принципе, у меня даже получилось полностью сделать, что я хочу. Табличка уже висит. Код забрал у них:

        private void addTable() {

        List people = Arrays.asList(
        new Person(«Nic», 1543),
        new Person(«Nic», 1544));

        VerticalLayout tableLayout = new VerticalLayout();

        Grid grid = new Grid);
        grid.setItems(people);
        grid.addColumn(Person::getName).setCaption(«Name»);
        tableLayout.addComponent(grid);
        root.addComponent(tableLayout);
        }

        Что не пойму: сейчас данные в табличку по сути попадают при инициализации внутри private void addTable. А как подвесить этот код в листенер? На сколько я могу предположить — надо городить отдельный метод у кого-то, который я потом подпихну туда? (заранее сорри за глупые вопросы)

Добавить комментарий для Anton Romanov Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *