This is an automated email from the ASF dual-hosted git repository.

davydotcom pushed a commit to branch feature/taglib-method-actions
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 4d8238ca016482b483e171ebf9cbfc563a2019c8
Author: David Estes <[email protected]>
AuthorDate: Thu Feb 26 07:52:03 2026 -0500

    Fix method taglib compatibility regressions across fields and gsp
    
    - restore attrs-reserved binding for paginate
    - route namespaced method tag calls via tag output capture
    - add fieldValue(Map) compatibility overload
    - harden form fields rendering/raw handling with method dispatch
    
    Co-Authored-By: Oz <[email protected]>
---
 .../plugin/formfields/FormFieldsTagLib.groovy      | 20 ++++++++--------
 .../org/grails/taglib/TagLibraryMetaUtils.groovy   | 28 ++++++++++++++++++++++
 .../plugins/web/taglib/UrlMappingTagLib.groovy     |  4 ++--
 .../plugins/web/taglib/ValidationTagLib.groovy     |  4 ++++
 4 files changed, 44 insertions(+), 12 deletions(-)

diff --git 
a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
 
b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
index 3631c82001..a790ddea8f 100644
--- 
a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
+++ 
b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy
@@ -247,7 +247,7 @@ class FormFieldsTagLib {
                 widgetAttrs.remove('class')
             }
             if (hasBody(body)) {
-                model.widget = raw(body(model + [attrs: widgetAttrs] + 
widgetAttrs))
+                model.widget = body(model + [attrs: widgetAttrs] + 
widgetAttrs)?.encodeAsRaw()
             } else {
                 model.widget = renderWidget(propertyAccessor, model, 
widgetAttrs, widgetFolder ?: templatesFolder, theme)
             }
