Friday, January 19, 2018

Using Controller $filters to prevent $digest performance issues

Using Controller $filters to prevent $digest performance issues



Filters in Angular massively contribute to slow performance, so let’s adopt a sensible way of doing things, which may take you an additional ten minute to code, but will dramatically enhance your application’s performance.
Let’s look at how removing filters in the DOM actually impacts our $digest cycles.

What’s a DOM filter?

A DOM filter is where we use a | pipe inside an expression to filter data:
<p>{{ foo | uppercase }}</p>
This might output a String 'todd' as 'TODD' as we’ve used the | uppercase filter. If you’re not well versed with filters, check out my article on custom Filters.

Why are DOM filters bad?

They’re easy to use, which means they probably have some internal performance overhead. And that’s true, filters in the DOM are slower than running filters in JavaScript, however the main concern here is how often DOM filters get run.
Take this example, it uses a DOM filter on an ng-repeat, type some letters to see how it filters:


This looks great, and has no immediate performance concerns, no page lag or input delay. However let’s dig a little deeper.

Filter $digest evaluation

For us Angular developers, DOM filters are branded as the “awesomeness” that ships with the core, and by all means, this is tremendous, but there’s actually very little about the performance impacts of filters.
Are you ready to realise how your $$watchers are being choked by using DOM filters? Okay here goes.
Let’s setup a basic test filter:
function testFilter() {
  var filterCount = 0;
  return function (values) {
    return values.map(function (value) {
      filterCount++;
      // don't do this, this is just a hack to inject
      // the filter count into the DOM
      // without forcing another $digest
      document.querySelector('.filterCount').innerHTML = (
        'Filter count: ' + filterCount
      );
      return value;
    });
  };
}

angular
  .module('app', [])
  .filter('testFilter', testFilter);
This majestic testFilter will be bound to an ng-repeat, and each time the filter is called, it’ll increment its internal counter and log it out in the console for us.
We can add it to the ng-repeat like so:
<ul>
  <li ng-repeat="user in user.filteredUsers | testFilter">
    {{ user.name }}
  </li>
</ul>
Let’s also add some data to a Controller and force a $digest every ~1000ms to see how quickly these filters start stacking up.
function UserCtrl($interval) {

  var users = [{
    name: 'Todd Motto'
  },{
    name: 'Ryan Clark'
  },{
    name: 'Niels den Dekker'
  },{
    name: 'Jurgen Van de Moere'
  },{
    name: 'Jilles Soeters'
  }];
  
  // force a $digest every ~1000ms
  $interval(function () {}, 1000);
  
  this.filteredUsers = users;

}
And the output (note the “Filter count: X” text in the code embed). Please note, the <input ng-model=""> functionality no longer exists however has an ng-model attribute bound, this is merely just to show filter evaluations inside the $digest loop.


Boom boom boom. Five filter calls every $digest loop. This can’t be very efficient right? Especially with an ng-repeat with 1000 items inside. Think about it.
What’s more, typing something inside the <input ng-model=""> runs a $digest and numbers soar into double and triple figures. By the time you’ve read down to this stage the figure is probably into the hundreds, if not thousands.
Why are our filters running when we’re not even filtering the ng-repeat with the ng-modelanymore?
The filters here are run every single $digest.

$filter in Controller

We almost certainly don’t want the desired behaviour above, which if you’ve got large collections inside an ng-repeat and DOM filters, this is going to grind your application’s performance to a halt very quickly.
Let’s look at implementing a filter in the Controller using $filter.
function UserCtrl($filter, $interval) {

  var users = [...];
  
  // force a $digest every ~1000ms
  $interval(function () {}, 1000);

  // updateUsers will call `testFilter` ourselves
  this.updateUsers = function (username) {
    this.filteredUsers = $filter('testFilter')(users);
  };
  
  this.filteredUsers = users;

}
And bind the this.updateUsers method to the <input ng-model=""> as an ng-change event:
<input 
  type="text" 
  placeholder="Type to filter" 
  ng-model="username" 
  ng-change="user.updateUsers(username);">
Yes, we’re still forcing a $digest every ~1000ms in this example. Let’s observe the output and see when our ng-repeat filter gets called…


Answer: no filters are run until you type, rather than every $digest. We’re making serious progress. Our filter is only called when we activate it through the this.updateUsers function, it’s not cycled through each $digest.
Okay that’s great, but how can I run my filter so it’ll work?
Currently the this.updateUsers method takes an argument (username), we can pass this into Angular’s generic (and so wonderfully named) filter filter:
this.updateUsers = function (username) {
  this.filteredUsers = $filter('filter')(users, username);
};
And that’s all we need. Now we have a $filter that is actually only called when we need to filter, rather than having the filter function run every single $digest loop. This will significantly reduce the $digest overhead when not filtering, and run filters when actually necessary.


“Chaining filters”

