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" }
