Question

I am trying to tackle a TODO in the ng-grid csv export plugin:

"add a config option for IE users which takes a URL. That URL should accept a POST request with a JSON encoded object in the payload and return a CSV. This is necessary because IE doesn't let you download from a data-uri link"

However, because of my somewhat limited understanding of AngularJS, ng-grid and ng-grid plugins, I am struggling with how to access the $http service (which I need to use to post the data) from within the plugin. I think I need to inject it but everything I've tried so far has failed.

I'm intending to attach something like this to the CSV Export button in IE while leaving it as is in other browsers:

       //post data to IEUrl
            $http({
                url: opts.iEUrl,
                method: "POST",
                data: csvData,
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}
            }).error(function(data, status, headers, config) {
                $window.alert(status);
            });

but so far even accessing $http is defeating me!

EDIT: WON THE BATTLE BUT NOT THE WAR (2 days later):

So, turns out I was overthinking it, to get $http, $window, etc. through to the plugin, they simply needed to be injected into the controller initiating the ng-grid and then passed through thus:

var csvOpts = { columnOverrides: { obj: function (o) {
    return o.no + '|' + o.timeOfDay + '|' + o.E + '|' + o.S+ '|' + o.I+ '|' + o.pH+ '|' + o.v;
    } },
    iEUrl: 'downloads/download_as_csv'
};

