2

I am working on a line of business application that is using Angular to create a SPA around a Node.js api server. I decided on using ui-router cause of the state-machine and their intuitive way of embedding urls but I chanced upon a slight challenge when creating dynamic URLs within a directive.

I am using jQuery Datatables for my data grid as a directive but any action links generated using 'fnRender' don't seem to compile the 'ui-sref' to their respective 'href'links. The directive code is as follows:

app.directive('datatable', function ($http) {
  return {
    restrict: 'A',
    link: function ($scope, $elem, attrs) {
        var responsiveHelper;
        var breakpointDefinition = {
            tablet: 1024,
            phone : 480
        };
        var options = {
            bDeferRender: true,
            sPaginationType: "full_numbers",
            oLanguage: {
                sEmptyTable: "No records returned",
                sSearch: "<span>Search:</span> ",
                sInfo: "Showing <span>_START_</span> to <span>_END_</span> of <span>_TOTAL_</span> entries",
                sLengthMenu: "_MENU_ <span>entries per page</span>",
                sProcessing: "Loading..."
            },
            sDom: "lfrtip",
            oColVis: {
                buttonText: "Change columns <i class='icon-angle-down'></i>"
            },
            oTableTools: {
                sSwfPath: "js/plugins/datatable/swf/copy_csv_xls_pdf.swf"
            },
            bAutoWidth     : false,
            fnPreDrawCallback: function () {
                if (!responsiveHelper) {
                    responsiveHelper = new ResponsiveDatatablesHelper($elem, breakpointDefinition);
                }
            },
            fnRowCallback  : function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
                responsiveHelper.createExpandIcon(nRow);
            },
            fnDrawCallback : function (oSettings) {
                responsiveHelper.respond();
            }
        };
        if (typeof $scope.dtOptions !== 'undefined') {
            angular.extend(options, $scope.dtOptions);
        }
        if (attrs['dtoptions'] === undefined) {
            for (property in attrs) {
                switch (property) {
                    case 'sajaxsource':
                        options['sAjaxSource'] = attrs[property];
                    break;
                    case 'sajaxdataprop':
                        options['sAjaxDataProp'] = attrs[property];
                    break;
                }
            }
        } else {
            angular.extend(options, $scope[attrs['dtoptions']]);
        }   

        if (typeof options['sAjaxSource'] === 'undefined') {
            throw "Ajax Source not defined! Use sajaxsource='/api/v1/*'";
        }
        if (typeof options['fnServerData'] === 'undefined') {
            options['fnServerData'] = function (sSource, aoData, resultCb) {
                $http.get(sSource, aoData).then(function (result) {
                    resultCb(result.data);
                });
            };
        }
        options.aoColumnDefs = [];
        $elem.find('thead th').each(function() {
            var colattr = angular.element(this).data();
            if (colattr.mdata) {
                if (colattr.mdata.indexOf("()") > 1) {
                    var fn = $scope[colattr.mdata.substring(0, colattr.mdata.length - 2)];
                    if (typeof fn === 'function') {
                        options.aoColumnDefs.push({
                            mData: fn,
                            sClass: colattr.sclass,
                            aTargets: [colattr.atargets]
                        });     
                    } else {
                        throw "mData function does not exist in $scope.";
                    }
                } else {
                    options.aoColumnDefs.push({
                        mData: colattr.mdata,
                        sClass: colattr.sclass,
                        bVisible: colattr.bvisible,
                        aTargets: [colattr.atargets]
                    }); 
                }
            } else {
                if (colattr.fnrender.indexOf("()") > 1) {
                    var fn = $scope[colattr.fnrender.substring(0, colattr.fnrender.length - 2)];
                    if (typeof fn === 'function') {
                        options.aoColumnDefs.push({
                            fnRender: fn,
                            sClass: colattr.sclass,
                            aTargets: [colattr.atargets]
                        });     
                    } else {
                        throw "fnRender function does not exist in $scope.";
                    }
                } else {
                    options.aoColumnDefs.push({
                        fnRender: function (oObj) {
                            return "<a tooltip class='btn' title='View' ui-sref=\""+colattr.tag+".show({slug:\'"+oObj.aData._id+"\'})\"><center class=\"icon-search\"></center></a>";
                        },
                        sClass: colattr.sclass,
                        bVisible: colattr.bvisible,
                        aTargets: [colattr.atargets]
                    }); 
                }
            }
        });
        $elem.dataTable(options);
        $(".dataTables_length select").wrap("<div class='input-mini'></div>").chosen({disable_search_threshold: 9999999 });
    }
  }
});

It runs with out complications and even generates the following anchor tag:

<a ui-sref="organisation.show({slug:'527a44c02aa9ce3a1c3fbc17'})"></a> 

However, ui-router doesn't compile it to a respective state url. What could be the issue? Is there some configuration I may have missed?

Thanks

1
  • 1
    This is not directly related to your question, but I noticed that you did not reference the $http dependency in square brackets in your code, like so: app.directive('datatable', ['$http', function ($http) { /*logic */ }]); If you don't get into the habit of doing so for every controller, directive, service, etc., minification of your scripts - in my experience, ASP.NET minifying - changes the $compile variable, and it'll be a nightmare to figure out which dependency is missing. I learned this the hard way, so I thought I'd share. Commented Feb 18, 2015 at 18:11

2 Answers 2

3

Are you using any other directives or expressions within your data table? I'm guessing those wouldn't work either, because it looks like Angular never gets the opportunity to compile any of the HTML you're generating.

This has nothing to do with uiSref and everything to do with writing directives correctly. In terms of best practices, this directive has way too much code. You should look at decomposing it into multiple nested directives and straight HTML. I'd suggest spending some time learning about transclusion and doing nested directives with directive controllers.

Sign up to request clarification or add additional context in comments.

3 Comments

Also, the $http call should be in a controller, and the result should be data-bound to the directive. If you really need to encapsulate it in one place, write a companion service and inject that.
Thanks a lot for your answer it does seem to help with regard to how to better structure my code. Red up on transclusion and nested directives should help however I'm still a bit cautious on services, what functionality would be better written as a service??
Any non-trivial piece of logic that is not directly related to DOM manipulation.
1

Leaving aside best practice, I just encountered and resolved this issue myself. Because DataTables is modifying the DOM outside of Angular's event loop, the ui-sref attributes don't get compiled. It's a simple fix: you need to call $compile on each row as it's created.

Here's my (much simpler) directive:

function datatable($compile) {
    return {
        restrict: "A",
        link: (scope, elem, attrs) => {


            // THE IMPORTANT BIT
            scope.options.createdRow = function (row) {
                $compile(row)(scope);
            };


            var api = elem.DataTable(scope.options);
            var handleUpdates = function (newData) {
                var data = newData || null;
                if (data) {
                    api.clear();
                    api.rows.add(data).draw();
                }
            };
            scope.$watch("options.data", handleUpdates, true);
        },
        scope: { options: "=" }
    };
}

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.