Skip to content

Commit dc1883b

Browse files
authored
Merge pull request #15560 from codeconsole/7.1.x-flashMessages
g:flashMessages tag
2 parents c8273e9 + 2be6391 commit dc1883b

File tree

12 files changed

+639
-12
lines changed

12 files changed

+639
-12
lines changed

grails-core/src/main/groovy/grails/config/Settings.groovy

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,62 @@ interface Settings {
6666

6767
String VIEWS_FILTERING_CODEC_FOR_CONTENT_TYPE = 'grails.views.filteringCodecForContentType'
6868

69+
/**
70+
* Default CSS class for {@code flash.message} alerts rendered by {@code <g:flashMessages />}.
71+
*
72+
* @since 7.1
73+
*/
74+
String VIEWS_GSP_FLASH_MESSAGES_MESSAGE_CLASS = 'grails.views.gsp.flashMessages.messageClass'
75+
76+
/**
77+
* Default icon class for {@code flash.message} alerts rendered by {@code <g:flashMessages />}.
78+
*
79+
* @since 7.1
80+
*/
81+
String VIEWS_GSP_FLASH_MESSAGES_MESSAGE_ICON = 'grails.views.gsp.flashMessages.messageIcon'
82+
83+
/**
84+
* Default CSS class for {@code flash.error} alerts rendered by {@code <g:flashMessages />}.
85+
*
86+
* @since 7.1
87+
*/
88+
String VIEWS_GSP_FLASH_MESSAGES_ERROR_CLASS = 'grails.views.gsp.flashMessages.errorClass'
89+
90+
/**
91+
* Default icon class for {@code flash.error} alerts rendered by {@code <g:flashMessages />}.
92+
*
93+
* @since 7.1
94+
*/
95+
String VIEWS_GSP_FLASH_MESSAGES_ERROR_ICON = 'grails.views.gsp.flashMessages.errorIcon'
96+
97+
/**
98+
* Default CSS class for {@code flash.warning} alerts rendered by {@code <g:flashMessages />}.
99+
*
100+
* @since 7.1
101+
*/
102+
String VIEWS_GSP_FLASH_MESSAGES_WARNING_CLASS = 'grails.views.gsp.flashMessages.warningClass'
103+
104+
/**
105+
* Default icon class for {@code flash.warning} alerts rendered by {@code <g:flashMessages />}.
106+
*
107+
* @since 7.1
108+
*/
109+
String VIEWS_GSP_FLASH_MESSAGES_WARNING_ICON = 'grails.views.gsp.flashMessages.warningIcon'
110+
111+
/**
112+
* Default ARIA role for alerts rendered by {@code <g:flashMessages />}.
113+
*
114+
* @since 7.1
115+
*/
116+
String VIEWS_GSP_FLASH_MESSAGES_ROLE = 'grails.views.gsp.flashMessages.role'
117+
118+
/**
119+
* Whether alerts rendered by {@code <g:flashMessages />} are dismissible by default.
120+
*
121+
* @since 7.1
122+
*/
123+
String VIEWS_GSP_FLASH_MESSAGES_DISMISSIBLE = 'grails.views.gsp.flashMessages.dismissible'
124+
69125
/**
70126
* Whether to disable caching of resources in GSP
71127
*/

grails-doc/src/en/guide/reference.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,11 @@ include::ref/Tags/fieldValue.adoc[]
11381138

11391139
include::ref/Tags/findAll.adoc[]
11401140

1141+
[[ref-tags-flashMessages]]
1142+
==== flashMessages
1143+
1144+
include::ref/Tags/flashMessages.adoc[]
1145+
11411146
[[ref-tags-form]]
11421147
==== form
11431148

grails-doc/src/en/guide/upgrading/upgrading71x.adoc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,3 +756,40 @@ class MySpec extends ContainerGebSpec {
756756
}
757757
}
758758
----
759+
760+
===== 2.11 Flash Messages Tag (`g:flashMessages`)
761+
762+
Grails 7.1 introduces a new `<g:flashMessages />` tag that renders `flash.message`, `flash.error`, and `flash.warning` as Bootstrap 5 dismissible alerts with appropriate styling (success, danger, and warning respectively).
763+
764+
The tag automatically prevents duplicate rendering -- if called in both a page and its layout, only the first invocation produces output. All flash content is HTML-encoded to prevent XSS.
765+
766+
The default layout (`main.gsp`) and all scaffolding templates now use this tag. If your application has custom views that render flash messages manually, you can replace the boilerplate:
767+
768+
[source,xml]
769+
----
770+
<%-- Before --%>
771+
<g:if test="${flash.message}">
772+
<div class="alert alert-primary" role="alert">${flash.message}</div>
773+
</g:if>
774+
775+
<%-- After --%>
776+
<g:flashMessages />
777+
----
778+
779+
The tag supports optional attributes for customizing CSS classes, icons, ARIA role, and dismissibility. Applications that do not use Bootstrap can override the defaults globally under `grails.views.gsp.flashMessages.*` in `application.yml`:
780+
781+
[source,yaml]
782+
----
783+
grails:
784+
views:
785+
gsp:
786+
flashMessages:
787+
messageClass: 'notification is-success'
788+
messageIcon: 'fa-solid fa-check'
789+
errorClass: 'notification is-danger'
790+
errorIcon: 'fa-solid fa-triangle-exclamation'
791+
warningClass: 'notification is-warning'
792+
warningIcon: 'fa-solid fa-circle-exclamation'
793+
----
794+
795+
See the {gspTagsRef}flashMessages.html[flashMessages] tag reference for full details.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
////
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
https://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
////
19+
20+
21+
== flashMessages
22+
23+
24+
25+
=== Purpose
26+
27+
28+
Renders `flash.message`, `flash.error`, and `flash.warning` as dismissible Bootstrap alert divs with appropriate styling. Automatically prevents duplicate rendering when used in both pages and layouts.
29+
30+
31+
=== Examples
32+
33+
34+
Basic usage in a view:
35+
36+
[source,xml]
37+
----
38+
<g:flashMessages />
39+
----
40+
41+
In a layout (safe to use alongside page-level usage -- the tag skips rendering if already called):
42+
43+
[source,xml]
44+
----
45+
<g:flashMessages />
46+
<g:layoutBody/>
47+
----
48+
49+
Non-dismissible alerts:
50+
51+
[source,xml]
52+
----
53+
<g:flashMessages dismissible="false" />
54+
----
55+
56+
Custom ARIA role:
57+
58+
[source,xml]
59+
----
60+
<g:flashMessages role="status" />
61+
----
62+
63+
Custom styling for success messages:
64+
65+
[source,xml]
66+
----
67+
<g:flashMessages messageClass="alert alert-info" messageIcon="bi bi-info-circle me-2" />
68+
----
69+
70+
Setting flash messages in a controller:
71+
72+
[source,groovy]
73+
----
74+
// Success message (renders as green alert)
75+
flash.message = "Book '${book.title}' saved successfully."
76+
77+
// Error message (renders as red alert)
78+
flash.error = "Unable to delete the record."
79+
80+
// Warning message (renders as yellow alert)
81+
flash.warning = "You have unsaved changes."
82+
----
83+
84+
85+
=== Description
86+
87+
88+
The `flashMessages` tag renders any combination of `flash.message`, `flash.error`, and `flash.warning` as Bootstrap 5 alert divs. Each flash key maps to a different alert style:
89+
90+
[cols="1,2,2"]
91+
|===
92+
| Flash Key | Default Alert Class | Default Icon
93+
94+
| `flash.message`
95+
| `alert alert-success alert-dismissible fade show`
96+
| `bi bi-check-circle me-2`
97+
98+
| `flash.error`
99+
| `alert alert-danger alert-dismissible fade show`
100+
| `bi bi-exclamation-triangle me-2`
101+
102+
| `flash.warning`
103+
| `alert alert-warning alert-dismissible fade show`
104+
| `bi bi-exclamation-circle me-2`
105+
|===
106+
107+
The tag automatically sets a `_flashRendered` request attribute after rendering. On subsequent calls within the same request, the tag detects this attribute and outputs nothing. This allows both pages and layouts to include `<g:flashMessages />` without producing duplicate alerts -- whichever renders first wins.
108+
109+
All flash message content is HTML-encoded to prevent XSS.
110+
111+
==== Configuration
112+
113+
The defaults target Bootstrap 5. Applications that use a different CSS framework can override the defaults globally in `application.yml`:
114+
115+
[source,yaml]
116+
----
117+
grails:
118+
views:
119+
gsp:
120+
flashMessages:
121+
messageClass: 'notification is-success'
122+
messageIcon: 'fa-solid fa-check'
123+
errorClass: 'notification is-danger'
124+
errorIcon: 'fa-solid fa-triangle-exclamation'
125+
warningClass: 'notification is-warning'
126+
warningIcon: 'fa-solid fa-circle-exclamation'
127+
role: 'alert'
128+
dismissible: true
129+
----
130+
131+
Resolution order for every default is:
132+
133+
. Tag attribute (e.g. `<g:flashMessages messageClass="..." />`)
134+
. Configured value under `grails.views.gsp.flashMessages.*`
135+
. Built-in Bootstrap fallback
136+
137+
Attributes
138+
139+
* `messageClass` (optional) - CSS class for `flash.message` alerts. Default: `alert alert-success alert-dismissible fade show`
140+
* `messageIcon` (optional) - Icon class for `flash.message` alerts. Default: `bi bi-check-circle me-2`
141+
* `errorClass` (optional) - CSS class for `flash.error` alerts. Default: `alert alert-danger alert-dismissible fade show`
142+
* `errorIcon` (optional) - Icon class for `flash.error` alerts. Default: `bi bi-exclamation-triangle me-2`
143+
* `warningClass` (optional) - CSS class for `flash.warning` alerts. Default: `alert alert-warning alert-dismissible fade show`
144+
* `warningIcon` (optional) - Icon class for `flash.warning` alerts. Default: `bi bi-exclamation-circle me-2`
145+
* `role` (optional) - ARIA role for alert divs. Default: `alert`
146+
* `dismissible` (optional) - Whether to show a close button. Default: `true`

grails-forge/grails-forge-core/src/main/resources/gsp/main.gsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
<div class="bg-body-tertiary">
2323
<div class="container-lg py-4">
24+
<g:flashMessages />
2425
<g:layoutBody/>
2526
</div>
2627
</div>

grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/ApplicationTagLib.groovy

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,30 @@ class ApplicationTagLib implements ApplicationContextAware, InitializingBean, Gr
7373
boolean useJsessionId = false
7474
boolean hasResourceProcessor = false
7575

76+
String flashMessagesMessageClass = 'alert alert-success alert-dismissible fade show'
77+
String flashMessagesMessageIcon = 'bi bi-check-circle me-2'
78+
String flashMessagesErrorClass = 'alert alert-danger alert-dismissible fade show'
79+
String flashMessagesErrorIcon = 'bi bi-exclamation-triangle me-2'
80+
String flashMessagesWarningClass = 'alert alert-warning alert-dismissible fade show'
81+
String flashMessagesWarningIcon = 'bi bi-exclamation-circle me-2'
82+
String flashMessagesRole = 'alert'
83+
boolean flashMessagesDismissible = true
84+
7685
void afterPropertiesSet() {
7786
def config = grailsApplication.config
7887

7988
useJsessionId = config.getProperty(Settings.GRAILS_VIEWS_ENABLE_JSESSIONID, Boolean, false)
8089
hasResourceProcessor = applicationContext.containsBean('grailsResourceProcessor')
8190

91+
flashMessagesMessageClass = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_MESSAGE_CLASS, String, flashMessagesMessageClass)
92+
flashMessagesMessageIcon = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_MESSAGE_ICON, String, flashMessagesMessageIcon)
93+
flashMessagesErrorClass = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_ERROR_CLASS, String, flashMessagesErrorClass)
94+
flashMessagesErrorIcon = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_ERROR_ICON, String, flashMessagesErrorIcon)
95+
flashMessagesWarningClass = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_WARNING_CLASS, String, flashMessagesWarningClass)
96+
flashMessagesWarningIcon = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_WARNING_ICON, String, flashMessagesWarningIcon)
97+
flashMessagesRole = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_ROLE, String, flashMessagesRole)
98+
flashMessagesDismissible = config.getProperty(Settings.VIEWS_GSP_FLASH_MESSAGES_DISMISSIBLE, Boolean, flashMessagesDismissible)
99+
82100
if (applicationContext.containsBean('requestDataValueProcessor')) {
83101
requestDataValueProcessor = applicationContext.getBean('requestDataValueProcessor', RequestDataValueProcessor)
84102
}
@@ -471,4 +489,68 @@ class ApplicationTagLib implements ApplicationContextAware, InitializingBean, Gr
471489
// encoding is handled in GroovyPage.invokeTag and GroovyPage.captureTagOutput
472490
body()
473491
}
492+
493+
/**
494+
* Renders flash.message, flash.error, and flash.warning as Bootstrap alert divs.
495+
* Automatically skips rendering if already called during this request, preventing
496+
* duplicate display when used in both pages and layouts.
497+
*
498+
* @emptyTag
499+
*
500+
* @attr messageClass CSS class for flash.message alerts (default: 'alert alert-success alert-dismissible fade show')
501+
* @attr messageIcon Icon class for flash.message alerts (default: 'bi bi-check-circle me-2')
502+
* @attr errorClass CSS class for flash.error alerts (default: 'alert alert-danger alert-dismissible fade show')
503+
* @attr errorIcon Icon class for flash.error alerts (default: 'bi bi-exclamation-triangle me-2')
504+
* @attr warningClass CSS class for flash.warning alerts (default: 'alert alert-warning alert-dismissible fade show')
505+
* @attr warningIcon Icon class for flash.warning alerts (default: 'bi bi-exclamation-circle me-2')
506+
* @attr role ARIA role for alert divs (default: 'alert')
507+
* @attr dismissible Whether to show a close button (default: true)
508+
*/
509+
Closure flashMessages = { attrs ->
510+
if (request.getAttribute('_flashRendered')) {
511+
return
512+
}
513+
514+
boolean rendered = false
515+
boolean dismissible = attrs.dismissible != null ? attrs.dismissible.toString().toBoolean() : flashMessagesDismissible
516+
String role = attrs.role ?: flashMessagesRole
517+
518+
if (flash.message) {
519+
renderFlashAlert(
520+
attrs.messageClass ?: flashMessagesMessageClass,
521+
attrs.messageIcon ?: flashMessagesMessageIcon,
522+
flash.message, dismissible, role)
523+
rendered = true
524+
}
525+
526+
if (flash.error) {
527+
renderFlashAlert(
528+
attrs.errorClass ?: flashMessagesErrorClass,
529+
attrs.errorIcon ?: flashMessagesErrorIcon,
530+
flash.error, dismissible, role)
531+
rendered = true
532+
}
533+
534+
if (flash.warning) {
535+
renderFlashAlert(
536+
attrs.warningClass ?: flashMessagesWarningClass,
537+
attrs.warningIcon ?: flashMessagesWarningIcon,
538+
flash.warning, dismissible, role)
539+
rendered = true
540+
}
541+
542+
if (rendered) {
543+
request.setAttribute('_flashRendered', true)
544+
}
545+
}
546+
547+
private void renderFlashAlert(String cssClass, String icon, Object message, boolean dismissible, String role) {
548+
out << "<div class=\"${cssClass}\" role=\"${role}\">"
549+
out << "<i class=\"${icon}\"></i>"
550+
out << message.toString().encodeAsHTML()
551+
if (dismissible) {
552+
out << '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>'
553+
}
554+
out << '</div>'
555+
}
474556
}

grails-profiles/web/skeleton/grails-app/views/layouts/main.gsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
<div class="bg-body-tertiary">
2323
<div class="container-lg py-4">
24+
<g:flashMessages />
2425
<g:layoutBody/>
2526
</div>
2627
</div>

grails-scaffolding/src/main/templates/scaffolding/create.gsp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@
2424
<section class="row">
2525
<div id="create-${propertyName}" class="col-12 content scaffold-create" role="main">
2626
<h1><g:message code="default.create.label" args="[entityName]" /></h1>
27-
<g:if test="\${flash.message}">
28-
<div class="message" role="status">\${flash.message}</div>
29-
</g:if>
27+
<g:flashMessages />
3028
<g:hasErrors bean="\${this.${propertyName}}">
3129
<ul class="alert alert-danger list-unstyled" role="alert">
3230
<g:eachError bean="\${this.${propertyName}}" var="error">

0 commit comments

Comments
 (0)