Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Angular Components - Optional Bindings

I want to create a bunch of generic components (angular 1.5) with multiple optional bindings that would be used inside multiple applications.

I am afraid it will create a lot of unnecessary watchers for an application that doesn't use most of the optional bindings.

Example:

Component declaration:

let dateRangeComponent = {
    bindings: {
        label: '@',
        name1: '@',
        name2: '@',
        model1: '>?',
        model2: '>?',
        extra1: '>?'
    },
    template: `<div ng-if="$ctrl.model1>stuff</div>
               <div ng-if="$ctrl.model2>stuff</div>
               <div ng-if="$ctrl.extra1>stuff</div>`
};

Component use example:

<date-rage-component label="Pretty Date" name1="Start" name2="end"/>

My question is if it is possible to automatically unwatch all the stuff related to the unused optional bindings, knowing they are undefined at compile time.

For instance, imagine I want to use a component in my application where it doesn't need any of the optional Binding, angular would create a lot of unnecessary watchers to keep the ng-if updated when we know they will always be false.

Am I doing an early performance optimization when not needed or misunderstanding any concept?

I thought of creating a custom wrapper directive to take advantage of the lazy transclude compilation in angular 1.5

Something like this (pseudo-code, not tested):

<optional-binding-once ng-if="::attrs.model1">
  <div ng-if="attrs.model1">
      stuff
  </div>
</optional-binding-once>

In this way I think the code inside optional-binding-once would only be compiled if ng-if is true, thus reducing one watcher if a binding is not defined.

(EDIT) Some Conclusions after some tests and research

Well, I guess there isn't a trivial solution to reduce the number of watchers inside a component when optional bindings are not filled.

I ran some tests through the $digest phase of angular, to check if the increased number of this kind of watchers is really a problem.

Here are my results:

The tests were against a worst-case scenario having 888 components with 4 optional bindings.

Chrome - Without optional bindings ( 888 component, total watchers 889)

  • Total Watchers: 889
  • Last Digest Cycle time: 0.9950000000026193
  • Average time for the last 1004 digest cycles: 1.0544920318724353 ms
  • starting dom loading (400ms)

Chrome - With optional bindings ( 888 component, 4 optional bindings, total watchers 4441)

  • Total Watchers:4441
  • Last Digest Cycle time: 1.1549999999988358
  • Average time for the last 1001 digest cycles: 1.6851748251747816 ms
  • starting dom loading (600ms)

Safari - Without optional bindings ( 888 component, total watchers 889)

  • Total Watchers: 889
  • Last Digest Cycle time: 1.0849999999991269
  • Average time for the last 530 digest cycles: 1.211632075471664 ms

Safari - With optional bindings ( 888 component, 4 optional bindings, total watchers 4441)

  • Total Watchers: 4441
  • Last Digest Cycle time: 1.7450000000026193
  • Average time for the last 588 digest cycles: 2.1167176870748237 ms

Conclusions:

In a worst-case scenario, the $digest time will be increased by 1ms. I don't think this rise will be a bottleneck for my application performance. This kind of watchers will fail in the first $digest condition ( value = get(current)) !== (last = watch.last) && etc ...), thus having a small impact in the processing time, because they never change or get the angular context dirty!

like image 373
Luis Neves Avatar asked Mar 15 '16 23:03

Luis Neves


1 Answers

I would utilise the fact that the template property can be a function (tElem, tAttrs) { ... } (docs) that returns a string to modify the template based on the attributes present.

The way I would do this is to use jQuery and some custom elements to indicate which parts of the template are conditional.

Here is a quick sample template function:

function template($element, $attrs) {
  var fullTemplate = $('<div><if-attr name="a"><div ng-if="$ctrl.a"></div></if-attr></div>');
  fullTemplate.find('if-attr').each(function() {
    if (attrs.hasOwnProperty($(this).attr('name'))) {
      $(this).replaceWith(this.innerHTML);
    } else {
      $(this).remove();
    }
  });
  return fullTemplate[0].outerHTML;
}

Sample output

template(null, {a: '1'}) => "<div><div ng-if="$ctrl.a"></div></div>"
template(null, {b: '1'}) => "<div></div>"

Known Limitations

This breaks down if you wanted to fetch the template from a URL (and it isn't prepopulated in the $templateCache) but that doesn't appear to be your situation.

If Minifying

The documentation states that if template is a function, it is injected with $element and $attrs. That means that if you are minifying your code, make sure you use a minification-safe method of specifying the function parameter names. e.g.

template: ['$element', '$attrs', function ($elem, $attrs) { 
    // ...
}],

or

function templateFn($elem, $attrs) { 
    // ...
}
templateFn['$inject'] = ['$element', '$attrs'];

template: templateFn,
like image 112
GregL Avatar answered Nov 15 '22 03:11

GregL