Skip to content

5. Види Thymeleaf

Повернемося до архітектури додатка Spring MVC.

У двох попередніх розділах було описано різні аспекти блоку [1] — дій. Тепер ми розглянемо:

  • блок [2], що відповідає видам V;
  • блок [3] моделі M, що відображається цими поданнями;

З моменту створення Spring MVC технологія генерації сторінок HTML, що надсилаються до клієнтських браузерів, базувалася на технології JSP (Java Server Pages). Останні кілька років також можна використовувати технологію [Thymeleaf] [http://www.thymeleaf.org/]. Саме її ми зараз і презентуємо.

5.1. Проєкт STS

Створюємо новий проєкт:

  • у [3] вкажіть, що проект потребує залежностей [Thymeleaf]. Це додасть до залежностей [Spring MVC] з попереднього проєкту ще й залежності фреймворку [Thymeleaf] та [5];

Тепер давайте змінимо цей проект наступним чином:

  

Ми беремо за основу попередній проект:

  • [istia.st.springmvc.controllers] міститиме контролери;
  • [istia.st.springmvc.models] міститиме моделі дій та представлень;
  • [istia.st.springmvc.main] — це пакет виконуваного класу Spring Boot;
  • [templates] міститиме Thymeleaf-види;
  • [i18n] міститиме інтернаціоналізовані повідомлення, що відображаються у поданнях;

Клас [Application] має такий вигляд:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}

Клас [Config] має такий вигляд:


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}

Наразі ця конфігурація дозволяє керувати локалізаціями.

Контролер [ViewController] має такий вигляд:


package istia.st.springmvc.actions;

import org.springframework.stereotype.Controller;

@Controller
public class ViewsController {

}
  • у рядку 5 анотація [@Controller] замінила анотацію [@RestController], оскільки відтепер дії не будуть генерувати відповідь клієнту. Вони будуть:
    • створювати модель M
    • повертати тип [String], який буде назвою подання [Thymeleaf], відповідального за відображення цієї моделі. Саме поєднання цього подання V та цієї моделі M генеруватиме потік HTML, що надсилається клієнту;

Файл [messages.properties] наразі порожній.

5.2. [/v01]: основи Thymeleaf

Розглянемо наступну дію у файлі [ViewsController]:


    // основи Thymeleaf — 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • рядок 3: дія повертає тип [String]. Це буде ім’я дії;
  • рядок 4: цей вигляд матиме назву [v01]. За замовчуванням він має знаходитися у папці [templates] і називатися [v01.html];

Вигляд [v01.html] має такий вигляд:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Les vues'">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="'Les vues dans Spring MVC'">Spring 4 MVC</h2>
</body>
</html>

Це файл HTML. Наявність Thymeleaf видно:

  • у просторі імен [th] у рядку 2;
  • за атрибутами [th:text] у рядках 4 та 8;

Це дійсний файл HTML, який можна переглянути. Ми розміщуємо його в папці [static] [2] під назвою [vue-01.html] і відкриваємо його безпосередньо у браузері:

Якщо ми переглянемо вихідний код сторінки [2], то побачимо, що атрибути [th:text] були надіслані сервером і проігноровані браузером. Коли вигляд є результатом дії, задіюється Thymeleaf, який інтерпретує атрибути [th] перед відправкою відповіді клієнту.

Тег HTML:


<title th:text="'Les vues'">Spring 4 MVC</title>

обробляється Thymeleaf наступним чином:

  • th:text має синтаксис th:text="вираз", де вираз — це вираз, що підлягає обчисленню. Якщо цей вираз є рядком символів, як у даному випадку, його потрібно взяти в апострофи;
  • значення [expression] замінює текст тегу HTML, у даному випадку — текст тегу [title];

Після обробки вищезазначений тег перетворився на:


<title>Les vues</title>

Запустимо дію [/v01]:

  • у [2] ми бачимо результати заміни, виконаної Thymeleaf;

