Monday, 12 July 2021

Web Tutorial: The JS Datepicker (Part 1/4)

Date inputs on the web are important where data entry is concerned. Not only does proper date formatting need to be observed, the user interface needs to be as idiot-proof as possible. These days, it's all easily doable via an increasingly wide array of datepickers that are available in libraries like Semantic UI and jQueryUI.

However, back in my day, this was a whole other proposition altogether. In fact, I first implemented a JavaScript datepicker by copying calendar widget code from some Indian blog, an act which caused the level of my JavaScript to soar once I understood how it was done. And today, I will be taking you through the same process.

It all begins with some HTML, and a simple span element for which we will provide an id of cal1. This will remain invisible for now, since we haven't placed any content within it.
<!DOCTYPE html>
<html>
    <head>
        <title>Calendar</title>

        <style>

        </style>

        <script>

        </script>
    </head>

    <body>
        <span id="cal1"></span>
    </body>
</html>


But this is where it escalates, because beyond that bit of HTML, we go right into the JavaScript. For this, we will need a JavaScript object. It will be called calendar. The first property, container, is set to null. This will later on hold the object cal1.
<script>
    var calendar =
    {
        container: null,
    }

</script>


Next is selectedDate. This will be the date that defines what values your datepicker is going to display.
<script>
    var calendar =
    {
        container: null,
        selectedDate: null,
    }
</script>


textbox is what the user will see initially. hiddenTextbox will hold the actual value that your widget will send to the server. btnDisplay, when clicked, shows or hides all other controls.
<script>
    var calendar =
    {
        container: null,
        selectedDate: null,
        textbox: undefined,
        hiddenTextbox: undefined,
        btnDisplay: undefined,

    }
</script>


monthPicker is the drop-down list we will create later, to hold all the months. datePickers is an array of all the current days in the selected month. daysContainer is the div that will hold the display of all the days.
<script>
    var calendar =
    {
        container: null,
        selectedDate: null,
        textbox: undefined,
        hiddenTextbox: undefined,
        btnDisplay: undefined,
        monthPicker: undefined,
        datePickers: undefined,
        daysContainer: undefined,

    }
</script>


days and months are arrays we can populate right away - with all possible values of weekdays and month names respectively.
<script>
    var calendar =
    {
        container: null,
        selectedDate: null,
        textbox: undefined,
        hiddenTextbox: undefined,
        btnDisplay: undefined,
        monthPicker: undefined,
        datePickers: undefined,
        daysContainer: undefined,
        days: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
        months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],

    }
</script>


Next, we have initialize() as a method. This is the method that transforms the span element into a badass datepicker! It will accept a string as a parameter. This will be the id of the span element we want to change into a datepicker.
var calendar =
{
    container: null,
    selectedDate: null,
    textbox: undefined,
    hiddenTextbox: undefined,
    btnDisplay: undefined,
    monthPicker: undefined,
    datePickers: undefined,
    daysContainer: undefined,
    days: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
    months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
    initialize: function(id)
    {

    },

}


Before we go further, we should add a script tag at the end of the body tag. In it, call initialize() and pass in "cal1" as an argument. So this will transform cal1.
<body>
    <span id="cal1"></span>

    <script>
        calendar.initialize("cal1");
    </script>

</body>


In here, set selectedDate to today's date.
initialize: function(id)
{
    this.selectedDate = new Date();
},


Then set container to the object pointed to by id.
initialize: function(id)
{
    this.selectedDate = new Date();
    this.container = document.getElementById(id);
},


Add the CSS classes calenderContainer and hide to container.
initialize: function(id)
{
    this.selectedDate = new Date();
    this.container = document.getElementById(id);
    this.container.classList.add("calendarContainer");
    this.container.classList.add("hide");

},


And now, let's add the CSS class calendarContainer. Here, we set the display property to inline-block so that we can set width and height. Then we add font properties. And a blue outline, just for visibility.
<style>
    .calendarContainer
    {
        width: 350px;
        height: 30px;
        font-size: 12px;
        font-family: arial;
        display: inline-block;
        outline: 1px solid blue;
    }

