1.3.x: Backport fixes for GRAILS-3168 #161

Closed
wants to merge 2 commits into
from
Jump to file or symbol
Failed to load files and symbols.
+156 −17
Split
@@ -125,16 +125,15 @@ class FormTagLib {
* @attr value the value of the checkbox
* @attr checked if evaluates to true sets to checkbox to checked
* @attr disabled if evaluates to true sets to checkbox to disabled
+ * @attr readonly if evaluates to true, sets to checkbox to read only
* @attr id DOM element id; defaults to name
*/
def checkBox = { attrs ->
attrs.id = attrs.id ?: attrs.name
def value = attrs.remove('value')
def name = attrs.remove('name')
- def disabled = attrs.remove('disabled')
- if (disabled && Boolean.valueOf(disabled)) {
- attrs.disabled = 'disabled'
- }
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'readonly')
// Deal with the "checked" attribute. If it doesn't exist, we
// default to a value of "true", otherwise we use Groovy Truth
@@ -196,6 +195,25 @@ class FormTagLib {
outputAttributes(attrs)
out << ">" << (escapeHtml ? value.encodeAsHTML() : value) << "</textarea>"
}
+
+ /**
+ * Some attributes can be defined as Boolean values, but the html specification
+ * mandates the attribute must have the same value as its name. For example,
+ * disabled, readonly and checked.
+ */
+ private void booleanToAttribute(def attrs, String attrName) {
+ def attrValue = attrs.remove(attrName)
+ // If the value is the same as the name or if it is a boolean value,
+ // reintroduce the attribute to the map, so it is output later
+ if (Boolean.valueOf(attrValue) ||
+ (attrValue instanceof String && attrValue?.equalsIgnoreCase(attrName))) {
+ attrs.put(attrName, attrName)
+ } else if (attrValue instanceof String && !attrValue?.equalsIgnoreCase('false')) {
+ // If the value is not the string 'false', then we should just pass it on to
+ // keep compatibility with existing code
+ attrs.put(attrName, attrValue)
+ }
+ }
/**
* Check required attributes, set the id to name if no id supplied, extract bean values etc.
@@ -220,6 +238,12 @@ class FormTagLib {
attrs.value = val
}
attrs.value = attrs.value != null ? attrs.value : "" // can't use ?: since 0 is groovy false
+
+ // Some attributes can be treated as boolean, but must be converted to the
+ // expected value
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'checked')
+ booleanToAttribute(attrs, 'readonly')
}
/**
@@ -323,6 +347,7 @@ class FormTagLib {
*
* @attr value REQUIRED The title of the button and name of action when not explicitly defined.
* @attr action The name of the action to be executed, otherwise it is derived from the value.
+ * @attr disabled Makes the button to be disabled. Will be interpreted as a Groovy Truth
*/
def actionSubmit = { attrs ->
if (!attrs.value) {
@@ -334,6 +359,7 @@ class FormTagLib {
// add action and value
def value = attrs.remove('value')
def action = attrs.remove('action') ?: value
+ booleanToAttribute(attrs, 'disabled')
out << "<input type=\"submit\" name=\"_action_${action}\" value=\"${value}\" "
@@ -355,6 +381,7 @@ class FormTagLib {
* @attr value REQUIRED The title of the button and name of action when not explicitly defined.
* @attr action The name of the action to be executed, otherwise it is derived from the value.
* @attr src The source of the image to use
+ * @attr disabled Makes the button to be disabled. Will be interpreted as a Groovy Truth
*/
def actionSubmitImage = { attrs ->
attrs.tagName = "actionSubmitImage"
@@ -366,6 +393,7 @@ class FormTagLib {
// add action and value
def value = attrs.remove('value')
def action = attrs.remove('action') ?: value
+ booleanToAttribute(attrs, 'disabled')
out << "<input type=\"image\" name=\"_action_${action}\" value=\"${value}\" "
@@ -392,6 +420,8 @@ class FormTagLib {
* @attr noSelection A single-entry map detailing the key and value to use for the "no selection made" choice in the select box. If there is no current selection this will be shown as it is first in the list, and if submitted with this selected, the key that you provide will be submitted. Typically this will be blank.
* @attr years A list or range of years to display, in the order specified. i.e. specify 2007..1900 for a reverse order list going back to 1900. If this attribute is not specified, a range of years from the current year - 100 to current year + 100 will be shown.
* @attr id the DOM element id
+ * @attr disabled Makes the resulting inputs and selects to be disabled. Is treated as a Groovy Truth.
+ * @attr readonly Makes the resulting inputs and selects to be made read only. Is treated as a Groovy Truth.
*/
def datePicker = { attrs ->
def out = out // let x = x ?
@@ -471,12 +501,22 @@ class FormTagLib {
}
years = (tempyear - 100)..(tempyear + 100)
}
+
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'readonly')
out.println "<input type=\"hidden\" name=\"${name}\" value=\"date.struct\" />"
// create day select
if (precision >= PRECISION_RANKINGS["day"]) {
- out.println "<select name=\"${name}_day\" id=\"${id}_day\">"
+ out.println "<select name=\"${name}_day\" id=\"${id}_day\""
+ if (attrs.disabled) {
+ out << ' disabled="disabled"'
+ }
+ if (attrs.readonly) {
+ out << ' readonly="readonly"'
+ }
+ out << '>'
if (noSelection) {
renderNoSelectionOptionImpl(out, noSelection.key, noSelection.value, '')
@@ -491,7 +531,14 @@ class FormTagLib {
// create month select
if (precision >= PRECISION_RANKINGS["month"]) {
- out.println "<select name=\"${name}_month\" id=\"${id}_month\">"
+ out.println "<select name=\"${name}_month\" id=\"${id}_month\""
+ if (attrs.disabled) {
+ out << ' disabled="disabled"'
+ }
+ if (attrs.readonly) {
+ out << ' readonly="readonly"'
+ }
+ out << '>'
if (noSelection) {
renderNoSelectionOptionImpl(out, noSelection.key, noSelection.value, '')
@@ -509,7 +556,14 @@ class FormTagLib {
// create year select
if (precision >= PRECISION_RANKINGS["year"]) {
- out.println "<select name=\"${name}_year\" id=\"${id}_year\">"
+ out.println "<select name=\"${name}_year\" id=\"${id}_year\""
+ if (attrs.disabled) {
+ out << ' disabled="disabled"'
+ }
+ if (attrs.readonly) {
+ out << ' readonly="readonly"'
+ }
+ out << '>'
if (noSelection) {
renderNoSelectionOptionImpl(out, noSelection.key, noSelection.value, '')
@@ -524,7 +578,14 @@ class FormTagLib {
// do hour select
if (precision >= PRECISION_RANKINGS["hour"]) {
- out.println "<select name=\"${name}_hour\" id=\"${id}_hour\">"
+ out.println "<select name=\"${name}_hour\" id=\"${id}_hour\""
+ if (attrs.disabled) {
+ out << ' disabled="disabled"'
+ }
+ if (attrs.readonly) {
+ out << ' readonly="readonly"'
+ }
+ out << '>'
if (noSelection) {
renderNoSelectionOptionImpl(out, noSelection.key, noSelection.value, '')
@@ -546,7 +607,14 @@ class FormTagLib {
// do minute select
if (precision >= PRECISION_RANKINGS["minute"]) {
- out.println "<select name=\"${name}_minute\" id=\"${id}_minute\">"
+ out.println "<select name=\"${name}_minute\" id=\"${id}_minute\""
+ if (attrs.disabled) {
+ out << ' disabled="disabled"'
+ }
+ if (attrs.readonly) {
+ out << ' readonly="readonly"'
+ }
+ out << '>'
if (noSelection) {
renderNoSelectionOptionImpl(out, noSelection.key, noSelection.value, '')
@@ -661,6 +729,7 @@ class FormTagLib {
* @attr valueMessagePrefix By default the value "option" element will be the result of a "toString()" call on each element in the "from" attribute list. Setting this allows the value to be resolved from the I18n messages. The valueMessagePrefix will be suffixed with a dot ('.') and then the value attribute of the option to resolve the message. If the message could not be resolved, the value is presented.
* @attr noSelection A single-entry map detailing the key and value to use for the "no selection made" choice in the select box. If there is no current selection this will be shown as it is first in the list, and if submitted with this selected, the key that you provide will be submitted. Typically this will be blank - but you can also use 'null' in the case that you're passing the ID of an object
* @attr disabled boolean value indicating whether the select is disabled or enabled (defaults to false - enabled)
+ * @attr readonly boolean value indicating whether the select is read only or editable (defaults to false - editable)
*/
def select = { attrs ->
def messageSource = grailsAttributes.getApplicationContext().getBean("messageSource")
@@ -683,10 +752,8 @@ class FormTagLib {
if (noSelection != null) {
noSelection = noSelection.entrySet().iterator().next()
}
- def disabled = attrs.remove('disabled')
- if (disabled && Boolean.valueOf(disabled)) {
- attrs.disabled = 'disabled'
- }
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'readonly')
writer << "<select name=\"${attrs.remove('name')?.encodeAsHTML()}\" "
// process remaining attributes
@@ -810,16 +877,16 @@ class FormTagLib {
* @attr name REQUIRED The name of the radio button
* @attr checked boolean to indicate that the radio button should be checked
* @attr disabled boolean to indicate that the radio button should be disabled
+ * @attr readonly boolean to indicate that the radio button should not be editable
* @attr id the DOM element id
*/
def radio = { attrs ->
def value = attrs.remove('value')
attrs.id = attrs.id ?: attrs.name
def name = attrs.remove('name')
- def disabled = attrs.remove('disabled')
- if (disabled && Boolean.valueOf(disabled)) {
- attrs.disabled = 'disabled'
- }
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'readonly')
+
def checked = attrs.remove('checked') ? true : false
out << "<input type=\"radio\" name=\"${name}\"${ checked ? ' checked="checked" ' : ' '}value=\"${value?.toString()?.encodeAsHTML()}\" "
// process remaining attributes
@@ -836,12 +903,17 @@ class FormTagLib {
* @attr values REQUIRED The list values for the radio buttons
* @attr value The current selected value
* @attr labels Labels for each value contained in the values list. If this is ommitted the label property on the iterator variable (see below) will default to 'Radio ' + value.
+ * @attr disabled Disables the resulting radio buttons.
+ * @attr readonly Makes the resulting radio buttons to not be editable
*/
def radioGroup = { attrs, body ->
def value = attrs.remove('value')
def values = attrs.remove('values')
def labels = attrs.remove('labels')
def name = attrs.remove('name')
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'readonly')
+
values.eachWithIndex {val, idx ->
def it = new Expando()
it.radio = "<input type=\"radio\" name=\"${name}\" "
@@ -145,4 +145,71 @@ class FormTagLibTests extends AbstractGrailsTagTests {
assertEquals '<textarea name="testField" id="testField" >1</textarea>', sw.toString()
}
}
+
+ private void doTestBoolean(def attributes, String expected) {
+ def sw = new StringWriter()
+ withTag('textField', new PrintWriter(sw)) { tag ->
+ tag.call(attributes)
+ assertEquals expected, sw.toString()
+ }
+ }
+
+ void testBooleanAttributes() {
+ // GRAILS-3468
+ // Test readonly for string as boolean true
+ def attributes = [name: 'myfield', value: '1', readonly: 'true']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" readonly="readonly" />')
+
+ // Test readonly for string as boolean false
+ attributes = [name: 'myfield', value: '1', readonly: 'false']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" />')
+
+ // Test readonly for real boolean true
+ attributes = [name: 'myfield', value: '1', readonly: true]
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" readonly="readonly" />')
+
+ // Test readonly for real boolean false
+ attributes = [name: 'myfield', value: '1', readonly: false]
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" />')
+
+ // Test readonly for its default value
+ attributes = [name: 'myfield', value: '1', readonly: 'readonly']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" readonly="readonly" />')
+
+ // Test readonly for a value different from the defined in the spec
+ attributes = [name: 'myfield', value: '1', readonly: 'other value']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" readonly="other value" />')
+
+ // Test readonly for null value
+ attributes = [name: 'myfield', value: '1', readonly: null]
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" />')
+
+ // Test disabled for string as boolean true
+ attributes = [name: 'myfield', value: '1', disabled: 'true']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" disabled="disabled" />')
+
+ // Test disabled for string as boolean false
+ attributes = [name: 'myfield', value: '1', disabled: 'false']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" />')
+
+ // Test disabled for real boolean true
+ attributes = [name: 'myfield', value: '1', disabled: true]
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" disabled="disabled" />')
+
+ // Test disabled for real boolean false
+ attributes = [name: 'myfield', value: '1', disabled: false]
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" />')
+
+ // Test disabled for its default value
+ attributes = [name: 'myfield', value: '1', disabled: 'disabled']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" disabled="disabled" />')
+
+ // Test disabled for a value different from the defined in the spec
+ attributes = [name: 'myfield', value: '1', disabled: 'other value']
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" disabled="other value" />')
+
+ // Test disabled for null value
+ attributes = [name: 'myfield', value: '1', disabled: null]
+ doTestBoolean(attributes, '<input type="text" name="myfield" value="1" id="myfield" />')
+ }
}