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

8

12

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-

Kocher answered 3/6, 2014 at 16:2 Comment(4)
In your plunkr if you bypass the Api mock and set $scope.data on the script itself works. So I guess there's the catch. Sorry for not being able to be more helpful. My approach is always doing custom filters (to put them in a different place than the header) and I have tons of working filters with selects that read from an external API. If you can see that as a solution I can post you an example.Ackerman
@Andión Yes, setting the data direct in the script gets round the timing issue and the filters then work fine - however, that means having the data in the script which seems a bit awkward for dynamic data. Having the filters external seems like a good solution if it gets round the issue - could you post an answer with an exmaple?Kocher
As I was writing my answer I think I got the "problem"! :)Ackerman
Nope, false alarm... :( I'll write what I am doing right now, that works for me.Ackerman
L
8

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'.

Lethalethal answered 31/8, 2015 at 5:21 Comment(3)
A full demo would have been handy but worked it out :) Nice work.Safety
Nice. Got a down vote with no comment despite the fact that this answer is pretty awesome. Haters gonna hate...Lethalethal
This works fine, just make sure you replace the template cache entry prior to creating your ngTables.Related
D
5

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 ;)

Detestation answered 6/6, 2014 at 10:14 Comment(1)
Can you please help me with a similar question here?Irksome
R
4

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;
};
Rematch answered 27/6, 2014 at 10:55 Comment(2)
Yes, this code as written may work but it doesn't really answer the question. This bypasses the issue by hard coding the docType variable into the local javascript which isn't ideal if the docType needs to be loaded from the DB (as it would mean dynamically writing this javascript file each page hit and embedding the data which causes caching issues and general code smell) - the question is: How to load the data for the filter in an async call and get the filter to reflect the loaded data?Kocher
Thanks Diablo, helped me realise the format for the expected array here is id:title for each element.Helm
W
3

As mentioned by @Andión You can achieve with custom filter.

It is easy to achieve Asynchronous data population with Promises (the $q service in Angular), interesting Andy Article about Promises

You can amend the $scope.names method and add $http service that return the asynchronous data and resolve the deferred object as:

$scope.names = function(column) {
  var def = $q.defer();

  /* http service is based on $q service */
  $http({
    url: siteurl + "app/application/fetchSomeList",
    method: "POST",

  }).success(function(data) {

    var 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
        });
      }
    });
    
    /* whenever the data is available it resolves the object*/
    def.resolve(names);

  });

  return def;
};
Widen answered 15/10, 2014 at 2:39 Comment(3)
Why it has to be a $q defer doesn't make sense to me. Seems like it should be able to take in a promise.Loader
Can you please help me with a similar question here?Irksome
Can you please update the "custom filter" link? It appears to be out of date and tries to install a browser plugin.Dimorphism
C
2

I encountered a similar issue but did not want to make the additional AJAX call to get the filter values.

The issue with the OP's code is that filter-data function executes before $scope.data is populated. To get around this I used the Angular $watch to listen for changes on $scope.data. Once $scope.data is valid the filter-data is populated correctly.

        $scope.names2 = function () {
        var def = $q.defer(),
             arr = [],
                names = [];
        $scope.data = "";
        $scope.$watch('data', function () {


            angular.forEach($scope.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;
    };

The original plunk forked with the change: http://plnkr.co/edit/SJXvpPQR2ZiYaSYavbQA

Also see this SO Question on $watch: How do I use $scope.$watch and $scope.$apply in AngularJS?

Cana answered 30/1, 2015 at 20:59 Comment(3)
What version of ng-table does this require as it doesn't seem to work with 0.3.2Labial
I believe this was with version 0.3.0 of ng-table. Haven't tried 0.3.2 yet. Please add a comment if you figure out what the issue is.Cana
Can you please help me with a similar question here?Irksome
W
1

I solved the issue with $q.defer() as mentioned by Diablo

However, the is code actually pretty simple and straightforward:

in HTML:

<td ... filter-data="countries">

in controller:

$scope.countries = $q.defer();
$http.get("/getCountries").then(function(resp){
  $scope.countries.resolve(resp.data.data);
})
Windtight answered 21/4, 2016 at 6:5 Comment(0)
H
0

"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')."

I added the overrided script below the script of ng-table and everything worked well...

<script id="ng-table/filters/select.html" type="text/ng-template">
 <select ng-options="data.id as data.title for data in {{$column.data}}" ng-table-select-filter-ds="$column" ng-disabled="$filterRow.disabled" ng-model="params.filter()[name]" class="filter filter-select form-control" name="{{name}}"> <option style="display:none" value=""></option> </select>
</script>
Heraldic answered 10/1, 2017 at 8:25 Comment(0)
H
0

What I did is just put the select tag with values and have the ng-model return the values for the filter.

This was helpful since I needed to translate the plain text.

<td data-title="'Status'| translate" ng-bind = "("cc_assignment_active"== '0') ? ('Inactive' | translate) : ('Active'| translate)" 
                    filter="{ cc_assignment_active: 'select3'}" >

</td>

<script id="ng-table/filters/select3.html" type="text/ng-template">
<select  class="filter filter-select form-control"  ng-model="params.filter()[name]" name="{{name}}">
    <option active value="" translate>---All---</option>
    <option value="1" translate>Active</option>
    <option value="0" translate>Inactive</option>
</select>

Hooks answered 28/2, 2018 at 15:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.