Datepickers in Liferay 7

16 minuten lezen

In one of my recent projects, I had to create a CRUD interface for a service builder portlet. Some of the fields in the object I was maintaining were date fields. To be specific, I had a start date and an end date, and I wanted the end date to always be after the start date. What follows is a step-by-step report of all the challenges I ran into and how I met them. At the end of the text is the final code I ended up with, just in case you are not interested in all the in-between steps.

liferay-ui:input-date

As I was using Alloy UI, the most logical thing to use seemed to be the tag. In the end, it took quite a bit of trial and error for me to find out that this wasn't what I wanted to use at all. But let's start at the beginning.

First enabled date

The first hurdle I encountered was when I wanted the current date to be the date picker default. Initially, the default displayed was 11/30/0002. As this was a portlet to record and showcase future events, a date that far in the past wasn't very useful. This is because you can't just skip from 11/30/0002 to the current date in the UI, you have to jump ahead one month at a time. You could enter the date as text of course, but that somehow defeats the purpose of a date picker. In Liferay 6.1, they had xxxRangeStart and xxxRangeEnd parameters for this (where xxx could be either day, month or year), but these were removed in Liferay 6.2. However, after quite a bit of searching, I found that they had replaced these parameters with a new parameter called firstEnabledDate.

After using that parameter, my code now looked like this:

<%@ page import="java.util.Date" %>
<%    Date defaultDate = new Date(); %>
<aui:form action="<%=editAction%>" method="post" name="fm">
    <aui:fieldset>
        <liferay-ui:input-date name="startDate" 
            firstEnabledDate="<%=defaultDate%>" />
    </aui:fieldset>
</aui:form>

Setting the shown value

While this does prevent the user from picking a date in the past, the initial date displayed is still 11/30/0002. In order to fix that, I had to set the values of day, month and year to the current date as well.

<%@ page import="java.util.Date" %>
<%
    Date defaultDate = new Date();
    int startDay, startMonth, startYear;
    startDay = defaultDate.getDate();
    // For some reason the input-date subtracts 1900 from the year it is given,
    // so we have to add that 1900 back to it
    startYear = defaultDate.getYear()+1900; 
    // However it does work with java.util.Date counting months from 0, 
	// so you don't have to add one extra month to it
    startMonth = defaultDate.getMonth(); 
%>


<aui:form action="<%=editAction%>" method="post" name="fm">
    <aui:fieldset>
        <liferay-ui:input-date name="startDate" 
            firstEnabledDate="<%=defaultDate%>" dayValue="<%=startDay%>" 
            monthValue="<%=startMonth%>" yearValue="<%=startYear%>"/>
    </aui:fieldset>
</aui:form>

Setting the date to the selected value

All of the above works fine if you are creating a new object, but when you want to edit an existing object, it does now show the correct date. As I'm using the same jsp-file for both creating new events and editing existing events, this wasn't very straightforward. However I could fix it with a simple if-statement.

<%@ page import="java.util.Date" %>
<%
    Date defaultDate = new Date();
    Event selectedEvent = (Event)request.getAttribute("event");
    int startDay, startMonth, startYear;
    if(selectedEvent.getStartDate() != null) {
        startDay = selectedEvent.getStartDate().getDate();
        startMonth = selectedEvent.getStartDate().getMonth();
        startYear = selectedEvent.getStartDate().getYear()+1900;
    } else {
        startDay = defaultDate.getDate();
        startMonth = defaultDate.getMonth();
        startYear = defaultDate.getYear()+1900;
    }
%>
<aui:form action="<%=editAction%>" method="post" name="fm">
    <aui:fieldset>
        <liferay-ui:input-date name="startDate" 
            firstEnabledDate="<%=defaultDate%>" dayValue="<%=startDay%>" 
            monthValue="<%=startMonth%>" yearValue="<%=startYear%>"/>
    </aui:fieldset>
</aui:form>

Setting the starting value of the end date based on the inputted value of the start date

