Populated select boxes for date

Ok, with you so far.

I’m home now, back from work, so I can get into a little more details, just in case.

The constructor function for the class is not doing anything at the moment. This will change, but right now I’m laying down the basics from which I start. It’s always good to start from small.

New objects will be based on the new MyDate class. There are things that are the same for every instance, every object. Those things belong in the class, the prototype object in this case, they don’t need to be duplicated for every new object created. As an aside, that’s one drawback of the module pattern: the code logic is duplicated for every other object based on it. We end up with a lot of redundant code logic, eating up memory.

We call some of these common properties “constants”. In current mainstream JavaScript, there is no constant concept implementation, so we declare them using all caps, merely a style convention.

Some other common properties present in my class so far are, of course, the class methods.

One thing to notice about these methods, is that getting the current date is no longer a one time thing upon object initialization. This is wrong. What if, between loading the page and the time the user actually uses the widget, the current date has changed?

The other method, like the today method, has no ties with outside systems (DOM) so I just copied it from your original code.

If there are no questions so far, I’ll continue in my next post with building the class.

Good stuff.
I’m always surprised that you can post so much from work.
You must have an understanding boss.

Ok.
It’s nice to see how you would approach this.
I basically started with a load of boilerplate and adapted it to suit.

Got it.

Same notation as Ruby, so seems familiar.

I don’t see that particular scenario as overly problematic, but ok.

No, that’s all quite clear.
That’s not to say that I would have approached the problem like this (I wouldn’t have), but I understand what you have done and why you have done it.
Where it’s going to get interesting is when it comes to tying this in to DOM elements.

Thanks for taking the time to improve upon my solution!

So what is the problem if it is working?

In the previous post I created an instance of MyDate class: date. So far, everything resides in the common prototype. But normally, every Mydate instance should have a different state to make it distinct, a state which should not be kept in the prototype, otherwise it would be the same for every date object created.

The first thing that is specific to each MyDate instance is the HTML element it targets. I will create a targetElement property to hold this reference for every instance independently.

date.html


<!doctype html>
  <html>
  <head>
    <meta charset="utf-8">
    <title>Date widget</title>
  </head>

  <body>
    <div class="dateDropdown">
      <label for="startDate">Please enter the start date:</label>
      <input id="startDate" type="text" placeholder="dd.mm.yyyy"/>
    </div>

    <div class="dateDropdown">
      <label for="endDate">Please enter the end date:</label>
      <input id="endDate" type="text" placeholder="dd.mm.yyyy"/>
    </div>
    
    <script src="date.js"></script>
    <script src="demo.js"></script>
  </body>
</html>

date.js


function MyDate(id) {
    this.targetElement = document.getElementById(id);
};

MyDate.prototype = {
  
    MONTHS: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sept','Oct','Nov','Dec'],
  
    YEAR_OFFSET: 21,
  
    getToday: function () {
        var today = new Date();
      
        return {
            day: today.getUTCDate(),
            month: today.getUTCMonth(),
            year: today.getUTCFullYear()
        }; 
    },
  
    getDaysInMonth: function (month, year) {
        return new Date(year, month, 0).getDate();
    },
    
    toggle: function () {
        var elemStyle = this.targetElement.style;
        
        if (elemStyle.display === "none") {
            elemStyle.display = "";
        } else {
            elemStyle.display = "none";
        };
    }
  
};

demo.js


var sdat = new MyDate("startDate"),
    edat = new MyDate("endDate");

sdat.toggle();

Now the constructor function is finally doing something useful. It receives an id and it saves a reference to the target element.

The toggle method is common to all MyDate instances so it goes in the common pot, the prototype. The only hasOwnProperty sdat and edat have is targetElement.

This will make it the only property which can be different from instance to instance. sdat’s targetElement is input#startDate, edat’s targetElement is input#endDate.

This way it’s possible to independently toggle every targeted HTML element, using a unique and common class method.

Or I must be doing my job well, which keeps my boss off my back :wink:

Actually, this thread displays two approaches that probably depend, as far as I can tell, on the way one enters JavaScript world, and webdev world for that matter.

Pullo’s approach is of a programmer that treats JavaScript as something which is HTML centric. It took JavaScript on because it had to, but it doesn’t see JavaScript as a programming language, outside HTML and DOM.

I give JavaScript more of a “classic” programming language approach, which only intersects HTML and DOM from time to time, when it absolutely needs to.

Pullo, I’m sorry for making assumptions like this. :slight_smile: Please correct me if I’m wrong.

No problem, just an opportunity :slight_smile:
The solution I gave you will work well and is relatively robust.
But if you follow this thread, the chances are you will end up with something even better.

