Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ ValueConverter instantValueConverter() {
return jsr310ConvertersConfiguration.instantValueConverter();
}

@Bean("instantStructuredBindingEditor")
TypedStructuredBindingEditor instantStructuredBindingEditor() {
return jsr310ConvertersConfiguration.instantStructuredBindingEditor();
}

@Bean("defaultUUIDConverter")
protected UUIDConverter defaultuuidConverter() {
return new UUIDConverter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,21 @@ class Jsr310ConvertersConfiguration {
}
}

@Bean
TypedStructuredBindingEditor instantStructuredBindingEditor() {
new CustomDateBindingEditor<Instant>() {
@Override
Instant getDate(Calendar c) {
c.toInstant()
}

@Override
Class<?> getTargetType() {
Instant
}
}
}

abstract class Jsr310DateValueConverter<T> implements ValueConverter {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ 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.LocalTime
import java.time.OffsetDateTime
import java.time.ZonedDateTime

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
Expand Down Expand Up @@ -725,7 +729,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')
}
Expand Down Expand Up @@ -947,12 +951,20 @@ class FormFieldsTagLib {
case Boolean:
g.formatBoolean(boolean: model.value)
break
case Calendar:
case Date:
case LocalDate:
case java.sql.Date:
g.formatDate(date: model.value, format: 'yyyy-MM-dd')
break
case java.sql.Time:
case LocalDate:
case LocalTime:
g.formatDate(date: model.value, type: 'TIME')
break
case Calendar:
case Date:
case LocalDateTime:
case Instant:
case ZonedDateTime:
case OffsetDateTime:
g.formatDate(date: model.value)
break
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +28,7 @@ class DisplayWidgetSpec extends AbstractFormFieldsTagLibSpec implements TagLibUn
def mockFormFieldsTemplateService = Mock(FormFieldsTemplateService)

def setupSpec() {
mockDomain(Person)
mockDomains(Person, Cyborg)
}

def setup() {
Expand All @@ -40,6 +41,16 @@ class DisplayWidgetSpec extends AbstractFormFieldsTagLibSpec implements TagLibUn
applyTemplate('<f:displayWidget bean="personInstance" property="dateOfBirth"/>', [personInstance: personInstance]) == applyTemplate('<g:formatDate date="${personInstance.dateOfBirth}"/>', [personInstance: personInstance])
}

void 'f:displayWidget without template and an instant value renders the formatted date'() {
expect:
applyTemplate('<f:displayWidget bean="cyborgInstance" property="timestamp"/>', [cyborgInstance: cyborgInstance]) == applyTemplate('<g:formatDate date="${cyborgInstance.timestamp}"/>', [cyborgInstance: cyborgInstance])
}

void 'f:displayWidget without template and a LocalDate value renders the formatted date'() {
expect:
applyTemplate('<f:displayWidget bean="cyborgInstance" property="birthDate"/>', [cyborgInstance: cyborgInstance]) == applyTemplate('<g:formatDate date="${cyborgInstance.birthDate}" format="yyyy-MM-dd"/>', [cyborgInstance: cyborgInstance])
}

void 'f:displayWidget without template and a boolean value renders the formatted boolean'() {
expect:
applyTemplate('<f:displayWidget bean="personInstance" property="minor"/>', [personInstance: personInstance]) == applyTemplate('<g:formatBoolean boolean="${personInstance.minor}"/>', [personInstance: personInstance])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@
*/
package grails.plugin.formfields.mock

import java.time.Instant
import java.time.LocalDate

import grails.gorm.annotation.AutoTimestamp
import grails.persistence.Entity

@Entity
class Cyborg extends Person {
@AutoTimestamp(AutoTimestamp.EventType.CREATED) Date created
@AutoTimestamp Date modified
Instant timestamp
LocalDate birthDate
}

@Entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: "[email protected]", school: "[email protected]"]
productInstance = new Product(netPrice: 12.33, name: "<script>alert('XSS');</script>")
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class DefaultGrailsTagDateHelper implements GrailsTagDateHelper {
TemporalAccessor instant
if (date instanceof java.sql.Date) {
instant = date.toLocalDate()
} else if (date instanceof java.sql.Time) {
instant = date.toLocalTime()
} else if (date instanceof Date) {
instant = date.toInstant()
} else if (date instanceof Calendar) {
Expand Down Expand Up @@ -134,6 +136,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()

Expand Down
Loading