Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9a3ce31
Add method-based TagLib handlers with legacy compatibility and docs u…
davydotcom Feb 26, 2026
106c92c
Document typed attribute-to-argument binding for method TagLib syntax
davydotcom Feb 26, 2026
1d708b1
Add typed method-argument TagLib example to simple tags guide
davydotcom Feb 26, 2026
f42e058
Bind Map tag method args by name except reserved attrs map
davydotcom Feb 26, 2026
0eb22dd
Refine FormTagLib typed overloads and guard non-public tag methods
davydotcom Feb 26, 2026
3854084
Simplify FormTagLib typed overloads by removing redundant name reassi…
davydotcom Feb 26, 2026
78c0d84
Optimize tag invocation dispatch with concurrent-safe method caching
davydotcom Feb 26, 2026
4d8238c
Fix method taglib compatibility regressions across fields and gsp
davydotcom Feb 26, 2026
40d2458
Fix method-based TagLib dispatch: prevent helper methods from being r…
davydotcom Feb 26, 2026
6683ca6
fixing more issues with testing
davydotcom Feb 26, 2026
e53430a
missing some methods
davydotcom Mar 3, 2026
60bcdae
Merge branch '8.0.x' into feature/taglib-method-actions
jdaugherty Mar 23, 2026
c3b4cf0
styling: fix import order
jdaugherty Mar 23, 2026
c1db2b3
Fix raw lookup to be CompileStatic
jdaugherty Mar 23, 2026
5103ee3
feature - ability to rerun tests without having to rerun all tasks
jdaugherty Apr 25, 2026
c1d07b0
Test pollution fixes & cleanup
jdaugherty Apr 25, 2026
b7a7a5f
Attempting to fix test pollution
jdaugherty Apr 27, 2026
5680d19
Merge branch '8.0.x' into feature/taglib-method-actions
jdaugherty Apr 27, 2026
567d14d
Prevent test pollution
jdaugherty Apr 27, 2026
1ce4322
Fix is* invocations
jdaugherty Apr 27, 2026
9d1c76b
Adjust code style
jdaugherty Apr 27, 2026
531150c
Always clear taglib metaclasses in web test cleanup
jdaugherty Apr 28, 2026
68a4049
Tighten method tag dispatch and preserve taglib remocking
jdaugherty Apr 29, 2026
06c93bf
Document Grails 8 taglib updates
jdaugherty Apr 29, 2026
9ef3130
Codestyle fixes
jdaugherty Apr 29, 2026
79d4d0b
Merge branch '8.0.x' into feature/taglib-method-actions
jdaugherty Apr 29, 2026
bad5577
Restore taglib namespace lookups after web cleanup
jdaugherty Apr 29, 2026
5f439ec
Fix calling a taglib directly
jdaugherty Apr 29, 2026
d8b13a0
Merge branch '8.0.x' into feature/taglib-method-actions
jdaugherty Apr 29, 2026
2e8ab1f
Remove unused import
jdaugherty Apr 29, 2026
9ab3547
Move taglib mocks to setup() instead of setupSpec()
jdaugherty Apr 29, 2026
4017fe0
Merge branch '8.0.x' into feature/taglib-method-actions
jdaugherty May 4, 2026
20e515d
Update rat-config for generated files
jdaugherty May 4, 2026
38d41c8
Fix bad merge
jdaugherty May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions gradle/rat-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ tasks.named('rat') {
'.github/**', // github configuration isn't shipped in the source distro
'**/.gitignore', // git configuration isn't code
'**/.gitkeep', // git configuration isn't code
'**/.classpath', // Eclipse generated files
'**/.project', // Eclipse generated files
'**/.settings', // Eclipse generated files
'etc/bin/results/**', // exclude build directories
'**/*.png', '**/*.svg', '**/*.ico', '**/*.eps', '**/*.icns', '**/*.jpg', '**/*.jpeg', '**/*.gif', // Image files
'**/*.db', // H2 database test files
Expand All @@ -61,6 +64,10 @@ tasks.named('rat') {
'bin/**', // scripts generated during build for grails-shell-cli
'build-logic/plugins/build/**', // exclude build artifacts
'build-logic/docs-core/build/**', // exclude build artifacts
'grails-test-examples/*/build/**', // build directories
'grails-gsp/*/build/**', // build directories
'grails-test-examples/gsp-spring-boot/app/build', // build directories
'*/build', // root build directories
'build-logic/docs-core/src/main/template/**', // template files that people are expected to use in the end application
'grails-common/src/main/groovy/org/apache/grails/common/compiler/asm/Attribute.java', // See license file, BSD licensed
'grails-common/src/main/groovy/org/apache/grails/common/compiler/asm/ByteVector.java', // See license file, BSD licensed
Expand Down
8 changes: 6 additions & 2 deletions grails-doc/src/en/guide/introduction/whatsNew.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ This release focuses on enhancing the developer experience, improving performanc
For detailed information on how to upgrade to Grails 8, including major dependency changes, please see the xref:upgrading#upgrading80x[Upgrading from Grails 7 to Grails 8] section.
Notable new features are included below.

==== No New Features at this time
==== GSP Tag Library Improvements

No new features at this time
Grails 8 continues the move toward method-based TagLib handlers while preserving compatibility with existing closure-based tags.
Method-defined tags now bind named attributes more predictably, exclude inherited framework and `Object` methods from tag dispatch, and preserve real namespace property getters.

Tag library unit tests also clean up and rebuild TagLib metadata automatically between features.
Tests that use `TagLibUnitTest` no longer need to manage `purgeTagLibMetaClass`, and specs that mock additional tag libraries continue to work across feature methods.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Adding the `TagLibUnitTest` trait to a test causes a new `tagLib` field to be
automatically created for the TagLib class under test. The `tagLib` property can
be used to test calling tags as function calls. The return value of a function
call is either a `org.grails.buffer,StreamCharBuffer`
instance or the object returned from the tag closure when
instance or the object returned from the tag handler when
`returnObjectForTags` feature is used.

To test a tag which accepts parameters, specify the parameter values as named
Expand Down Expand Up @@ -93,6 +93,12 @@ In order to test a tag library which invokes tags from another tag library,
the second tag library needs to be explicitly mocked by invoking the
`mockTagLib` method.

NOTE: Mocked tag libraries are cleared after each feature method as part of the
web test cleanup lifecycle. Call `mockTagLib` in `setup()` rather than
`setupSpec()` to ensure the mock is active for every feature. Tests that
implement `TagLibUnitTest` handle this automatically, but tests that use
`GrailsWebUnitTest` directly must re-mock in each feature or in `setup()`.

[source,groovy]
.grails-app/taglib/demo/FirstTagLib.groovy
----
Expand Down
49 changes: 42 additions & 7 deletions grails-doc/src/en/guide/theWebLayer/gsp/taglibs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,71 @@ class SimpleTagLib {
}
----

Now to create a tag create a Closure property that takes two arguments: the tag attributes and the body content:
Now create tags using methods. You can access tag attributes through the implicit `attrs` map and body through the implicit `body` closure:

[source,groovy]
----
class SimpleTagLib {
def simple = { attrs, body ->
def simple() {
// ...

}
}
----

The `attrs` argument is a Map of the attributes of the tag, whilst the `body` argument is a Closure that returns the body content when invoked:
Closure field-style tags are still supported for backward compatibility, but method-based tags are the recommended syntax.

The implicit `attrs` property is a `Map` of the tag attributes, while `body()` returns the tag body content when invoked:

[source,groovy]
----
class SimpleTagLib {
def emoticon = { attrs, body ->
def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
----

For method-based tags, named attributes can also bind directly to method signature arguments:

[source,groovy]
----
class SimpleTagLib {
def greeting(String name) {
out << "Hello, ${name}!"
}
}
----

Used as:

[source,xml]
----
<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]
----
<g:emoticon happy="true">Hi John</g:emoticon>
----

NOTE: To help IDEs like Spring Tool Suite (STS) and others autocomplete tag attributes, you should add Javadoc comments to your tag closures with `@attr` descriptions. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.
NOTE: To help IDEs like Spring Tool Suite (STS) and others autocomplete tag attributes, add Javadoc comments with `@attr` descriptions to your tag methods. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.

For example:

Expand All @@ -71,7 +106,7 @@ class SimpleTagLib {
* @attr happy whether to show a happy emoticon ('true') or
* a sad emoticon ('false')
*/
def emoticon = { attrs, body ->
def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
Expand All @@ -89,7 +124,7 @@ class SimpleTagLib {
* @attr name REQUIRED the field name
* @attr value the field value
*/
def passwordField = { attrs ->
def passwordField() {
attrs.type = "password"
attrs.tagName = "passwordField"
fieldImpl(out, attrs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Iterative tags are easy too, since you can invoke the body multiple times:

[source,groovy]
----
def repeat = { attrs, body ->
def repeat() {
attrs.times?.toInteger()?.times { num ->
out << body(num)
}
Expand All @@ -48,7 +48,7 @@ That value is then passed as the default variable `it` to the tag. However, if y

[source,groovy]
----
def repeat = { attrs, body ->
def repeat() {
def var = attrs.var ?: "num"
attrs.times?.toInteger()?.times { num ->
out << body((var):num)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ You can also create logical tags where the body of the tag is only output once a

[source,groovy]
----
def isAdmin = { attrs, body ->
def isAdmin() {
def user = attrs.user
if (user && checkUserPrivs(user)) {
out << body()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ By default, tags are added to the default Grails namespace and are used with the
class SimpleTagLib {
static namespace = "my"

def example = { attrs ->
def example() {
//...
}
}
Expand Down
15 changes: 12 additions & 3 deletions grails-doc/src/en/guide/theWebLayer/gsp/taglibs/simpleTags.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ As demonstrated in the previous example it is easy to write simple tags that hav

[source,groovy]
----
def dateFormat = { attrs, body ->
def dateFormat() {
out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
}
----
Expand All @@ -33,11 +33,20 @@ The above uses Java's `SimpleDateFormat` class to format a date and then write i
<g:dateFormat format="dd-MM-yyyy" date="${new Date()}" />
----

With method-based tags, attributes may also bind directly to method parameters by name:

[source,groovy]
----
def dateFormat(String format, Date date) {
out << new java.text.SimpleDateFormat(format).format(date)
}
----

With simple tags sometimes you need to write HTML mark-up to the response. One approach would be to embed the content directly:

[source,groovy]
----
def formatBook = { attrs, body ->
def formatBook() {
out << "<div id=\"${attrs.book.id}\">"
out << "Title : ${attrs.book.title}"
out << "</div>"
Expand All @@ -48,7 +57,7 @@ Although this approach may be tempting it is not very clean. A better approach w

[source,groovy]
----
def formatBook = { attrs, body ->
def formatBook() {
out << render(template: "bookTemplate", model: [book: attrs.book])
}
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ under the License.

A taglib can be used in a GSP as an ordinary tag, or it might be used as a function in other taglibs or GSP expressions.

Internally Grails intercepts calls to taglib closures.
Internally Grails intercepts calls to tag handlers (method-based or closure-based).
The "out" that is available in a taglib is mapped to a `java.io.Writer` implementation that writes to a buffer
that "captures" the output of the taglib call. This buffer is the return value of a tag library call when it's
used as a function.

If the tag is listed in the library's static `returnObjectForTags` array, then its return value will be written to
the output when it's used as a normal tag. The return value of the tag lib closure will be returned as-is
the output when it's used as a normal tag. The return value of the tag method/closure will be returned as-is
if it's used as a function in GSP expressions or other taglibs.

If the tag is not included in the returnObjectForTags array, then its return value will be discarded.
Expand All @@ -36,10 +36,10 @@ Example:
----
class ObjectReturningTagLib {
static namespace = "cms"
static returnObjectForTags = ['content']
static returnObjectForTags = ['content']

def content = { attrs, body ->
CmsContent.findByCode(attrs.code)?.content
def content() {
CmsContent.findByCode(attrs.code)?.content
}
}
----
Expand Down
49 changes: 42 additions & 7 deletions grails-doc/src/en/guide/theWebLayer/taglibs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,71 @@ class SimpleTagLib {
}
----

Now to create a tag create a Closure property that takes two arguments: the tag attributes and the body content:
Now create tags using methods. You can access tag attributes through the implicit `attrs` map and body through the implicit `body` closure:

[source,groovy]
----
class SimpleTagLib {
def simple = { attrs, body ->
def simple() {
// ...

}
}
----

The `attrs` argument is a Map of the attributes of the tag, whilst the `body` argument is a Closure that returns the body content when invoked:
Closure field-style tags are still supported for backward compatibility, but method-based tags are the recommended syntax.

The implicit `attrs` property is a `Map` of the tag attributes, while `body()` returns the tag body content when invoked:

[source,groovy]
----
class SimpleTagLib {
def emoticon = { attrs, body ->
def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
----

For method-based tags, named attributes can also bind directly to method signature arguments:

[source,groovy]
----
class SimpleTagLib {
def greeting(String name) {
out << "Hello, ${name}!"
}
}
----

Used as:

[source,xml]
----
<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]
----
<g:emoticon happy="true">Hi John</g:emoticon>
----

NOTE: To help IDEs autocomplete tag attributes, you should add Javadoc comments to your tag closures with `@attr` descriptions. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.
NOTE: To help IDEs autocomplete tag attributes, add Javadoc comments with `@attr` descriptions to your tag methods. Since taglibs use Groovy code it can be difficult to reliably detect all usable attributes.

For example:

Expand All @@ -71,7 +106,7 @@ class SimpleTagLib {
* @attr happy whether to show a happy emoticon ('true') or
* a sad emoticon ('false')
*/
def emoticon = { attrs, body ->
def emoticon() {
out << body() << (attrs.happy == 'true' ? " :-)" : " :-(")
}
}
Expand All @@ -89,7 +124,7 @@ class SimpleTagLib {
* @attr name REQUIRED the field name
* @attr value the field value
*/
def passwordField = { attrs ->
def passwordField() {
attrs.type = "password"
attrs.tagName = "passwordField"
fieldImpl(out, attrs)
Expand Down
Loading
Loading