Тепер замінимо URL на [http://localhost:8080/v01.html]:

 

Як це слід інтерпретувати? Чи було подано перегляд [templates/v01.html] безпосередньо, без проходження через дію? Щоб прояснити ситуацію, створимо таку дію [/v02]:


    // основи Thymeleaf — 2
    @RequestMapping(value = "/v02", method = RequestMethod.GET)
    public String v02() {
        System.out.println("action v02");
        return "vue-02";
}

Вигляд [vue-02.html] є копією [v01.html]:

  

Тепер запустимо URL та [http://localhost:8080/vue-02.html]:

 

URL не знайдено. Тепер запитаємо URL та [http://localhost:8080/v02.html]

  • у журналах консолі для [1] видно, що було викликано дію [/v02], яка відобразила перегляд [vue-02.html] у [2];

Тепер ми знаємо, що URL [http://localhost:8080/v02.html] може також вказувати на файл [/v02.html] у папці [static]. Що станеться, якщо цей файл існує? Спробуємо. У папці [static] ми створюємо такий файл [v02.html]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2>Spring 4 MVC</h2>
</body>
</html>

потім запитуємо URL [http://localhost:8080/v02.html]:

[1] та [2] показують, що було викликано дію [/v02]. Отже, слід пам’ятати, що коли запитувана дія URL має вигляд [/x.html], Spring / Thymeleaf:

  • виконують дію [/x], якщо вона існує;
  • подають сторінку [/static/x.html], якщо вона існує;
  • інакше генерує виняток 404 Not found;

Щоб уникнути плутанини, відтепер дії та перегляди не матимуть однакових назв.

5.3. [/v03]: інтернаціоналізація подань

Інтеграція Spring / Thymeleaf дозволяє Thymeleaf використовувати файли повідомлень Spring. Розглянемо таку нову дію [/v03]:


    // інтернаціоналізація подань
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "vue-03";
}

Вона відображає такий вигляд [vue-03.html]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="#{title}">Spring 4 MVC</h2>
</body>
</html>

У рядках 4 та 8 вираз атрибута [th:text] має значення #{title}, яке відповідає повідомленню з ключем [title]. Ми створюємо такі файли: [messages_fr.properties] та [messages_en.properties]:

[messages_fr.properties]


title=Les vues dans Spring MVC

[messages_en.properties]


title=Views in Spring MVC

Запросимо URL, [http://localhost:8080/v03.html?lang=fr] та [http://localhost:8080/v03.html?lang=en]:

Зауважимо, що ми застосували те, чого нещодавно навчилися. Замість того, щоб позначити дію [v03] як [/v03], ми позначили її як [/v03.html].

5.4. [/v04]: створення шаблону M для подання V

Розглянемо таку нову дію [/v04]:


    // створення шаблону M для подання V
    @RequestMapping(value = "/v04", method = RequestMethod.GET)
    public String v04(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        System.out.println(String.format("Modèle=%s", model));
        return "vue-04";
}
  • рядок 4: шаблон подання вводиться в параметри дії. За замовчуванням цей початковий шаблон порожній. Ми побачимо, що його можна попередньо заповнити;
  • рядок 4: шаблон типу [Model] є своєрідним словником елементів типу <String, Object>. У рядку 4 ми додаємо запис до цього словника з ключем [personne], пов’язаним із значенням типу [Personne];
  • рядок 5: виводимо модель на консоль, щоб побачити, як вона виглядає;
  • у рядку 6 виводимо представлення [vue-04.html];

Клас [Personne] — це той самий клас, що використовувався в попередньому розділі:

  

package istia.st.springmvc.models;

public class Personne {

    // ідентифікатор
    private Integer id;
    // ім'я
    private String nom;
    // вік
    private int age;

    // конструктори
    public Personne() {

    }

    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }

    public Personne(Integer id, String nom, int age) {
        this(nom, age);
        this.id = id;
    }

    @Override
    public String toString() {
        return String.format("[id=%s, nom=%s,  age=%d]", id, nom, age);
    }

    // методи getter та setter
...
}

Вигляд [vue-04.html] має такий вигляд:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}">Nom :</span>
            <span th:text="${personne.nom}">Bill</span>
        </p>
        <p>
            <span th:text="#{personne.age}">Age :</span>
            <span th:text="${personne.age}">56</span>
        </p>
    </body>
</html>
  • у рядку 10 вводиться новий тип виразу Thymeleaf ${var}, де var — це ключ шаблону M цього виду. Нагадаємо, що дія [/v04] додала до шаблону ключ [personne], пов’язаний із типом Personne[id, nom, age];
  • рядок 10: відображає ім’я особи, що міститься в моделі;
  • рядок 14: виводить її вік;

Файли повідомлень змінено для додавання ключів [personne.nom] та [personne.age] із рядків 9 та 13. Результат такий:

а тип шаблону M можна знайти в журналах консолі [2].

Можна задатися питанням, чому не записано представлення [vue-04] у такий спосіб:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}"></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}" /></span>
            <span th:text="${personne.nom}"></span>
        </p>
        <p>
            <span th:text="#{personne.age}"></span>
            <span th:text="${personne.age}"></span>
        </p>
    </body>
</html>

Цей вигляд є цілком допустимим і дасть той самий результат, що й попередній. Однією з цілей Thymeleaf є те, щоб сторінка Thymeleaf могла відображатися навіть тоді, коли вона не проходить через Thymeleaf. Отже, створимо дві нові статичні сторінки:

  

Вигляд [vue-04b.html] є копією вигляду [vue-04.html]. Те саме стосується вигляду [vue-04a.html], але з нього було видалено статичний текст. Якщо переглянути обидві сторінки, отримаємо такі результати:

У випадку з [1] структура сторінки не відображається, тоді як у випадку з [2] вона добре видна. Ось у чому полягає перевага розміщення статичних текстів у поданнях Thymeleaf, навіть якщо під час виконання вони будуть замінені іншими текстами.

Тепер розглянемо технічну деталь. У подання [vue-04.html] ми форматуємо код за допомогою [ctrl-Maj-F]. Отримуємо такий результат:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <p>
        <span th:text="#{personne.nom}">Nom :</span> <span
            th:text="${personne.nom}">Bill</span>
    </p>
    <p>
        <span th:text="#{personne.age}">Age :</span> <span
            th:text="${personne.age}">56</span>
    </p>
</body>
</html>

Теги вирівняні неправильно, і код стає важчим для читання. Якщо ми перейменуємо [vue-04.html] на [vue-04.xml] і переформатуємо код, теги знову вирівняються. Отже, суфікс [xml] був би зручнішим. З цим суфіксом можна працювати. Для цього потрібно налаштувати Thymeleaf. Щоб не скасувати те, що ми зробили, ми дублюємо розглянутий проект [springmvc-vues] у проект [springmvc-vues-xml]

  

Ми змінюємо файл [pom.xml] наступним чином:


    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-vues-xml</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springmvc-vues-xml</name>
<description>Les vues dans Spring MVC</description>

Назва проєкту змінена у рядках 2 та 6. Крім того, ми змінюємо розширення файлів, що містяться у папці [templates]:

  

У документі [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] наведено перелік властивостей конфігурації Spring Boot, які можна використовувати у файлі [application.properties]:

  

Цей документ містить властивості, які Spring Boot використовує під час автоконфігурації та які можна змінити, вказавши іншу конфігурацію у файлі [application.properties]. Для Thymeleaf властивості автоконфігурації такі:


# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ; додано charset=<encoding>
spring.thymeleaf.cache=true # встановлено значення false для гарячого оновлення

Отже, можна просто додати такий рядок


spring.thymeleaf.suffix=.xml

у файлі [application.properties]. Однак ми оберемо інший шлях — конфігурацію за допомогою програмування. Ми налаштуємо Thymeleaf у класі [Config]:


package istia.st.springmvc.main;

import java.util.Locale;

...
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    ...

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCharacterEncoding("UTF-8");
     templateResolver.setCacheable(true);
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

}
  • рядки 16–24 налаштовують об’єкт [TemplateResolver] для Thymeleaf. Саме цей об’єкт завантажується на основі імені подання, наданого дією, для пошуку відповідного файлу;
  • рядки 18 і 19 встановлюють префікс і суфікс, які слід додати до імені подання для пошуку файлу. Отже, якщо ім’я подання — [vue04], шуканий файл матиме ім’я [classpath:/templates/vue04.xml]. [classpath:/templates] — це синтаксис Spring, який позначає папку [/templates], розташовану в кореневому каталозі Classpath проєкту;
  • рядок 21: щоб у відповіді, що надсилається клієнту, був заголовок HTTP:

Content-Type:text/html;charset=UTF-8
  • рядок 20: вказує, що представлення відповідає стандарту HTML5;
  • рядок 22: вказує, що подання Thymeleaf можна кешувати;
  • рядки 26–31: встановлюють механізм вирішення подання для комбінації Spring / Thymeleaf як попередній механізм вирішення;

Запустимо виконуваний файл цього нового проєкту та запросимо URL [http://localhost:8080/v04.html?lang=en]:

 

Можна помітити, що в URL дія [/v04] знову була замінена на [v04.html].

5.5. [/v05]: факторизація об’єкта у подання Thymeleaf

Створюємо таку дію [/v05]:


    // створено шаблон M для подання V - 2
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-05";
}

Вона ідентична дії [/v04]. Вигляд [vue-05.xml] має такий вигляд:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • рядки 8–17: у цих рядках об’єкт Thymeleaf визначається атрибутом [th:object="${personne}"] (рядок 8). Цей об’єкт тут є об’єктом із ключем [personne], який міститься в моделі:
  • рядок 11: вираз Thymeleaf [*{nom}] еквівалентний [${objet.nom}], де [objet] — це поточний об’єкт Thymeleaf. Отже, тут вираз [*{nom}] еквівалентний [${personne.nom}];
  • рядок 15: те саме;

Результат:

 

5.6. [/v06]: тестування у Thymeleaf-вигляді

Розглянемо таку дію [/v06]:


    // створення моделі M для подання V — 3
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-06";
}