No, that’s quite correct.
I got into web design first and programming second (and unfortunately I studied neither :))
I’m very aware of the increasing omnipotence of JavaScript and I’m eager to learn more about the “classic” programming language approach.

Hi myty,

I’ve read through your post and I’ve understood everything this far.
It’s interesting to see that path this is taking and that you prefer to initialize each widget separately.
Looking forward to the next part :slight_smile:

Hey Pullo. :slight_smile:

Just for fun, run this in your console, where you opened my last example:


console.dir(window.document.childNodes[1].childNodes[2].childNodes[1].childNodes[3])

It displays the input native “widget” object. Inspect it using the tree arrows.

Looking at what is created based on the markup, I consider this to be the normal approach. Each widget should be an object, like it naturally is in JavaScript.

This is the prototype chain for the native input “widget”, the one you get by writing <input…>:

HTMLInputElement -> HTMLElement -> Element -> Node -> EventTarget -> Object.

Looking at the tree, you’ll see that every input object/HTML element has it’s own set of properties, like className, defaultValue, outerHTML, style, etcetera, but it inherits functionality from up the prototype chain. It may be a scary looking tree, but that’s the gist of it.

Following the same pattern, MyDate widget will fit right in. And this is what I’m doing.

I think you nailed it when you said that I treat JavaScript as something which is HTML-centric - (which I do / have done).

I’m familiar with the concepts of classes, inheritance, polymorphism etc from the world of Ruby.

E.g.

class Parent
  def parent_method
    puts "Parent method"
  end
end

class Child < Parent
  def child_method
    puts "Child method"
  end
end

p = Parent.new
p.parent_method
=> Parent method

c = Child.new
c.child_method
c.parent_method
=> "Child method"
=> "Parent method"

p Parent.ancestors
p Child.ancestors

=> [Parent, Object, Kernel, BasicObject]
=> [Child, Parent, Object, Kernel, BasicObject]

But I hadn’t really thought about applying this to JS, as my day to day work doesn’t really require me to do more than manipulate the DOM.

Next step is adding date helper elements. The target input text element will be hidden and three new select elements will help the user choose the date.

I’ve added a HELPER_ELEMENTS_HTML_FRAGMENT constant and a addHelperElements method. Now I can create an adjacent fieldset after the target element, with the helper select elements for day, month and year.

date.html


<!doctype html>
  <html>
  <head>
    <meta charset="utf-8">
    <title>Date widget</title>
  </head>

  <body>
    <div class="dateDropdown">
      <label for="startDate">Please enter the start date:</label>
      <input id="startDate" type="text" placeholder="dd.mm.yyyy"/>
    </div>

    <div class="dateDropdown">
      <label for="endDate">Please enter the end date:</label>
      <input id="endDate" type="text" placeholder="dd.mm.yyyy"/>
    </div>
    
    <script src="date.js"></script>
    <script src="demo.js"></script>
  </body>
</html>

date.js


function MyDate(id) {
    this.targetElement = document.getElementById(id);
};

MyDate.prototype = {
  
    MONTHS: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sept','Oct','Nov','Dec'],
  
    YEAR_OFFSET: 21,

    HELPER_ELEMENTS_HTML_FRAGMENT: '<fieldset class="mydate"><select class="day"></select><select class="month"></select><select class="year"></select></fieldset>',
  
    getToday: function () {
        var today = new Date();
      
        return {
            day: today.getUTCDate(),
            month: today.getUTCMonth(),
            year: today.getUTCFullYear()
        }; 
    },
  
    getDaysInMonth: function (month, year) {
        return new Date(year, month, 0).getDate();
    },
    
    toggle: function () {
        var elemStyle = this.targetElement.style;
        
        if (elemStyle.display === "none") {
            elemStyle.display = "";
        } else {
            elemStyle.display = "none";
        };
    },

    addHelperElements: function() {
          this.targetElement.insertAdjacentHTML('afterend', this.HELPER_ELEMENTS_HTML_FRAGMENT);
    }
};

demo.js


var sdat = new MyDate("startDate"),
    edat = new MyDate("endDate");

sdat.toggle();
sdat.addHelperElements();

Ok, neat!

I’m always wary of creating strings of HTML and then inserting them in the DOM having heard that this is “bad practice”.
Is there any reason that you are doing this, as opposed to creating a fieldset and three select elements (using document.createElement) and inserting them?

I do actually prefer your approach as it is considerably less verbose.

It depends on how you insert them in the DOM, rather than what it is that you add to the DOM: one by one or all in one go.

DOM insert or delete operations are slow. The practice being painted as “bad practice” is the one when you add one element at one time in the DOM.

What you should do is create an HTML fragment, containing all the elements. After that, you should add the whole node to the DOM in one go.

