Первая часть
Третья часть
Первое, что покажет наша страница /WEB-INF/templates/seedstartermng.html, — это список с начальными стартовыми данными, которые в данный момент сохранены. Для этого нам потребуются некоторые внешние сообщения, а также некоторая работа выражений для атрибутов модели. Как это:
Здесь много чего посмотреть. Давайте посмотрим на каждый фрагмент отдельно.
Прежде всего, этот раздел будет отображаться только при наличии seed стартеров. Мы достигаем этого с помощью атрибута th:never и функции #lists.isEmpty(...).
Обратите внимание, что все служебные объекты, такие как #lists, доступны в выражениях Spring EL так же, как и в выражениях OGNL в стандартном диалекте.
Следующее, что нужно увидеть, это много интернационализированных (экстернализованных) текстов, таких как:
<h2 th:text="#{title.list}">List of Seed Starters
Это приложение Spring MVC, мы уже определили bean-компонент MessageSource в нашей конфигурации Spring (объекты MessageSource являются стандартным способом управления внешними текстами в Spring MVC):
… и это свойство basename указывает, что в нашем пути к классам у нас будут файлы, такие как Messages_es.properties или Messages_en.properties. Давайте посмотрим на испанскую версию:
В первом столбце таблицы мы покажем дату, когда был подготовлен стартер. Но мы покажем, что он отформатирован так, как мы определили в нашем DateFormatter. Для этого мы будем использовать синтаксис двойной скобки (${{...}}), который будет автоматически применять сервис преобразования Spring, в том числе DateFormatter, который мы зарегистрировали при настройке.
Далее показано, покрыт ли начальный контейнер seed starter или нет, путем преобразования значения свойства булевого покрытого бина в интернационализированное «да» или «нет» с буквальным выражением подстановки:
Теперь мы должны показать тип начального seed starter контейнера. Тип представляет собой java-перечисление с двумя значениями (WOOD и PLASTIC), и поэтому мы определили два свойства в нашем файле Messages с именами seedstarter.type.WOOD и seedstarter.type.PLASTIC.
Но чтобы получить интернационализированные имена типов, нам нужно добавить seedstarter.type. префикс к значению enum с помощью выражения, результат которого мы затем будем использовать в качестве ключа сообщения:
Самая сложная часть этого списка — колонка фич. В нем мы хотим отобразить все функции нашего контейнера, которые представлены в виде массива перечислений Feature, разделенных запятыми. Как «Электрическое отопление, газон».
Обратите внимание, что это особенно сложно, потому что эти значения перечисления также должны быть выведены, как мы делали с Types. Поток вывода следующий:
Для этого мы создаем следующий код:
Последний столбец нашего списка на самом деле будет довольно простым. Даже если у него есть вложенная таблица для отображения содержимого каждой строки в контейнере:
Объект команды — это имя, которое Spring MVC дает бинам поддержки форм, то есть объектам, которые моделируют поля формы и предоставляют методы получения и установки, которые будут использоваться платформой для установления и получения значений, введенных пользователем в браузере.
Thymeleaf требует, чтобы вы указали объект команды, используя атрибут th:object в вашем теге <form>:
Это согласуется с другим использованием th:object, но на самом деле этот конкретный сценарий добавляет некоторые ограничения для правильной интеграции с инфраструктурой Spring MVC:
Давайте теперь посмотрим, как добавить input в нашу форму:
Как видите, мы вводим новый атрибут: th:field. Это очень важная функция для интеграции Spring MVC, поскольку она выполняет всю тяжелую работу по связыванию вашего input со свойством в компоненте поддержки формы. Вы можете видеть его как эквивалент атрибута пути в теге из библиотеки тегов JSP Spring MVC.
Атрибут th:field ведет себя по-разному в зависимости от того, прикреплен ли он к тегу <input>, <select> или <textarea> (а также в зависимости от конкретного типа тега <input>). В этом случае (input[type = text]) приведенная выше строка кода похожа на:
… Но на самом деле это немного больше, потому что th:field также будет применять зарегистрированную службу преобразования Spring, включая DateFormatter, который мы видели ранее (даже если выражение поля не заключено в квадратные скобки). Благодаря этому дата будет отображаться правильно отформатированной.
Значения для атрибутов th:field должны быть выражениями выбора (*{…}), что имеет смысл, учитывая тот факт, что они будут оцениваться на компоненте, поддерживающем форму, а не на переменных контекста (или атрибутах модели в жаргоне Spring MVC). ).
В отличие от выражений в th:object, эти выражения могут включать в себя навигацию по свойствам (фактически здесь позволено любое выражение, разрешенное для атрибута пути тега <form: input> JSP).
Обратите внимание, что th:field также понимает новые типы элемента <input>, представленные в HTML5, такие как <input type = «datetime» … />, <input type = «color» … /> и т. д., эффективно добавляя полную поддержку HTML5 для Spring MVC.
th:field также позволяет определять входные данные checkbox флажков. Давайте посмотрим пример с нашей HTML-страницы:
Обратите внимание, что здесь есть кое-что еще, кроме самого флажка, например, внешняя метка, а также использование функции #ids.next(‘closed’) для получения значения, которое будет применено к атрибуту id входных данных флажка.
Зачем нам нужно динамическое создание атрибута id для этого поля? Поскольку флажки являются потенциально многозначными, и, следовательно, к их значениям идентификатора всегда будет добавлен суффикс порядкового номера (внутренне используя функцию #ids.seq(…)), чтобы гарантировать, что каждый из флажков input одного и того же свойства имеет другое значение идентификатора.
Нам будет легче это увидеть, если мы посмотрим на такое многозначное поле флажка:
Обратите внимание, что на этот раз мы добавили атрибут th:value, потому что поле функций не является логическим, как было описано выше, а представляет собой массив значений.
Давайте посмотрим вывод HTML, сгенерированный этим кодом:
Здесь мы видим, как суффикс последовательности добавляется к каждому атрибуту id input и как функция #ids.prev(…) позволяет нам извлечь последнее значение последовательности, сгенерированное для определенного идентификатора ввода.
Не беспокойтесь об этих скрытых входах с name = "_ features": они автоматически добавляются, чтобы избежать проблем с браузерами, не отправляющими невыбранные значения флажков на сервер при отправке формы.
Также обратите внимание, что если бы наше свойство features содержало некоторые выбранные значения в нашем form-backing bean, то th:field позаботилось бы об этом и добавило бы атрибут checked=«checked» в соответствующие входные теги.
Поля переключателей задаются аналогично не булевым (многозначным) флажкам, за исключением того, что, конечно, они не многозначны:
Поля выбора состоят из двух частей: тега <select> и его вложенных тегов <option>. При создании поля такого типа только тег <select> должен включать атрибут th:field, но атрибуты th:value во вложенных тегах <option> будут очень важны, поскольку они будут обеспечивать возможность узнать, какова текущая выбранная опция (аналогично не булевым флажкам и переключателям).
Давайте перестроим поле типа выпадающий список:
На данный момент, понять этот кусок кода довольно легко. Просто обратите внимание, как приоритет атрибута позволяет нам устанавливать атрибут th:each в самом теге <option>.
Благодаря расширенным возможностям связывания полей формы в Spring MVC, мы можем использовать сложные выражения Spring EL для привязки динамических полей формы к нашему form-backing bean. Это позволит нам создавать новые объекты Row в нашем компоненте SeedStarter и добавлять поля этих строк в нашу форму по запросу пользователя.
Для этого нам понадобится пара новых замапированных методов в нашем контроллере, которые добавят или удалят строку из нашего SeedStarter в зависимости от наличия определенных параметров запроса:
И теперь мы можем добавить динамическую таблицу в нашу форму:
Достаточно много вещей здесь, но не так много, чтобы не понимать… за исключением одной странной вещи:
Если вы помните из учебника "Использование Thymeleaf", то синтаксис __${…}__ является выражением предварительной обработки, которое является внутренним выражением, которое вычисляется до фактической оценки всего выражения. Но почему такой способ указания индекса строки? Не будет ли этого достаточно с:
…вообще-то нет. Проблема в том, что Spring EL не оценивает переменные в скобках индекса массива, поэтому при выполнении вышеприведенного выражения мы получим ошибку, сообщающую нам, что rows[rowStat.index] (вместо rows[0], rows[1] и т. д.) недопустимая позиция в коллекции строк. Вот почему здесь необходима предварительная обработка.
Давайте посмотрим на фрагмент полученного HTML-кода после того, как несколько раз нажали «Add Row»:
Третья часть
5 Отображение Seed Starter Data
Первое, что покажет наша страница /WEB-INF/templates/seedstartermng.html, — это список с начальными стартовыми данными, которые в данный момент сохранены. Для этого нам потребуются некоторые внешние сообщения, а также некоторая работа выражений для атрибутов модели. Как это:
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
<h2 th:text="#{title.list}">List of Seed Starters</h2>
<table>
<thead>
<tr>
<th th:text="#{seedstarter.datePlanted}">Date Planted</th>
<th th:text="#{seedstarter.covered}">Covered</th>
<th th:text="#{seedstarter.type}">Type</th>
<th th:text="#{seedstarter.features}">Features</th>
<th th:text="#{seedstarter.rows}">Rows</th>
</tr>
</thead>
<tbody>
<tr th:each="sb : ${allSeedStarters}">
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
<td th:text="#{|bool.${sb.covered}|}">yes</td>
<td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td>
<td th:text="${#strings.arrayJoin(
#messages.arrayMsg(
#strings.arrayPrepend(sb.features,'seedstarter.feature.')),
', ')}">Electric Heating, Turf</td>
<td>
<table>
<tbody>
<tr th:each="row,rowStat : ${sb.rows}">
<td th:text="${rowStat.count}">1</td>
<td th:text="${row.variety.name}">Thymus Thymi</td>
<td th:text="${row.seedsPerCell}">12</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
Здесь много чего посмотреть. Давайте посмотрим на каждый фрагмент отдельно.
Прежде всего, этот раздел будет отображаться только при наличии seed стартеров. Мы достигаем этого с помощью атрибута th:never и функции #lists.isEmpty(...).
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
Обратите внимание, что все служебные объекты, такие как #lists, доступны в выражениях Spring EL так же, как и в выражениях OGNL в стандартном диалекте.
Следующее, что нужно увидеть, это много интернационализированных (экстернализованных) текстов, таких как:
<h2 th:text="#{title.list}">List of Seed Starters
<table>
<thead>
<tr>
<th th:text="#{seedstarter.datePlanted}">Date Planted</th>
<th th:text="#{seedstarter.covered}">Covered</th>
<th th:text="#{seedstarter.type}">Type</th>
<th th:text="#{seedstarter.features}">Features</th>
<th th:text="#{seedstarter.rows}">Rows</th>
...
Это приложение Spring MVC, мы уже определили bean-компонент MessageSource в нашей конфигурации Spring (объекты MessageSource являются стандартным способом управления внешними текстами в Spring MVC):
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("Messages");
return messageSource;
}
… и это свойство basename указывает, что в нашем пути к классам у нас будут файлы, такие как Messages_es.properties или Messages_en.properties. Давайте посмотрим на испанскую версию:
title.list=Lista de semilleros
date.format=dd/MM/yyyy
bool.true=si
bool.false=no
seedstarter.datePlanted=Fecha de plantacion
seedstarter.covered=Cubierto
seedstarter.type=Tipo
seedstarter.features=Caracteristicas
seedstarter.rows=Filas
seedstarter.type.WOOD=Madera
seedstarter.type.PLASTIC=Plastico
seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato especifico para semilleros
seedstarter.feature.FERTILIZER=Fertilizante
seedstarter.feature.PH_CORRECTOR=Corrector de PH
В первом столбце таблицы мы покажем дату, когда был подготовлен стартер. Но мы покажем, что он отформатирован так, как мы определили в нашем DateFormatter. Для этого мы будем использовать синтаксис двойной скобки (${{...}}), который будет автоматически применять сервис преобразования Spring, в том числе DateFormatter, который мы зарегистрировали при настройке.
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
Далее показано, покрыт ли начальный контейнер seed starter или нет, путем преобразования значения свойства булевого покрытого бина в интернационализированное «да» или «нет» с буквальным выражением подстановки:
<td th:text="#{|bool.${sb.covered}|}">yes</td>
Теперь мы должны показать тип начального seed starter контейнера. Тип представляет собой java-перечисление с двумя значениями (WOOD и PLASTIC), и поэтому мы определили два свойства в нашем файле Messages с именами seedstarter.type.WOOD и seedstarter.type.PLASTIC.
Но чтобы получить интернационализированные имена типов, нам нужно добавить seedstarter.type. префикс к значению enum с помощью выражения, результат которого мы затем будем использовать в качестве ключа сообщения:
<td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td>
Самая сложная часть этого списка — колонка фич. В нем мы хотим отобразить все функции нашего контейнера, которые представлены в виде массива перечислений Feature, разделенных запятыми. Как «Электрическое отопление, газон».
Обратите внимание, что это особенно сложно, потому что эти значения перечисления также должны быть выведены, как мы делали с Types. Поток вывода следующий:
- Подставьте соответствующий префикс ко всем элементам массива фич.
- Получите внешние сообщения, соответствующие всем ключам из шага 1.
- Присоедините все сообщения, полученные на шаге 2, используя запятую в качестве разделителя.
Для этого мы создаем следующий код:
<td th:text="${#strings.arrayJoin(
#messages.arrayMsg(
#strings.arrayPrepend(sb.features,'seedstarter.feature.')),
', ')}">Electric Heating, Turf</td>
Последний столбец нашего списка на самом деле будет довольно простым. Даже если у него есть вложенная таблица для отображения содержимого каждой строки в контейнере:
<td>
<table>
<tbody>
<tr th:each="row,rowStat : ${sb.rows}">
<td th:text="${rowStat.count}">1</td>
<td th:text="${row.variety.name}">Thymus Thymi</td>
<td th:text="${row.seedsPerCell}">12</td>
</tr>
</tbody>
</table>
</td>
6 Создание Форм
6.1 Обработка командного объекта
Объект команды — это имя, которое Spring MVC дает бинам поддержки форм, то есть объектам, которые моделируют поля формы и предоставляют методы получения и установки, которые будут использоваться платформой для установления и получения значений, введенных пользователем в браузере.
Thymeleaf требует, чтобы вы указали объект команды, используя атрибут th:object в вашем теге <form>:
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
...
</form>
Это согласуется с другим использованием th:object, но на самом деле этот конкретный сценарий добавляет некоторые ограничения для правильной интеграции с инфраструктурой Spring MVC:
- Значения атрибутов th:object в тегах формы должны быть выражениями переменных (${...}), указывающими только имя атрибута модели, без навигации по свойствам. Это означает, что выражение типа ${seedStarter} является допустимым, но $ {seedStarter.data} не будет.
- Внутри тега <form> другой атрибут th:object не может быть указан. Это согласуется с тем фактом, что HTML-формы не могут быть вложенными.
6.2 Inputs
Давайте теперь посмотрим, как добавить input в нашу форму:
<input type="text" th:field="*{datePlanted}" />
Как видите, мы вводим новый атрибут: th:field. Это очень важная функция для интеграции Spring MVC, поскольку она выполняет всю тяжелую работу по связыванию вашего input со свойством в компоненте поддержки формы. Вы можете видеть его как эквивалент атрибута пути в теге из библиотеки тегов JSP Spring MVC.
Атрибут th:field ведет себя по-разному в зависимости от того, прикреплен ли он к тегу <input>, <select> или <textarea> (а также в зависимости от конкретного типа тега <input>). В этом случае (input[type = text]) приведенная выше строка кода похожа на:
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
… Но на самом деле это немного больше, потому что th:field также будет применять зарегистрированную службу преобразования Spring, включая DateFormatter, который мы видели ранее (даже если выражение поля не заключено в квадратные скобки). Благодаря этому дата будет отображаться правильно отформатированной.
Значения для атрибутов th:field должны быть выражениями выбора (*{…}), что имеет смысл, учитывая тот факт, что они будут оцениваться на компоненте, поддерживающем форму, а не на переменных контекста (или атрибутах модели в жаргоне Spring MVC). ).
В отличие от выражений в th:object, эти выражения могут включать в себя навигацию по свойствам (фактически здесь позволено любое выражение, разрешенное для атрибута пути тега <form: input> JSP).
Обратите внимание, что th:field также понимает новые типы элемента <input>, представленные в HTML5, такие как <input type = «datetime» … />, <input type = «color» … /> и т. д., эффективно добавляя полную поддержку HTML5 для Spring MVC.
6.3 Checkbox fields
th:field также позволяет определять входные данные checkbox флажков. Давайте посмотрим пример с нашей HTML-страницы:
<div>
<label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">Covered</label>
<input type="checkbox" th:field="*{covered}" />
</div>
Обратите внимание, что здесь есть кое-что еще, кроме самого флажка, например, внешняя метка, а также использование функции #ids.next(‘closed’) для получения значения, которое будет применено к атрибуту id входных данных флажка.
Зачем нам нужно динамическое создание атрибута id для этого поля? Поскольку флажки являются потенциально многозначными, и, следовательно, к их значениям идентификатора всегда будет добавлен суффикс порядкового номера (внутренне используя функцию #ids.seq(…)), чтобы гарантировать, что каждый из флажков input одного и того же свойства имеет другое значение идентификатора.
Нам будет легче это увидеть, если мы посмотрим на такое многозначное поле флажка:
<ul>
<li th:each="feat : ${allFeatures}">
<input type="checkbox" th:field="*{features}" th:value="${feat}" />
<label th:for="${#ids.prev('features')}"
th:text="#{${'seedstarter.feature.' + feat}}">Heating</label>
</li>
</ul>
Обратите внимание, что на этот раз мы добавили атрибут th:value, потому что поле функций не является логическим, как было описано выше, а представляет собой массив значений.
Давайте посмотрим вывод HTML, сгенерированный этим кодом:
<ul>
<li>
<input id="features1" name="features" type="checkbox" value="SEEDSTARTER_SPECIFIC_SUBSTRATE" />
<input name="_features" type="hidden" value="on" />
<label for="features1">Seed starter-specific substrate</label>
</li>
<li>
<input id="features2" name="features" type="checkbox" value="FERTILIZER" />
<input name="_features" type="hidden" value="on" />
<label for="features2">Fertilizer used</label>
</li>
<li>
<input id="features3" name="features" type="checkbox" value="PH_CORRECTOR" />
<input name="_features" type="hidden" value="on" />
<label for="features3">PH Corrector used</label>
</li>
</ul>
Здесь мы видим, как суффикс последовательности добавляется к каждому атрибуту id input и как функция #ids.prev(…) позволяет нам извлечь последнее значение последовательности, сгенерированное для определенного идентификатора ввода.
Не беспокойтесь об этих скрытых входах с name = "_ features": они автоматически добавляются, чтобы избежать проблем с браузерами, не отправляющими невыбранные значения флажков на сервер при отправке формы.
Также обратите внимание, что если бы наше свойство features содержало некоторые выбранные значения в нашем form-backing bean, то th:field позаботилось бы об этом и добавило бы атрибут checked=«checked» в соответствующие входные теги.
6.4 Radio Button fields
Поля переключателей задаются аналогично не булевым (многозначным) флажкам, за исключением того, что, конечно, они не многозначны:
<ul>
<li th:each="ty : ${allTypes}">
<input type="radio" th:field="*{type}" th:value="${ty}" />
<label th:for="${#ids.prev('type')}" th:text="#{${'seedstarter.type.' + ty}}">Wireframe</label>
</li>
</ul>
6.5 Dropdown/List selectors
Поля выбора состоят из двух частей: тега <select> и его вложенных тегов <option>. При создании поля такого типа только тег <select> должен включать атрибут th:field, но атрибуты th:value во вложенных тегах <option> будут очень важны, поскольку они будут обеспечивать возможность узнать, какова текущая выбранная опция (аналогично не булевым флажкам и переключателям).
Давайте перестроим поле типа выпадающий список:
<select th:field="*{type}">
<option th:each="type : ${allTypes}"
th:value="${type}"
th:text="#{${'seedstarter.type.' + type}}">Wireframe</option>
</select>
На данный момент, понять этот кусок кода довольно легко. Просто обратите внимание, как приоритет атрибута позволяет нам устанавливать атрибут th:each в самом теге <option>.
6.6 Dynamic fields
Благодаря расширенным возможностям связывания полей формы в Spring MVC, мы можем использовать сложные выражения Spring EL для привязки динамических полей формы к нашему form-backing bean. Это позволит нам создавать новые объекты Row в нашем компоненте SeedStarter и добавлять поля этих строк в нашу форму по запросу пользователя.
Для этого нам понадобится пара новых замапированных методов в нашем контроллере, которые добавят или удалят строку из нашего SeedStarter в зависимости от наличия определенных параметров запроса:
@RequestMapping(value="/seedstartermng", params={"addRow"})
public String addRow(final SeedStarter seedStarter, final BindingResult bindingResult) {
seedStarter.getRows().add(new Row());
return "seedstartermng";
}
@RequestMapping(value="/seedstartermng", params={"removeRow"})
public String removeRow(
final SeedStarter seedStarter, final BindingResult bindingResult,
final HttpServletRequest req) {
final Integer rowId = Integer.valueOf(req.getParameter("removeRow"));
seedStarter.getRows().remove(rowId.intValue());
return "seedstartermng";
}
И теперь мы можем добавить динамическую таблицу в нашу форму:
<table>
<thead>
<tr>
<th th:text="#{seedstarter.rows.head.rownum}">Row</th>
<th th:text="#{seedstarter.rows.head.variety}">Variety</th>
<th th:text="#{seedstarter.rows.head.seedsPerCell}">Seeds per cell</th>
<th>
<button type="submit" name="addRow" th:text="#{seedstarter.row.add}">Add row</button>
</th>
</tr>
</thead>
<tbody>
<tr th:each="row,rowStat : *{rows}">
<td th:text="${rowStat.count}">1</td>
<td>
<select th:field="*{rows[__${rowStat.index}__].variety}">
<option th:each="var : ${allVarieties}"
th:value="${var.id}"
th:text="${var.name}">Thymus Thymi</option>
</select>
</td>
<td>
<input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}" />
</td>
<td>
<button type="submit" name="removeRow"
th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button>
</td>
</tr>
</tbody>
</table>
Достаточно много вещей здесь, но не так много, чтобы не понимать… за исключением одной странной вещи:
<select th:field="*{rows[__${rowStat.index}__].variety}">
...
</select>
Если вы помните из учебника "Использование Thymeleaf", то синтаксис __${…}__ является выражением предварительной обработки, которое является внутренним выражением, которое вычисляется до фактической оценки всего выражения. Но почему такой способ указания индекса строки? Не будет ли этого достаточно с:
<select th:field="*{rows[rowStat.index].variety}">
...
</select>
…вообще-то нет. Проблема в том, что Spring EL не оценивает переменные в скобках индекса массива, поэтому при выполнении вышеприведенного выражения мы получим ошибку, сообщающую нам, что rows[rowStat.index] (вместо rows[0], rows[1] и т. д.) недопустимая позиция в коллекции строк. Вот почему здесь необходима предварительная обработка.
Давайте посмотрим на фрагмент полученного HTML-кода после того, как несколько раз нажали «Add Row»:
<tbody>
<tr>
<td>1</td>
<td>
<select id="rows0.variety" name="rows[0].variety">
<option selected="selected" value="1">Thymus vulgaris</option>
<option value="2">Thymus x citriodorus</option>
<option value="3">Thymus herba-barona</option>
<option value="4">Thymus pseudolaginosus</option>
<option value="5">Thymus serpyllum</option>
</select>
</td>
<td>
<input id="rows0.seedsPerCell" name="rows[0].seedsPerCell" type="text" value="" />
</td>
<td>
<button name="removeRow" type="submit" value="0">Remove row</button>
</td>
</tr>
<tr>
<td>2</td>
<td>
<select id="rows1.variety" name="rows[1].variety">
<option selected="selected" value="1">Thymus vulgaris</option>
<option value="2">Thymus x citriodorus</option>
<option value="3">Thymus herba-barona</option>
<option value="4">Thymus pseudolaginosus</option>
<option value="5">Thymus serpyllum</option>
</select>
</td>
<td>
<input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" />
</td>
<td>
<button name="removeRow" type="submit" value="1">Remove row</button>
</td>
</tr>
</tbody>