Não faz muito tempo, vimos como construir um aplicativo HTMX com JavaScript. HTMX também funciona com Java, então agora vamos tentar isso usando Spring Boot e Thymeleaf. Esta pilha incrível oferece todo o poder e versatilidade do Java com Spring, combinado com a simplicidade engenhosa do HTMX.

HTMX: uma estrela em ascensão

HTMX é uma tecnologia mais recente que pega o HTML antigo e oferece poderes extras, como trocas de Ajax e DOM. Está incluído na minha lista pessoal de boas idéias porque elimina toda uma gama de complexidade do aplicativo da web típico. HTMX funciona convertendo entre JSON e HTML. Pense nisso como uma espécie de Ajax declarativo.

Leia uma entrevista com o criador do HTMX, Carson Gross.

Java, Spring e Thymeleaf

Do outro lado desta equação está Java: uma das plataformas de servidor mais maduras e inovadoras, sem exceção. Spring é uma escolha fácil para adicionar uma variedade de recursos baseados em Java, incluindo o projeto Spring Boot Web bem projetado para lidar com endpoints e roteamento.

Thymeleaf é um mecanismo de modelagem completo do lado do servidor e o padrão para Spring Boot Web. Quando combinado com HTMX, você tem tudo que precisa para construir um aplicativo web full-stack sem usar muito JavaScript.

Aplicativo de exemplo HTML e Java

Vamos construir o aplicativo canônico Todo. Isso parecerá assim:

Listamos as tarefas existentes e permitimos criar novas, excluí-las e alterar seu status de conclusão.

Visão geral

Esta é a aparência do aplicativo Todo finalizado no disco:


$ tree
.
├── build.gradle
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── iwjavaspringhtmx
        │               ├── DemoApplication.java
        │               ├── controller
        │               │   └── MyController.java
        │               └── model
        │                   └── TodoItem.java
        └── resources
            ├── application.properties
            ├── static
            │   └── style.css
            └── templates
                ├── index.html
                ├── style.css
                └── todo.html

Portanto, além do material típico do Gradle, o aplicativo possui duas partes principais contidas no /src diretório: O /main diretório contém o código Java e /resources contém o arquivo de propriedades e dois subdiretórios com os modelos CSS e Thymeleaf.

Você pode encontrar a fonte deste projeto em seu repositório GitHub. Para executá-lo, vá até a raiz e digite $ gradle bootRun. Você pode então usar o aplicativo em localhost:8080.

Se quiser iniciar o aplicativo do zero, você pode começar com: $ spring init --dependencies=web,thymeleaf spring-htmx. Isso instalará o Thymeleaf e o Spring Boot em um projeto Gradle.

O aplicativo é um aplicativo Spring Boot normal executado por DemoApplication.java.

A classe de modelo Java Spring HTML

Vamos começar examinando nossa classe de modelo: com/example/iwjavaspringhtmx/TodoItem.java. Esta é a classe do modelo do lado do servidor que representará uma tarefa. Aqui está o que parece:


public class TodoItem {
  private boolean completed;
  private String description;
  private Integer id;
  public TodoItem(Integer id, String description) {
    this.description = description;
    this.completed = false;
    this.id = id;
  }
  public void setCompleted(boolean completed) {
    this.completed = completed;
  }
  public boolean isCompleted() {
    return completed;
  }
  public String getDescription() {
    return description;
  }
  public Integer getId(){ return id; }
  public void setId(Integer id){ this.id = id; }
  @Override
  public String toString() {
    return id + " " + (completed ? "(COMPLETED) " : "( ) ") + description;
  }
}

Esta é uma classe de modelo simples com getters e setters. Nada sofisticado, que é exatamente o que queremos.

A classe do controlador Java Spring HTML

No servidor, o controlador é o chefe. Aceita solicitações, orquestra a lógica e formula a resposta. No nosso caso, precisamos de quatro endpoints usados ​​para listar os itens, alterar seu status de conclusão, adicionar itens e excluí-los. Aqui está a classe do controlador:


@Controller
public class MyController {

  private static List items = new ArrayList();
  static {
    TodoItem todo = new TodoItem(0,"Make the bed");
    items.add(todo);
    todo = new TodoItem(1,"Buy a new hat");
    items.add(todo);
    todo = new TodoItem(2,"Listen to the birds singing");
    items.add(todo);
  }