Вона ідентична двом попереднім діям. Вона відображає такий вигляд [vue-06.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
            <p th:if="*{age} >= 18" th:text="#{personne.majeure}">Vous êtes majeur</p>
            <p th:if="*{age} &lt; 18" th:text="#{personne.mineure}">Vous êtes mineur</p>
        </div>
    </body>
</html>
  • рядок 17: атрибут [th:if] обчислює булеве вираження. Якщо це вираження істинне, тег відображається, інакше — ні. Отже, якщо ${personne.age}>=18, буде відображено текст [#{personne.majeure}], тобто повідомлення з ключем [personne.majeure] у файлах повідомлень;
  • рядок 18: не можна написати [*{age} < 18], оскільки знак < є зарезервованим символом. Тому слід використовувати його еквівалент HTML [&lt;], який також називають ентітетом HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

Файли повідомлень змінено:

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.mineure=You are under 18
personne.majeure=You are over 18

Результат такий:

5.7. [/v07]: ітерація у поданнях Thymeleaf

Розглянемо таку дію [/v07]:


    // створення моделі M для подання V — 4
    @RequestMapping(value = "/v07", method = RequestMethod.GET)
    public String v07(Model model) {
        model.addAttribute("liste", new Personne[] { new Personne(7, "martin", 17), new Personne(8, "lucie", 32),
                new Personne(9, "paul", 7) });
        return "vue-07";
}
  • ця дія створює список із трьох осіб, розміщує його в шаблоні, пов’язаному з ключем [liste], і відображає вигляд [vue-07];

Вигляд [vue-07.xml] має такий вигляд:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h3 th:text="#{liste.personnes}">Liste de personnes</h3>
        <ul>
            <li th:each="element : ${liste}" th:text="'['+ ${element.id} + ', ' +${element.nom}+ ', ' + ${element.age} + ']'">[id,nom,age]</li>
        </ul>
    </body>
</html>
  • рядок 10: атрибут [th:each] повторює тег, у якому він знаходиться, у даному випадку тег <li>. Він має тут два параметри [element : collection], де [collection] — це колекція об’єктів, у даному випадку список осіб. Thymeleaf пройде по колекції та згенерує стільки тегів <li>, скільки елементів міститься в колекції. Для кожного тегу <li> [element] буде представляти елемент колекції, пов’язаний із цим тегом. Для цього елемента буде обчислено атрибут [th:text]. Його вираз тут являє собою конкатенацію рядків, що дає результат [id, nom, age];
  • рядок 8: додаємо ключ [liste.personnes] у файли повідомлень;

Ось результат:

5.8. [/v08-/v10] : @ModelAttribute

Повернемося до того, що ми вже розглядали під час вивчення дій, — до ролі анотації [@ModelAttribute]. Додаємо таку нову дію:


    // --------------- Прив'язка та ModelAttribute ----------------------------------

    // якщо параметр є об’єктом, він інстанціюється та, за необхідності, модифікується параметрами запиту
    // він автоматично стане частиною моделі подання з ключем [key]
    // для параметра @ModelAttribute("xx") ключ дорівнюватиме xx
    // для параметра @ModelAttribute ключ дорівнюватиме імені класу параметра, що починається з малої літери
    // якщо @ModelAttribute відсутній, то все відбувається так, ніби він присутній без ключа
    // Слід зауважити, що така автоматична наявність у моделі не відбувається, якщо параметр не є об’єктом

    @RequestMapping(value = "/v08", method = RequestMethod.GET)
    public String v08(@ModelAttribute("someone") Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-08";
}
  • рядок 11: анотація [@ModelAttribute("someone")] автоматично додасть об’єкт [Personne p] до моделі, пов’язаний із ключем [someone];
  • рядок 12: для перевірки моделі;
  • рядок 13: відображає вигляд [vue-08.xml];

Вигляд [vue-08.xml] має такий вигляд:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${someone}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • рядок 8: об’єкт Thymeleaf ініціалізується з об’єктом-ключем [someone];

Результат такий:

 

а в консолі з’являється такий запис:

Modèle={someone=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Тепер розглянемо таку дію [/v09]:


    @RequestMapping(value = "/v09", method = RequestMethod.GET)
    public String v09(Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-09";
}
  • рядок 1: наявність параметра [Personne p] автоматично додасть особу [p] до шаблону. Оскільки ключ не вказано, використовується ім’я класу з першою літерою, написаною з малої літери. Отже, [Personne p] еквівалентно [@ModelAttribute("personne") Personne p];

Вигляд [vue.09.xml] такий:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • рядок 8: використовується шаблонний ключ [personne];

Ось результат:

 

а ось запис у консолі сервера:

Modèle={personne=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Тепер розглянемо таку нову дію [/v10]:


    @ModelAttribute("uneAutrePersonne")
    private Personne getPersonne(){
        return new Personne(24,"pauline",55);
    }

    @RequestMapping(value = "/v10", method = RequestMethod.GET)
    public String v10(Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-10";
}
  • рядки 1–4: визначають метод, який у шаблоні кожного запиту створює елемент ключа [uneAutrePersonne], пов’язаний з об’єктом [new Personne(24,"pauline",55)];
  • рядки 6–10: дія [/v10] нічого не робить, окрім передачі отриманої моделі до подання [vue-10.xml]. Слід зауважити, що параметр [Model model] необхідний лише для інструкції у рядку 8. Без нього він є зайвим;

Вигляд [vue-10.xml] має такий вигляд:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${uneAutrePersonne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Результат такий:

 

а вивід консолі такий:

Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55]}

5.9. [/v11] : @SessionAttributes

Повернемося до того, що ми розглядали під час вивчення дій, — до ролі анотації [@SessionAttributes]. Додаємо таку нову дію [/v11]:


    @ModelAttribute("jean")
    private Personne getJean(){
        return new Personne(33,"jean",10);
    }

    @RequestMapping(value = "/v11", method = RequestMethod.GET)
    public String v11(Model model, HttpSession session) {
        System.out.println(String.format("Modèle=%s, Session[jean]=%s", model, session.getAttribute("jean")));
        return "vue-11";
}

Ми маємо щось аналогічне до того, що щойно розглядали. Відмінність полягає в анотації [@SessionAttributes], розміщеній безпосередньо на класі:


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • рядок 2: вказано, що ключ [jean] моделі має бути розміщений у сесії;

Саме тому в рядку 7 дії було введено сесію. У рядку 8 відображається значення сесії, пов’язане з ключем [jean].

Вигляд [vue-11.xml] такий:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${jean}">
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
        <hr />
        <div th:object="${session.jean}">
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Відображаються дві особи:

  • рядки 8–21: особа з ключем [jean] у моделі;
  • рядки 23–36: особа з ключем [jean] у сесії;

Результати такі:

  • у [1] — особа з ключем [jean] у моделі;
  • в [2] — особа з ключем [jean] у сеансі;

Журнал консолі виглядає наступним чином:


Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55], jean=[id=33, nom=jean,  age=10]}, Session[jean]=null

З наведеного вище видно, що ключ [jean] відсутній у сесії, яка отримує дію. Звідси випливає, що ключ [jean] був доданий до сесії після виконання дії та перед відображенням подання.

Тепер розглянемо випадок, коли на ключ одночасно посилаються [@ModelAttribute] та [@SessionAttributes]. Створимо такі дві дії:


    @RequestMapping(value = "/v12a", method = RequestMethod.GET)
    @ResponseBody
    public void v12a(HttpSession session) {
        session.setAttribute("paul", new Personne(51, "paul", 33));
    }

    // у випадку, коли ключ [@ModelAttribute] є також ключем [@SessionAttributes]
    // у цьому випадку відповідний параметр ініціалізується значенням сесії
    @RequestMapping(value = "/v12b", method = RequestMethod.GET)
    public String v12b(Model model, @ModelAttribute("paul") Personne p) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-12";
}

Дія [/v12a] служить лише для того, щоб додати до сесії елемент ['paul',new Personne(51, "paul", 33)]. Більше вона нічого не робить. Те, що вона позначена тегом [@ResponseBody], вказує на те, що саме вона генерує відповідь клієнту. Оскільки її тип — [void], відповідь не генерується.

Дія [/v12b] приймає як параметр [@ModelAttribute("paul") Personne p]. Якщо не вжити інших заходів, об’єкт [Personne] буде інстанційовано, а потім ініціалізовано з параметрами запиту, і цей об’єкт не матиме нічого спільного з об’єктом-ключем [paul], доданим до сесії дією [/v12a]. Ми додамо ключ [paul] до атрибутів сесії класу:


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • у рядку 2 тепер є два атрибути сеансу;