</style>


Here, you see the blue outline denoting your span element.


Then we add the CSS class hide. For this, the overflow property is set to hidden. This will not hide your date field, but it will hide all the other controls in the datepicker.
<style>
    .calendarContainer
    {
        width: 350px;
        height: 30px;
        font-size: 12px;
        font-family: arial;
        display: inline-block;
        outline: 1px solid blue;
    }

    .hide
    {
        overflow: hidden;
    }

</style>


Back to the initialize() method. Use the createElement() method of the document object to create two divs, top and bottom. Add CSS classes top and bottom respectively.
initialize: function(id)
{
    this.selectedDate = new Date();
    this.container = document.getElementById(id);
    this.container.classList.add("calendarContainer");
    this.container.classList.add("hide");

    var top = document.createElement("div");
    top.classList.add("top");

    var bottom = document.createElement("div");
    bottom.classList.add("bottom");

},


Then append top and bottom to container.
var top = document.createElement("div");
top.classList.add("top");

var bottom = document.createElement("div");
bottom.classList.add("bottom");

this.container.appendChild(top);
this.container.appendChild(bottom);


You still won't see anything yet. We will need to add the CSS classes. Here, you'll see that widths and heights have been defined for top and bottom. For bottom, I've even made the background color grey and added round corners.
<style>
    .calendarContainer
    {
        width: 350px;
        height: 30px;
        font-size: 12px;
        font-family: arial;
        display: inline-block;
        outline: 1px solid blue;
    }

    .hide
    {
        overflow: hidden;
    }

    .calendarContainer .top
    {
        width: 100%;
        height: 30px;
    }


    .calendarContainer .bottom
    {
        width: 100%;
        height: 200px;
        border: 1px solid rgba(200, 200, 200, 1);
        border-radius: 5px;
        padding: 2px 0 2px 0;
        background-color: rgba(250, 250, 250, 1);
    }

</style>


Let's temporarily disable this line so we can see what's been added.
initialize: function(id)
{
    this.selectedDate = new Date();
    this.container = document.getElementById(id);
    this.container.classList.add("calendarContainer");
    //this.container.classList.add("hide");

    var top = document.createElement("div");
    top.classList.add("top");

    var bottom = document.createElement("div");
    bottom.classList.add("bottom");
},


Here, you'll see that bottom is the conspicuous grey box. Where's top, then? Well, top is currently occupying that space within the blue box. Remember that the overflow property is no longer set to hidden (due to us commenting away the line that adds the hide CSS class); so now that container contains both top and bottom, since they both have 100% width, bottom naturally goes below top.


Next, we create an input element, give it an id based on "cal1", and then append it to top. It will be read-only, because it's meant to look like a text input, but without actually functioning like one.
this.container.appendChild(top);
this.container.appendChild(bottom);

var textbox = document.createElement("input");
textbox.type = "text";
textbox.readOnly = true;
textbox.id = "txt_" + id;
top.appendChild(textbox);


There, this is the textbox we just added.


Here, we do something similar with a newly-created div element, btnDisplay. This is supposed to be the button that hides and displays your controls.
var textbox = document.createElement("input");
textbox.type = "text";
textbox.readOnly = true;
textbox.id = "txt_" + id;
top.appendChild(textbox);

var btnDisplay = document.createElement("div");
btnDisplay.classList.add("button");
btnDisplay.classList.add("spaceleft");
btnDisplay.id = "btndisplay_" + id;
btnDisplay.innerHTML = "&#9660;";
top.appendChild(btnDisplay);


Here, you can see the tiny down arrow icon we just added. This all needs styling, and that's what we will do next. Note that the CSS class for this is button, and one called spaceleft.


Here, we style the textbox, giving it a width and height and rounded corners. More importantly, display is set to inline-block so it functions as a block element, and in turn allows us to use the float property.
.calendarContainer .bottom
{
    width: 100%;
    height: 200px;
    border: 1px solid rgba(200, 200, 200, 1);
    border-radius: 5px;
    padding: 2px 0 2px 0;
    background-color: rgba(250, 250, 250, 1);
}

