Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to populate select filters on ng-table from async call

tl:dr

How can I populate an ng-table including 'select' filters using ajax/json?

Plunk showing the problem: http://plnkr.co/Zn09LV


Detail

I am trying to get to grips with AngualrJS and the ng-table extension and although I can get some nice tables with working filters and such when I'm using static data defined in the javascript - once I get to trying to load real data into the table I hit a snag.

The main body of the ng-table is populated correctly and as long as I only use the text filter everthing seems to be working:

        <td data-title="'Name'" filter="{ 'Name': 'text' }" sortable="'Name'">
            {{user.Name}}
        </td>

Works just fine.

However, if I update this to use the select filter:

        <td data-title="'Name'" filter="{ 'Name': 'select' }" sortable="'Name'"  filter-data="Names($column)">
            {{user.Name}}
        </td>

I run into a syncronisation issue in that the Names variable is always evaluated before the data has returned from the server. (Possibly the Names varibale is evaluated before the request to the server is even sent.) This means I get an empty list for the filter.

Once the data returns from the server - I can't seem to find a way of updating the select filter. Re-running the code that creates the filter list initially seems to have no effect - I'm not sure how to trigger the ng-table to re-check its filters so the updated variable isn't read. I also can't figure out a way to postpone the evaluation of the variable until after the async call is complete.

For my javascript I have pretty much used the example ajax code from the ng-table GitHub page and added onto it the example code for the select filter.

    $scope.tableParams = new ngTableParams({
        page: 1,            // show first page
        count: 10,          // count per page
        sorting: {
            name: 'asc'     // initial sorting
        }
    }, {
        total: 0,           // length of data
        getData: function($defer, params) {
            // ajax request to api
            Api.get(params.url(), function(data) {
                $timeout(function() {
                    // update table params
                    var orderedData = params.sorting ?
                    $filter('orderBy')(data.result, params.orderBy()) :
                    data.result;
                    orderedData = params.filter ?
                    $filter('filter')(orderedData, params.filter()) :
                    orderedData;

                    $scope.users = orderedData.slice((params.page() - 1) * params.count(), params.page() * params.count());

                    params.total(orderedData.length); // set total for recalc pagination
                    $defer.resolve($scope.users);
                }, 500);
            });
        }
    });

    var inArray = Array.prototype.indexOf ?
    function (val, arr) {
        return arr.indexOf(val)
    } :
    function (val, arr) {
        var i = arr.length;
        while (i--) {
            if (arr[i] === val) return i;
        }
        return -1
    };
$scope.names = function(column) {
    var def = $q.defer(),
        arr = [],
        names = [];
    angular.forEach(data, function(item){
        if (inArray(item.name, arr) === -1) {
            arr.push(item.name);
            names.push({
                'id': item.name,
                'title': item.name
            });
        }
    });
    def.resolve(names);
    return def;
};

I've tried a few attempts at adding on an additional $q.defer() and wrapping the initial data get followed by the $scope.names function - but my understanding of promise and defer isn't strong enough to get anything working.

There are a few notes on GitHub suggesting this is a bug in ng-table, but I'm not sure if that's the case or I'm just doing something daft.

https://github.com/esvit/ng-table/issues/186

Pointers on how to procede greatly appreciated

-Kaine-

like image 431
Kaine Avatar asked Jun 03 '14 16:06

Kaine


3 Answers

I had a similar but slightly more complex issue. I wanted to be able to update the list of filters dynamically which seemed totally doable since they should just in a $scope variable anyway. Basically, I expected that, if I have $scope.filterOptions = []; then I could set filter-data="filterOptions" and any update to that list would be automatically reflected. I was wrong.

But I found a solution that I think is pretty good. First, you need to override the ngTable select filter template (in case you don't know how to do this, it involves using $templateCache and the key you need to override is 'ng-table/filters/select.html').

In the normal template, you'll find something like this ng-options="data.id as data.title for data in $column.data" and the problem with that is that $column.data is a fixed value that won't change when we update $scope.filterOptions.

My solution is to pass only the $scope key as filter-data instead of passing the whole list of options. So, instead of filter-data="filterOptions", I'll pass filter-data="'filterOptions'" and then, put a small change in the template like: ng-options="data.id as data.title for data in {{$column.data}}".

Obviously, this is a significant change to how the select filter works. In my case, it was for a very small app that only had one table but you may be concerned that a change like this will break your other selects. If that's the case, you may want to build this solution into a custom filter instead of just overriding 'select'.

like image 155
Okonomiyaki3000 Avatar answered Oct 22 '22 04:10

Okonomiyaki3000


You can achieve that with a custom filter:

The code for the standard select filter on ngtable says:

<select ng-options="data.id as data.title for data in column.data"
    ng-model="params.filter()[name]"
    ng-show="filter == 'select'"
    class="filter filter-select form-control" name="{{column.filterName}}">
</select>

When you call this data you pass: filter-data="names($column)" and ngtable takes care of getting the data for you. I don't know why this does not work with an external resource. I bet it has something to do with the $column and the promise, as you pointed out.

I did a quick workaround in my code to avoid that. Writing my own select filter template like:

<select id="filterTest" class="form-control" 
    ng-model="tableParams.filter()['test']" 
    ng-options="e.id as e.title for e in externaldata">
</select>

You fetch this externaldata in your controller:

$scope.externaldata = Api.query(); // Your custom api call

It works perfectly, but I do have an id on my data, so no need of the name function.

I understand this solution is not optimal. Let's see if somebody writes here more than this 'workaround' and enlightens us. Even esvit is here sometimes ;)

like image 5
Andión Avatar answered Oct 22 '22 04:10

Andión


This works for me:

HTML:

<td data-title="'Doc type'" filter="{ 'doc_types': 'select' }" filter-data="docTypes()" sortable="'doc_types'">
    {{task.doc_type}}
</td>

AngularJS:

$scope.docTypes = function ($scope) 
{
    var def = $q.defer();
    //var docType = [
    //    {'id':'4', 'title':'Whatever 1'},
    //    {'id':'9', 'title':'Whatever 2'},
    //    {'id':'11', 'title':'Whatever 3'}
    //];

    // Or get data from API.
    // Format should be same as above.
    var docType = $http.get('http://whatever.dev', {
        params: { param1: data1 }
    });

    //Or with Restangular 
    var docType = Restangular.all('/api/doctype').getList();

    def.resolve(docType);
    return def;
};
like image 4
Diablo Avatar answered Oct 22 '22 03:10

Diablo