Повернемося до параметрів дії [/v12b]:


public String v12b(Model model, @ModelAttribute("paul") Personne p) {

Тепер об’єкт [Personne p] не буде інстанційовано, а буде посилатися на об’єкт-ключ [paul] у сесії. Далі процедура залишається незмінною. Зокрема, об’єкт-ключ [paul] опиниться в шаблоні подання, яке буде відображено. Саме це ми хочемо побачити в рядку 11 дії [/v12b].

Вигляд [vue-12.xml] матиме такий вигляд:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${paul}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • рядок 8: посилання на ключ [paul] з моделі подання;

Це дає такий результат (після виконання дії [/v12a], яка вносить ключ [paul] у сесію):

 

Журнал консолі виглядає так:


Modèle={jean=[id=33, nom=jean,  age=10], uneAutrePersonne=[id=24, nom=pauline,  age=55], paul=[id=51, nom=paul,  age=33], org.springframework.validation.BindingResult.paul=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Ключ [paul] дійсно був доданий до шаблону зі значенням, пов’язаним із ключем [paul] у сесії.

5.10. [/v13]: створення форми для введення даних

Тепер перейдемо до введення даних у форми та їх перевірки. Створимо першу форму за допомогою такої дії [/v13]:


  // генерує форму для введення даних про особу
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

яка просто відображає такий вигляд [vue-13.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v14.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${personne}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="''" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
        </form>
    </body>
</html>

Якщо ми помістимо цей вигляд у папку [static] під назвою [vue-13.html] і запросимо URL [http://localhost:8080/vue-13.html], ми отримаємо таку сторінку:

 
  • у 8-му рядку форми міститься тег <form> з атрибутом [th:action]. Цей атрибут буде оброблено Thymeleaf, а його значення замінить поточне значення атрибута [action], який, отже, присутній лише для декоративних цілей. У цьому випадку значення атрибута [th:action] дорівнюватиме [/v14.html];
  • у рядках 17, 23 та 29 значення атрибута [th:value] замінить значення атрибута [value]. У цьому випадку це значення буде порожнім рядком;

При запиті URL [/v13.html] отримуємо такий результат:

 

Розглянемо вихідний код, згенерований Thymeleaf:


<!DOCTYPE html>

<html>
    <head>
        <title>Views in Spring MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/v14.html" method="post">
            <h2>Please, enter information and validate</h2>
            <div>
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td>Identifier:</td>
                            <td>
                                <input type="text" name="id" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Name:</td>
                            <td>
                                <input type="text" name="nom" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Age:</td>
                            <td>
                                <input type="text" name="age" value="" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Validate" />
        </form>
    </body>
</html>

У рядках 9, 18, 24 та 30 видно, як Thymeleaf обчислює атрибути [th:action] та [th:value].

5.11. [/v14]: обробка значень, надісланих через форму

Акція [/v14] — це акція, яка отримує відправлені значення. Вона виглядає наступним чином:


  // обробляє значення з форми
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Personne p) {
    return "vue-14";
}
  • рядок 3: відправлені значення інкапсульовані в об’єкт [Personne p]. Відомо, що цей об’єкт автоматично стає частиною моделі M подання V, яке буде відображатися цією дією, пов’язаною з ключем [personne];
  • рядок 4: відображується вигляд [vue-14.xml];

Вигляд [vue-14.xml] має такий вигляд:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • рядок 9: з моделі отримується об’єкт, пов’язаний із ключем [personne];
  • рядки 12, 16 та 20: відображаються характеристики цього об’єкта;

У результаті отримуємо таке:

5.12. [/v15-/v16]: перевірка моделі

На прикладі попереднього прикладу розглянемо таку послідовність:

  • у [1] вводяться неправильні значення для полів [id] та [age] типу [int];
  • у [2] відповідь сервера вказує, що сталося дві помилки;

Ми будемо використовувати ту саму форму, але в разі помилок перевірки ми будемо перенаправляти на сторінку з повідомленням про ці помилки, щоб користувач міг їх виправити.

Дія [/v15] виглядає наступним чином:


    // ---------------------- відображення форми
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "vue-15";
}

Вона отримує як параметр тип [SecuredPerson], що має такий вигляд:

  

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class SecuredPerson {

    @Range(min = 1)
    private int id;
    
    @Length(min = 4, max = 10)
    private String nom;
    
    @Range(min = 8, max = 14)
    private int age;

    // конструктори
    public SecuredPerson() {

    }

    public SecuredPerson(int id, String nom, int age) {
        this.id=id;
        this.nom = nom;
        this.age = age;
    }

    // методи getter та setter
...
}

Поля [id, nom, age] були позначені обмеженнями перевірки. Вигляд [vue-15.xml], що відображається дією [/v15], є таким:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v16.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${securedPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" style="color: red">Identifiant erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>
  • рядки 10–47: отримується об’єкт моделі сторінки, пов’язаний із ключем [securedPerson]. Після виконання GET отримуємо об’єкт із значенням інстанціювання [id=0, nom=null, age=0];
  • рядок 17: значення поля [securedPerson.id];
  • рядок 20: вираз [${#fields.hasErrors('id')}] дозволяє визначити, чи були помилки перевірки у полі [securedPerson.id]. Якщо так, атрибут [th:errors="*{id}"] відображає відповідне повідомлення про помилку;
  • цей сценарій повторюється у рядках 29 для поля [nom] та 38 для поля [age];
  • рядок 45: вираз [${#fields.errors('*')}] позначає сукупність помилок у полях об’єкта [securedPerson]. Отже, саме ця сукупність помилок буде відображена в рядках 44–46;
  • рядок 16: бачимо, що значення форми будуть передані до дії [/v16]. Вона має такий вигляд:

    // -------------------- перевірка моделі------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // помилки?
        if (result.hasErrors()) {
            return "vue-15";
        } else {
            return "vue-16";
        }
}
  • у рядку 3 анотація [@Valid SecuredPerson p] примусово перевіряє відправлені значення;
  • рядок 5: перевіряє, чи є шаблон дії помилковим;
  • рядок 6: якщо вона помилкова, повертається форма [vue-15.xml]. Оскільки вона відображає повідомлення про помилки, ми їх переглянемо;
  • рядок 8: якщо шаблон дії пройшов перевірку, то відображається наступний вигляд [vue-16.xml]:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${securedPerson}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Ось приклади виконання:

5.13. [/v17-/v18]: перевірка повідомлень про помилки

При першому виклику дії [/v15] отримуємо такий результат:

 

Можливо, замість нулів у полях [Identifiant, Age] бажано отримати порожню форму. Щоб це зробити, ми змінюємо модель дії наступним чином:


package istia.st.springmvc.models;

import javax.validation.constraints.Digits;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class StringSecuredPerson {

    @Range(min = 1)
    @Digits(fraction = 0, integer = 4)
    private String id;

    @Length(min = 4, max = 10)
    private String nom;

    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

    // конструктори
    public StringSecuredPerson() {

    }

    public StringSecuredPerson(String id, String nom, String age) {
        this.id = id;
        this.nom = nom;
        this.age = age;
    }

    // геттери та сеттери
...

}
  • рядки 12 та 19: типи полів [id] та [age] змінено на [String];
  • рядок 11: вказується, що поле [id] має бути числом, що складається не більше ніж з чотирьох цифр, без десяткових знаків;
  • рядок 18: те саме стосується поля [age], яке має бути цілим числом, що складається не більше ніж з двох цифр;

Дія [/v17] виглядає наступним чином:


    // ---------------------- відображення форми
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "vue-17";
}

Вигляд [vue-17.xml], що відображається дією [/v17], є таким:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v18.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${stringSecuredPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:each="err,status : ${#fields.errors('id')}" th:if="${status.index}==0" th:text="${err}" style="color: red">
                                    Identifiant erroné
                                </span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>

Зміни відбуваються в таких рядках:

  • рядок 10: тепер ми працюємо з об’єктом шаблону ключа [stringSecuredPerson];
  • рядок 20: відбувається перебір списку помилок поля [id]. У синтаксисі [th:each="err,status : ${#fields.errors('id')}"] перебір списку здійснює змінна [err]. Змінна [status] надає інформацію про кожну ітерацію. Це об’єкт [index, count, size, current], де:
    • index — номер поточного елемента,
    • current — значення цього поточного елемента,
    • count, size — розмір списку, що обходиться;
  • рядок 20: відображається лише перший елемент списку [th:if="${status.index}==0"];

Дія [/v18], яка обробляє POST з дії [/v17], виглядає наступним чином:


    // -------------------- перевірка моделі------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // помилки?
        if (result.hasErrors()) {
            return "vue-17";
        } else {
            return "vue-18";
        }
}

Файли повідомлень змінюються наступним чином:

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
notNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.id=Identifier:
personne.mineure=You are under 18
personne.majeure=You are over 18
liste.personnes=Persons' list
personne.formulaire.titre=Please, enter information and validate
personne.formulaire.valider=Validate
personne.formulaire.saisies=Here are your inputs
NotNull=Data is required
Range.securedPerson.id=Identifier must be an integer >=1
Range.securedPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.securedPerson.nom=Name must be 4 to 10 characters long
typeMismatch=Invalid format
Range.stringSecuredPerson.id=Identifier must be an integer >=1
Range.stringSecuredPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.stringSecuredPerson.nom=Name must be 4 to 10 characters long
Digits.stringSecuredPerson.id=Should be an integer with at most four digits
Digits.stringSecuredPerson.age=Should be an integer with at most two digits

Розглянемо кілька прикладів:

 

З [1] видно, що обидва валідатори поля [age] були виконані:


    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

Чи існує певний порядок виведення повідомлень про помилки? Щодо поля [age], здається, що валідатори були виконані в порядку [Digits, Range]. Але якщо виконати кілька запитів, можна помітити, що цей порядок може змінюватися. Отже, не можна покладатися на порядок виконання валідаторів. У [2] відображається лише одне з двох повідомлень поля [id]. У [3] відображаються всі повідомлення про помилки.

5.14. [/v19-/v20]: використання різних валідаторів

Розглянемо таку нову модель дії:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;

public class Form19 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;

    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotEmpty
    private String strNotEmpty;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    @NotBlank
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    @NotBlank
    private String url;

    // геттери та сеттери
...
}

Вона відображатиметься за допомогою наступної дії [/v19]:


    // ------------------ відображення форми
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • рядок 3: дія отримує як параметр об’єкт [Form19 formulaire]. Якщо GET не отримує параметрів, цей об’єкт буде ініціалізовано зі значеннями за замовчуванням у Java;
  • рядок 4: відображається представлення [vue-19.xml]. Воно має такий вигляд:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Validations côté serveur</h3>
        <form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Contrainte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Erreur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">@NotEmpty</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotEmpty}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@NotBlank</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotBlank}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotBlank')}" th:errors="*{strNotBlank}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertTrue</td>
                        <td class="col2">
                            <select th:field="*{assertTrue}">
                                <option value="true">True</option>
                                <option value="false">False</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Past</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Future</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Max</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMax100}" th:value="*{intMax100}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Min</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMin10}" th:value="*{intMin10}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Size</td>
                        <td class="col2">
                            <input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Email</td>
                        <td class="col2">
                            <input type="text" th:field="*{email}" th:value="*{email}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Length</td>
                        <td class="col2">
                            <input type="text" th:field="*{str4}" th:value="*{str4}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('str4')}" th:errors="*{str4}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Range</td>
                        <td class="col2">
                            <input type="text" th:field="*{int1014}" th:value="*{int1014}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@URL</td>
                        <td class="col2">
                            <input type="text" th:field="*{url}" th:value="*{url}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Цей код відображає такий вигляд:

 

На сторінці відображається таблиця з трьома стовпцями:

  • 1-й стовпець: валідатор поля введення;
  • 2-й стовпець: поле введення;
  • стовпець 3: повідомлення про помилки у полі введення;

Розглянемо, наприклад, код подання [/v19.html] для валідатора [@Pattern]:


                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
                        </td>
</tr>

Тут ми бачимо код, який щойно розглядали для форм типу [Personne]:

  • рядок 2: перший стовпець — назва тестованого валідатора;
  • рядок 4: атрибут Thymeleaf [th:field="*{hhmmss}] згенерує атрибути HTML, [id="hhmmss"] та [name="hhmmss"]. Атрибут Thymeleaf [th:value="*{hhmmss}"] згенерує атрибут HTML [value="valeur de [form19.hhmmss]]";
  • рядок 7: якщо значення, введене для поля [form19.hhmmss], є помилковим, то в рядку 7 відображаються повідомлення про помилки, пов’язані з цим полем;

Опубліковані значення обробляються наступною дією [/v20]:


    // ----------------- перевірка моделі форми
    @RequestMapping(value = "/v20", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v20(@Valid Form19 formulaire, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "vue-19";
        } else {
            // перенаправлення на [vue-19]
            redirectAttributes.addFlashAttribute("form19", formulaire);
            return "redirect:/v19.html";
        }
}
  • рядок 3: занесені значення заповнять поля об’єкта [Form19 formulaire], якщо вони є дійсними;
  • рядки 4–6: якщо занесені значення не є дійсними, то знову відображається форма [vue-19] із повідомленнями про помилки;
  • рядки 6–10: якщо відправлені значення є дійсними, то об’єкт [Form19 formulaire], створений на основі цих значень, надається для наступного запиту, в даному випадку — запиту на перенаправлення. Потім він знищується;
  • рядок 9: клієнт перенаправляється до дії [/v19.html]. Ця дія знову відобразить форму [vue-19], яка містить такий код:

<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">

Атрибут [th:object="${form19}"] отримає об’єкт, пов’язаний з атрибутом Flash [form19], і таким чином знову відобразить форму в тому вигляді, в якому вона була заповнена.

Код форми потребує ще деяких пояснень. Розглянемо такий код:


                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
                        </td>
</tr>

Це генерує такий код HTML:


<tr>
  <td class="col1">@assertFalse</td>
  <td class="col2">
    <input type="radio" value="true" id="assertFalse1" name="assertFalse" />
    <label for="assertFalse1">True</label>
    <input type="radio" value="false" id="assertFalse2" name="assertFalse" />
    <label for="assertFalse2">False</label>
  </td>
  <td class="col3">
  </td>
</tr>

У коді


<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>

атрибути Thymeleaf у рядках 1 і 3 [th:field="*{assertFalse}"] створюють проблему. Було зазначено, що цей атрибут генерує атрибути HTML, [id=assertFalse] та [name=assertFalse]. Складність полягає в тому, що, оскільки це генерується в рядках 1 і 3, ми отримуємо два однакові атрибути [name] і два однакові атрибути [id]. Якщо це можливо з атрибутом [name], то з атрибутом [id] це неможливо. Як видно з згенерованого коду HTML, Thymeleaf згенерував два різних атрибути [id]: [id=asserFalse1] та [id=assertFalse2]. Це добре. Проблема полягає в тому, що ми не знаємо цих ідентифікаторів, а вони можуть нам знадобитися. Це стосується тегу [label] у рядку 2. Атрибут [for] тегу HTML [label] повинен посилатися на атрибут [id], а саме на той, що згенеровано для тегу [input] у рядку 1. У документації Thymeleaf зазначено, що вираз [${#ids.prev('assertFalse')}"] дозволяє отримати останній атрибут [id], згенерований для поля [assertFalse].

Тепер розглянемо код випадаючого списку у формі:


<select th:field="*{assertTrue}">
   <option value="true">True</option>
   <option value="false">False</option>
</select>

Цей код генерує код HTML для випадаючого списку:

1
2
3
4
<select id="assertTrue" name="assertTrue">
  <option value="true">True</option>
  <option value="false">False</option>
</select>

Значення, що надсилається, матиме ім’я [name="assertTrue"].

Вигляд [vue-19.xml] використовує таблицю стилів:


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
</head>

У рядку 4 використовуваний стильовий аркуш має бути розміщений у папці [static] проекту:

  

Його вміст такий:


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Тепер розглянемо дати:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;

Аналіз мережевого трафіку в інструменті розробника Chrome (Ctrl-Shift-I) показує, що дати надсилаються у форматі (rrrr-mm-dd):

 

Саме тому дати були позначені валідатором:


@DateTimeFormat(pattern = "yyyy-MM-dd")

який визначає очікуваний формат для переданих значень дат.

Наостанок — файл повідомлень французькою мовою [messages_fr.properties]:


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
NotNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus
Future.form19.dateInFuture=La date doit être postérieure à celle d''aujourd'hui
Past.form19.dateInPast=La date doit être antérieure à celle d''aujourd'hui
Size.form19.strBetween4and6=la chaîne doit avoir entre 4 et 6 caractères
Min.form19.intMin10=La valeur doit être supérieure ou égale à 10
Max.form19.intMax100=La valeur doit être inférieure ou égale à 100
Length.form19.str4=La chaîne doit avoir quatre caractères exactement
Email.form19.email=Adresse mail invalide
URL.form19.url=URL invalide
Range.form19.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form19.hhmmss=Tapez l''heure sous la forme hh:mm:ss
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide

Розглянемо кілька прикладів виконання:

 
 

Вище, між [1] і [2], складається враження, що нічого не відбулося. Однак, якщо подивитися на мережевий обмін даними (Ctrl-Shift-I), можна побачити, що відбулося два обміни даними з сервером:

  • у [1] — початковий запит POST до [/v20];
  • на [2] — відповідь на цю дію є перенаправленням;
  • у [3] — другий запит, цього разу до [/v19];

Потім виконується дія [/v19]:


    // ------------------ відображення форми
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • рядок 3, параметр [Form19 formulaire] ініціалізується атрибутом Flash ключа [form19], який був створений попередньою дією [/v19] і який був об’єктом типу [Form19] із значеннями, значеннями, переданими до дії [/v19];
  • рядок 4: буде відображено представлення [vue-19.xml], у моделі якого міститься об’єкт [Form19 formulaire], ініціалізований за допомогою відправлених значень. Саме тому користувач бачить форму такою, якою він її відправив;

Чому відбувається перенаправлення? Чому ми просто не надіслали дані до дії [/v19], зазначеної вище? Ми отримали б той самий результат. За винятком кількох відмінностей:

  • браузер вказав би в адресному рядку [http://localhost:8080/v20.html] замість [http://localhost:8080/v19.html], як це було зроблено тут, оскільки він відображає останню викликану дію URL;
  • якщо користувач оновить сторінку (F5), результат буде зовсім іншим:
    • у разі перенаправлення відображається URL, отриманий із [http://localhost:8080/v19.html], який, у свою чергу, був отриманий із GET. Браузер повторно виконає цю останню команду і отримає абсолютно нову форму (атрибут Flash використовується лише один раз),
    • у разі відсутності перенаправлення відображається URL, отриманий з [http://localhost:8080/v20.html], який, у свою чергу, отримано з POST. Браузер повторно виконає цю останню команду і, отже, знову сформує POST з тими самими значеннями, що й раніше. У цьому випадку це не має наслідків, але часто є небажаним, тому зазвичай краще використовувати перенаправлення;

5.15. [/v21-/v22]: обробка перемикачів

Розглянемо такий компонент Spring [Listes]:

  

package istia.st.springmvc.models;

import org.springframework.stereotype.Component;

@Component
public class Listes {

    private String[] deplacements = new String[] { "0", "1", "2", "3", "4" };
    private String[] libellesDeplacements = new String[] { "vélo", "marche", "train", "avion", "autre" };
    private String[] libellesBijoux = new String[] { "émeraude", "rubis", "diamant", "opaline" };

    // методи getter та setter
  ...

}
  • рядок 5: клас [Listes] буде компонентом Spring;
  • рядки 8–10: списки, що використовуються для наповнення перемикачів, прапорців та випадаючих списків;

У класі конфігурації [Config] зазначено:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • рядок 2: пакет [models], у якому знаходиться компонент [Listes], буде належним чином проаналізований Spring;

Ми створюємо такі нові дії:


    // ------------------ форма з перемикачами
    @Autowired
    private Listes listes;

    @RequestMapping(value = "/v21", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v21(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-21";
    }

    @RequestMapping(value = "/v22", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v22(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("form", formulaire);
        return "redirect:/v21.html";
}
  • рядки 2–3: компонент [Listes] вводиться в контролер;
  • рядок 6: ми обробляємо форму типу [Form21], яку ми опишемо. Зазначимо, що ми вказали її ключ [form] у моделі подання. Нагадаємо, що за замовчуванням це був би [form21];
  • рядок 7: ми вставляємо компонент [Listes] у модель. Вид потребуватиме його;
  • рядок 8: відображається вигляд [vue-21.xml]. Цей вигляд відображатиме форму [Form21], а відправлені значення будуть передані до дії [/v22] у рядках 12–15;
  • рядки 12–15: дія [/v22] просто перенаправляє до дії [/v21], поміщаючи отримані відправлені значення в атрибут Flash із ключем [form]. Важливо, щоб цей ключ був таким самим, як той, що використовується в рядку 6;

Шаблон [Form21] має такий вигляд:

  

package istia.st.springmvc.models;

public class Form21 {

    // передані значення
    private String marie = "non";
    private String deplacement = "4";
    private String[] couleurs;
    private String strCouleurs;
    private String[] bijoux;
    private String strBijoux;
    private int couleur2;
    private int[] bijoux2;
    private String strBijoux2;

    // методи getter та setter
    ...
}

Вигляд [vue-21.xml] такий:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Boutons radio</h3>
        <form action="/someURL" th:action="@{/v22.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" th:field="*{marie}" value="oui" />
                            <label th:for="${#ids.prev('marie')}">Oui</label>
                            <input type="radio" th:field="*{marie}" value="non" />
                            <label th:for="${#ids.prev('marie')}">Non</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{marie}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span th:each="mode, status : ${listes.deplacements}">
                                <input type="radio" th:field="*{deplacement}" th:value="${mode}" />
                                <label th:for="${#ids.prev('deplacement')}" th:text="${listes.libellesDeplacements[status.index]}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{deplacement}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • рядки 36–40: зверніть увагу на використання компонента [Listes], доданого до шаблону, для формування текстів позначок;
  • у стовпці 3 можна дізнатися значення, введене для POST, або початкове значення форми під час початкового GET;

Цей код відображає таку сторінку:

 

що відповідає такому коду HTML:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Boutons radio</h3>
        <form action="/v22.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" value="oui" id="marie1" name="marie" />
                            <label for="marie1">Oui</label>
                            <input type="radio" value="non" id="marie2" name="marie" checked="checked" />
                            <label for="marie2">Non</label>
                        </td>
                        <td class="col3">
                            <span>non</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span>
                                <input type="radio" value="0" id="deplacement1" name="deplacement" />
                                <label for="deplacement1">vélo</label>
                            </span>
                            <span>
                                <input type="radio" value="1" id="deplacement2" name="deplacement" />
                                <label for="deplacement2">marche</label>
                            </span>
                            <span>
                                <input type="radio" value="2" id="deplacement3" name="deplacement" />
                                <label for="deplacement3">train</label>
                            </span>
                            <span>
                                <input type="radio" value="3" id="deplacement4" name="deplacement" />
                                <label for="deplacement4">avion</label>
                            </span>
                            <span>
                                <input type="radio" value="4" id="deplacement5" name="deplacement" checked="checked" />
                                <label for="deplacement5">autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span>4</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Видно, що відправлені значення (атрибути name) розміщуються в таких полях шаблону [Form21]:


    private String marie = "non";
    private String deplacement = "4";

Читачеві пропонується провести тестування. Слід звернути увагу, що саме атрибут [value] перемикачів відправляється.

5.16. [/v23-/v24]: управління прапорцями

Ми додаємо таку нову дію:


    // ------------------ форма з прапорцями
    @RequestMapping(value = "/v23", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String av20(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-23";
}
  • рядок 3: ми продовжуємо використовувати шаблон [Form21];

Вигляд [vue-23.xml] такий:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Cases à cocher</h3>
        <form action="/someURL" th:action="@{/v24.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" th:field="*{couleurs}" value="0" />
                            <label th:for="${#ids.prev('couleurs')}">rouge</label>
                            <input type="checkbox" th:field="*{couleurs}" value="1" />
                            <label th:for="${#ids.prev('couleurs')}">vert</label>
                            <input type="checkbox" th:field="*{couleurs}" value="2" />
                            <label th:for="${#ids.prev('couleurs')}">bleu</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{strCouleurs}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span th:each="label, status : ${listes.libellesBijoux}">
                                <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
                                <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • рядки 37–41: зверніть увагу на використання компонента [Listes] для генерації текстів для прапорців;

Цей код відображає таку сторінку:

 

на основі такого коду HTML:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Formulaire - Cases à cocher</h3>
        <form action="/v24.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" value="0" id="couleurs1" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs1">rouge</label>
                            <input type="checkbox" value="1" id="couleurs2" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs2">vert</label>
                            <input type="checkbox" value="2" id="couleurs3" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs3">bleu</label>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span>
                                <input type="checkbox" value="0" id="bijoux1" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux1">émeraude</label>
                            </span>
                            <span>
                                <input type="checkbox" value="1" id="bijoux2" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux2">rubis</label>
                            </span>
                            <span>
                                <input type="checkbox" value="2" id="bijoux3" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux3">diamant</label>
                            </span>
                            <span>
                                <input type="checkbox" value="3" id="bijoux4" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux4">opaline</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Слід зазначити, що відправлені значення (атрибути name) розміщуються в таких полях [Form21]:


    private String[] couleurs;
    private String[] bijoux;

Це масиви, оскільки для кожного поля існує кілька прапорців із назвою поля. Тому можлива ситуація, коли надходить кілька значень з однаковою назвою (атрибут name форми). Отже, для їхнього отримання потрібен масив.

Повернемося до коду Thymeleaf у 3-му стовпці сторінки:


  <td class="col3">
    <span th:text="*{strCouleurs}"></span>
  </td>
</tr>
<tr>
  <td class="col1">Pierres préférées</td>
  <td class="col2">
    <span th:each="label, status : ${listes.libellesBijoux}">
      <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
      <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
    </span>
  </td>
  <td class="col3">
    <span th:text="*{strBijoux}"></span>
  </td>
</tr>

Поля, на які посилаються рядки 2 та 14, такі:


    private String strCouleurs;
    private String strBijoux;

Вони обчислюються за допомогою дії [/v24], яка керує POST:


    // маппер Джексона / jSON
    private ObjectMapper mapper = new ObjectMapper();

    @RequestMapping(value = "/v24", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String av21(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
        redirectAttributes.addFlashAttribute("form", formulaire);
        formulaire.setStrCouleurs(mapper.writeValueAsString(formulaire.getCouleurs()));
        formulaire.setStrBijoux(mapper.writeValueAsString(formulaire.getBijoux()));
        return "redirect:/v23.html";
}

Тут слід пам’ятати, що бібліотека jackson / jSON входить до залежностей проєкту.

  • рядок 2: створюється тип [ObjectMapper], який дозволяє серіалізувати/десеріалізувати об’єкти у форматі jSON,
  • рядок 7: масив кольорів серіалізується у формат jSON. Результат розміщується у полі [strCouleurs];
  • рядок 8: масив прикрас серіалізується у формат jSON. Результат розміщується у полі [strBijoux];

Ось приклад виконання:

Зверніть увагу, що надсилається саме атрибут [value] прапорців.

5.17. [/25-/v26]: управління списками

Ми додаємо таку дію [/v25]:


  // ------------------ форма зі списками
  @RequestMapping(value = "/v25", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v25(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-25";
}

Вигляд [vue-25.xml] такий:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Listes</h3>
        <form action="/someURL" th:action="@{/v26.html}" method="post"
            th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select th:field="*{couleur2}">
                                <option value="0">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{couleur2}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select th:field="*{bijoux2}" multiple="multiple" size="3">
                                <option th:each="label, status : ${listes.libellesBijoux}"
                                    th:text="${label}" th:value="${status.index}">
                                </option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux2}"></span>
                        </td>
                    </tr>

                </tbody>
            </table>
            <input type="submit" value="Valider" />
        </form>
    </body>
</html>
  • рядки 38–42: формування списку з кількома варіантами вибору, де назви беруться з компонента [Listes], який ми вже використовували;

Сторінка, що відображається, виглядає так:

 

згенерована за допомогою такого коду HTML:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Formulaire - Listes</h3>
        <form action="/v26.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select id="couleur2" name="couleur2">
                                <option value="0" selected="selected">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span>0</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select multiple="multiple" size="3" id="bijoux2" name="bijoux2">
                                <option value="0">émeraude</option>
                                <option value="1">rubis</option>
                                <option value="2">diamant</option>
                                <option value="3">opaline</option>
                            </select>
                            <input type="hidden" name="_bijoux2" value="1" />
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • рядок 44: можна помітити, що Thymeleaf створив приховане поле. Я не зрозумів його призначення:
  • значення, що надсилаються (атрибути value тегів option), будуть розміщені в наступних полях (атрибути name) тегу [Form21]:

    private int couleur2;
    private int[] bijoux2;
  • рядок 38: список [bijoux2] є списком з можливістю вибору декількох варіантів. Отже, з іменем [bijoux2] може бути надіслано кілька значень. Щоб їх отримати, поле [bijoux2] має бути масивом. Слід зауважити, що це масив цілих чисел. Це можливо, оскільки відправлені значення можна перетворити на цей тип;

Значення передаються до наступної дії [/v26]:


  @RequestMapping(value = "/v26", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
  public String v26(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
    redirectAttributes.addFlashAttribute("form", formulaire);
    formulaire.setStrBijoux2(mapper.writeValueAsString(formulaire.getBijoux2()));
    return "redirect:/v25.html";
}

Тут немає нічого, чого ми б раніше не бачили. Ось приклад виконання:

5.18. [/v27]: налаштування повідомлень

Розглянемо таку дію [/v27]:


  // ------------------ повідомлення з параметрами
  @RequestMapping(value = "/v27", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v27(Model model) {
        model.addAttribute("param1","paramètre un");
        model.addAttribute("param2","paramètre deux");
        model.addAttribute("param3","paramètre trois");
        model.addAttribute("param4","messages.param4");        
        return "vue-27";
}

Ця дія просто підставляє чотири значення в шаблон і відображає такий вигляд [vue-27.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{messages.titre}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2 th:text="#{messages.titre}">Spring 4 MVC</h2>
        <p th:text="#{messages.msg1(${param1})}"></p>
        <p th:text="#{messages.msg2(${param2},${param3})}"></p>
        <p th:text="#{messages.msg3(#{${param4}})}"></p>
    </body>
</html>
  • рядок 8: повідомлення без параметрів;
  • рядок 9: повідомлення з одним параметром [$param1], взятим із шаблону;
  • рядок 10: повідомлення з двома параметрами [$param2, $param3], взятими з шаблону;
  • рядок 11: повідомлення з одним параметром. Цей параметр сам по собі є ключем повідомлення (наявність символу #). Ключ надається [$param4];

Файл повідомлень французькою мовою має такий вигляд:

[messages_fr.properties]


messages.titre=Messages paramétrés
messages.msg1=Un message avec un paramètre : {0}
messages.msg2=Un message avec deux paramètres : {0}, {1}
messages.msg3=Un message avec une clé de message comme paramètre : {0}
messages.param4=paramètre quatre

Щоб вказати наявність параметрів у повідомленні, використовуються символи {0}, {1}, ...

Об’єднання шаблону, створеного дією [/v27], з видом [vue-27] дасть такий код HTML:


<!DOCTYPE html>

<html>
    <head>
        <title>Messages paramétrés</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Messages paramétrés</h2>
        <p>Un message avec un paramètre : paramètre un</p>
        <p>Un message avec deux paramètre : paramètre deux, paramètre trois</p>
        <p>Un message avec une clé de message comme paramètre : paramètre quatre</p>
    </body>
</html>

що дає такий вигляд:

 

Файл англійських повідомлень має такий вигляд:

[messages_fr.properties]


messages.titre=Parameterized messages
messages.msg1=Message with one parameter: {0}
messages.msg2=Message with two parameters: {0}, {1}
messages.msg3=Message with a message key as a parameter: {0}
messages.param4=parameter four

Об’єднання шаблону, створеного дією [/v27], з видом [vue-27] дасть такий код HTML:


<!DOCTYPE html>

<html>
    <head>
        <title>Parameterized messages</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Parameterized messages</h2>
        <p>Message with one parameter: paramètre un</p>
        <p>Message with two parameters: paramètre deux, paramètre trois</p>
        <p>Message with a message key as a parameter: parameter four</p>
    </body>
</html>

що дасть такий вигляд:

 

Бачимо, що останнє повідомлення було повністю інтернаціоналізовано, чого не можна сказати про два попередні.

5.19. Використання шаблону

У веб-додатку часто трапляється, що подання мають спільні елементи, які можна винести в шаблон. Ось приклад:

Вище наведено дві схожі сторінки, де фрагмент [1] було замінено на фрагмент [2]. Вигляд відповідає шаблону сторінки, що містить три фіксовані фрагменти [3-5] та один змінний фрагмент [6].

5.19.1. Проєкт

Ми створюємо проект [springmvc-masterpage], дотримуючись підходу, описаного в розділі 5.1.

  

Файл [pom.xml] має такий вигляд:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-masterpage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springmvc-masterpage</name>
    <description>Page maître</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.9.RELEASE</version>
        <relativePath/> <!-- пошук батьківського елемента у сховищі -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.springmvc.main.Main</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Одна із залежностей, що містяться в цьому файлі, необхідна для головної сторінки:

 

Пакеты [config] та [main] ідентичні пакетам з такими самими назвами з попереднього проєкту.

5.19.2. Головна сторінка

  

Шаблон — це наступний вигляд [layout.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <title>Layout</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <table style="width: 400px">
            <tr>
                <td colspan="2" bgcolor="#ccccff">
                    <div th:include="entete" />
                </td>
            </tr>
            <tr style="height: 200px">
                <td bgcolor="#ffcccc">
                    <div th:include="menu" />
                </td>
                <td>
                    <section layout:fragment="contenu">
                        <h2>Contenu</h2>
                    </section>
                </td>
            </tr>
            <tr bgcolor="#ffcc66">
                <td colspan="2">
                    <div th:include="basdepage" />
                </td>
            </tr>
        </table>
    </body>
</html>
  • рядок 2: головна сторінка повинна визначити простір імен [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], елемент якого використовується в рядку 19;
  • рядки 10–12: генерують область [1], наведену нижче. Тег Thymeleaf [th:include] дозволяє включити в поточний вигляд фрагмент, визначений в іншому файлі. Це дає змогу об’єднати фрагменти, що використовуються в декількох виглядах;
  • рядки 15–17: генерують зону [2], наведену нижче;
  • рядки 19–20: генерують зону [3], наведену нижче. Атрибут [layout:fragment] є атрибутом простору імен [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Він вказує на область, яка під час виконання може бути замінена іншою;
  • рядки 24–28: генерують поле [4], наведене нижче;

5.19.3. Фрагменти

Фрагменти [entete.xml], [menu.xml] та [basdepage.xml] мають такий вигляд:

[entete.xml]


<!DOCTYPE html>
<html>
    <h2>entête</h2>
</html>

[menu.xml]


<!DOCTYPE html>
<html>
    <h2>menu</h2>
</html>

[basdepage.xml]


<!DOCTYPE html>
<html>
    <h2>bas de page</h2>
</html>

Фрагмент [page1.xml] виглядає так:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 1</h2>
        <form action="/someURL" th:action="@{/page2.html}" method="post">
            <input type="submit" value="Page 2" />
        </form>
    </section>
</html>
  • рядок 2: атрибут [layout:decorator="layout"] вказує, що поточна сторінка [page1.xml] є «оформленою», тобто належить до шаблону. Це значення атрибуту, в даному випадку — вид [layout.xml];
  • рядок 3: вказується, у який фрагмент головної сторінки буде вставлено [page1.xml]. Атрибут [layout:fragment="contenu"] вказує, що [page1.xml] буде вставлено у фрагмент із назвою [contenu], тобто в зону [3] шаблону;
  • рядки 5–7: вміст фрагмента — це форма, яка містить кнопку POST для переходу до дії [/page2.html];

Фрагмент [page2.xml] є аналогічним:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 2</h2>
        <form action="/someURL" th:action="@{/page1.html}" method="post">
            <input type="submit" value="Page 1" />
        </form>
    </section>
</html>

5.19.4. Дії

 

Контролер [Layout.java] виглядає так:


package istia.st.springmvc.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class Layout {
    @RequestMapping(value = "/page1")
    public String page1() {
        return "page1";
    }

    @RequestMapping(value = "/page2", method=RequestMethod.POST)
    public String page2() {
        return "page2";
    }
}
  • рядки 10–12: дія [/page1] просто відображає вигляд [page1.xml];
  • рядки 15–17: те саме стосується дії [/page2], яка відображає вигляд [page2.xml];