3
\$\begingroup\$

I have been writing a jQuery widget for a couple of days now. Originally I needed this functionality for something I am writing at work, but I felt like I could abstract it a little more so here I am. I think it's about finished for now, I may add some features depending on how It gets used (if it does).

This is my first jQuery widget and I have been programming in JavaScript for just over a month now, so I don't think the code will be particularly great, but I've tried my hardest.

I have done some vague testing and it does seem to work in all browsers.

My main gripes are validation of the options, of which I don't really know how to do, however, I am going to do some proper research on this and come back to it.

There a few other problems with it but I think that this is not the right place to ask about those, so will leave it for now.

I would just like some feedback on my coding, how to improve/do it better, now and in the future. It's quite a big chunk of code but I didn't see anything in the FAQ about length of code.

I've also attached a jsFiddle so you can mess with it: http://jsfiddle.net/jCVzp/2/

I've tried to comment as best as I can and have noted all the options in the file description, and more guidance needed, just ask.

All the CSS is present in the jsFiddle, didn't think it was necessary to post it here.

Any time and help is greatly appreciated.

(function($) {
            /* 
            *   Document   : list-widget-0.1
            *   Created on : 06-Aug-2012, 20:53:49
            *   Author     : aydin hassan
            *   Description:
            *       This widget is designed to be attatched to a div
            *       
            *       Using the options you can specify one of three list types:
            *           1.) a normal list if items (normal)
            *           2.) a list of items with a cross/tick img depending on its status (act-deact)
            *               ---Clicking the image will toggle its status (change the img and class) and run a provided callback
            *           3.) a list of items with a trash can icon (remove)
            *               ---Click the image will run a supplied calback and then remove the item
            *
            *       Each of the lists has various callbacks:
            *           1.) The global click callback (selectabble.clickCallBack), if passed, will launch on clicked an element for any list type
            *           2.) The global click off callback (selectable.clickOffCallBack), if passed, will launch when an item is clicked off.
            *               ---Works in conjuction with selectable.sticky - if this is set to true elements will stay higlighted until they are clicked off
            *               ---NOTE: click off callback will not be processed if selectable.sticky is set to true
            *           3.) The act-deact callback, if passed, will run after a user has clicked the img, or, if there is no global click callback (selectabble.clickCallBack) it will 
            *               run when the li element is clicked
            *           4.) The remove callback, if passed, will run after the user clicks on the trash-can icon
            *           5.) The add button callback, if passed, will run when a user clicks the add button
            *
            *       List data:
            *           The list data must be an object of the following form:
            *               act-deact:
            *               var data = {
            *                   "0":{"name":"list item 1","activated":true},
            *                   "1":{"name":"list item 2","activated":true},
            *                   "2":{"name":"list item 3","activated":false}
            *               };
            *               
            *               normal/remove:
            *               var data = {
            *                   "0":{"name":"list item 1"},
            *                   "1":{"name":"list item 2"},
            *                   "2":{"name":"list item 3"}
            *               };
            *       
            *       Other options:
            *           title:          ---The title of the list box
            *           description:    ---Some description of the data
            *           searchBar:      ---BOOL - To display a filter box
            *           addButton:      ---BOOL - To display an add button
            *           idStyle:        ---A string to prepend to the id of the item id in the data object to use as the li element id attribute
            *           
            */      

          $.widget( "widgets.adminList", {


            // These options will be used as defaults

            options: { 
              list: null,
              title: "Title",
              description: "Some description",
              searchBar: true,
              addButton: false,

              addCallBack: null,
              listType: "normal",
              idStyle: "li-id-",
              removeCallBack: null,
              toggleCallBack: null,
              selectable: null    

            },


            // Set up the widget
            _create: function() {

            },

            _init: function() {
                var options = this.options;
                //Build the container elements
                this._buildTableHead();

                //switch type of list and run setup function for specified type
                switch (options.listType) {
                    case "normal": this._setUpNormal();
                        break;
                    case "act-deact": this._setUpActDeact();
                        break;
                    case "remove": this._setUpRemove();
                        break;
                }


                //If the selectable options are configured then we may want sticky elements (hover css stays until clciked again, or another elements is clicked)
                //And there may also be click on and click off callbacks
                if(options.selectable != null) {
                    $(".list-widget-list-container .list-widget-list li a").live('click',(function(e) {
                        //prevent the href("") from being followed, href is needed for IE to display hover
                        e.preventDefault();
                        //If we want elements to stick
                        if(options.selectable.stick) {
                            //If this element is stuck already
                            //unstick it and run click off call back if supplied
                            if($(this).hasClass("selected")) {
                                $(this).removeClass("selected");
                                if(options.selectable.clickOffCallBack != null){
                                    options.selectable.clickOffCallBack(this);
                                }
                            //Else unstick any other elements
                            //Stick this element
                            //and run click callback
                            } else {
                                $(".list-widget-list-container .list-widget-list").find(".selected").removeClass("selected");
                                $(this).addClass("selected");
                                if(options.selectable.clickCallBack != null){
                                    options.selectable.clickCallBack(this);
                                }
                            }
                        //If we don't want to stick just run click on callback if supplied
                        } else {
                            if(options.selectable.clickCallBack != null){
                                options.selectable.clickCallBack(this);
                            }
                        }
                    }));
                }

                //If add callback is supplied
                //create click event
                if(options.addButton && options.addCallBack != null) {
                    $(".list-widget-head-div .header-table .add-but").click(function() {
                        options.addCallBack(this);
                    });
                }


                //create event for filter box
                var that = this;                    
                $(".list-widget-head-div .header-table .list-search-box").bind("keyup",function(){
                    that._listFilter(this);
                });

                //Add the list elements
                this.add(options.list);
            },

            _listFilter:function(e){
                var filter = $(e).val(); // get the value of the input, which we filter on
                var list = $(".list-widget-list-container .list-widget-list");

                //only filter if value exists
                if(!filter || filter.length < 1 ){
                    list.find("li").show();
                }
                else{
                    list.find("li").each(function(){
                        //if a element text contains the filter value, hide it
                        if($("a", this).text().toLowerCase().indexOf(filter.toLowerCase()) == -1  ){
                            $(this).hide();
                        }
                        else{
                            //else show it
                            $(this).show();
                        }
                    });
                }
            },

            _buildTableHead:function(){
                //get the current options
                var options = this.options;
                //get the div the widget has been attatched to
                var self = this.element;

                //build table header
                var headCells = $("<tr />")
                        .append($('<td  />')
                            .attr("width","33%")
                            .attr("height","45px")
                            .append($('<p />')
                                .addClass("title")
                                .text(options.title)
                            )
                        );

                //depending on the options;
                //add a search bar and add button

                //if we want searchbar and add button..
                if(options.searchBar && options.addButton) {
                    headCells.append($('<td  />')
                        .attr("width","33%")
                        .attr("height","45px")
                        .attr("text-align","center")
                        .append($('<img />')
                            .addClass("add-but")
                            .attr("src","add-image.png")
                        )
                    )
                    .append($('<td  />')
                        .attr("width","33%")
                        .attr("height","45px")
                        .append($('<input />')
                            .attr("type","text")
                            .addClass("list-search-box")
                        )
                    );
                //if we only want add button
                } else if (!options.searchBar && options.addButton) {
                    headCells.append($('<td  />')
                        .attr("width","50%")
                        .attr("align","center")
                        .append($('<img />')
                            .addClass("add-but")
                            .attr("src","add-image.png")
                        )
                    );
                //if we only want search bar
                } else if (options.searchBar && !options.addButton) {
                    headCells.append($('<td  />')
                        .attr("width","50%")
                        .append($('<input />')
                            .attr("type","text")
                            .addClass("list-search-box")
                        )
                    );
                }

                //Build the tables and append the cells
                self.append($('<div />')
                        .addClass("list-widget-head-div")
                        .addClass("ui-corner-top")
                        .append($('<table />')
                            .addClass("header-table")
                            .append(headCells)
                        )
                        //Add the description if present
                        .append($("<p />")
                            .addClass("descript")
                            .text(options.description)
                    )
                );

                //Add the actual list element
                self.append($("<div />")
                    .addClass("list-widget-list-container")
                    .addClass("ui-corner-bottom")
                        .append($("<ul />")
                        .addClass("list-widget-list")
                    )
                );
            },

            _setUpNormal:function(){
                //get the current options
                var options = this.options;

            },
            _setUpActDeact:function(){
                //get the current options
                var options = this.options;

                //click event upon clicking the item image
                //Changes the status and can be provided with a call back
                //which has access to the clicked element


                //If there is no clickCallBack supplied, we apply the toggle action to the whole li element
                if(options.selectable.clickCallBack == null) {
                    $(".list-widget-list-container .list-widget-list li").live('click',function(e){
                        //get the clicked element
                        var imgElem = $(this).find("img.list-img");
                        //var to store status
                        var active;
                        //get status of item
                        //and remove the class
                        if(imgElem.hasClass("deactivated")) {
                            active = false;
                            imgElem.removeClass("deactivated");
                        } else {
                            active = true;
                            imgElem.removeClass("activated");
                        }
                        //add the loading class
                        imgElem.addClass("loading");
                        imgElem.attr("src","ajax-loader.gif")
                        //if callback has been provided, run it
                        if(options.toggleCallBack != null) {
                            options.toggleCallBack(this)
                        }
                        //remove loading class
                        imgElem.removeClass("loading");
                        //add the oposite class(item has been toggled)
                        if(active) {
                            imgElem.addClass("deactivated");
                            imgElem.attr("src","cross.png")
                        } else {
                            imgElem.addClass("activated");
                            imgElem.attr("src","tick.png")
                        }
                    });
                //if there is a clickCallBack then we just apply the toggle event to the image
                } else {
                    $(".list-widget-list-container .list-widget-list li img.list-img").live('click',function(e){
                        //get the clicked element
                        var imgElem = $(this);
                        //var to stored status
                        var active;
                        //get status of item
                        //and remove the class
                        if(imgElem.hasClass("deactivated")) {
                            active = false;
                            imgElem.removeClass("deactivated");
                        } else {
                            active = true;
                            imgElem.removeClass("activated");
                        }
                        //add the loading class
                        imgElem.addClass("loading");
                        imgElem.attr("src","ajax-loader.gif")
                        //if callback has been provided, run it
                        if(options.toggleCallBack != null) {
                            options.toggleCallBack(this)
                        }
                        //remove loading class
                        imgElem.removeClass("loading");
                        //add the oposite class(item has been toggled
                        if(active) {
                            imgElem.addClass("deactivated");
                            imgElem.attr("src","cross.png")
                        } else {
                            imgElem.addClass("activated");
                            imgElem.attr("src","tick.png")
                        }
                    });
                }
            },
            _setUpRemove:function(){
                //get the current options
                var options = this.options;

                //click event upon clicking the delete image
                //runs a provided callback and the removes item
                $(".list-widget-list-container .list-widget-list li img.list-img").live('click',function(e){
                    //if callback has been provided, run it
                    if(options.removeCallBack != null) {
                        options.removeCallBack(this)
                    }
                    //remove element
                    $(this).parent().remove();
                });
            },

            // Use the _setOption method to respond to changes to options

            _setOption: function( key, value ) {

              switch( key ) {

                case "clear":
                  // handle changes to clear option

                  break;

              }

              // In $ UI 1.8, you have to manually invoke the _setOption method from the base widget

              $.Widget.prototype._setOption.apply( this, arguments );
              // In $ UI 1.9 and above, you use the _super method instead
              this._super( "_setOption", key, value );

            },

            // Use the destroy method to clean up any modifications your widget has made to the DOM

            destroy: function() {

              // In $ UI 1.8, you must invoke the destroy method from the base widget

              $.Widget.prototype.destroy.call( this );
              // In $ UI 1.9 and above, you would define _destroy instead of destroy and not call the base method

            },

            /* This fuction when presented with an object in the following format
             * var data = {
             *      "0":{"name":"list item 1","activated":true},
             *      "1":{"name":"list item 2","activated":true},
             *      "2":{"name":"list item 3","activated":false}
             *  };
             * Will append the items to the bottom of the list
             */ 

            add: function(list) {
                //Pick add function depending on listType
                switch(this.options.listType) {
                    case "normal":this._addNormal(list);
                        break;
                    case "act-deact":this._addActDeact(list);
                        break;
                    case "remove":this._addRemove(list);
                        break;
                }
            },

            _addNormal: function(list) {
                var options = this.options;

                //Get the list elemenent
                var ulElem = $(".list-widget-list-container .list-widget-list");

                //For each list item
                $.each(list, function(key,val){
                    //Append the item to the list
                    ulElem.append($("<li />")
                        .attr("id",options.idStyle + key)
                            .append($("<a />")
                            .text(val.name)
                        )
                    );
                });
            },

            _addActDeact: function(list) {
                var options = this.options;

                //Get the list elemenent
                var ulElem = $(".list-widget-list-container .list-widget-list");

                //For each list item
                $.each(list, function(key,val){
                    //creat li element
                    var li = $("<li />")
                        .attr("id",options.idStyle + key)
                            .append($("<img />")
                                .addClass("list-img")
                            )
                            .append($("<a />")
                                .text(val.name)
                                .attr("href","")
                                .addClass("actdeact")
                        );
                    //add activated/deactivated class
                    //depending on item status 
                    if(val.activated === true) {
                        li.find("img").addClass("activated");
                        li.find("img").attr("src","tick.png");
                    } else {
                        li.find("img").addClass("deactivated");
                        li.find("img").attr("src","cross.png");
                    }
                    //Append the list item to the list
                    ulElem.append(li);
                });

            },

            _addRemove: function(list) {
                var options = this.options;

                //Get the list elemenent
                var ulElem = $(".list-widget-list-container .list-widget-list");

                //For each list item
                $.each(list, function(key,val){
                    //Append the item to the list
                    ulElem.append($("<li />")
                        .append($("<img />")
                                .addClass("list-img")
                                .attr("src","trash_can.png")
                            )
                        .attr("id",options.idStyle + key)
                            .append($("<a />")
                                .text(val.name)
                                .addClass("remove")
                        )
                    );
                });
            },

            //Function to empty the list
            empty: function() {
                $(".list-widget-list-container .list-widget-list").empty();
            }

        });
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Just skimmed through it, and it seems well-structured and nicely commented. Kudos! I also tried running it through jshint which found a few things like missing semicolons and such. Try it out; it's a good way to catch the small stuff. Oh, and for checking callbacks, use typeof someCallback === 'function' rather than someCallback != null. \$\endgroup\$
    – Flambino
    Nov 13, 2012 at 10:49
  • \$\begingroup\$ Thankyou, I will make those changes and checkout jshint! \$\endgroup\$ Nov 13, 2012 at 11:02

1 Answer 1

2
\$\begingroup\$

From a once over:

  • Do not do this:

    if(options.selectable != null)
    

    do either

    if(options.selectable)
    

    or

    if(options.selectable !== null) //Notice the ==
    
  • You are not consistent with semicolons, use jshint.com to fix your code
  • Extract some constants out, particularly widths and heights in your element building code
  • You use both single and double quotes to delimit strings, pick one, preferably single quotes
  • Considering using $().toggleClass(), you seem to be toggling a few times classnames yourself
  • I would allow toggleCallBack to cancel the toggeling so that I could use it validate the toggle

Other than that I think your code is solid.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.