  public MyController(){ }

  @GetMapping("https://www.infoworld.com/")
  public String items(Model model) {
    model.addAttribute("itemList", items);
    return "index";
  }

  @PostMapping("/todos/{id}/complete")
  public String completeTodo(@PathVariable Integer id, Model model) {
    TodoItem item = null;
    for (TodoItem existingItem : items) {
      if (existingItem.getId().equals(id)) {
        item = existingItem;
        break; 
      }
    }
    if (item != null) {
      item.setCompleted(!item.isCompleted());
    }
    model.addAttribute("item",item);
    return "todo"; 
  }

  @PostMapping("/todos")
  public String createTodo(Model model, @ModelAttribute TodoItem newTodo) {
    int nextId = items.stream().mapToInt(TodoItem::getId).max().orElse(0) + 1;
    newTodo.setId(nextId);
    items.add(newTodo);
    model.addAttribute("item", newTodo);
    return "todo";
  }

  @DeleteMapping("/todos/{id}/delete")
  @ResponseBody
  public String deleteTodo(@PathVariable Integer id) {
    for (int i = 0;  i < items.size(); i++) {
      TodoItem item = items.get(i);
      if (item.getId() == id) {
        items.remove(i);
        break;
      }
    }
    return "";
  }
}

Você notará que acabei de criar uma estática List para manter os itens na memória. Na vida real, usaríamos um armazenamento de dados externo.

Para este passeio, existem alguns pontos de interesse adicionais.

Primeiro, os pontos finais são anotados com @GetMapping, @PostMappinge @DeleteMapping. É assim que você mapeia caminhos do Spring Web para manipuladores. Cada anotação corresponde ao seu método HTTP (GET, POST, DELETE).

O Spring Boot também facilita a captura de parâmetros do caminho usando a anotação de argumento @PathParameter. Então, para o caminho /todos/{id}/delete, @PathVariable Integer id conterá o valor no {id} parte do caminho.

No caso do createTodo() método, o argumento anotado @ModelAttribute TodoItem newTodoassumirá automaticamente o POST corpo e aplicar seus valores ao newTodo objeto. (Esta é uma maneira rápida e fácil de transformar um envio de formulário em um objeto Java.)

A seguir, usamos os IDs dos itens para manipular a lista de itens. Esta é a tarifa padrão da API REST.

Existem duas maneiras de enviar uma resposta. Se o @ResponseBody anotação está presente no método (como é para deleteTodo()) então tudo o que for retornado será enviado literalmente. Caso contrário, a string de retorno será interpretada como um caminho de modelo do Thymeleaf (você verá isso em um momento).

O Model o argumento do modelo é especial. É usado para adicionar atributos ao escopo que é transferido para o Thymeleaf. Podemos interpretar o seguinte items método dizendo: Dado um GET solicitação para a raiz/caminho, adicione a variável items ao escopo como “itemList”E renderize uma resposta usando o“index” modelo.


@GetMapping("https://www.infoworld.com/")
  public String items(Model model) {
    model.addAttribute("itemList", items);
    return "index";
  }

Nos casos em que estamos lidando com uma solicitação AJAX enviada do front end pelo HTMX, a resposta será usada pelo componente HTMX para atualizar a UI. Veremos isso na prática em breve.

Os modelos Thymeleaf

Agora vamos dar uma olhada no modelo de índice do Thymeleaf. Ele vive no /resources/templates/index.html arquivo. Spring Boot mapeia o “index”string retornada do items() método para este arquivo usando convenções. Aqui está o nosso index.html modelo:




  
    
    Items List
    
    
  
  
    
      
      

A ideia básica no Thymeleaf é pegar uma estrutura HTML e usar variáveis ​​Java nela. (Isso equivale a usar um sistema de modelo como o Pug.)

Thymeleaf usa atributos HTML ou elementos prefixados por th: para denotar onde ele faz seu trabalho. Lembre-se de que quando mapeamos a raiz/caminho no controlador, adicionamos o itemList variável para o escopo. Aqui, estamos usando isso dentro de um th:block com um th:each atributo. O th:each atributo é o mecanismo iterador no Thymeleaf. Nós o usamos para acessar os elementos de itemList e exponha cada um como um item com nome de variável: item : ${itemList}.

