javascriptc#jqueryrazordatatable

Razor - Convert DataTable to Array in JavaScript


I have a datatable in Razor Project with rows containing id, name, title, parent, and other fields. I need to bind a JavaScript array dynamically, but I tried this code and got an error. Please help.

It works fine with static data like below, and my organization chart is shown with no error.

var testData = [
    { id: 1, name: "John", title: "CEO", parent: 0 }, 
    { id: 2, name: "Tom", title: "SellManager", parent: 1 }, 
    { id: 3, name: "Jerry", title: "SupportManager", parent: 1 }, 
    { id: 4, name: "Robert", title: "SellExpert", parent: 2 },
];

With the generated data from JSON doesn't work and shows an error:

Uncaught TypeError: Cannot read properties of undefined (reading 'render') jquery-3.7.1.min.js:2 jQuery.Deferred exception: Cannot read properties of undefined (reading 'render') TypeError: Cannot read properties of undefined (reading 'render')

<script type="text/javascript">        
/////////////////////   Chart Script Start   /////////////////////
    var jq = jQuery.noConflict();

    (function (jq) {
        jq.fn.orgChart = function (options) {
            var opts = jq.extend({}, jq.fn.orgChart.defaults, options);
            return new OrgChart(jq(this), opts);
        }

        jq.fn.orgChart.defaults = {
            data: [{ id: 1, name: 'Root', title: 'Amount', parent: 0, hierarchy: 0 }],
            showControls: false,
            allowEdit: false,
            onAddNode: null,
            onDeleteNode: null,
            onClickNode: null,
            newNodeText: 'Add',
            delNodeText: 'del'
        };

        function OrgChart(jqcontainer, opts) {
            var data = opts.data;
            var nodes = {};
            var rootNodes = [];
            this.opts = opts;
            this.jqcontainer = jqcontainer;
            var self = this;

            this.draw = function () {
                jqcontainer.empty().append(rootNodes[0].render(opts));
                //document.getElementsByClassName('org-del-button')[0].style.display = 'none';
                jqcontainer.find('.node').click(function () {
                    if (self.opts.onClickNode !== null) {
                        self.opts.onClickNode(nodes[jq(this).attr('node-id')]);
                    }
                });

                if (opts.allowEdit) {
                    jqcontainer.find('.node h2').click(function (e) {
                        var thisId = jq(this).parent().attr('node-id');
                        self.startEdit(thisId);
                        e.stopPropagation();
                    });
                    jqcontainer.find('.node h3').click(function (e) {
                        var thisId = jq(this).parent().attr('node-id');
                        self.startEdith3(thisId);
                        e.stopPropagation();
                    });
                }

                // add "add button" listener
                jqcontainer.find('.org-add-button').click(function (e) {
                    var thisId = jq(this).parent().attr('node-id');

                    if (self.opts.onAddNode !== null) {
                        self.opts.onAddNode(nodes[thisId]);
                    }
                    else {
                        self.newNode(thisId);
                    }
                    e.stopPropagation();
                });

                jqcontainer.find('.org-del-button').click(function (e) {
                    var thisId = jq(this).parent().attr('node-id');

                    if (self.opts.onDeleteNode !== null) {
                        self.opts.onDeleteNode(nodes[thisId]);
                    }
                    else {
                        self.deleteNode(thisId);
                    }
                    e.stopPropagation();
                });
            }

            this.startEdit = function (id) {
                var inputElement = jq('<input class="org-input" type="text" value="'+nodes[id].data.name+'"/>');
                jqcontainer.find('div[node-id=' + id + '] h2').replaceWith(inputElement);
                var commitChange = function () {
                    var h2Element = jq('<h2>' + nodes[id].data.name + '</h2>');
                    if (opts.allowEdit) {
                        h2Element.click(function () {
                            self.startEdit(id);
                        })
                    }
                    inputElement.replaceWith(h2Element);
                }
                inputElement.focus();
                inputElement.keyup(function (event) {
                    if (event.which == 13) {
                        commitChange();
                    }
                    else {
                        nodes[id].data.name = inputElement.val();
                    }
                });
                inputElement.blur(function (event) {
                    commitChange();
                })
            }

            //////////////////////title field//////////////////////////////
            this.startEdith3 = function (id) {
                var inputElement = jq('<input class="org-input" type="text" value="' + nodes[id].data.title + '"/>');
                jqcontainer.find('div[node-id=' + id + '] h3').replaceWith(inputElement);
                var commitChange = function () {
                    var h3Element = jq('<h3>' + nodes[id].data.title + '</h3>');
                    if (opts.allowEdit) {
                        h3Element.click(function () {
                            self.startEdith3(id);
                        })
                    }
                    inputElement.replaceWith(h3Element);
                }
                inputElement.focus();
                inputElement.keyup(function (event) {
                    if (event.which == 13) {
                        commitChange();
                    }
                    else {
                        nodes[id].data.title = inputElement.val();
                    }
                });
                inputElement.blur(function (event) {
                    commitChange();
                })
            }
            //////////////////////title field ends////////////////////////
            this.newNode = function (parentId) {
                var nextId = Object.keys(nodes).length;
                while (nextId in nodes) {
                    nextId++;
                }

                self.addNode({ id: nextId, name: '(New name)', title: '0.0', hierarchy: parentId, parent: parentId });
            }

            this.addNode = function (data) {
                var newNode = new Node(data);
                nodes[data.id] = newNode;
                nodes[data.parent].addChild(newNode);
                self.draw();
                self.startEdit(data.id);
                self.startEdith3(data.id);
            }

            this.deleteNode = function (id) {
                for (var i = 0; i < nodes[id].children.length; i++) {
                    self.deleteNode(nodes[id].children[i].data.id);
                }
                nodes[nodes[id].data.parent].removeChild(id);
                delete nodes[id];
                self.draw();
            }

            this.getData = function () {
                var outData = [];
                for (var i in nodes) {
                    outData.push(nodes[i].data);
                }
                return outData;
            }

            // constructor
            for (var i in data) {
                var node = new Node(data[i]);
                nodes[data[i].id] = node;
            }

            // generate parent child tree
            for (var i in nodes) {
                if (nodes[i].data.parent == 0) {
                    rootNodes.push(nodes[i]);
                }
                else {
                    nodes[nodes[i].data.parent].addChild(nodes[i]);
                }
            }

            // draw org chart
            jqcontainer.addClass('orgChart');
            self.draw();
        }

        function Node(data) {
            this.data = data;
            this.children = [];
            var self = this;

            this.addChild = function (childNode) {
                this.children.push(childNode);
            }

            this.removeChild = function (id) {
                for (var i = 0; i < self.children.length; i++) {
                    if (self.children[i].data.id == id) {
                        self.children.splice(i, 1);
                        return;
                    }
                }
            }

            this.render = function (opts) {
                var childLength = self.children.length,
                    mainTable;

                mainTable = "<table cellpadding='0' cellspacing='0' border='0'>";
                var nodeColspan = childLength > 0 ? 2 * childLength : 2;
                mainTable += "<tr><td colspan='" + nodeColspan + "'>" + self.formatNode(opts) + "</td></tr>";

                if (childLength > 0) {
                    var downLineTable = "<table cellpadding='0' cellspacing='0' border='0'><tr class='lines x'><td class='line left half'></td><td class='line right half'></td></table>";
                    mainTable += "<tr class='lines'><td colspan='" + childLength * 2 + "'>" + downLineTable + '</td></tr>';

                    var linesCols = '';
                    for (var i = 0; i < childLength; i++) {
                        if (childLength == 1) {
                            linesCols += "<td class='line left half'></td>";    // keep vertical lines aligned if there's only 1 child
                        }
                        else if (i == 0) {
                            linesCols += "<td class='line left'></td>";     // the first cell doesn't have a line in the top
                        }
                        else {
                            linesCols += "<td class='line left top'></td>";
                        }

                        if (childLength == 1) {
                            linesCols += "<td class='line right half'></td>";
                        }
                        else if (i == childLength - 1) {
                            linesCols += "<td class='line right'></td>";
                        }
                        else {
                            linesCols += "<td class='line right top'></td>";
                        }
                    }
                    mainTable += "<tr class='lines v'>" + linesCols + "</tr>";

                    mainTable += "<tr>";
                    for (var i in self.children) {
                        mainTable += "<td colspan='2'>" + self.children[i].render(opts) + "</td>";
                    }
                    mainTable += "</tr>";
                }
                mainTable += '</table>';
                return mainTable;
            }

            this.formatNode = function (opts) {
                var nameString = '',
                    descString = '';
                titleString = '';
                if (typeof data.name !== 'undefined') {
                    nameString = '<h2>' + self.data.name + '</h2>';
                }
                if (typeof data.title !== 'undefined') {
                    titleString = '<h3>' + self.data.title + '</h3>';
                }
                if (typeof data.description !== 'undefined') {
                    descString = '<p>' + self.data.description + '</p>';
                }
                if (opts.showControls) {
                    var buttonsHtml = "<div class='org-add-button'>" + opts.newNodeText + "</div><div class='org-del-button'>" + opts.delNodeText + "</div>";
                }
                else {
                    buttonsHtml = '';
                }
                return "<div class='node' node-id='" + this.data.id + "'>" + nameString + titleString + descString + buttonsHtml + "</div>";
            }
        }

    })(jQuery);
    var data1 = @Html.Raw(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(Model.Table_Public)));
    testData = JSON.stringify(data1);
    alert(testData);

    jq(function () {
        org_chart = jq('#orgChart').orgChart({
            data: testData,
            showControls: false,
            allowEdit: false,
            onAddNode: function (node) {
                log('Created new node on node ' + 
                   node.data.id);
                org_chart.newNode(node.data.id);
            },
            onDeleteNode: function (node) {
                log('Deleted node ' + node.data.id);
                org_chart.deleteNode(node.data.id);
            },
            onClickNode: function (node) {
                log('Clicked node ' + node.data.id);
            }        
        });
    });
</script>

Solution

  • You can access the Model in JavaScript directly without iterating over each DataRow in the @foreach statement. Then, you deserialise from the DataTable into a list of objects.

    Aside, it would be better to return the data as an array instead of a DataTable in the model.

    In your model class:

    public class GetDataTableInJsModel
    {
        public DataTable Table_Public { get; set; }
        // Optional: Define the model class instead of dynamic
        public List<dynamic> Table_Public_List { get; set; }
    }
    

    While in your controller action:

    var dt = <Your DataTable instance>;
    
    return View(new GetDataTableInJsModel
    {
        Table_Public = dt,
        Table_Public_List = JsonConvert.DeserializeObject(
            JsonConvert.SerializeObject(dt)
        )
    });
    

    To display the JSON string in the alert dialog, you should serialize the data with JSON.stringfy().

    @using Newtonsoft.Json
    
    <script type="text/javascript">
      
        var data = @Html.Raw(
            JsonConvert.DeserializeObject(JsonConvert.SerializeObject(Model.Table_Public))
        );
        // For using Model.Table_Public_List
        // var data = @Html.Raw(Model.Table_Public_List);
        alert(JSON.stringify(data));
    </script>