@@ -389,7 +389,7 @@ class FormFieldsTagLib {
                 out << render(template: "/templates/_fields/$template", model: 
attrs + [domainClass: domainClass, domainProperties: properties]) { prop ->
                     BeanPropertyAccessor propertyAccessor = 
resolveProperty(bean, prop.name)
                     Map model = buildModel(propertyAccessor, attrs, 'HTML')
-                    out << raw(renderDisplayWidget(propertyAccessor, model, 
attrs, templatesFolder, theme))
+                    out << renderDisplayWidget(propertyAccessor, model, attrs, 
templatesFolder, theme)?.encodeAsRaw()
                 }
             }
         } else {
@@ -415,7 +415,7 @@ class FormFieldsTagLib {
             String widgetsFolderToUse = widgetFolder ?: templatesFolder
 
             if (hasBody(body)) {
-                model.widget = raw(body(model + [attrs: widgetAttrs] + 
widgetAttrs))
+                model.widget = body(model + [attrs: widgetAttrs] + 
widgetAttrs)?.encodeAsRaw()
                 model.value = body(model)
             } else {
                 model.widget = renderDisplayWidget(propertyAccessor, model, 
widgetAttrs, widgetsFolderToUse, theme)
@@ -426,7 +426,7 @@ class FormFieldsTagLib {
             if (template) {
                 out << render(template: template.path, plugin: 
template.plugin, model: model + [attrs: wrapperAttrs] + wrapperAttrs)
             } else {
-                out << raw(renderDisplayWidget(propertyAccessor, model, attrs, 
widgetsFolderToUse, theme))
+                out << renderDisplayWidget(propertyAccessor, model, attrs, 
widgetsFolderToUse, theme)?.encodeAsRaw()
             }
         }
 
@@ -715,11 +715,11 @@ class FormFieldsTagLib {
         }
     }
 
-    CharSequence renderDefaultInput(Map model, Map attrs = [:]) {
+    protected CharSequence renderDefaultInput(Map model, Map attrs = [:]) {
         renderDefaultInput(null, model, attrs)
     }
 
-    CharSequence renderDefaultInput(BeanPropertyAccessor propertyAccessor, Map 
model, Map attrs = [:]) {
+    protected CharSequence renderDefaultInput(BeanPropertyAccessor 
propertyAccessor, Map model, Map attrs = [:]) {
         Constrained constrained = (Constrained) model.constraints
         attrs.name = (model.prefix ?: '') + model.property
         attrs.value = model.value
@@ -778,7 +778,7 @@ class FormFieldsTagLib {
         }
     }
 
-    CharSequence renderDateTimeInput(Map model, Map attrs) {
+    protected CharSequence renderDateTimeInput(Map model, Map attrs) {
         attrs.precision = model.type in [java.sql.Time, LocalDateTime] ? 
'minute' : 'day'
         if (!model.required) {
             attrs.noSelection = ['': '']
@@ -787,7 +787,7 @@ class FormFieldsTagLib {
         return g.datePicker(attrs)
     }
 
-    CharSequence renderStringInput(Map model, Map attrs) {
+    protected CharSequence renderStringInput(Map model, Map attrs) {
         Constrained constrained = (Constrained) model.constraints
 
         if (!attrs.type) {
@@ -819,7 +819,7 @@ class FormFieldsTagLib {
         return g.field(attrs)
     }
 
-    CharSequence renderNumericInput(BeanPropertyAccessor propertyAccessor, Map 
model, Map attrs) {
+    protected CharSequence renderNumericInput(BeanPropertyAccessor 
propertyAccessor, Map model, Map attrs) {
         Constrained constrained = (Constrained) model.constraints
 
         if (!attrs.type && constrained?.inList) {
@@ -992,7 +992,7 @@ class FormFieldsTagLib {
         buffer << render(template: '/templates/_fields/list', model: 
[domainClass: domainClass, domainProperties: properties]) { prop ->
             def propertyAccessor = resolveProperty(bean, prop.name)
             def model = buildModel(propertyAccessor, attrs)
-            out << raw(renderDisplayWidget(propertyAccessor, model, attrs, 
templatesFolder, theme))
+            out << renderDisplayWidget(propertyAccessor, model, attrs, 
templatesFolder, theme)?.encodeAsRaw()
         }
         buffer.buffer
     }
diff --git 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
index bad77a6070..dd9adf4378 100644
--- 
a/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
+++ 
b/grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy
@@ -176,6 +176,34 @@ class TagLibraryMetaUtils {
         Object[] args = makeObjectArray(argsParam)
         final GroovyObject tagBean = 
gspTagLibraryLookup.lookupTagLibrary(namespace, name)
         if (tagBean != null) {
+            Object tagLibProp = 
TagMethodInvoker.getClosureTagProperty(tagBean, name)
+            if (tagLibProp instanceof Closure || 
TagMethodInvoker.hasInvokableTagMethod(tagBean, name)) {
+                Map attrs = [:]
+                Object body = null
+                switch (args.length) {
+                    case 0:
+                        break
+                    case 1:
+                        if (args[0] instanceof Map) {
+                            attrs = (Map) args[0]
+                        } else if (args[0] instanceof Closure || args[0] 
instanceof CharSequence) {
+                            body = args[0]
+                        } else {
+                            attrs = [(name): args[0]]
+                        }
+                        break
+                    case 2:
+                        if (args[0] instanceof Map) {
+                            attrs = (Map) args[0]
+                            body = args[1]
+                        }
+                        break
+                }
+                if (addMethodsToMetaClass) {
+                    registerMethodMissingForTags(mc, gspTagLibraryLookup, 
namespace, name)
+                }
+                return captureTagOutputForMethodCall(gspTagLibraryLookup, 
namespace, name, attrs, body)
+            }
             MetaClass tagBeanMc = tagBean.getMetaClass()
             final MetaMethod method = tagBeanMc.respondsTo(tagBean, name, 
args).find { it }
             if (method != null) {
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
index 2d695e987c..181667313a 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/UrlMappingTagLib.groovy
@@ -119,8 +119,8 @@ class UrlMappingTagLib implements TagLibrary {
      * @attr mapping The named URL mapping to use to rewrite the link
      * @attr fragment The link fragment (often called anchor tag) to use
      */
-    def paginate(Map attrsMap) {
-        TypeConvertingMap attrs = (TypeConvertingMap) attrsMap
+    def paginate(Map attrs) {
+        attrs = (TypeConvertingMap) attrs
         def writer = out
         if (attrs.total == null) {
             throwTagError('Tag [paginate] is missing required attribute 
[total]')
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
index 7037c317f8..6e961c48e3 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ValidationTagLib.groovy
@@ -122,6 +122,10 @@ class ValidationTagLib implements TagLibrary {
         }
     }
 
+    def fieldValue(Map attrs) {
+        fieldValue(attrs, null)
+    }
+
     private Object parseForRejectedValue(bean, field) {
         def rejectedValue = bean
         for (String fieldPart in field.split('\\.')) {

Reply via email to