The next thing I wanted to do was to change the value of the end date based on what the user entered for the start date. After all, no end date will ever be before the start date. I looked into changing the current selected value with javascript, but then I found out that the underlying hidden fields for day, month and year don't have unique id's. More googling resulted in adding the xxxParam parameters, which adds these parameters to the id in the generated HTML. But then I found out that onchange is not a supported parameter for the input-date tag. And so this path was killed in its tracks. I wasn't all devastated, because as you can see the code was already becoming cumbersome. This is especially true when you realize that events have both a start date and an end date, and I have to duplicate that bit of code. Later on I added two more dates for the start and end of the call for papers. I was, in fact, only keeping at it just to see if it could be done.

alloyui datepicker

My next experiment was to use the Alloy UI datepicker. The examples in the Alloy UI tutorials don't go very deeply into the details of how to use this, so this was a bit of a puzzle to get working. My first challenge was to find out what worked as a trigger. The example shows that you can use the tag name as a trigger, but as I was using input fields for both the dates and non-dates, this was not a good option. I tried combining the tag with a class input.form-control, but this didn't seem to work. So I ended up creating a separate class to tag the date fields, using .date as a trigger. I found out that you can also use # as trigger, although I didn't use this at first. Another thing I found out during my first attempt was not to set the zIndex to 1 as the examples do. This is because it might well fall behind some buttons if you do. A bit of debugging told me that buttons have a zIndex in the 900s, so I set it to a 1000 for the datepicker. This all led me to the following code:

<input name="startDate" class="form-control date" type="text" placeholder="dd-mm-yyyy" value="<%=startDate%>">


<aui:script>
    AUI().use(
        'aui-datepicker',
        function(A) {
            new A.DatePicker({
                trigger: '.date',
                mask: '%d-%m-%Y',
                popover: {
                    zIndex: 1000
                }
            });
        }
    );
</aui:script>

Formatting the date

This works, however, the date is now formatted as "Tue May 30 11:49:22 GMT 2017". This doesn't comply with the mask we want to use for the input field, so we will have to format it.

<%@ page import="java.text.SimpleDateFormat"%>


<% SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy"); %>


<input name="startDate" class="form-control date" type="text" 
    placeholder="dd-mm-yyyy" value="<%=formatter.format(startDate)%>">

Because I now no longer need separate values for day, month and year, the code for startDay, startMonth, and startYear could be simplified to just startDate as follows:

<%
    Date startDate;


    if(selectedEvent.getStartDate() != null) {
        startDate = selectedEvent.getStartDate();
    } else {
        startDate = defaultDate;
    }
%>

Setting the minimum date

My next step was to find out how to set the minimum date. This turned out to be ridiculously hard as most of the examples I found were based on older versions (using minDate and Strings). And I think I was also hindered by my browser sometimes using the cached older version of the code, exhibiting erroneous behavior when it should have worked. For example, it took me a while to figure out that, in this context , new Date() did actually initialize to the current date. I had tried to use that before, but apparently in a different context so that it started at some weird point in the past. Anyway, the correct way to do this is as follows:

new A.DatePicker({
    trigger: '.date',
    mask: '%d-%m-%Y',
    popover: {
        zIndex: 1000
    },
    calendar: {
        minimumDate: new Date()
    }
});

You can of course also set the maximumDate, but I didn't need that for this code. One other thing I learned while trying to achieve this is that you cannot use <input type="date"> for the Alloy UI datepicker.

Setting the starting value of the end date based on the inputted value of the start date

My next quest was to set the minimum end date based on the start date the user had entered, which was the point where I got stuck before.

My first attempt was based on a sample I found online:

var toDatePicker;
var fromDatePicker = new A.DatePicker({
    trigger: '#startDate',
    mask: '%d-%m-%Y',
    popover: {
        zIndex: 1000
    },
    on: {
        selectionChange: function(event) {
            if (event.newSelection[0]) {
                toDatePicker.getCalendar().set('minimumDate', 
                    event.newSelection[0]);
            }
        }
    },
    calendar: {
        minimumDate: new Date()
    }
});


toDatePicker = new A.DatePicker({
    trigger: '#endDate',
    mask: '%d-%m-%Y',
    popover: {
        zIndex: 1000
    },
    calendar: {
        minimumDate: new Date()
    }
});

More generic approach?

As you can see, I switched to triggering on id's here, as I now wanted to target individual fields instead of all date fields. This code works nicely, but I had hoped to write a more generic bit of code, as it's once again getting long, and I would have to duplicate it for the CFP date fields. I tried to achieve this by leaving out the 'on' bit and adding it later with:

var fromDate = A.one('#startDate');
var toDate = A.one('#endDate');


fromDate.on('selectionChange', function(event) {
    if (event.newSelection[0]) {
        toDate.getCalendar().set('minimumDate', event.newSelection[0]);
    }
});

However, for some reason I couldn't get this to work--the event handler just wasn't called at all--so I switched back to code I had before.

Update value as well as minimum date

The next problem I tackled was that, because I had initialized the value of the date fields, the datepicker would still show the current date, even if that was no longer a valid option. What I really wanted was for the datepicker to display the minimum end date as its initial date even when I changed it. After a bit of puzzling, I came up with the following:

<aui:script>
    AUI().use(
        'aui-datepicker', 'datatype-date',
        function(A) {
            var fromDatePicker = new A.DatePicker({
                trigger: '#startDate',
                mask: '%d-%m-%Y',
                popover: {
                    zIndex: 1000
                },
                on: {
                    selectionChange: function(event) {
                        if (event.newSelection[0]) {
                            toDatePicker.getCalendar().set('minimumDate', 
                                event.newSelection[0]);
                            document.getElementById("endDate").value = 
                                A.Date.format(event.newSelection[0], 
                                    {format: "%d-%m-%Y"});
                        }
                    }
                },
                calendar: {
                    minimumDate: new Date()
                }
            });
        }
    );
</aui:script>

Beware: you have to add the datatype-date library in order to use the Date.format method!

Compare date values

I quickly found out that picking a new start date that was before the end date would also change the end date to the start date. While it was fine to change the minimum date in that case, it was of course not fine to change the selected value as well. So I changed the selectionChange function to the following:

selectionChange: function(event) {
    if (event.newSelection[0]) {
        var oldToValue = 
            A.Date.parse("%d-%m-%Y", document.getElementById("endDate").value);
        toDatePicker.getCalendar().set('minimumDate', event.newSelection[0]);
        if (A.Date.isGreater(event.newSelection[0], oldToValue)) {
            document.getElementById("endDate").value = 
                A.Date.format(event.newSelection[0], {format: "%d-%m-%Y"});
        }
    }
}

For brevity's sake, I haven't added the surrounding code, which does mean that a little bit of important information is now missing. In order to use A.Date.parse(), you have to add the datatype-date-parse library, and in order to use A.Date.isGreater() you have to add the datatype-date-math library. Both of these go in the same line where we added the datatype-date library.

Namespacing!

At this point I was more or less satisfied with the behavior of the frontend and I went and tested it with the backend (in other words, I tried saving the data). To my surprise, the dates didn't come through in the backend. I first thought this was because I had forgotten to change the formatter I was using in the backend to parse the data with the same mask I used in the frontend. But when I changed that, it still didn't work. So, aided by the debugger, I delved into the code to find out what was wrong. Old hands at Liferay have probably long since spotted the error: I had forgotten that while fields do not need namespacing, regular HTML fields do need it.

Final code

So here is my final version of the code. I've left out all of the non-date fields and the CFP fields, as the code snippet is long enough without them.

<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %>
<%@ taglib uri="http://liferay.com/tld/portlet" prefix="liferay-portlet" %>


<%@ page import="java.text.SimpleDateFormat"%>
<%@ page import="java.util.Date" %>
<%@ page import="com.liferay.counter.kernel.service.CounterLocalServiceUtil" %>
<%@ page import="org.jduchess.event.service.model.Event" %>
<%@ page import="org.jduchess.event.service.service.EventLocalServiceUtil" %>


<liferay-theme:defineObjects />
<portlet:defineObjects />


<liferay-portlet:actionURL name="addEvent" var="editAction">
    <portlet:param name="mvcActionCommand" value="addEvent" />
</liferay-portlet:actionURL>


<%
    Date defaultDate = new Date();
    Event selectedEvent = (Event)request.getAttribute("event");


    if(selectedEvent == null) {
        // create an event, so it's not null
        selectedEvent = EventLocalServiceUtil.createEvent(
            CounterLocalServiceUtil.increment(Event.class.getName()));
    }


    Date startDate, endDate;
    SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");


    if(selectedEvent.getStartDate() != null) {
        startDate = selectedEvent.getStartDate();
    } else {
        startDate = defaultDate;
    }


    if(selectedEvent.getEndDate() != null) {
        endDate = selectedEvent.getEndDate();
    } else {
        endDate = defaultDate;
    }