In short, a good practice is a one time DOM modification with all the elements, not several DOM modifications for each element.

As the method says, it’s for HTML fragments. It means it will first parse the whole HTML, and then add all four elements: one fieldset and three select elements, in one go. The bad practice here would be to call createElement four times.

There are many ways to first create a HTML fragment and then add it to the DOM. This is just one of them. But the principle stands: if you’re going to touch the DOM, do it rarely and make sure it’s worth it.

That’s a good rule of thumb. Thanks.

We have a template for the widget. Next step is filling the template with data. Our helper select elements should be populated with corresponding values.

The constructor function will also initialize the widget now.

The init method will hide the target element and it also produces the helper elements.

The addHelperElements method will also add select data.

For this purpose, three quite similar getXList methods are added to the prototype (next step: refactoring) and the HELPER_ELEMENTS_HTML_FRAGMENT constant is now a template with placeholders a la Mustache.

date.html


<!doctype html>
  <html>
  <head>
    <meta charset="utf-8">
    <title>Date widget</title>
  </head>

  <body>
    <div class="dateDropdown">
      <label for="startDate">Please enter the start date:</label>
      <input id="startDate" type="text" placeholder="dd.mm.yyyy"/>
    </div>

    <div class="dateDropdown">
      <label for="endDate">Please enter the end date:</label>
      <input id="endDate" type="text" placeholder="dd.mm.yyyy"/>
    </div>
    
    <script src="date.js"></script>
    <script src="demo.js"></script>
  </body>
</html>

date.js


function MyDate(id) {
    this.targetElement = document.getElementById(id);
    this.init();
};

MyDate.prototype = {
  
    MONTHS: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sept','Oct','Nov','Dec'],
  
    YEAR_OFFSET: 21,

    HELPER_ELEMENTS_HTML_FRAGMENT: '<fieldset class="mydate"><select class="day">{{dd}}</select><select class="month">{{mm}}</select><select class="year">{{yyyy}}</select></fieldset>',

    init: function () {
        this.toggle();
        this.addHelperElements();
    },
  
    toggle: function () {
        var elemStyle = this.targetElement.style;
        
        if (elemStyle.display === "none") {
            elemStyle.display = "";
        } else {
            elemStyle.display = "none";
        };
    },
    
    addHelperElements: function () {
        var today = this.getToday(),
            yyyy, mm, dd, fragment;

        yyyy = this.getYearsList(today.year);
        mm = this.getMonthsList(today.month);
        dd = this.getDaysList(today.year, today.month);
        
        fragment = this.HELPER_ELEMENTS_HTML_FRAGMENT
            .split('{{yyyy}}').join(yyyy)
            .split('{{mm}}').join(mm)
            .split('{{dd}}').join(dd);

        this.targetElement.insertAdjacentHTML('afterend', fragment);
    },

    getToday: function () {
        var today = new Date();
      
        return {
            day: today.getUTCDate(),
            month: today.getUTCMonth(),
            year: today.getUTCFullYear()
        }; 
    },

    getYearsList: function (year) {
        var yearsList;

        for(var i = 0; i < this.YEAR_OFFSET; i++){
            var y = year + i;
            yearsList = yearsList + '<option value="' + y + '">' +  y + '</option>';
        };

        return yearsList;
    },

    getMonthsList: function () {
        var monthsList;

        for(var i = 0; i < 12; i++){
            var m = this.MONTHS[i];
            monthsList = monthsList + '<option value="' + m + '">' +  m + '</option>';
        };

        return monthsList;
    },

    getDaysList: function (year, month) {
        var n = this.getDaysInMonth(year, month),
            daysList;

        for(var i = 0; i < n; i++){
            var d = i + 1;
            daysList = daysList + '<option value="' + d  + '">' +  d + '</option>';
        };

        return daysList;
    },
  
    getDaysInMonth: function (year, month) {
        return new Date(year, month, 0).getDate();
    }
};

demo.js


var sdat = new MyDate("startDate"),
    edat = new MyDate("endDate");

Looking good :slight_smile:

The code is easy to read and that is a nice trick with the handlebars (and the split/join).

I also got to the point that I thought that I had three getXList methods (although in my case it was three addX methods) and thought that they should probably be refactored.

I will be interested to see how you approach that.

Tomorrow I’m getting at it. Let’s return to your Ruby analogy. :slight_smile:

In JavaScript, a “classical” inheritance model looks like this:


function Parent() {};

Parent.prototype.parent_method = function () {
    console.log("Parent method");
};

function Child() {};

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.child_method = function () {
    console.log("Child method");
};

var p = new Parent();
p.parent_method();

var c = new Child();
c.child_method();
c.parent_method();

In this model the child inheritance is a bit awkward to code, but it’s closest to the JavaScript way. To note that there is no ancestors method to follow the inheritance chain: the prototype.

