1
\$\begingroup\$

I know this is a lot of code, but I'm not looking for any kind of detailed review of the code. I'm just hoping some nice Javascript guru can give it a once over and offer any needed advice for using OOP in Javascript better than I am.

Here are a few areas of concern that I have, that don't quite seem like they're ideal.

  • The page has 4 objects that interact with each other, just floating in the middle of nowhere.

  • The objects are all created by invoking anonymous constructors, since I didn't want named functions that are only used once hanging around.

  • Most of the constructors have as the first line var me = this; since this changes context.

  • There are about 4 or 5 global fields and functions that didn't really fit into any objects, so I just left them hanging in the global context.

Any pointers on utilizing object oriented programming in JavaScript would be much appreciated.

    var mode = "dev";
    var currentSrc = [];
    var initialTitleValue = "   [Title]              ";
    var initialSelectedSubject = { id: -1, txt: "Books' Subjects" };
    var currentDisplay = "";


    $(function () {
        $("#header h1").text("Knockout / jQuery Book Demo");

        $("#tbTitleSearch").blur(function () {
            if (!$(this).val().trim()) {
                viewModel.titleValue(initialTitleValue);
                $(this).addClass("watermark");
            }
        });

        $("#tbTitleSearch").click(function () {
            if ($(this).val() == initialTitleValue) {
                viewModel.titleValue("");
                $(this).removeClass("watermark");
            }
        });

        $("#tbGenreSearch").click(function () {
            if (viewModel.selectedSubjects().length == 1 && viewModel.selectedSubjects()[0] == initialSelectedSubject) {
                $(this).removeClass("watermark");
                viewModel.selectedSubjects.removeAll();
            }
            openSubjectWindow();
        });

        viewModel.selectedSubjects.subscribe(function () {
            if (viewModel.selectedSubjects().length == 1 && viewModel.selectedSubjects()[0] == initialSelectedSubject)
                $("#tbGenreSearch").addClass("watermark");
        });

        $(window).bind('resize', function () {
            gridManager.sizeGrid();
        }).trigger('resize');

        $(".bookItem").live("mouseover", function () { $(this).addClass("highlight"); });
        $(".bookItem").live("mouseout", function () { $(this).removeClass("highlight"); });

        $(".smCovTempl").live("mouseover", function () { $(this).addClass("highlight"); });
        $(".smCovTempl").live("mouseout", function () { $(this).removeClass("highlight"); });

        $(".bookItem").live("click", function () { $(this).toggleClass("selected"); });

        $(".smCovTempl > img").live("click", function () { bookDetailsPopup($(this).data("id")); });

        theme("grid");

        ko.applyBindings(viewModel);
        dataLoader.loadSubjects();
    });


    var viewModel = new (function () {
        var me = this;

        this.rows = ko.observableArray([]);
        this.currentTemplate = ko.observable("bookTemplate1");
        this.displayTemplate = function () { return me.currentTemplate(); };

        this.subjects = ko.observableArray([]);
        this.selectedSubjects = ko.observableArray([initialSelectedSubject]);
        this.selectedSubjectNames = function () { return $(me.selectedSubjects()).map(function (i, o) { return o.txt; }).get(); };
        this.selectedSubjectIds = function () { return $(me.selectedSubjects()).map(function (i, o) { return o.id; }).get(); };
        this.manageSingleSelectedSubject = function () {
            if (me.selectedSubjects().length == 0)
                me.selectedSubjects.push(initialSelectedSubject);
        };

        this.readStatus = ko.observable(-1);
        this.includeChildSubjects = ko.observable(true);
        this.titleValue = ko.observable(initialTitleValue);

        this.animateOnEntry = true;
        this.templateItemRendered = function (element) {
            if (me.animateOnEntry)
                $(element).fadeIn();
        };
    })();

    var dataLoader = new (function () {
        var me = this;

        this.loadAllBooks = function () {
            dataManager.clearCurrentData();

            if (mode == 'dev')
                me.loadTestData();
            else
                post("../PublicServices/KnockoutService.asmx/ReadAll", '{}',
                    function (d) {
                        currentSrc = d.d;
                        dataManager.refreshCurrentData();
                    });
        };

        this.searchForBooks = function () {
            alert(viewModel.titleValue());
        };

        this.loadSubjects = function () {
            viewModel.subjects([{ name: "Sub1", id: 1, children: [] },
                { name: "Sub2", id: 2, children: [{ name: "Sub2a", id: 4, children: [{ name: "Sub2a1", id: 6, children: []}] }, { name: "Sub2b", id: 5, children: []}] },
                { name: "Sub3", id: 3, children: []}]);

            $("#jsTreeTarget").jstree({
                "themes": {
                    "theme": "default",
                    "dots": false,
                    "icons": false
                },
                "checkbox": { "two_state": true },
                "plugins": ["themes", "html_data", "checkbox", "ui"]
            });

            $("#jsTreeTarget").bind("change_state.jstree", function () {
                viewModel.selectedSubjects(
                    $("li.jstree-checked > a", "#jsTreeTarget").map(function (i, o) { return { txt: $(o).text(), id: $(o).data("id") }; }).get());
            });
        };

        this.loadTestData = function () {
            currentSrc = [{ id: 1, title: "Mr. Jefferson's Hammer.  William Henry Harrison and the foundation of Indian Policy", authors: ["a1", "a2"], subjects: ["sub1", "sub2"], smImg: 1, medImg: 1, pub: "Oxford", pubYear: 2004, pages: 250, asin: "a", isRead: false },
                { id: 1, title: "Founding Brothers", authors: ["a1", "a2"], subjects: ["sub1", "sub2"], smImg: 1, medImg: 1, pub: "Oxford", pubYear: 2004, pages: 250, asin: "a", isRead: false },
                { id: 1, title: "American Creation", authors: ["a1", "a2"], subjects: ["sub1", "sub2"], smImg: 1, medImg: 1, pub: "Oxford", pubYear: 2004, pages: 250, asin: "a", isRead: false}];

            for (var i = 0; i < 50; i++)
                currentSrc.push({ id: 1, title: "American Creation", authors: ["a1", "a2"], subjects: ["sub1", "sub2"], smImg: 1, medImg: 1, pub: "Oxford", pubYear: 2004, pages: 250, asin: "a", isRead: false });
            dataManager.refreshCurrentData();
        };
    })();


    var dataManager = new (function () {
        var me = this;
        this.templateDataNotLoaded = false;

        this.refreshCurrentData = function () {
            if (currentDisplay == "grid")
                gridManager.reloadGrid();
            else {
                me.templateDataNotLoaded = false;
                me.cascadeData();
            }
        };

        this.cascadeData = function (index) {
            me.animateOnEntry = true;

            if (!index)
                index = 0;

            if (index == currentSrc.length - 1)
                return;

            if (index >= 30) {
                viewModel.animateOnEntry = false;
                for (var i = index; i < currentSrc.length; i++)
                    viewModel.rows.push(currentSrc[i]);
            } else {
                viewModel.rows.push(currentSrc[index]);
                setTimeout(function () { me.cascadeData(index + 1); }, 60);
            }
        };

        this.dumpDataWithoutCascade = function () {
            me.templateDataNotLoaded = false;
            viewModel.rows(currentSrc);
        };

        this.clearCurrentData = function () {
            currentSrc.length = 0;
            viewModel.rows.removeAll();
            gridManager.unloadGrid();
            me.templateDataNotLoaded = true;
        };
    })();

    var gridManager = new (function () {
        var me = this;

        this.gridDataNotLoaded = false;

        this.reloadGrid = function () {
            me.gridDataNotLoaded = false;

            $("#jqGridElement").jqGrid({
                datastr: { rows: currentSrc },
                jsonReader: { repeatitems: false },
                datatype: "jsonstring",
                colNames: ['', 'Title', 'Pages', 'Publisher', 'Authors', "Subjects"],
                colModel: [
                    { name: 'smImg', index: 'smImg', width: 65, fixed: true, sortable: false, formatter: gridCoverFormatter, title: false },
                    { name: 'title', width: 250, formatter: titleFormatter, title: false },
                    { name: 'pages', width: 80, fixed: true, align: 'center', title: false },
                    { name: 'pub', index: 'pub', width: 100, formatter: publisherFormatter, title: false },
                    { name: 'authors', width: 100, formatter: authorFormatter, sortable: false, title: false },
                    { name: 'subjects', width: 100, formatter: subjectFormatter, sortable: false, title: false}],
                height: 'auto',
                gridview: true,

                rowNum: -1
            });

            me.sizeGrid();
        };

        this.unloadGrid = function () {
            me.gridDataNotLoaded = true;
            $("#jqGridElement").jqGrid('GridUnload');
        };

        this.sizeGrid = function () {
            var targetContainer = "#contentParent";
            var gridID = "#jqGridElement";

            var width = $(targetContainer).width();
            var height = $(targetContainer).height();

            width = width - 20; // Fudge factor to prevent horizontal scrollbars
            height = height - 40;

            if (width > 0) //&& Math.abs(width - $(gridID).width()) > 5) {
                $(gridID).jqGrid('setGridWidth', width);

            if (height > 0) // && Math.abs(height - jQuery(gridID).height()) > 5)
                $(gridID).jqGrid('setGridHeight', height);
        };

        function gridCoverFormatter(cellvalue, options, rowObject) {
            return "<div style='height:85px;'><img src='../Books/Covers/Small/" + cellvalue + ".jpg' style='margin-top:5px;' /></div>";
        }

        function titleFormatter(cellvalue, options, rowObject) {
            return "<div title='" + cellvalue.toString().replace("'", "") + "'>" + cellvalue + "</div><a href='http://www.google.com?a=" + rowObject.asin + "' target='_blank'><img src='../Img/Misc/Amazon.png'</a>";
        }

        function publisherFormatter(cellvalue, options, rowObject) {
            var formattedVal = cellvalue.toString().replace("'", "");
            if ((formattedVal != "") && (rowObject.pubYear))
                formattedVal += " - " + rowObject.pubYear;

            return "<div title='" + formattedVal + "'>" + formattedVal + "</div>";
        }

        function authorFormatter(cellvalue, options, rowObject) {
            var result = "";
            for (var i = 0; i < cellvalue.length; i++)
                result += "<li><span>" + cellvalue[i] + "</span></li>";
            return "<ul class='grAUL'>" + result + "</ul>";
        }

        function subjectFormatter(cellvalue, options, rowObject) {
            var result = "";
            for (var i = 0; i < cellvalue.length; i++)
                result += "<li title='" + cellvalue[i] + "'>" + "Oxford - 2004" + "</li>";
            return "<ul class='grSUL'>" + result + "</ul>";
        }
    })();


    function theme(value) {
        if (value == "grid") {
            manageDisplay("#gridParent", "#templateElement", 'emptyBookTemplate');
            if (gridManager.gridDataNotLoaded)
                gridManager.reloadGrid();
        } else {
            manageDisplay("#templateElement", "#gridParent", value);
            if (dataManager.templateDataNotLoaded)
                dataManager.dumpDataWithoutCascade();
        }
        currentDisplay = value;
    }

    function manageDisplay(showID, hideID, newTemplate) {
        $(showID).show();
        $(hideID).hide();
        viewModel.currentTemplate(newTemplate);
    }


    function bookDetailsPopup(id) {
        var divToPopup = $("#bookInfoPopup").clone();
        $(divToPopup).dialog({ width: 400, height: 400 });
        $.get("PartialBookInfo.aspx?id=" + id, function (response) {
            $(".popupContent", divToPopup).html(response);
            $(".tabDiv", divToPopup).tabs();
            $(".loadingMsg", divToPopup).remove();
        });
    }

    function openSubjectWindow() {
        $("#treeWrapper").dialog({
            title: "Header",
            close: function () { viewModel.manageSingleSelectedSubject(); }
        });
    }

    function post(targetURL, dataInput, success) {
        $.ajax({
            url: targetURL,
            type: "POST",
            contentType: "application/json; charset=utf-8",
            data: dataInput,
            dataType: "json",
            success: success,
            async: true
        });
    }
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

