1

Well, I have this plunkr trying to simulate my situation:

The idea is that the user type a word on the textbox, and when click the button, an angular service returns an answer (result) from a DB according to the typed on the textbox (I have simulated this process with requesting data to a json file, so it is not important whatever you type, always will return the whole data) and populated a table.

But now, I'm working with the filter search. In this textbox, you can search for a person defined by:

  • first_name
  • middle_name
  • first_surname
  • second_surname

I have implemented two kind of visual filter:

1) Visual Filter for hiding and showing results: (defined in appCtrl.js)

$scope.changedValue=function(){
    var condition = $scope.filter.condition;
    $scope.Model.filteredlist = filterFilter($scope.Model.expenses,function(value, index, array){
      var fullname = (value.first_name+' '+value.middle_name+' '+value.first_surname+' '+value.second_surname).toLowerCase();
      if (fullname.indexOf(condition.replace(/\s\s+/g, ' ').toLowerCase()) > -1 ) {
        return array;
      }
    });
    if (typeof $scope.Model.filteredlist != 'undefined') { // When page loads for first time
      $scope.setPage();
    }
  } 

2) Visual Filter for highlight the results: (defined in appDrct.js)

app.directive('highLight', function ($document, $sce) {
  var component = function(scope, element, attrs) {

    if (!attrs.highlightClass) {
      attrs.highlightClass = 'angular-highlight';
    }

    var replacer = function(match, item) {
      return '<span class="'+attrs.highlightClass+'">'+match+'</span>';
    }

    var tokenize = function(keywords) {
      keywords = keywords.replace(new RegExp(',$','g'), '').split(' ');
      var i;
      var l = keywords.length;
      for (i=0;i<l;i++) {
        keywords[i] = keywords[i].replace(new RegExp('^ | $','g'), '');
      }
      return keywords;
    }

    scope.$watch('keywords', function(newValue, oldValue) {
      console.log("new: " + newValue + " old " + oldValue);

        var tokenized = tokenize(newValue);
        var regex     = new RegExp(tokenized.join('|'), 'gmi');

        if(newValue.length>=1 || oldValue.length>=1){
          for(i=0;i<=1;i++){
            element[0].cells[i].innerHTML = element[0].cells[i].innerText.replace(regex, replacer);
          }
        }
    });
  }
  return {
    link:       component,
    replace:    false,
    scope:      {
      keywords:  '=highLight'
    }
  };
});

The html calling those filters: (defined in table.html)

<input type="text" class="form-control" id="filter-list" placeholder="Name(s) and/or Lastname(s)" ng-model="filter.condition" ng-change="changedValue()">
......
<tr ng-repeat="expense in Model.filteredlist | pagination: pagination.currentPage : numPerPage" x-high:light="filter.condition">
        <td>{{expense.first_name}} {{expense.middle_name}}</td>
        <td>{{expense.first_surname}} {{expense.second_surname}}</td>
        <td>{{expense.age}}</td>
      </tr>

But I got some problems, because sometimes the person don't have middle_name or sometimes don't have second_surname.

To reproduce my issue, type in the search box: Lora and then erase it, and you will see that some data it is not rendered in the correct way. And if you type Loras and the erase the s the word don't highlight again, but if you continue erasing, the word highlight again.

So, what I'm doing wrong? I think it's a problem with the $scope.changeValue filter, but I'm lost.

Any ideas?

robe007
  • 3,523
  • 4
  • 33
  • 59

3 Answers3

2

I believe the problem you have is in your highLight directive. It is trying to modify its contents and is making assumptions about what its content is...

element[0].cells[i].innerHTML = element[0].cells[i].innerText.replace(regex, replacer);

In fact the problem is one of timing. The highLight directive sometimes modifies the HTML before the interpolation happens. So you end up with stuff like:

<td class="ng-binding">{{expense.first_name}} {{expense.midd<span class="angular-highlight">l</span>e_name}}</td>

which obviously Angular does not understand.

Pete BD
  • 10,151
  • 3
  • 31
  • 30
1

Seems to be an open issue with Angular - https://github.com/angular/angular.js/issues/11716

If you change your {{ }} bindings with ng-bind, the filtering works as you expect it to -

    <td><span ng-bind="expense.first_name"></span><span ng-bind="expense.middle_name"></span></td>
    <td><span ng-bind="expense.first_surname"></span><span ng-bind="expense.second_surname"></span></td>
    <td><span ng-bind="expense.age"></span></td>

----- UPDATE - Jan,4, 2016 -----

