1.8+ Pre-Assign Bindings Plugin
Pre-assigning component/directive bindings
What is a bindings pre-assignment?
In AngularJS, bindings assignment refers to the process of assigning values to a component's or directive's controller instance or to its isolate scope (depending on the value of bindToController
).
Earlier versions of AngularJS supported assigning bindings to a controller instance before calling its constructor function, thus making the bound values available during instantiation. This was called "bindings pre-assignment".
Brief overview of bindings pre-assignment history
- Before version 1.5.10, bindings would always be pre-assigned and there was no way to change this behavior.
- In version 1.5.10 (with commit f86576d), the default behavior still was to pre-assign bindings (same as earlier versions), but a new option was introduced to disable bindings pre-assignment using $compileProvider#preAssignBindingsEnabled(). When turned off (i.e. when bindings were not pre-assigned), one could access the bound values in the
$onInit()
lifecycle hook (but not directly in the controller constructor). - In version 1.6.0 (with commit bcd0d4d), the default behavior changed to not pre-assign bindings, but it was still possible to change it back to the old behavior of pre-assigning bindings.
- Finally, in version 1.7.0 (with commit 38f8c97), the option was removed and bindings were never pre-assigned.
When to use bindings pre-assignment
Bindings pre-assignment made a lot of sense before the introduction of lifecycle hooks as it provided for an easy way to access bindings inside a controller. However, it comes with its problems (see caveats). Without getting into details here, it can lead to unpredictable behavior, and it complicates things for third-party libraries.
With the introduction of lifecycle hooks, there was a better way to access bindings while avoiding the problems of pre-assignment. Therefore, it is recommended to not rely on bindings pre-assignment and use lifecycle hooks to access values whenever possible.
With that said, there are cases were updating an application to not rely on bindings pre-assignment is not possible. There can be several reasons for that:
- A third-party library required by the application may depend on bindings pre-assignment.
- The switch from bindings pre-assignment to no pre-assignment is a major breaking change, which may require a prohibitively large amount of work (depending on the size of the application).
- There is no reliable way to ensure that all components (including those from third-party libraries) have been correctly updated to deal with the lack of bindings pre-assignment. There may be no build or runtime errors, yet the application could be broken. Therefore, unless an application is relatively small or adequately covered by automated tests, ensuring that it continues to work when switching to no bindings pre-assignment is a tedious, manual process.
Therefore, in some situations sticking to bindings pre-assignment may be desirable.
How to pre-assign bindings using the latest version
There are clear benefits to using the latest version of a tool, such as taking advantage of critical security and bug fixes, new features and performance improvements.
Sometimes, the lack of bindings pre-assignment is the only obstacle preventing teams from upgrading their projects to the latest version of AngularJS. For these cases, it is desirable to be able to bring bindings pre-assignment back into the latest version of AngularJS NES.
This is exactly what the ngCompileExtPreAssignBindings
module does!
The ngCompileExtPreAssignBindings
module
The ngCompileExtPreAssignBindings
module re-introduces the option of enabling bindings pre-assignment, even when using the latest version of AngularJS NES.
It augments $compileProvider
with a preAssignBindingsEnabled()
method that can be used to turn bindings pre-assignment on/off.
What's with the name?
Following naming conventions used in other core modules, such as ngParseExt, the ngCompileExt...
part of the name indicates that this module extends the functionality of the $compile
service. The ...PreAssignBindings
part refers to the specific feature that it adds, which is pre-assigning bindings.
How to install
The ngCompileExtPreAssignBindings
module is included in the @neverendingsupport/angularjs@X.Y.Z-compile-ext-pre-assign-bindings
package. This is a paid extension for the AngularJS NES product. Please contact sales@herodevs.com to obtain a license and the installation documentation.
How to use
Once you have installed the @neverendingsupport/angularjs@X.Y.Z-compile-ext-pre-assign-bindings
package, follow these steps to enable bindings pre-assignment:
- Include the package in your application the same way you include other AngularJS packages. For example, you may import it into your main JavaScript/TypeScript file or add it to the list of files that get bundled together when building the application.
- Load the module in your application by adding it as a dependent module:
Make sure that ngCompileExtPreAssignBindings comes before other modules (including third-party ones) that might access the $compileProvider.preAssignBindingsEnabled() method. A simple way to guarantee that is to put ngCompileExtPreAssignBindings first in the list of dependent modules.
- Finally, use
$compileProvider.preAssignBindingsEnabled()
to enable bindings pre-assignment:
Using with TypeScript
The @neverendingsupport/angularjs@X.Y.Z-compile-ext-pre-assign-bindings
package ships with its own TypeScript types. If you use TypeScript, the types will be picked up automatically as soon as you import the package into your application.
Note that the ngCompileExtPreAssignBindings
types depend on the types of the core angular
module, which are distributed as @types/angular. Make sure you have those installed as well in order to take full advantage of TypeScript types.
Unit Testing
The ngMock
module used for unit testing AngularJS applications offers a $controller service to aid in testing the controllers of components and directives.
When the ngCompileExtPreAssignBindings
module is loaded, ngMock
's $controller
service will detect whether bindings pre-assignment is enabled, and it will act accordingly. This ensures that the tests will behave as closely as possible to the actual application, resulting in more reliable tests.
Caveats / Known issues with bindings pre-assignment
Incompatibility with third-party libraries
A third-party library may be incompatible or behave differently with bindings pre-assignment, causing your application to break. This is less of a concern if you are already using a library with a version of AngularJS that still supports bindings pre-assignment, but this is something to keep in mind if you update to a more recent version of the library.
For example, AngularJS Material used to respect bindings pre-assignment, but this feature was removed in v1.2.0 (commit 579a327).
So, even if you enable bindings pre-assignment, there is a small chance you might still need to update your usage of third-party library APIs to not rely on bindings pre-assignment.
Different behaviors with ES5 vs ES2015
Bindings pre-assignment relies on some JavaScript techniques that are incompatible with ES2015 classes. It only works for ES5 constructor functions.
So, if you define a component's controller using an ES2015 class, bindings will not be pre-assigned:
In order to take advantage of bindings pre-assignment, you need to define component controllers using ES5 constructor functions:
If you are using TypeScript, keep in mind that the behavior of your application may change with regard to bindings pre-assignment if you switch the TypeScript target
compile option from ES5
to ES2015
(or newer).
This is because TypeScript transpiles classes differently depending on the ECMAScript version it is targeting. For ES5, classes are transpiled to constructor functions (which do support bindings pre-assignment), while they remain classes when targeting ES2015 (which do not support bindings pre-assignment).
Incompatibility with property initializers
A pattern that is sometimes used in JavaScript constructors is assigning default values for properties, with the assumption that they may be overwritten later. However, this pattern will break when using bindings pre-assignment, because the default values set in the constructor will overwrite the binding values already set (before calling the constructor).
Consider the following component:
Here, the component sets a default value in the constructor and accesses the someProp
property in the $onInit()
lifecycle hook.
Now, assume that the component is used in a template as follows:
Let's see how the component will behave differently with and without bindings pre-assignment:
Without bindings pre-assignment:
someProp
is set to default value in the constructor.- AngularJS updates
someProp
tobound value
due to the template binding. $onInit()
logsbound value
(which is the current value ofsomeProp
).
With bindings pre-assignment:
- AngularJS updates
someProp
tobound value
due to the template binding (before calling the constructor). someProp
is set todefault value
in the constructor.$onInit()
logsdefault value
(which is the current value ofsomeProp
).
As you can see, with bindings pre-assignment enabled, the value from the template binding is overwritten by the default value set in the constructor, which is undesirable.
TypeScript Users
If you are using TypeScript, beware that class field initializers transpile to the same pattern of assigning values in the constructor, which again is not compatible with bindings pre-assignment.
For example, the following TypeScript code:
...will be transpiled to something equivalent to: