diff --git a/grails-databinding/src/main/groovy/org/grails/databinding/converters/DefaultConvertersConfiguration.java b/grails-databinding/src/main/groovy/org/grails/databinding/converters/DefaultConvertersConfiguration.java index 44e6099b7fc..1907a9423ef 100644 --- a/grails-databinding/src/main/groovy/org/grails/databinding/converters/DefaultConvertersConfiguration.java +++ b/grails-databinding/src/main/groovy/org/grails/databinding/converters/DefaultConvertersConfiguration.java @@ -177,6 +177,11 @@ ValueConverter instantValueConverter() { return jsr310ConvertersConfiguration.instantValueConverter(); } + @Bean("instantStructuredBindingEditor") + TypedStructuredBindingEditor instantStructuredBindingEditor() { + return jsr310ConvertersConfiguration.instantStructuredBindingEditor(); + } + @Bean("defaultUUIDConverter") protected UUIDConverter defaultuuidConverter() { return new UUIDConverter(); diff --git a/grails-databinding/src/main/groovy/org/grails/databinding/converters/Jsr310ConvertersConfiguration.groovy b/grails-databinding/src/main/groovy/org/grails/databinding/converters/Jsr310ConvertersConfiguration.groovy index ca8f0cf556b..f89991491ed 100644 --- a/grails-databinding/src/main/groovy/org/grails/databinding/converters/Jsr310ConvertersConfiguration.groovy +++ b/grails-databinding/src/main/groovy/org/grails/databinding/converters/Jsr310ConvertersConfiguration.groovy @@ -388,6 +388,21 @@ class Jsr310ConvertersConfiguration { } } + @Bean + TypedStructuredBindingEditor instantStructuredBindingEditor() { + new CustomDateBindingEditor() { + @Override + Instant getDate(Calendar c) { + c.toInstant() + } + + @Override + Class getTargetType() { + Instant + } + } + } + abstract class Jsr310DateValueConverter implements ValueConverter { @Override 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 2aa852ee4f4..ec88cf18df0 100644 --- a/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy +++ b/grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy @@ -20,8 +20,11 @@ package grails.plugin.formfields import java.sql.Blob import java.text.NumberFormat +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZonedDateTime import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -725,7 +728,7 @@ class FormFieldsTagLib { } // TODO: https://github.com/apache/grails-core/issues/14198 - boolean datePicker = model.type in [Date, Calendar, java.sql.Date, java.sql.Time, LocalDate, LocalDateTime] + boolean datePicker = model.type in [Date, Calendar, java.sql.Date, java.sql.Time, LocalDate, LocalDateTime, Instant, ZonedDateTime, OffsetDateTime] if (!datePicker) { attrs.remove('selectDateClass') } @@ -947,12 +950,17 @@ class FormFieldsTagLib { case Boolean: g.formatBoolean(boolean: model.value) break + case LocalDate: + case java.sql.Date: + g.formatDate(date: model.value, format: 'yyyy-MM-dd') + break case Calendar: case Date: - case java.sql.Date: case java.sql.Time: - case LocalDate: case LocalDateTime: + case Instant: + case ZonedDateTime: + case OffsetDateTime: g.formatDate(date: model.value) break default: diff --git a/grails-fields/src/test/groovy/grails/plugin/formfields/DisplayWidgetSpec.groovy b/grails-fields/src/test/groovy/grails/plugin/formfields/DisplayWidgetSpec.groovy index 8b6f27f27fd..66c88af9eae 100644 --- a/grails-fields/src/test/groovy/grails/plugin/formfields/DisplayWidgetSpec.groovy +++ b/grails-fields/src/test/groovy/grails/plugin/formfields/DisplayWidgetSpec.groovy @@ -18,6 +18,7 @@ */ package grails.plugin.formfields +import grails.plugin.formfields.mock.Cyborg import grails.plugin.formfields.mock.Person import grails.plugin.formfields.taglib.AbstractFormFieldsTagLibSpec import grails.testing.web.taglib.TagLibUnitTest @@ -27,7 +28,7 @@ class DisplayWidgetSpec extends AbstractFormFieldsTagLibSpec implements TagLibUn def mockFormFieldsTemplateService = Mock(FormFieldsTemplateService) def setupSpec() { - mockDomain(Person) + mockDomains(Person, Cyborg) } def setup() { @@ -40,6 +41,16 @@ class DisplayWidgetSpec extends AbstractFormFieldsTagLibSpec implements TagLibUn applyTemplate('', [personInstance: personInstance]) == applyTemplate('', [personInstance: personInstance]) } + void 'f:displayWidget without template and an instant value renders the formatted date'() { + expect: + applyTemplate('', [cyborgInstance: cyborgInstance]) == applyTemplate('', [cyborgInstance: cyborgInstance]) + } + + void 'f:displayWidget without template and a LocalDate value renders the formatted date'() { + expect: + applyTemplate('', [cyborgInstance: cyborgInstance]) == applyTemplate('', [cyborgInstance: cyborgInstance]) + } + void 'f:displayWidget without template and a boolean value renders the formatted boolean'() { expect: applyTemplate('', [personInstance: personInstance]) == applyTemplate('', [personInstance: personInstance]) diff --git a/grails-fields/src/test/groovy/grails/plugin/formfields/mock/Person.groovy b/grails-fields/src/test/groovy/grails/plugin/formfields/mock/Person.groovy index e452e722649..b14749a7e4b 100644 --- a/grails-fields/src/test/groovy/grails/plugin/formfields/mock/Person.groovy +++ b/grails-fields/src/test/groovy/grails/plugin/formfields/mock/Person.groovy @@ -18,6 +18,9 @@ */ package grails.plugin.formfields.mock +import java.time.Instant +import java.time.LocalDate + import grails.gorm.annotation.AutoTimestamp import grails.persistence.Entity @@ -25,6 +28,8 @@ import grails.persistence.Entity class Cyborg extends Person { @AutoTimestamp(AutoTimestamp.EventType.CREATED) Date created @AutoTimestamp Date modified + Instant timestamp + LocalDate birthDate } @Entity diff --git a/grails-fields/src/test/groovy/grails/plugin/formfields/taglib/AbstractFormFieldsTagLibSpec.groovy b/grails-fields/src/test/groovy/grails/plugin/formfields/taglib/AbstractFormFieldsTagLibSpec.groovy index 7d39cf98e24..28f8850be5e 100644 --- a/grails-fields/src/test/groovy/grails/plugin/formfields/taglib/AbstractFormFieldsTagLibSpec.groovy +++ b/grails-fields/src/test/groovy/grails/plugin/formfields/taglib/AbstractFormFieldsTagLibSpec.groovy @@ -19,6 +19,9 @@ package grails.plugin.formfields.taglib +import java.time.Instant +import java.time.LocalDate + import grails.core.support.proxy.DefaultProxyHandler import grails.plugin.formfields.BeanPropertyAccessorFactory import grails.plugin.formfields.FieldsGrailsPlugin @@ -45,7 +48,7 @@ abstract class AbstractFormFieldsTagLibSpec extends Specification implements Gra personInstance.address = new Address(street: "94 Evergreen Terrace", city: "Springfield", country: "USA") personInstance.emails = [home: "bart@thesimpsons.net", school: "bart.simpson@springfieldelementary.edu"] productInstance = new Product(netPrice: 12.33, name: "") - cyborgInstance = new Cyborg(name: "Hal", password: "monolith", gender: null) + cyborgInstance = new Cyborg(name: "Hal", password: "monolith", gender: null, timestamp: Instant.parse("2025-10-16T00:12:15.195Z"), birthDate: LocalDate.of(2025, 10, 15)) } def cleanup() { diff --git a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/DefaultGrailsTagDateHelper.groovy b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/DefaultGrailsTagDateHelper.groovy index 0678aa34891..0ba8e39fc52 100644 --- a/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/DefaultGrailsTagDateHelper.groovy +++ b/grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/DefaultGrailsTagDateHelper.groovy @@ -134,6 +134,8 @@ class DefaultGrailsTagDateHelper implements GrailsTagDateHelper { zonedDateTime = ZonedDateTime.of(date, ZoneId.systemDefault()) } else if (date instanceof LocalDate) { zonedDateTime = ZonedDateTime.of(date, LocalTime.MIN, ZoneId.systemDefault()) + } else if (date instanceof Instant) { + zonedDateTime = ZonedDateTime.ofInstant(date, ZoneId.systemDefault()) } else if (date instanceof OffsetDateTime) { zonedDateTime = ((OffsetDateTime) date).toZonedDateTime()