Reputation: 821
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.
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)
Chrome - With optional bindings ( 888 component, 4 optional bindings, total watchers 4441)
Safari - Without optional bindings ( 888 component, total watchers 889)
Safari - With optional bindings ( 888 component, 4 optional bindings, total watchers 4441)
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!
Upvotes: 62
Views: 5700
Reputation: 564
A good way is the answer of @GregL, use a different template with different attributes, but you can use too a ng-attr-[nameCustomAttr]="value"
, for the optional bindings, see my related answer, and in this way angular use a type of bindingOnce and check if the attribute has value and add or not the attribute depending on the value.
For that, the attributes need to stay on the template of the directive/component.
Note: angularjs create a watch only for variables which appear in the UI.
Well, good luck, I hope it helps you in something.
Upvotes: 0
Reputation: 380
You need to Destroy component binding after a component call.
Please check and let me know if you need more details.
https://toddmotto.com/angular-1-5-lifecycle-hooks#using-ondestroy
Upvotes: 0
Reputation: 87
All you need to keep one time binding with {{::variable}} to get prevented from multiple watchers. Then you won't face any performance issue.
Upvotes: 0
Reputation: 463
Another suggestion would be to NOT create a generic component. Instead, defer the business logic to services and create specific components. Let consumer code decide which component to use depending on their needs. If you have lot's of common behavior shared across the multiple components, you can inject the service in to the components to reuse them. Consider splitting the business logic into multiple services and use hierarchical design to reuse code but allowing for specification depending on the case. Push shared to code to an abstract service and extend them into concrete services. Inject the concrete services into your components. Be happy :).
Upvotes: 0
Reputation: 38103
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;
}
template(null, {a: '1'})
=> "<div><div ng-if="$ctrl.a"></div></div>"
template(null, {b: '1'})
=> "<div></div>"
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.
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,
Upvotes: 1