In Spring Roo (1.1.5) I have an Entity "Book" that CAN have a reference to Entity "Publisher".
class Book {
@ManyToOne(optional=true)
Publisher publisher
}
Now I have the Roo generated Controller and JSPX files.
In the GUI the for creating and updating the Book there is the Roo generated Drop Down Box (decorated by dijit.form.FilteringSelect
) to select the Publisher.
But the user MUST select a Publisher; there is no "Empty" field!
My first try was simply to add a null
value to the list which represents the options for the drop downbox.
But that failed. (java.lang.IllegalArgumentException: Bean object must not be null
) -- So this may be the wrong way.
So before I try to extend the select.tagx
file by my own, I want to ask if someone have already solved that problem (having a optional drop downbox with Spring Roo/Dojo), or do I something completely wrong and it should work in normmal case with out implmenting something new?
I have found a solution, but this works only because my application is not a standard Roo application any more. Anyway, I will explain my solution, maybe someone find an way to adapt it for standard Roo applications.
The idea is to add an empty selection in the dropdown box when the required
attribute is false
.
The main problem is that the dijti/dojo extension will not work correct if there is one option in the dropdown box without an value
.
So my solution was to give them for example the value
"null"
(<option value="null></option>
).
On server side one must change the converter that convert the database id (that is the normal value) to an Entity (by loading it from the database) a bit,
so that it converts the String "null"
to null
instead of an entity.
But that is the problem with spring Roo. Roo uses the org.springframework.core.convert.support.IdToEntityConverter
that is automatically registered
(not documented https://jira.springsource.org/browse/SPR-7461) and will try to convert every object to an entity if the entity class as a static finder method.
I have found no way to modify its behaviour.
But I personally have a lot of luck, because some time ago I changed my application that it does not have that static finder, so I have my own generic Id to entity converter that is easy to change. The converter converts String to Entity. If the String is "null" it returns null, else it converts the String to a number and load the Entity by this number/id.
For the view, it seams that one have to extend the select.tagx
file.
The select.tagx
file contains 12 different ways to fill the select box.
line 75, 130,
<c:if test="${not required}">
<option value="null"></option>
</c:if>
...
<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" />
...
<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" itemLabel="${sec_itemLabel}"/>
...
<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" itemValue="${fn:escapeXml(itemValue)}" />
...
<form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" itemValue="${fn:escapeXml(itemValue)}" itemLabel="${sec_itemLabel}"/>
They one need to replace the complete tag by (I will only demonstrate it for the last of that 4, but the other are similar, except one has to remove the itemVlaue and or itemLabel parameter)
<form:select id="_${sec_field}_id" path="${sec_field}" disabled="${disabled}">
<c:if test="${not required}">
<option value="null"></option>
</c:if>
<form:options items="${items}" itemValue="${fn:escapeXml(itemValue)}" itemLabel="${sec_itemLabel}"/>
</form:select>
Now it should work.
But it has a small flaw. If there is a Book with no Publisher, then the empty dropdown option will not have the select attribute. This is not so bad, because it is the top most option and will be displayed if no other option is selected.
If someone can not accept this flaw, then one way to handle this problem is to write an own jsp tag extending org.springframework.web.servlet.tags.form.Option
(the class that does the spring option tag).
There are only two things that one really need to change:
1) the method isSelected(Object resolvedValue)
must return true if the bind status is null (so this method becomes really easy)
private boolean isSelected(Object resolvedValue) {
BindStatus bindStatus = getBindStatus();
return bindStatus == null || bindStatus.getValue() == null || bindStatus.getActualValue() == null;
}
2) if the tag is rendered without or empty body (method renderDefaultContent
) the content of the rendered html option
should be empty but not the value
.
So the second parameter of the renderOption(SpecialWay) method must be set fix to an empty string.
@Override
protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
Object value = this.pageContext.getAttribute(VALUE_VARIABLE_NAME);
renderOptionSpecialWay(value, "", tagWriter);
}
But because the isSelected
method is private and can not be override, one must copy the renderOption
(can rename it) and must change it so that it invokes the "new" isSelected method. The same must be done to the two methods renderDefaultContent
and renderFromBodyContent
because renderOption
is private too.
So one came up with this class:
public class NullOptionTag extends OptionTag {
@Override
protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
Object value = this.pageContext.getAttribute(VALUE_VARIABLE_NAME);
renderOptionSpecialWay(value, "", tagWriter);
}
@Override
protected void renderFromBodyContent(BodyContent bodyContent, TagWriter tagWriter) throws JspException {
Object value = this.pageContext.getAttribute(VALUE_VARIABLE_NAME);
String label = bodyContent.getString();
renderOptionSpecialWay(value, label, tagWriter);
}
private void renderOptionSpecialWay(Object value, String label, TagWriter tagWriter) throws JspException {
tagWriter.startTag("option");
writeOptionalAttribute(tagWriter, "id", resolveId());
writeOptionalAttributes(tagWriter);
String renderedValue = getDisplayString(value, getBindStatus().getEditor());
tagWriter.writeAttribute(OptionTag.VALUE_VARIABLE_NAME, renderedValue);
if (isSelected(value)) {
tagWriter.writeAttribute("selected", "selected");
}
if (isDisabled()) {
tagWriter.writeAttribute("disabled", "disabled");
}
tagWriter.appendValue(label);
tagWriter.endTag();
}
private boolean isSelected(Object resolvedValue) {
BindStatus bindStatus = getBindStatus();
return bindStatus == null || bindStatus.getValue() == null || bindStatus.getActualValue() == null;
}
}
Next thing to do is add this class to an tag lib definition so that it can be used in the select.tagx
<form:select id="_${sec_field}_id" path="${sec_field}" disabled="${disabled}">
<c:if test="${not required}">
<formExtension:nulloption value="null"></formExtension:nulloption>
</c:if>
<form:options items="${items}" itemValue="${fn:escapeXml(itemValue)}" itemLabel="${sec_itemLabel}"/>
</form:select>