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 0eb22dd0039c564338fed33125249a8b57a8303a
Author: David Estes <[email protected]>
AuthorDate: Wed Feb 25 22:20:47 2026 -0500

    Refine FormTagLib typed overloads and guard non-public tag methods
    
    - use private implementation helpers to avoid recursive dispatch in typed 
overloads
    - keep Map-based handlers for validation-safe fallback behavior
    - add regression test ensuring private/protected methods are not exposed as 
tag methods
    - document overload pattern for typed signatures with existing validation 
paths
    
    Co-Authored-By: Oz <[email protected]>
---
 .../src/en/guide/theWebLayer/gsp/taglibs.adoc      | 14 ++++++
 grails-doc/src/en/guide/theWebLayer/taglibs.adoc   | 14 ++++++
 .../grails/plugins/web/taglib/FormTagLib.groovy    | 55 ++++++++++++++++++++++
 .../web/taglib/MethodDefinedTagLibSpec.groovy      | 21 +++++++++
 4 files changed, 104 insertions(+)

diff --git a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc 
b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
index e9d6e2ad4c..cfe5b738cd 100644
--- a/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
@@ -71,6 +71,20 @@ Used as:
 <g:greeting name="Graeme" />
 ----
 
+For tags with strict validation/error handling, you can keep a `Map attrs` 
handler and add typed overloads that delegate to it:
+
+[source,groovy]
+----
+def field(Map attrs) {
+    // existing validation + rendering path
+}
+
+def field(String type, Map attrs) {
+    attrs.type = type
+    field(attrs)
+}
+----
+
 As demonstrated above there is an implicit `out` variable that refers to the 
output `Writer` which you can use to append content to the response. Then you 
can reference the tag inside your GSP; no imports are necessary:
 
 [source,xml]
diff --git a/grails-doc/src/en/guide/theWebLayer/taglibs.adoc 
b/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
index e3d14b35ef..fa63946ca7 100644
--- a/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
+++ b/grails-doc/src/en/guide/theWebLayer/taglibs.adoc
@@ -71,6 +71,20 @@ Used as:
 <g:greeting name="Graeme" />
 ----
 
+For tags with strict validation/error handling, keep a `Map attrs` handler and 
add typed overloads that delegate to it:
+
+[source,groovy]
+----
+def field(Map attrs) {
+    // existing validation + rendering path
+}
+
+def field(String type, Map attrs) {
+    attrs.type = type
+    field(attrs)
+}
+----
+
 As demonstrated above there is an implicit `out` variable that refers to the 
output `Writer` which you can use to append content to the response. Then you 
can reference the tag inside your GSP; no imports are necessary:
 
 [source,xml]
diff --git 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
index c4e084c9bb..5b357f107f 100644
--- 
a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
+++ 
b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy
@@ -104,8 +104,19 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      *
      * @attr name REQUIRED the field name
      * @attr value the field value
+     * @param name required field name
+     * @param attrs optional tag attributes including value, id, class and 
other HTML attributes
      */
     def textField(Map attrs) {
+        textFieldImpl(attrs)
+    }
+
+    def textField(String name, Map attrs) {
+        attrs.name = name
+        textFieldImpl(attrs)
+    }
+
+    private void textFieldImpl(Map attrs) {
         attrs.type = 'text'
         attrs.tagName = 'textField'
         fieldImpl(out, attrs)
@@ -118,8 +129,19 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      *
      * @attr name REQUIRED the field name
      * @attr value the field value
+     * @param name required field name
+     * @param attrs optional tag attributes including value, id, class and 
other HTML attributes
      */
     def passwordField(Map attrs) {
+        passwordFieldImpl(attrs)
+    }
+
+    def passwordField(String name, Map attrs) {
+        attrs.name = name
+        passwordFieldImpl(attrs)
+    }
+
+    private void passwordFieldImpl(Map attrs) {
         attrs.type = 'password'
         attrs.tagName = 'passwordField'
         fieldImpl(out, attrs)
@@ -130,8 +152,19 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      *
      * @attr name REQUIRED the field name
      * @attr value the field value
+     * @param name required field name
+     * @param attrs optional tag attributes including value and additional 
HTML attributes
      */
     def hiddenField(Map attrs) {
+        hiddenFieldTagImpl(attrs)
+    }
+
+    def hiddenField(String name, Map attrs) {
+        attrs.name = name
+        hiddenFieldTagImpl(attrs)
+    }
+
+    private void hiddenFieldTagImpl(Map attrs) {
         hiddenFieldImpl(out, attrs)
     }
 
@@ -150,8 +183,19 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
      * @attr value the button text
      * @attr type input type; defaults to 'submit'
      * @attr event the webflow event id
+     * @param name required field name
+     * @param attrs optional tag attributes including value, type, event and 
additional HTML attributes
      */
     def submitButton(Map attrs) {
+        submitButtonImpl(attrs)
+    }
+
+    def submitButton(String name, Map attrs) {
+        attrs.name = name
+        submitButtonImpl(attrs)
+    }
+
+    private void submitButtonImpl(Map attrs) {
         attrs.type = attrs.type ?: 'submit'
         attrs.tagName = 'submitButton'
         if (request.flowExecutionKey) {
@@ -173,6 +217,17 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
         fieldImpl(out, attrs)
     }
 
+    /**
+     * A general tag for creating fields with method-argument binding for 
required type.
+     *
+     * @param type required input type
+     * @param attrs tag attributes, including required name/field and optional 
value/id/class/etc
+     */
+    def field(String type, Map attrs) {
+        attrs.type = type
+        field(attrs)
+    }
+
     @CompileStatic
     private def fieldImpl(GrailsPrintWriter out, Map attrs) {
         resolveAttributes(attrs)
diff --git 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
index 3deeb6171c..4c9e3d6dfb 100644
--- 
a/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
+++ 
b/grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/MethodDefinedTagLibSpec.groovy
@@ -21,6 +21,7 @@ package org.grails.web.taglib
 import grails.artefact.Artefact
 import grails.compiler.GrailsCompileStatic
 import grails.testing.web.taglib.TagLibUnitTest
+import org.grails.taglib.GrailsTagException
 import spock.lang.Specification
 
 class MethodDefinedTagLibSpec extends Specification implements 
TagLibUnitTest<MethodTagLib> {
@@ -76,6 +77,18 @@ class MethodDefinedTagLibSpec extends Specification 
implements TagLibUnitTest<Me
         expect:
         applyTemplate('<g:staticBodyTag>abc</g:staticBodyTag>') == 
'before-abc-after'
     }
+
+    void "private and protected methods are not exposed as tags"() {
+        when:
+        applyTemplate('<g:privateOnlyTag/>')
+        then:
+        thrown(GrailsTagException)
+
+        when:
+        applyTemplate('<g:protectedOnlyTag/>')
+        then:
+        thrown(GrailsTagException)
+    }
 }
 
 @GrailsCompileStatic
@@ -117,6 +130,14 @@ class MethodTagLib {
         out << "${attrs.blah}"
     }
 
+    private def privateOnlyTag() {
+        out << 'private'
+    }
+
+    protected def protectedOnlyTag() {
+        out << 'protected'
+    }
+
     def bodyTag() {
         out << "before-${body()}-after"
     }

Reply via email to