.calendarContainer input[type=text]
{
    width: 310px;
    height: 20px;
    border-radius: 5px;
    display: inline-block;
    float: left;
}


There, now that we styled the textbox, it's longer and fancier. And the float property ensures that the div element btnDisplay now lines up nicely next to it instead of under it.


Now, here's some styling for the CSS class button. This is a generic class for all the buttons we will be incorporating into the widget. I've made them square with rounded corners, a grey border and an even lighter grey background, with the background darkening upon a mouseover. Get creative!
.calendarContainer input[type=text]
{
    width: 310px;
    height: 20px;
    border-radius: 5px;
    display: inline-block;
    float: left;
}

.calendarContainer .button
{
    width: 23px;
    height: 23px;
    border: 1px solid rgba(200, 200, 200, 1);
    border-radius: 5px;
    text-align: center;
    cursor: pointer;
    float: left;
    background-color: rgba(255, 255, 255, 1);
    color: rgba(0, 0, 0, 1);
    font-size: 14px;
}

.calendarContainer .button:hover
{
    background-color: rgba(200, 200, 200, 1);
}


There's your nicely styled button.


Now we define the spaceleft CSS class. It basically sets the margin-left property so that there's a bit of space to the left. Nothing much to it!
.calendarContainer .button:hover
{
    background-color: rgba(200, 200, 200, 1);
}

.spaceleft
{
    margin-left: 2px;
}


There you can see that tiny space.


All right, time for the next bit!

We need to place more controls in there. Here, we do what we did for btnDisplay, except we append it to bottom. This one is btnMonthDec, and it will be used to decrement months. For the icon, we use ◀.
var btnDisplay = document.createElement("div");
btnDisplay.classList.add("button");
btnDisplay.classList.add("spaceleft");
btnDisplay.id = "btndisplay_" + id;
btnDisplay.innerHTML = "&#9660;";
top.appendChild(btnDisplay);

var btnMonthDec = document.createElement("div");
btnMonthDec.classList.add("button");
btnMonthDec.classList.add("spaceleft");
btnMonthDec.innerHTML = "&#9664;";
bottom.appendChild(btnMonthDec);


Now you see, we've already created the styling. So it renders nicely.


Next up is the month selector. For this, we create a select element, and style it using spaceleft. As with all the other elements so far, we give it an id based on "cal1".
var btnMonthDec = document.createElement("div");
btnMonthDec.classList.add("button");
btnMonthDec.classList.add("spaceleft");
btnMonthDec.innerHTML = "&#9664;";
bottom.appendChild(btnMonthDec);

var monthPicker = document.createElement("select");
monthPicker.classList.add("spaceleft");
monthPicker.id = "monthpicker_" + id;


Remember the months array? Now we get to use it. Iterate through it and create a series of option elements that will be placed within monthPicker.
var monthPicker = document.createElement("select");
monthPicker.classList.add("spaceleft");
monthPicker.id = "monthpicker_" + id;

for (var i = 0; i < this.months.length; i++)
{
    var option = document.createElement("option");
    option.value = i + 1;
    option.innerHTML = this.months[i];
    monthPicker.appendChild(option);
}


And then append monthPicker to bottom.
var monthPicker = document.createElement("select");
monthPicker.classList.add("spaceleft");
monthPicker.id = "monthpicker_" + id;

for (var i = 0; i < this.months.length; i++)
{
    var option = document.createElement("option");
    option.value = i + 1;
    option.innerHTML = this.months[i];
    monthPicker.appendChild(option);
}

bottom.appendChild(monthPicker);


And here's the drop-down list of months.


Pause a moment here to style this. We will adjust the width, height and font size, and give rounded corners. display will be set to inline-block and float to left.
.calendarContainer .button:hover
{
    background-color: rgba(200, 200, 200, 1);
}

.calendarContainer select
{
    width: 100px;
    height: 24px;
    border-radius: 5px;
    display: inline-block;
    float: left;
    font-size: 1.6em;
}


.spaceleft
{
    margin-left: 2px;
}


Looking nicer now!