Yes, chaining filters is great, but their order of declaration affects the output of the filtered collection, and two, you’re going to make the above $digest filter issues even worse.
How can I chain filters inside a Controller?
Easy, it’s not essentially chaining, filters are just function calls. Filter your first collection, and pass the filtered collection off to another filter, then assign that finalised collection to the public property:
this.updateUsers = function (username) {
  var filtered = $filter('filter')(users, username);
  filtered = $filter('orderBy')(filtered, 'name');
  this.filteredUsers = filtered;
};
With this example, I can use ng-init="user.updateUsers(username);" to instantly show you how this works:


The filter runs $filter('filter')(users, username) followed by $filter('orderBy')(filtered, 'name');. Which passes in var filtered... as a variable. Essentially it’s doing this inside itself, passing a filter into a filter:
this.updateUsers = function (username) {
  this.filteredUsers = $filter('orderBy')($filter('filter')(users, username), 'name');
};
Filters in Controllers: do it.

Angular.js: ng-select and ng-options

Angular.js: ng-select and ng-options



von 

Angular.js has a powerful directive waiting for you: it's ng-select. With it you can fill an HTML-select box from your model. It supports a pretty cool selector syntax, which is - unfortunately - a little bit tricky.
Let's assume you have a JSON stream which looks like this:
{
    "myOptions": [
        {
            "id": 106,
            "group": "Group 1",
            "label": "Item 1"
        },
        {
            "id": 107,
            "group": "Group 1",
            "label": "Item 2"
        },
...
}

// Get stream in the object called data
$scope.myOptions = data.myOptions;
The good thing is, you have a pretty flat data stream. When I started with this feature, I had an array of groups each containing the specific items. It was not possible for me to fill a select box that way. Instead, I refactored some of my code to what you can see above. Angular.js would take care of the grouping on it's own.
Here is my select definition:
<select
    ng-model="myOption"
    ng-options="value.id as value.label group by value.group for value in myOptions">
    <option>--</option>
</select>
ng-model is the name of the property which will reference the selected option. ng-options is the directive which fills the dropdown. It deserves a bit more attention.
You will understand it more easily if you read it from right to left. First there is:
for value in myOptions
It means you'll iterate through elements which are stored in myOptions. Every element will become available in this expression with the name "value".
The next part is:
group by value.group
This will tell Angular.js to make up
<optgroup>
tags and the label attribute will be filled with the content of the group field.
The last one is:
value.id as value.label
In this case, value.id will become the model (stored in ng-model), if your users have chosen an option. If you would delete "value.id as", simply the whole value object would become the model.
value.label
does exactly what it looks like: it's the label of the select box.
If you run your code, you'll see something like that:
<optgroup label="Group 1">
   <option value="0">Item 1</option>
   <option value="1">Item 2</option>
</optgroup>
Please look again and check the value attribute of the options. You might have expected it's matching the IDs from your JSON, but that is not the case (and yes, I thought like this initially). Actually this is an increasing number and references the position of the model (which is an array in my case). Don't worry about that - if you select your option the correct ID will be selected and put into your model. Or, if you leave out the value.id part of the expression, the whole selected object will become your model.
You can easily test it.
<select 
    ng-change="selectAction()"
    ng-model="myOption"
    ng-options="value.id as value.label group by value.group for value in myOptions">
    <option>--</option>
</select>
ng-change will fire if the user has chosen something. You can output it by typing:
$scope.selectAction = function() {
    console.log($scope.myOption);
};
I find the syntax of ng-options pretty much counter intuitive. It took me a good while to understand it and I was glad about the nice help on the AngularJS mailinglist. That said, I think more examples on the docs and an improved syntax would really help. Like:
foreach value in myOptions use value.label as label use value.id as model group by value.group
Here is a working JS fiddle example which illustrates the use of the ng-select directive: http://jsfiddle.net/jm6of9bu/2/

Tuesday, January 09, 2018

Solution: InAppBrowser opens PDF on Android device

Basic idea: use Android PDF viewer (default is chrome) to open the PDF file.

trick 1: target is '_system' to call PDF viewer

trick 2: pre_url to ensure refresh current page in IAB

Example code:



      let open_window = function(iab, url, target, options){

        let browser = iab.create(url, target, options);
        let pre_url = url;

        browser.on("loadstart")
         .subscribe(
            (res) => {
              if(res.url.indexOf('pdf') > -1) {
                iab.create(res.url, '_system');

                open_window(iab, pre_url, target, options);
              }
              pre_url = res.url;
            },
            err => {
              console.log("InAppBrowser exit Event Error: " + err);
        });


        browser.on("loadSuccess")
         .subscribe(
            (res) => {
              splashScreen.hide();
            },
            err => {
              console.log("InAppBrowser exit Event Error: " + err);
        });

        browser.on("exit")
         .subscribe(
            (res) => {
              platform.exitApp();
              console.log("InAppBrowser exit successfully");
            },
            err => {
              console.log("InAppBrowser exit Event Error: " + err);
        });
      }

      open_window(this.iab, url, target, options);

Fix of Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.

For ionic 2, when execute $cordova build android

Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.

fix:

cordova platform update android@6.2.2