$scope.gridOptions = { 
        data: 'experiment.runs',
        showGroupPanel: true,
        plugins: [new ngGridCsvExportPlugin(csvOpts,$http,$window,$compile,$filter)]

However, that's when things got difficult. Turns out that it is not possible to cause a file to be downloaded, without some extra trickery anyway, as a results of an ajax request - the csv data is returned nicely by the request but doesn't trigger the download.

So, having failed with $http, I thought I'd make a form, put the csv data into a textarea and then post the data off as normal to my PHP action:

var fp = grid.$root.find(".ngFooterPanel");
        if(isIEAndUrlProvided){
            var csvDataLinkPrevious = grid.$root.find('.ngFooterPanel .csv-data-form');
            if (csvDataLinkPrevious != null) {csvDataLinkPrevious.remove() ; }
            fp.append($compile(angular.element('<form class="csv-data-form" name="csvDataForm" action="'+opts.iEUrl+'">'))(scope));
            var csvDataLinkCurrent = grid.$root.find('.ngFooterPanel .csv-data-form');
            csvDataLinkCurrent.append($compile(angular.element('<textarea form="csvDataForm" class="csv-data-form">csvData</textarea>'))(scope));
            csvDataLinkCurrent.append($compile(angular.element('<input class="csv-data-form" type="submit" value="CSV Export">'))(scope));
        }
        else {
            var csvDataLinkPrevious = grid.$root.find('.ngFooterPanel .csv-data-link-span');
            if (csvDataLinkPrevious != null) {csvDataLinkPrevious.remove() ; }
            var csvDataLinkHtml = "<span class=\"csv-data-link-span pull-right\">";
            csvDataLinkHtml += "<br><a href=\"data:text/csv;charset=UTF-8,";
            csvDataLinkHtml += encodeURIComponent(csvData);
            csvDataLinkHtml += "\" download=\"Export.csv\" class=\"btn btn-default btn-xs\" role=\"button\">CSV Export</a></br></span>" ;
            fp.append(csvDataLinkHtml);
        }

This certainly posted nicely to the action and allowed the file download...but it was empty - for some reason, I just couldn't get the actual data to be posted...so, I'm afraid I have given up on the csv export plugin and am leaving it for someone more capable than I.

EDIT: A FLANKING MANOEUVRE (2 days later):

Instead, I'm already saving the data which populates the grid every time it changes in a cookie (my app is completely client-side so this prevents data loss in the event of a page refresh). So, when I fire off my csv file download request in IE, I'm now reading the cookie in PHP and using that to create the csv file. A cop-out I know but I've run out of time on this and it works.

FWIW, modified plugin below:

function ngGridCsvExportPlugin (opts, $http,$window,$compile,$filter) {
var self = this;
self.grid = null;
self.scope = null;
self.init = function(scope, grid, services) {
    self.grid = grid;
    self.scope = scope;
    //self.$http = services.http;
    function showDs() {
        var keys = [];
        var downloadNotSupportedAndUrlProvided = false;
        //test for support of download attribute: http://stackoverflow.com/questions/12112844/how-to-detect-support-for-the-html5-download-attribute
        var a = document.createElement('a');
        if(opts != null && opts.iEUrl !=null && typeof a.download == "undefined")downloadNotSupportedAndUrlProvided=true;//DY
        for (var f in grid.config.columnDefs) { keys.push(grid.config.columnDefs[f].field);}
        var csvData = '';
        function csvStringify(str) {
            if (str == null) { // we want to catch anything null-ish, hence just == not ===
                return '';
            }
            if (typeof(str) === 'number') {
                return '' + str;
            }
            if (typeof(str) === 'boolean') {
                return (str ? 'TRUE' : 'FALSE') ;
            }
            if (typeof(str) === 'string') {
                return str.replace(/"/g,'""');
            }

            return JSON.stringify(str).replace(/"/g,'""');
        }
        function swapLastCommaForNewline(str) {
            var newStr = str.substr(0,str.length - 1);
            return newStr + "\n";
        }
        for (var k in keys) {
            csvData += '"' + csvStringify(keys[k]) + '",';
            }
        csvData = swapLastCommaForNewline(csvData);
        var gridData = grid.data;
        for (var gridRow in gridData) {
            for ( k in keys) {
                var curCellRaw;
                if (opts != null && opts.columnOverrides != null && opts.columnOverrides[keys[k]] != null) {
                    curCellRaw = opts.columnOverrides[keys[k]](gridData[gridRow][keys[k]]);
                }
                else {
                    curCellRaw = gridData[gridRow][keys[k]];
                }
                csvData += '"' + csvStringify(curCellRaw) + '",';
            }
            csvData = swapLastCommaForNewline(csvData);
        }
        var fp = grid.$root.find(".ngFooterPanel");
        var csvDataLinkPrevious = grid.$root.find('.ngFooterPanel .csv-data-link-span');
        if (csvDataLinkPrevious != null) {csvDataLinkPrevious.remove() ; }
        if(gridData.length>0){
            if(downloadNotSupportedAndUrlProvided){
                fp.append($compile(angular.element('<span class=\"csv-data-link-span pull-right\">'))(scope));
                var csvDataSpan = grid.$root.find('.ngFooterPanel .csv-data-link-span');
                csvDataSpan.append($compile(angular.element('<form class="csv-data-form" name="csvDataForm" action="'+opts.iEUrl+'">'))(scope));
                var csvDataForm = grid.$root.find('.ngFooterPanel .csv-data-form');
                csvDataForm.append($compile(angular.element('<input class="btn-default btn-xs" type="submit" value="CSV Export">'))(scope));
                }
            else {
                var csvDataLinkHtml = "<span class=\"csv-data-link-span pull-right\">";
                csvDataLinkHtml += "<br><a href=\"data:text/csv;charset=UTF-8,";
                csvDataLinkHtml += encodeURIComponent(csvData);
                csvDataLinkHtml += "\" download=\"Export.csv\" class=\"btn btn-default btn-xs\" role=\"button\">CSV Export</a></br></span>" ;
                fp.append(csvDataLinkHtml);
                }
            }
        }
    setTimeout(showDs, 0);
    scope.catHashKeys = function() {
        var hash = '';
        for (var idx in scope.renderedRows) {
            hash += scope.renderedRows[idx].$$hashKey;
        }
        return hash;
    };
    if (opts.customDataWatcher) {
        scope.$watch(opts.customDataWatcher, showDs);
    } else {
        scope.$watch(scope.catHashKeys, showDs);
    }

    scope.downloadCsv = function(){
        $http({
            url: opts.iEUrl,
            method: 'POST',
            data: $filter('json')(scope.csvData),
            //headers: {'Content-Type': 'application/x-www-form-urlencoded'}
        }).error(function(data, status, headers, config) {
            $window.alert(status);
        });
    };

};

}

Was it helpful?

Solution

That isn't really necessary. Modify the plugin to use FileSaver.js and use something like:

 	// now save to a file!
	var getArrayBuffer = function() {
		var data = csvData, len = data.length,
			ab = new ArrayBuffer(len), u8 = new Uint8Array(ab);
		while(len--) u8[len] = data.charCodeAt(len);
		return ab;
	};
		
	var getBlob = function() {
		return new Blob([getArrayBuffer()], { type : "text/csv" });
	},

saveAs(getBlob(), fileName);

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top