Now it's time for btnMonthInc, the element that does the opposite of btnMonthDec. It will use ▶ as an icon.
bottom.appendChild(monthPicker);

var btnMonthInc = document.createElement("div");
btnMonthInc.classList.add("button");
btnMonthInc.classList.add("spaceleft");
btnMonthInc.innerHTML = "&#9654;";
bottom.appendChild(btnMonthInc);


Taking shape here...


Next, we do another series of buttons. This time, it's for the year. This one is btnYearDecTen. This decrements the year by 10. Notice that, this time, we style it using spaceleft_large. We'll use the empty triangle left pointer ◁.
var btnMonthInc = document.createElement("div");
btnMonthInc.classList.add("button");
btnMonthInc.classList.add("spaceleft");
btnMonthInc.innerHTML = "&#9654;";
bottom.appendChild(btnMonthInc);

var btnYearDecTen = document.createElement("div");
btnYearDecTen.classList.add("button");
btnYearDecTen.classList.add("spaceleft_large");
btnYearDecTen.innerHTML = "&#9665;";
bottom.appendChild(btnYearDecTen);


It's like spaceleft, but with a much larger margin.
.spaceleft
{
    margin-left: 2px;
}

.spaceleft_large
{
    margin-left: 5px;
}


Great stuff!


Now for a "normal" increment button, which will decrement the year by 1. This uses spaceleft.
var btnYearDecTen = document.createElement("div");
btnYearDecTen.classList.add("button");
btnYearDecTen.classList.add("spaceleft_large");
btnYearDecTen.innerHTML = "&#9665;";
bottom.appendChild(btnYearDecTen);

var btnYearDec = document.createElement("div");
btnYearDec.classList.add("button");
btnYearDec.classList.add("spaceleft");
btnYearDec.innerHTML = "&#9664;";
bottom.appendChild(btnYearDec);


Yep. No real surprises, there.


And then we display the year. This is not a control because the user is not supposed to interact directly with it. It's more of a label, and therefore we call it lblYear, and even have a CSS class named the same. We will set its contents to the year potion of selectedDate.
var btnYearDec = document.createElement("div");
btnYearDec.classList.add("button");
btnYearDec.classList.add("spaceleft");
btnYearDec.innerHTML = "&#9664;";
bottom.appendChild(btnYearDec);

var lblYear = document.createElement("div");
lblYear.classList.add("lblYear");
lblYear.classList.add("spaceleft");
lblYear.id = "lblyear_" + id;
lblYear.innerHTML = this.selectedDate.getFullYear();
bottom.appendChild(lblYear);


Not looking great, that's for sure, but nothing a little styling won't cure.


Here we style lblYear by setting the width, height, text alignment and font size. We'll also float left to be consistent.
.calendarContainer select
{
    width: 100px;
    height: 24px;
    border-radius: 5px;
    display: inline-block;
    float: left;
    font-size: 1.6em;
}

.calendarContainer .lblYear
{
    width: 80px;
    height: 24px;
    text-align: center;
    float: left;
    font-size: 1.6em;
}


.spaceleft
{
    margin-left: 2px;
}


Better... much better.


For the last two buttons, I think you should pretty much get the idea by now, so I won't belabor the point.
var lblYear = document.createElement("div");
lblYear.classList.add("lblYear");
lblYear.classList.add("spaceleft");
lblYear.id = "lblyear_" + id;
lblYear.innerHTML = this.selectedDate.getFullYear();
bottom.appendChild(lblYear);

var btnYearInc = document.createElement("div");
btnYearInc.classList.add("button");
btnYearInc.classList.add("spaceleft");
btnYearInc.innerHTML = "&#9654;";
bottom.appendChild(btnYearInc);

var btnYearIncTen = document.createElement("div");
btnYearIncTen.classList.add("button");
btnYearIncTen.classList.add("spaceleft");
btnYearIncTen.innerHTML = "&#9655;";
bottom.appendChild(btnYearIncTen);


Give yourself a pat on the back. You've made through Part 1!


This widget isn't looking at all complete. A datepicker can be a bit of a tricky business, so stay tuned for more.

Next

Displaying days in the current month.

No comments:

Post a Comment