%>


<aui:form action="<%=editAction%>" method="post" name="fm">
    <aui:fieldset>
        <label class="control-label" for="<portlet:namespace />startDate">
            Start date
        </label>
        <input name="<portlet:namespace />startDate" 
            id="<portlet:namespace />startDate" 
            class="form-control date" 
            type="text" placeholder="dd-mm-yyyy" 
            value="<%=formatter.format(startDate)%>">


        <span id="endDateShown">
            <label class="control-label" for="<portlet:namespace />endDate">
                End date
            </label>
            <input name="<portlet:namespace />endDate" 
                id="<portlet:namespace />endDate" 
                class="form-control date" 
                type="text" 
                placeholder="dd-mm-yyyy" 
                value="<%=formatter.format(endDate)%>">
        </span>
        <label class="control-label" for="<portlet:namespace/>toggleEndDate">
            <input type="checkbox" id="endDateToggle" onclick="toggleEndDate()"/>
            Single day event
        </label>
        <aui:button type="submit"></aui:button>
    </aui:fieldset>
</aui:form>


<script type="text/javascript">
    // This function allows me to turn the end date on and off 
    // based on whether the user clicks the checkbox
    function toggleEndDate() {
        if (document.getElementById('endDateToggle').checked) {
            document.getElementById('endDateShown').style.display = 'none';
            document.getElementById('<portlet:namespace/>endDate').disabled = 
                "true";
        } else {
            document.getElementById('endDateShown').style.display = 'block';
            // for some reason you shouldn't set disabled to false, 
            // but to an empty string instead...
            document.getElementById('<portlet:namespace/>endDate').disabled = "";
        }
    }
</script>
<aui:script>
    AUI().use(
        'aui-datepicker', 
        'datatype-date', 
        'datatype-date-math', 
        'datatype-date-parse',
        function(A) {
            var toDatePicker;
            var fromDatePicker = new A.DatePicker({
                trigger: '#<portlet:namespace />startDate',
                mask: '%d-%m-%Y',
                popover: {
                    zIndex: 1000
                },
                on: {
                    selectionChange: function(event) {
                        if (event.newSelection[0]) {
                            var oldToValue = A.Date.parse("%d-%m-%Y", 
                                document.getElementById(
                                    "<portlet:namespace />endDate").value);
                            toDatePicker.getCalendar().set('minimumDate', 
                                event.newSelection[0]);
                            if (A.Date.isGreater(event.newSelection[0], 
                                    oldToValue)) {
                                document.getElementById(
                                    "<portlet:namespace />endDate").value = 
                                    A.Date.format(event.newSelection[0], 
                                        {format: "%d-%m-%Y"});
                            }
                        }
                    }
                },
                calendar: {
                    minimumDate: new Date()
                }
            });


            toDatePicker = new A.DatePicker({
                trigger: '#<portlet:namespace />endDate',
                mask: '%d-%m-%Y',
                popover: {
                    zIndex: 1000
                },
                calendar: {
                    minimumDate: new Date()
                }
            });
        }
    );
  </aui:script>

Conclusion

I hope the lessons I learned along the way were useful to you too, and that you won't have to struggle quite as much as I did to accomplish what is such a seemingly simple challenge.

Meer weten

Geert van der Ploeg

Solution Architect[email protected]LinkedIn
Meer van Geert van der Ploeg

Meer artikelen

liferay
dxp
freemarker
fragments
webcontent

Using Liferay Fragments as modular webcontent templates

Danielle Ardon
9 minuten lezen
Lees het artikel Using Liferay Fragments as modular webcontent templates
liferay
dxp
theme
inheritance

Liferay themes with a custom base theme

Geert van der Ploeg
6 minuten lezen
Lees het artikel Liferay themes with a custom base theme
liferay
freemarker
dxp

Preferences for embedded portlets in Freemarker

Geert van der Ploeg
4 minuten lezen
Lees het artikel Preferences for embedded portlets in Freemarker