Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #160 from denisfalqueto/2.0.x

2.0.x: backport fix for GRAILS-3468
  • Loading branch information...
commit 86a129b84cccd2df7c0c9f2a35288f4e238fd11e 2 parents 91c1b91 + f2857c7
@graemerocher graemerocher authored
View
107 grails-plugin-gsp/src/main/groovy/org/codehaus/groovy/grails/plugins/web/taglib/FormTagLib.groovy
@@ -132,16 +132,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
*/
Closure 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
@@ -206,6 +205,25 @@ class FormTagLib {
outputAttributes(attrs, out)
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 according to the w3c rules, 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.
@@ -230,6 +248,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')
}
/**
@@ -355,6 +379,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
*/
Closure actionSubmit = { attrs ->
if (!attrs.value) {
@@ -372,6 +397,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}\" "
@@ -395,6 +421,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
*/
Closure actionSubmitImage = { attrs ->
attrs.tagName = "actionSubmitImage"
@@ -406,6 +433,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}\" "
@@ -435,6 +463,8 @@ class FormTagLib {
* @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 relativeYears A range of int representing values relative to value. For example, a relativeYears of -2..7 and a value of today will render a list of 10 years starting with 2 years ago through 7 years in the future. This can be useful for things like credit card expiration dates or birthdates which should be bound relative to today.
* @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.
*/
Closure datePicker = { attrs ->
def out = out // let x = x ?
@@ -534,12 +564,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, '')
@@ -554,7 +594,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, '')
@@ -572,7 +619,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, '')
@@ -587,7 +641,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, '')
@@ -609,7 +670,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, '')
@@ -732,6 +800,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)
*/
Closure select = { attrs ->
if (!attrs.name) {
@@ -760,12 +829,9 @@ class FormTagLib {
if (noSelection != null) {
noSelection = noSelection.entrySet().iterator().next()
}
- def disabled = attrs.remove('disabled')
- if (disabled && Boolean.valueOf(disabled)) {
- attrs.disabled = 'disabled'
- }
-
writer << "<select name=\"${attrs.remove('name')?.encodeAsHTML()}\" "
+ booleanToAttribute(attrs, 'disabled')
+ booleanToAttribute(attrs, 'readonly')
// process remaining attributes
outputAttributes(attrs, writer)
@@ -894,16 +960,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
*/
Closure 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
@@ -920,12 +986,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
*/
Closure 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 = new StringBuilder("<input type=\"radio\" name=\"${name}\" ")
View
67 grails-test-suite-web/src/test/groovy/org/codehaus/groovy/grails/web/taglib/FormTagLibTests.groovy
@@ -181,4 +181,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" />')
+ }
}
Please sign in to comment.
Something went wrong with that request. Please try again.