If you want to be safer, wrap the whole thing in a executed closure (function () { /* code goes here */ })().

I don't think you need var me = this since at quick glance all the functions are called on the instances, which would make the keyword this the instance object.

There is nothing wrong with using anonymous constructors, as far as I know, but it would probably be better to use {} to create your objects and drop the new keyword. It will save you a reference lookup, like

var gridManager =  (function () {

    function gridCoverFormatter(cellvalue, options, rowObject) {
            return "<div style='height:85px;'><img src='../Books/Covers/Small/" + cellvalue + ".jpg' style='margin-top:5px;' /></div>";
    }

    function titleFormatter(cellvalue, options, rowObject) {
        return "<div title='" + cellvalue.toString().replace("'", "") + "'>" + cellvalue + "</div><a href='http://www.google.com?a=" + rowObject.asin + "' target='_blank'><img src='../Img/Misc/Amazon.png'</a>";
    }

    function publisherFormatter(cellvalue, options, rowObject) {
        var formattedVal = cellvalue.toString().replace("'", "");
        if ((formattedVal != "") && (rowObject.pubYear))
            formattedVal += " - " + rowObject.pubYear;

        return "<div title='" + formattedVal + "'>" + formattedVal + "</div>";
    }

    function authorFormatter(cellvalue, options, rowObject) {
        var result = "";
        for (var i = 0; i < cellvalue.length; i++)
            result += "<li><span>" + cellvalue[i] + "</span></li>";
        return "<ul class='grAUL'>" + result + "</ul>";
    }

    function subjectFormatter(cellvalue, options, rowObject) {
        var result = "";
        for (var i = 0; i < cellvalue.length; i++)
            result += "<li title='" + cellvalue[i] + "'>" + "Oxford - 2004" + "</li>";
        return "<ul class='grSUL'>" + result + "</ul>";
    }

    return {
        "gridDataNotLoaded": falsse,

        "reloadGrid": function () {
            me.gridDataNotLoaded = false;

            $("#jqGridElement").jqGrid({
                datastr: { rows: currentSrc },
                jsonReader: { repeatitems: false },
                datatype: "jsonstring",
                colNames: ['', 'Title', 'Pages', 'Publisher', 'Authors', "Subjects"],
                colModel: [
                    { name: 'smImg', index: 'smImg', width: 65, fixed: true, sortable: false, formatter: gridCoverFormatter, title: false },
                    { name: 'title', width: 250, formatter: titleFormatter, title: false },
                    { name: 'pages', width: 80, fixed: true, align: 'center', title: false },
                    { name: 'pub', index: 'pub', width: 100, formatter: publisherFormatter, title: false },
                    { name: 'authors', width: 100, formatter: authorFormatter, sortable: false, title: false },
                    { name: 'subjects', width: 100, formatter: subjectFormatter, sortable: false, title: false}],
                height: 'auto',
                gridview: true,

                rowNum: -1
            });

            this.sizeGrid();
        },

        "unloadGrid": function () {
            this.gridDataNotLoaded = true;
            $("#jqGridElement").jqGrid('GridUnload');
        },

        "sizeGrid": function () {
            var targetContainer = "#contentParent";
            var gridID = "#jqGridElement";

            var width = $(targetContainer).width();
            var height = $(targetContainer).height();

            width = width - 20; // Fudge factor to prevent horizontal scrollbars
            height = height - 40;

            if (width > 0) //&& Math.abs(width - $(gridID).width()) > 5) {
                $(gridID).jqGrid('setGridWidth', width);

            if (height > 0) // && Math.abs(height - jQuery(gridID).height()) > 5)
                $(gridID).jqGrid('setGridHeight', height);
        }
    };
})();

But all in all, JavaScript is a very flexible language when it comes to objects. You can force a class based structure, or use the prototyping delegation. It goes further with module patterns and just extending objects. The choice is up to you.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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