I couldn't find a satisfactory explanation yet. The behavior seems to be related to how ng-bind is used to $watch things and {{ }} is used to $observe, I am not quite sure.

As per angular best practice - https://github.com/angular/angular.js/blob/2a156c2d7ec825ff184480de9aac4b0d7fbd5275/src/ng/directive/ngBind.js#L16, ng-bind is the preferred way to bind values that are in scope unless these are DOM attributes, in which case you could $observer the attribute in the directive. Reference - AngularJS : Difference between the $observe and $watch methods

One more difference - the {{ }} watcher is fired on every $digest as compared to ng-bind, which is $watching for changes and therefore performance of ng-bind is better, even though you end up writing more html. Reference - AngularJS : Why ng-bind is better than {{}} in angular?

----- UPDATE - Jan,5, 2016 -----

Correct answer see below from Pete BD

Community
  • 1
  • 1
FrailWords
  • 886
  • 7
  • 19
  • Wow, amazing !, but why this happens? Why the behavior of angular not works as expected when I use `{{}}`? – robe007 Jan 03 '16 at 16:45
  • @robe007 I'd have to dig through Angular code to figure this out :) but seems intriguing. To my current understanding, they both should work the same way but looks like there's something else going on. – FrailWords Jan 03 '16 at 17:49
  • @robe007 only thing I can understand right now is that the problem seems to be with object references and not primitives. – FrailWords Jan 03 '16 at 18:18
  • I'm accepting your answer, because solved my issue, but if you can give me more explanation, it will be more helpful. – robe007 Jan 03 '16 at 18:28
  • 1
    @robe007 I'll edit my answer as soon as I figure out an explanation. Thanks for accepting my answer. – FrailWords Jan 03 '16 at 18:46
  • Thank you, I'll be waiting for it. It's a rather intriguing thing (: – robe007 Jan 04 '16 at 03:46
  • 1
    @robe007 checkout my updates on the answer. Its still not convincing enough but its in the right direction. – FrailWords Jan 04 '16 at 17:54
  • @robe007 See the answer below by 'Pete BD', that is the correct answer. My answer is a work around but not the right one. – FrailWords Jan 05 '16 at 03:29
  • 1
    Thank you very much for your effort, I learned a lot from your answer. Now, I marked my own answer as the accepted, because it was exactly what I needed. – robe007 Jan 05 '16 at 21:38
0

Well, based on the excellent answers from FrailWords and PeteBD, I got an idea, and now works !

The trick is in the interpolation. Reviewing the docs and with an excellent fiddle found, the solution came using $interpolate and $eval with a non isolated scope.

var interpolation = $interpolate(element[0].cells[i].innerText);
element[0].cells[i].innerHTML = scope.$eval(interpolation).replace(regex, replacer);

The whole directive's code:

app.directive('highLight', ['$interpolate', function ($interpolate) {
  var component = function(scope, element, attrs) {

    if (!attrs.highlightClass) {
      attrs.highlightClass = 'angular-highlight';
    }

    var replacer = function(match, item) {
      return '<span class="'+attrs.highlightClass+'">'+match+'</span>';
    }

    var tokenize = function(keywords) {
      keywords = keywords.replace(new RegExp(',$','g'), '').split(' ');
      var i;
      var l = keywords.length;
      for (i=0;i<l;i++) {
        keywords[i] = keywords[i].replace(new RegExp('^ | $','g'), '');
      }
      return keywords;
    }

    scope.$watch(attrs.highLight, function(newValue, oldValue) {
      console.log("new: " + newValue + " old " + oldValue);

        var tokenized = tokenize(newValue);
        var regex     = new RegExp(tokenized.join('|'), 'gmi');

        if(newValue.length>=1 || oldValue.length>=1){
          for(i=0;i<=1;i++){
            var interpolation = $interpolate(element[0].cells[i].innerText);
            element[0].cells[i].innerHTML = scope.$eval(interpolation).replace(regex, replacer);
          }
        }
    });
  }
  return {
    link:       component,
    replace:    false
  };
}]);

And the html always like this:

<tr ng-repeat="expense in Model.filteredlist | pagination: pagination.currentPage : numPerPage" x-high:light="filter.condition">
 <td>{{expense.first_name}} {{expense.middle_name}}</td>
 <td>{{expense.first_surname}} {{expense.second_surname}}</td>
 <td>{{expense.age}}</td>
</tr>

Now everything works like a charm. Amazing, but really true.

robe007
  • 3,523
  • 4
  • 33
  • 59