My InstantRenderer
class
You can easily create your own implementation of a renderer.
Here is the renderer I wrote to handle a Grid
widget displaying objects that include an Instant
object. An Instant
is a moment, a specific point on the timeline, as seen in UTC (an offset of zero hours-minutes-seconds). The Instant
class is the basic building block class used in the java.time framework.
The idea here is that we take the Instant
object, apply a specified ZoneId
to get a ZonedDateTime
object. That ZonedDateTime
object uses the specified DateTimeFormatter
object to generate text in a String
object. The text represents the contents of the ZonedDateTime
object automatically localized to the specified Locale
object's human language and cultural norms.

The ZoneId
and the Locale
are attached to the DateTimeFormatter
passed by the calling programmer.
My code here is based on the code published by the Vaadin Ltd company for their LocalDateTimeRenderer
class’ source-code found on their GitHub site.
I pruned the API of that class. Their API allowed for passing a formatting-pattern string rather than a DateTimeFormatter
object. I do not believe it should be the responsibility of the renderer to produce a formatter object from such a string, and therefore too handle any resulting error conditions. And their API allowed for passing a Locale
object. The Locale
object can be attached to the DateTimeFormatter
object being passed by the calling programmer. I do not see how this renderer class should be getting needlessly involved in assigning the passed locale to the passed formatter. The calling programming can do that assignment before passing the formatter to our renderer.
Here is typical usage in defining a InstantRenderer
for rendering a Instant
object for display within a Grid
in Vaadin 14.
invoicesGrid
.addColumn(
new InstantRenderer <>( Invoice :: getWhenCreated ,
DateTimeFormatter
.ofLocalizedDateTime( FormatStyle.SHORT , FormatStyle.MEDIUM )
.withLocale( Locale.CANADA_FRENCH )
.withZone( ZoneId.of( "America/Montreal" ) )
)
)
.setHeader( "Created" )
;
Specify a proper time zone name in the format of Continent/Region
, such as America/Montreal
, Africa/Casablanca
, or Pacific/Auckland
. Never use the 2-4 letter abbreviation such as EST
or IST
as they are not true time zones, not standardized, and not even unique(!).
Be aware that the java.time classes use immutable objects. The withZone
and withLocale
methods produce a new fresh DateTimeFormatter
rather than altering the original. So you might want to keep a global singleton DateTimeFormatter
with your preference for short date and longer time-of-day.
DateTimeFormatter f = DateTimeFormatter
.ofLocalizedDateTime(
FormatStyle.SHORT , // Length of date portion.
FormatStyle.MEDIUM // Length of time-of-day portion.
)
;
Then elsewhere in your code, apply each user’s own preferred zone and locale. You get another, specialized, DateTimeFormatter
object while the original remains unaffected due to the immutable objects pattern used in java.time.
invoicesGrid
.addColumn(
new InstantRenderer <>( Invoice :: getWhenCreated ,
f
.withLocale( user.getPreferredLocale() )
.withZone( user.getPreferredZone() )
)
)
.setHeader( "Created" )
;
By the way, there is a third optional argument to the constructors: a String
to be used in case the Instant
object being rendered is null. The default is to present no text at all to the user, an empty ""
string. You may pass some other string if you wish, such as null
or void
.
And here is the source-code of my class. Notice that I put some discussion in the Javadoc near the top.
I am using the same Apache License 2 as does Vaadin Ltd, so you may use and change this code for yourself. Your feedback is welcome.
package work.basil.example.ui;
/*
* Copyright 2000-2020 Vaadin Ltd.
* Copyright 2020 Basil Bourque.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.Objects;
import com.vaadin.flow.data.renderer.BasicRenderer;
import com.vaadin.flow.function.ValueProvider;
/*
* This class is based on source-code directly copied from
* `LocalDateTimeRenderer.java` of Vaadin 14.1.x
* as written and published by Vaadin Ltd. from their GitHub page.
*
* https://github.com/vaadin/flow/blob/master/flow-data/src/main/java/com/vaadin/flow/data/renderer/LocalDateTimeRenderer.java
*
* I re-purposed that class to handle `Instant` objects rather than `LocalDateTime`
* objects. An `Instant` represents a moment, whereas `LocalDateTime` cannot because
* of it lacking any concept of time zone or offset-from-UTC. In contrast, `Instant`
* represents a moment in UTC (an offset-from-UTC of zero hours-minutes-seconds).
*
* By default, a `Instant` object renders in Vaadin by way of its `toString` method
* generating text in standard ISO 8601 format YYYY-MM-DDTHH:MM:SS.SSSSSSSSSZ.
*
* If you want other than ISO 8601 format in UTC, use this class. In this class, we
* apply a time zone (`ZoneId`) to the `Instant` to adjust from UTC.
*
* The `ZoneId` object comes from one of three places:
* - Passed implicitly by being set as a property on a `DateTimeFormatter`
* object passed as an argument. This is the best case.
* - Defaults to calling `ZoneId.systemDefault` if not found
* on the `DateTimeFormatter` object (where `getZone` returns null).
*
* I deleted the constructors taking a formatting pattern string. Parsing such a string
* and instantiating a `DateTimeFormatter` and handling resulting error conditions
* should *not* be the job of this class. I believe the Vaadin team made a poor choice
* in having constructors taking a string formatting pattern rather than just a
* `DateTimeFormatter` object.
*
* Locale is another critical issue. A `Locale` object determines:
*
* (a) The human language used for translating items such as name of month and
* name of day.
*
* (b) The cultural norms used in deciding localization issues such as the ordering
* of elements (ex: day comes before or after month), abbreviation, capitalization,
* punctuation, and so on.
*
* Again, I deleted the constructors taking a `Locale` object. The `DateTimeFormatter`
* object passed by the calling programmer carries a `Locale`. That calling programmer
* should have attached their intended locale object to that `DateTimeFormatter` object
* by calling `DateTimeFormatter::withLocale`. Usually a `DateTimeFormatter` has a default
* `Locale` assigned. But if found lacking, here we attach the JVM’s current default locale.
*
* Following the logic discussed above, I chose to not take a `ZoneId` as an argument.
* A `ZoneId` can be attached to the `DateTimeFormatter` by calling `withZoneId`.
* If the passed `DateTimeFormatter` is found lacking, here we attach the JVM’s current
* default time zone.
*
* Typical usage, passing 2 arguments, a method reference and a `DateTimeFormatter` object
* while omitting 3rd optional argument for null-representation to go with an blank empty string:
*
* myGrid
* .addColumn(
* new InstantRenderer <>( TheBusinessObject :: getWhenCreated ,
* DateTimeFormatter
* .ofLocalizedDateTime( FormatStyle.SHORT , FormatStyle.MEDIUM )
* .withLocale( Locale.CANADA_FRENCH )
* .withZone( ZoneId.of( "America/Montreal" ) )
* )
* )
*
* This code is written for Java 8 or later.
*
* For criticisms and suggestions, contact me via LinkedIn at: basilbourque
*/
/**
* A template renderer for presenting {@code Instant} objects.
*
* @param <SOURCE> the type of the input item, from which the {@link Instant}
* is extracted
* @author Vaadin Ltd
* @since 1.0.
*/
public class InstantRenderer < SOURCE >
extends BasicRenderer < SOURCE, Instant >
{
private DateTimeFormatter formatter;
private String nullRepresentation;
/**
* Creates a new InstantRenderer.
* <p>
* The renderer is configured to render with the format style
* {@code FormatStyle.LONG} for the date and {@code FormatStyle.SHORT} for
* time, with an empty string as its null representation.
*
* @param valueProvider the callback to provide a {@link Instant} to the
* renderer, not <code>null</code>
* @see <a href=
* "https://docs.oracle.com/javase/8/docs/api/java/time/format/FormatStyle.html#LONG">
* FormatStyle.LONG</a>
* @see <a href=
* "https://docs.oracle.com/javase/8/docs/api/java/time/format/FormatStyle.html#SHORT">
* FormatStyle.SHORT</a>
*/
public InstantRenderer (
ValueProvider < SOURCE, Instant > valueProvider )
{
this(
valueProvider ,
DateTimeFormatter
.ofLocalizedDateTime( FormatStyle.LONG )
.withZone( ZoneId.systemDefault() )
.withLocale( Locale.getDefault() ) ,
""
);
}
/**
* Creates a new InstantRenderer.
* <p>
* The renderer is configured to render with the given formatter, with the
* empty string as its null representation.
*
* @param valueProvider the callback to provide a {@link Instant} to the
* renderer, not <code>null</code>
* @param formatter the formatter to use, not <code>null</code>
*/
public InstantRenderer (
ValueProvider < SOURCE, Instant > valueProvider ,
DateTimeFormatter formatter
)
{
this(
valueProvider ,
formatter ,
""
);
}
/**
* Creates a new InstantRenderer.
* <p>
* The renderer is configured to render with the given formatter.
*
* @param valueProvider the callback to provide a {@link Instant} to the
* renderer, not <code>null</code>
* @param formatter the formatter to use, not <code>null</code>
* @param nullRepresentation the textual representation of the <code>null</code> value
*/
public InstantRenderer (
final ValueProvider < SOURCE, Instant > valueProvider ,
final DateTimeFormatter formatter ,
final String nullRepresentation
)
{
super( valueProvider );
this.formatter = Objects.requireNonNull( formatter , "formatter may not be null" );
this.nullRepresentation = Objects.requireNonNull( nullRepresentation , "null-representation may not be null" );
// If the formatter provided by the calling programmer lacks a time zone, apply the JVM's current default zone.
// This condition is less than ideal. The calling programmer should have set an appropriate zone.
// Often the appropriate zone is one specifically chosen or confirmed by the user.
if ( Objects.isNull( this.formatter.getZone() ) )
{
this.formatter = this.formatter.withZone( ZoneId.systemDefault() );
}
// If the formatter provided by the calling programmer lacks a locale, apply the JVM's current default locale.
// This condition is less than ideal. The calling programmer should have set an appropriate locale.
// Often the appropriate locale is one specifically chosen or confirmed by the user.
if ( Objects.isNull( this.formatter.getLocale() ) )
{
this.formatter = this.formatter.withLocale( Locale.getDefault() );
}
}
@Override
protected String getFormattedValue ( final Instant instant )
{
// If null, return the null representation.
// If not null, adjust the `Instant` from UTC into the time zone attached to the `DateTimeFormatter` object.
// This adjustment, made by calling `Instant::atZone`, produces a `ZonedDateTime` object.
// We then create a `String` with text representing the value of that `ZonedDateTime` object.
// That text is automatically localized per the `Locale` attached to the `DateTimeFormatter` object.
String s = Objects.isNull( instant ) ? nullRepresentation : formatter.format( instant.atZone( this.formatter.getZone() ) );
return s;
}
}
Perhaps I can later do something similar for the other java.time types seen listed in the Question.