Repository: freemarker Updated Branches: refs/heads/3 2d799d492 -> 034b056c8
FREEMARKER-55: Handling selected for select/options/option directives Project: http://git-wip-us.apache.org/repos/asf/freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/freemarker/commit/034b056c Tree: http://git-wip-us.apache.org/repos/asf/freemarker/tree/034b056c Diff: http://git-wip-us.apache.org/repos/asf/freemarker/diff/034b056c Branch: refs/heads/3 Commit: 034b056c8b12caa4f80fe6ff8d61cb23b3f8951b Parents: 2d799d4 Author: Woonsan Ko <[email protected]> Authored: Fri Apr 27 16:25:06 2018 -0400 Committer: Woonsan Ko <[email protected]> Committed: Fri Apr 27 16:25:06 2018 -0400 ---------------------------------------------------------------------- .../spring/model/form/OptionOutputHelper.java | 9 +- .../form/OptionTemplateDirectiveModel.java | 4 - .../form/SelectableValueComparisonUtils.java | 3 + .../spring/example/mvc/users/User.java | 3 +- .../example/mvc/users/UserController.java | 16 +--- .../example/mvc/users/UserRepository.java | 15 ++++ .../spring/model/ElementAttributeMatcher.java | 86 ++++++++++++++++++++ .../model/MissingElementAttributeMatcher.java | 31 +++++++ .../form/SelectTemplateDirectiveModelTest.java | 81 ++++++++++++++---- .../model/form/select-directive-usages.f3ah | 17 +++- 10 files changed, 228 insertions(+), 37 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionOutputHelper.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionOutputHelper.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionOutputHelper.java index f8a5537..d06b1bf 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionOutputHelper.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionOutputHelper.java @@ -31,6 +31,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.servlet.support.BindStatus; +/** + * Helper to write <option> HTML element. + */ class OptionOutputHelper { private Object optionItems; @@ -113,15 +116,16 @@ class OptionOutputHelper { valueDisplayString = processOptionValue(valueDisplayString); - // allows render values to handle some strange browser compat issues. tagOut.writeAttribute("value", valueDisplayString); if (isOptionSelected(value) || (value != item && isOptionSelected(item))) { tagOut.writeAttribute("selected", "selected"); } + if (isOptionDisabled()) { tagOut.writeAttribute("disabled", "disabled"); } + tagOut.appendValue(labelDisplayString); tagOut.endTag(); } @@ -146,8 +150,7 @@ class OptionOutputHelper { } private boolean isOptionSelected(Object resolvedValue) throws TemplateException { - // FIXME: need to compare selected value based on bindStatus values. - return false; + return SelectableValueComparisonUtils.isEqualValueBoundTo(resolvedValue, bindStatus); } protected boolean isOptionDisabled() throws TemplateException { http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java index 7ad31ad..b6f8c76 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java @@ -131,10 +131,6 @@ class OptionTemplateDirectiveModel extends AbstractHtmlInputElementTemplateDirec tagOut.writeAttribute("selected", "selected"); } - if (isDisabled()) { - tagOut.writeAttribute("disabled", "disabled"); - } - tagOut.appendValue(label); tagOut.endTag(); } http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java index e17ab05..842d5fc 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java @@ -28,6 +28,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.servlet.support.BindStatus; +/** + * Utility to check if a value (mostly from an option item) equals to the value bound to. + */ class SelectableValueComparisonUtils { private SelectableValueComparisonUtils() { http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java index c271b0e..028f920 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java @@ -102,6 +102,7 @@ public class User { @Override public String toString() { return super.toString() + " {id=" + id + ", firstName='" + firstName + "', lastName='" + lastName + "', email='" - + email + "', birthDate='" + birthDate + "', description='" + description + "'}"; + + email + "', birthDate='" + birthDate + "', description='" + description + "', favoriteSport='" + + favoriteSport + "'}"; } } http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java index 71ba8e0..dfc1ed8 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java @@ -20,9 +20,6 @@ package org.apache.freemarker.spring.example.mvc.users; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -46,15 +43,6 @@ import org.springframework.web.bind.annotation.RequestParam; @Controller public class UserController { - public static final List<String> INDOOR_SPORTS = Collections - .unmodifiableList(Arrays.asList("bowling", "gymnastics", "handball")); - - public static final List<String> OUTDOOR_SPORTS = Collections - .unmodifiableList(Arrays.asList("baseball", "football", "marathon")); - - public static final List<String> ALL_SPORTS = Collections - .unmodifiableList(Arrays.asList("bowling", "gymnastics", "handball", "baseball", "football", "marathon")); - private static final String DEFAULT_USER_LIST_VIEW_NAME = "example/users/userlist"; private static final String DEFAULT_USER_EDIT_VIEW_NAME = "example/users/useredit"; @@ -84,8 +72,8 @@ public class UserController { @RequestMapping(value = "/users/{id:\\d+}", method = RequestMethod.GET) public String getUser(@PathVariable("id") Long id, @RequestParam(value = "viewName", required = false) String viewName, Model model) { - model.addAttribute("indoorSports", INDOOR_SPORTS); - model.addAttribute("outdoorSports", OUTDOOR_SPORTS); + model.addAttribute("indoorSports", UserRepository.INDOOR_SPORTS); + model.addAttribute("outdoorSports", UserRepository.OUTDOOR_SPORTS); User user = userRepository.getUser(id); http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java index 73c8d4d..e2a2283 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java @@ -19,7 +19,10 @@ package org.apache.freemarker.spring.example.mvc.users; +import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -30,6 +33,15 @@ import org.springframework.stereotype.Repository; @Repository public class UserRepository { + public static final List<String> INDOOR_SPORTS = Collections + .unmodifiableList(Arrays.asList("bowling", "gymnastics", "handball")); + + public static final List<String> OUTDOOR_SPORTS = Collections + .unmodifiableList(Arrays.asList("baseball", "football", "marathon")); + + public static final List<String> ALL_SPORTS = Collections + .unmodifiableList(Arrays.asList("bowling", "gymnastics", "handball", "baseball", "football", "marathon")); + private Map<Long, User> usersMap = new ConcurrentHashMap<>(); { Long id = 101L; @@ -45,6 +57,7 @@ public class UserRepository { user.setBirthDate(birthDate.getTime()); user.setDescription("Lorem ipsum dolor sit amet, \r\nconsectetur adipiscing elit, \r\n" + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + user.setFavoriteSport("baseball"); usersMap.put(id, user); id = 102L; @@ -60,6 +73,7 @@ public class UserRepository { user.setBirthDate(birthDate.getTime()); user.setDescription("Ut enim ad minim veniam, \r\n" + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."); + user.setFavoriteSport("marathon"); usersMap.put(id, user); } @@ -105,6 +119,7 @@ public class UserRepository { clone.setLastName(source.getLastName()); clone.setBirthDate(source.getBirthDate()); clone.setDescription(source.getDescription()); + clone.setFavoriteSport(source.getFavoriteSport()); return clone; } } http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ElementAttributeMatcher.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ElementAttributeMatcher.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ElementAttributeMatcher.java new file mode 100644 index 0000000..b194dbf --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ElementAttributeMatcher.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.spring.model; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.w3c.dom.Attr; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +/** + * DOM Element Attribute value matcher. + * This matches if there's an attribute having the same value as {@code attrName}. + */ +public class ElementAttributeMatcher extends BaseMatcher<Node> { + + private String attrName; + private String expectedAttrValue; + private String actualAttrValue; + + public ElementAttributeMatcher(String attrName, String expectedAttrValue) { + if (attrName == null) { + throw new IllegalArgumentException("Attribute name must not be null."); + } + + this.attrName = attrName; + this.expectedAttrValue = expectedAttrValue; + } + + @Override + public boolean matches(Object item) { + actualAttrValue = getAttributeValue((Node) item, attrName); + return (expectedAttrValue == null) ? (actualAttrValue == null) : expectedAttrValue.equals(actualAttrValue); + } + + @Override + public void describeTo(Description description) { + if (expectedAttrValue == null) { + description.appendText("The attribute value (@" + attrName + ") should be null"); + } else { + description.appendText("The attribute value (@" + attrName + ") should be '" + expectedAttrValue + "'"); + } + } + + @Override + public void describeMismatch(Object item, Description description) { + description.appendText("was ").appendValue(actualAttrValue); + } + + private String getAttributeValue(final Node node, final String attrName) { + final NamedNodeMap attrsMap = node.getAttributes(); + + if (attrsMap == null) { + return null; + } + + final int length = attrsMap.getLength(); + + for (int i = 0; i < length; i++) { + final Attr attr = (Attr) attrsMap.item(i); + + if (attrName.equals(attr.getName())) { + return attr.getValue(); + } + } + + return null; + } +} http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MissingElementAttributeMatcher.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MissingElementAttributeMatcher.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MissingElementAttributeMatcher.java new file mode 100644 index 0000000..329970d --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MissingElementAttributeMatcher.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.spring.model; + +/** + * DOM Element Attribute missing matcher. + * This matches if there's no attribute by {@code attrName}. + */ +public class MissingElementAttributeMatcher extends ElementAttributeMatcher { + + public MissingElementAttributeMatcher(String attrName) { + super(attrName, null); + } +} http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java index eafd098..a9bba59 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java @@ -20,8 +20,9 @@ package org.apache.freemarker.spring.model.form; import org.apache.freemarker.spring.example.mvc.users.User; -import org.apache.freemarker.spring.example.mvc.users.UserController; import org.apache.freemarker.spring.example.mvc.users.UserRepository; +import org.apache.freemarker.spring.model.ElementAttributeMatcher; +import org.apache.freemarker.spring.model.MissingElementAttributeMatcher; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +33,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.XpathResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -63,30 +65,81 @@ public class SelectTemplateDirectiveModelTest { public void testBasicUsages() throws Exception { final Long userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); + final String favoriteSport = user.getFavoriteSport(); + final ResultActions resultAcctions = mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/form/select-directive-usages") .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()); - for (int i = 0; i < UserController.INDOOR_SPORTS.size(); i++) { - String sport = UserController.INDOOR_SPORTS.get(i); - resultAcctions.andExpect(xpath("//form[@id='form1']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); + XpathResultMatchers xPathMatchers = xpath("//form[@id='form1']//select[@name='favoriteSport']"); + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("size"))); + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("multiple"))); + + for (int i = 0; i < UserRepository.INDOOR_SPORTS.size(); i++) { + final String sport = UserRepository.INDOOR_SPORTS.get(i); + xPathMatchers = xpath("//form[@id='form1']//select[@name='favoriteSport']//option[" + (i + 1) + "]"); + resultAcctions.andExpect(xPathMatchers.string(sport)); + + if (sport.equals(favoriteSport)) { + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("selected", "selected"))); + } else { + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("selected"))); + } } - for (int i = 0; i < UserController.OUTDOOR_SPORTS.size(); i++) { - String sport = UserController.OUTDOOR_SPORTS.get(i); - resultAcctions.andExpect(xpath("//form[@id='form2']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); + xPathMatchers = xpath("//form[@id='form2']//select[@name='favoriteSport']"); + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("size"))); + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("multiple"))); + + for (int i = 0; i < UserRepository.OUTDOOR_SPORTS.size(); i++) { + final String sport = UserRepository.OUTDOOR_SPORTS.get(i); + xPathMatchers = xpath("//form[@id='form2']//select[@name='favoriteSport']//option[" + (i + 1) + "]"); + resultAcctions.andExpect(xPathMatchers.string(sport)); + + if (sport.equals(favoriteSport)) { + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("selected", "selected"))); + } else { + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("selected"))); + } } - for (int i = 0; i < UserController.ALL_SPORTS.size(); i++) { - String sport = UserController.ALL_SPORTS.get(i); - resultAcctions.andExpect(xpath("//form[@id='form3']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); + xPathMatchers = xpath("//form[@id='form3']//select[@name='favoriteSport']"); + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("size", "3"))); + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("multiple", "multiple"))); + + for (int i = 0; i < UserRepository.ALL_SPORTS.size(); i++) { + final String sport = UserRepository.ALL_SPORTS.get(i); + xPathMatchers = xpath("//form[@id='form3']//select[@name='favoriteSport']//option[" + (i + 1) + "]"); + resultAcctions.andExpect(xPathMatchers.string(sport)); + + if (sport.equals(favoriteSport)) { + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("selected", "selected"))); + } else { + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("selected"))); + } } - resultAcctions.andExpect(xpath("//form[@id='form4']//select[@name='favoriteSport']//option[1]").string("--- Select ---")); - for (int i = 0; i < UserController.OUTDOOR_SPORTS.size(); i++) { - String sport = UserController.OUTDOOR_SPORTS.get(i); - resultAcctions.andExpect(xpath("//form[@id='form4']//select[@name='favoriteSport']//option[" + (i + 2) + "]").string(sport)); + xPathMatchers = xpath("//form[@id='form4']//select[@name='favoriteSport']"); + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("size"))); + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("multiple"))); + + xPathMatchers = xpath("//form[@id='form4']//select[@name='favoriteSport']//option[1]"); + resultAcctions.andExpect(xPathMatchers.string("--- Select ---")); + + for (int i = 0; i < UserRepository.OUTDOOR_SPORTS.size(); i++) { + final String sport = UserRepository.OUTDOOR_SPORTS.get(i); + xPathMatchers = xpath("//form[@id='form4']//select[@name='favoriteSport']//option[" + (i + 2) + "]"); + resultAcctions.andExpect(xPathMatchers.string(sport)); + + if (sport.equals(favoriteSport)) { + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("selected", "selected"))); + } else { + resultAcctions.andExpect(xPathMatchers.node(new MissingElementAttributeMatcher("selected"))); + } } + + xPathMatchers = xpath("//form[@id='form5']//select[@name='favoriteSport']//option[1]"); + resultAcctions.andExpect(xPathMatchers.node(new ElementAttributeMatcher("disabled", "disabled"))); } } http://git-wip-us.apache.org/repos/asf/freemarker/blob/034b056c/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah index 327bf4a..34319d3 100644 --- a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah @@ -54,7 +54,7 @@ <tr> <th>Favorite Sport:</th> <td> - <@form.select 'user.favoriteSport' items=indoorSports> + <@form.select 'user.favoriteSport' items=indoorSports multiple=true size="3"> <@form.options items=outdoorSports /> </@form.select> </td> @@ -78,5 +78,20 @@ </table> </form> + <h1>Form 5</h1> + <hr/> + <form id="form5"> + <table> + <tr> + <th>Favorite Sport:</th> + <td> + <@form.select 'user.favoriteSport'> + <@form.option value="" label="--- Select ---" disabled=true /> + </@form.select> + </td> + </tr> + </table> + </form> + </body> </html>