Em cada iteração de itemList, transferimos a renderização para outro modelo. Esse tipo de reutilização de modelos é fundamental para evitar a duplicação de código. A linha



diz ao Thymeleaf para renderizar o todo.html modelo e forneça o item como argumento.

Veremos o todo modelo a seguir, mas primeiro observe que estamos usando o mesmo modelo no controlador, em ambos completeTodo e createTodo, para fornecer a marcação que enviamos de volta ao HTMX durante as solicitações Ajax. Dito de outra forma, estamos usando o todo.html como parte da renderização inicial da lista e para enviar atualizações para a UI durante solicitações Ajax. Reutilizar o modelo Thymeleaf nos mantém SECOS.

O modelo de tarefas

Agora aqui está todo.html:


  • Você pode ver que estamos fornecendo um elemento de item de lista e usando uma variável, item, para preenchê-lo com valores. É aqui que iniciamos um trabalho interessante com HTMX e Thymeleaf.

    Primeiro, usamos th:checked para aplicar o status verificado de item.isComplete para a entrada da caixa de seleção.

    Ao clicar na caixa de seleção, emitimos uma solicitação Ajax para o back-end usando HTMX:

    1. hx-trigger="click" diz ao HTMX para iniciar o Ajax com um clique.
    2. hx-target="closest li" informa ao HTMX onde colocar a resposta da solicitação Ajax. No nosso caso, queremos substituir o elemento do item da lista mais próximo. (Lembre-se que nosso delete endpoint retorna toda a marcação do item de lista para o item.)
    3. hx-swap="outerHTML" informa ao HTMX como trocar o novo conteúdo, neste caso, substituindo todo o elemento.
    4. th:hx-post="|/todos/${item.id}/complete|" diz ao HTMX que este é um elemento Ajax ativo que emite um POST solicitação para o URL especificado (nosso to completeTodo ponto final).

    Algo a ser observado ao usar o Thymeleaf com HTMX é que você acaba com prefixos de atributos complexos, como você pode ver em th:hx-post. Essencialmente, o Thymeleaf é executado primeiro no servidor (o th: prefixo) e preenche o ${item.id} interpolação, então hx-post funciona normalmente no cliente.

    A seguir, para o spanapenas exibimos o texto de item.description. (Observe que a linguagem de expressão do Thymelef nos permite acessar campos sem usar o get prefixo.) Também digno de nota é como aplicamos a classe de estilo concluída ao span elemento. Aqui está o que nosso CSS usará para colocar a decoração tachada em itens concluídos:

    
    th:classappend="${item.isCompleted ? 'complete' : ''}"
    

    Este atributo Thymeleaf simplifica a aplicação condicional de uma classe com base em condições booleanas como item.isComplete.

    Nosso botão Excluir funciona de forma semelhante à caixa de seleção completa. Enviamos a solicitação Ajax para o URL usando o fornecido pelo Thymeleaf item.ide quando a resposta retornar, atualizamos o item da lista. Lembre-se de que enviamos de volta uma string vazia de deleteTodo(). O efeito será, portanto, remover o item da lista do DOM.

    A folha de estilo CSS

    A folha de estilo CSS está em src/main/resources/static/style.css e não é nada notável. A única parte interessante é lidar com a decoração tachada na spané:

    
    span {
      flex-grow: 1;
      font-size: 1rem;
      text-decoration: none;
      color: #333;
      opacity: 0.7;
    }
    
    span.complete {
      text-decoration: line-through;
      opacity: 1;
    }
    

    Conclusão

    A combinação de HTMX, Java, Spring e Thymeleaf abre um mundo de possibilidades para a construção de interações bastante sofisticadas com uma quantidade verdadeiramente mínima de código padrão. Podemos fazer uma enorme interatividade típica sem nunca escrever JavaScript.

    À primeira vista, a pilha Java-HTMX parece finalmente cumprir a promessa do Ajax centrado em Java; algo parecido com o que o Google Web Toolkit se propôs a fazer. Mas há mais. HTMX é uma tentativa de reorientar aplicações web para a verdadeira natureza do REST, e esta pilha nos mostra o caminho. O HTMX é independente do lado do servidor, portanto, podemos integrá-lo ao nosso back-end Java sem dificuldade.