Using only objects, without constructor functions, one could do this:


var parent = {
    parent_method: function () {
        console.log("Parent method");
    }
};

var child = Object.create(parent);

child.child_method = function () {
    console.log("Child method");
};

var grandchild = Object.create(child);

grandchild.grandchild_method = function () {
    console.log("Grandchild method");
};

var p = Object.create(parent);
p.parent_method();

var c = Object.create(child);
c.child_method();
c.parent_method();

var g = Object.create(grandchild);
g.grandchild_method();
g.child_method();
g.parent_method();

This model is cleaner to code but it doesn’t use the prototype. This means that any method later added to the parent, will not be available by default to child objects.

The future ES6 could support something like this:


class Parent {
    parent_method() {
        console.log("Parent method");
    }
};

class Child extends Parent {
    child_method() {
        console.log("Child method");
    }
};

var p = new Parent;
p.parent_method();

var c = new Child;
c.child_method();
c.parent_method();

I’ve combined the three getXList methods into a single getHelperLists method. I’ve also added the selected attribute so that the widget starts with the today date selected.

The refactoring was an exercise. Personally, I would keep the three separate methods. The combined one is horrible readability-wise. I could expand the ternary operators into if or switch statements, but it would still be somewhat difficult to follow, and to maintain, of course.

date.html and demo.js are the same.

date.js


function MyDate(id) {
    this.targetElement = document.getElementById(id);
    this.init();
};

MyDate.prototype = {
  
    MONTHS: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sept','Oct','Nov','Dec'],
  
    YEAR_OFFSET: 21,

    HELPER_ELEMENTS_HTML_FRAGMENT: '<fieldset class="mydate"><select class="day">{{dd}}</select><select class="month">{{mm}}</select><select class="year">{{yyyy}}</select></fieldset>',

    init: function () {
        this.toggle();
        this.addHelperElements();
    },
  
    toggle: function () {
        var elemStyle = this.targetElement.style;
        
        if (elemStyle.display === "none") {
            elemStyle.display = "";
        } else {
            elemStyle.display = "none";
        };
    },
    
    addHelperElements: function () {
        var today = this.getToday(),
            lists = this.getHelperLists(today),
            fragment;
        
        fragment = this.HELPER_ELEMENTS_HTML_FRAGMENT
            .split('{{yyyy}}').join(lists.yyyy)
            .split('{{mm}}').join(lists.mm)
            .split('{{dd}}').join(lists.dd);

        this.targetElement.insertAdjacentHTML('afterend', fragment);
    },

    getToday: function () {
        var today = new Date();
      
        return { dd: today.getUTCDate(), mm: today.getUTCMonth(), yyyy: today.getUTCFullYear() }; 
    },
    
    getHelperLists: function (dayObj) {
        var counter = [this.YEAR_OFFSET, this.MONTHS.length, this.getDaysInMonth(dayObj.yyyy, dayObj.mm)],
            lists = {};
        
        for (var j = 0, listName; j < counter.length; j++) {
            listName = (j === 0) ? "yyyy" : (j === 1) ? "mm" : "dd";

            for(var i = 0, optionValue, optionSelected; i < counter[j]; i++){
                optionValue = (j === 0) ? dayObj.yyyy + i : (j === 1) ? this.MONTHS[i] : i + 1;
                optionSelected = ( (listName !== "dd" && dayObj[listName] === i) ? "selected" : (dayObj[listName] === i + 1) ? "selected" : "" );
                lists[listName] = lists[listName] + '<option value="' + optionValue + '"' + optionSelected + '>' +  optionValue + '</option>';
            };
        };
        
        return lists;
    },
  
    getDaysInMonth: function (year, month) {
        return new Date(year, month, 0).getDate();
    }
};

Next step: event hooks. With those, the basic widget is complete.

After that: AMD.

Thanks for the concise explanation. That really helps.

One question:

Could you clarify this a little.
This seems to work:

var parent = {
    parent_method: function () {
        console.log("Parent method");
    },
};

var child = Object.create(parent);

var c = Object.create(child);
c.parent_method();

// Method added later
parent.random_method = function(){
  console.log("Random method");
}

c.random_method();

=> "Random method"

You’re not kidding :slight_smile:
Readability wise I much prefer the previous version.
Also, if the conditions change (e.g. it should be possible to select past years) it would be a nightmare to update six months down the road.

It’s also good to see that this was a challenge for you to do.
I spent a long time messing around with this code last night, but the main sticking point was the fact that the option value and option text was entirely different for each case.
I came up with a solution which had a separate function call to get these and although that shortened the over all code, it was so ugly I just binned it.

:homer voice: Woohoo!