TMS Web Core and Flatpickr

Flatpickr is a third-party library that provides a pretty good datetime picker. This post describes a bit about my (positive!) experiences using it in a TMS Web Core project.

Motivation: I was looking for a solid date picker that could be easily themed entirely through CSS (no canvas elements please!) that supported a number of basic things like week numbers and range selection. Bonus if a time picker could also be part of the mix but I didn't end up using that anyway.

Flatpickr came up in my searches as a pretty decent and well-supported package and also plays very well with the other third-party JS libraries in my project, most notably providing date picker functionality for Tabulator. Getting started is easy enough, either by adding it to your project like any other JS library or referencing it via a CDN by adding Project.html entries, my favourite approach of late:

<script src="https://cdn.jsdelivr.net/npm/flatpickr@latest/dist/flatpickr.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@latest/dist/flatpickr.min.css">

The first thing I wanted was an "open" calendar-type display on a page. Flatpickr refers to this as an "inline" date picker. Dropping a TWebHTMLDiv component on the form and setting the ElementID property to "calendar", adding the Flatpickr control looks like this. The date entry in this case is limited to between the first of the current month and the last date of the month four months from now.

asm
  var date = new Date();
  var minDate = new Date(date.getFullYear(), date.getMonth(), 1);
  var maxDate = new Date(date.getFullYear(), date.getMonth() + 4, 0);

  this.Calendar = flatpickr("#calendar", {
    inline: true,
    minDate: minDate,
    maxDate: maxDate,
    weekNumbers: true,
  });
end;

If you want to further limit the date picker to have only certain dates enabled, you can adjust like this:

this.Calendar = flatpickr("#calendar", {
    inline: true,
    minDate: minDate,
    maxDate: maxDate,
    weekNumbers: true,
    enable: []
  });

And then populate enable with a list of dates, typically in YYY-MM-DD format but other options are available. For example, I use an XData service to retrieve a list of available dates from a database. Then I populate the Flatpickr controls' enable value with something along the lines of the following:

  DateList := TSTringList.Create;
  DateList.Sorted := True;
  DateList.Duplicates := dupIgnore;

  while not(DateDS.EOF) do
  begin
    DateList.Add(FormatDateTime('yyyy-mm-dd',DateDS.FieldByName('AVAILDATE').AsDateTime));
    DateDS.Next;
  end;

  i := 0;
  DateString := '[';
  while i < DateList.Count do
  begin
    DateString := DateString+'"'+Copy(DateList[i],1,10)+'",';
    i := i + 1;
  end;
  DateString := Copy(DateString,1,Length(DateString)-1);
  DateString := DateString + ']';

  asm 
    var obj = JSON.parse(DateString);
    this.Calendar.set('enable', obj);
  end;

I'm sure there are prettier ways to go from the dataset values to the JSON string, but hey, it works without issue. You can see what it looks like live at the project site: https://www.everydayipm.com.

To use Flatpickr as the date picker in a Tabulator date column, you can add this JS code before the Tabulator table definition, and in the date column just add editor:dateEditor as one of the properties:

  asm 
    var dateEditor = (cell, onRendered, success, cancel, editorParams) => {
      var editor = document.createElement("input");
      editor.value = cell.getValue();

      var date = new Date();
      var minDate = new Date(date.getFullYear(), date.getMonth(), 1);
      var maxDate = new Date(date.getFullYear(), date.getMonth() + 4, 0);

      var datepicker = flatpickr(editor, {
        minDate: minDate,
        maxDate: maxDate,
        weekNumbers: true,
        onClose: (selectedDates, dateStr, instance) => {
          success(dateStr);
          instance.destroy();
        },
      });
      onRendered(() => {
        editor.focus();
       });
      return editor;
    };

    this.Availability = new Tabulator("#Availability", {
      columns:[
        {title: "Date",        field: "AVAILDATE",   width:     80, editor: dateEditor,  bottomCalc: "count" }
      ]});
end;

Finally, if you're looking to select a range of dates, you can add mode: "range" to the datepicker definition and the user can then select a range of dates at one time. It is a little confusing in how this works in combination with enabled dates, but worth checking out if this is of interest.

Styling for the date control (either popup or inline) can be handled entirely through CSS and there is an insane amount of control available over how everything looks. Quite happy about that!

There are a number of other features that are also available that I don't use personally. These include things like a time picker, highlighting dates (seperately from making them active/inactive), working with multiple date pickers at once, and so on.

And of course there is an onChange callback which can call a Delphi procedure when the user selects a date. Or the date (or range) can be retrieved from the control when the user clicks on a button or something. Working with ranges was a bit tricky, but this is what I ended up with for my particular type of date input:

asm
  var date = new Date();
  var minDate = new Date(date.getFullYear(), date.getMonth(), 1);
  var maxDate = new Date(date.getFullYear(), date.getMonth() + 4, 0);

  this.Calendar = flatpickr("#AvailabilityRange", {
    inline: true,
    mode: "range",
    minDate: minDate,
    maxDate: maxDate,
    weekNumbers: true,
    onChange: function(selectedDates, dateStr, instance) {
        var day1 = '';
        var day2 = '';
        if (selectedDates.length == 1) {
          day1 = selectedDates[0].toLocaleDateString('en-CA');
          day2 = selectedDates[0].toLocaleDateString('en-CA');
        } else if (selectedDates.length == 2) {
          day1 = selectedDates[0].toLocaleDateString('en-CA');
          day2 = selectedDates[1].toLocaleDateString('en-CA');
        };
        pas.CoreDataModule.DM1.DateChanged(day1, day2);
    }
  });
end;

So in this example, a procedure called DateChanged is called in my WebDataModule called DM1 with string parameters of the start/end date selected. Works pretty well.

Happy to answer any questions or would also love to hear about what others have used as date pickers in their projects?

2 Likes

That is very cool!
Maybe this is something to package and publish via
https://www.tmssoftware.com/site/webpartners.asp
?