Search...
Toggle theme

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".

.component('myComponent', {
  bindings: {
    someProp: '<'
  },
  controller: function MyController() {
    // With bindings pre-assignment, `someProp` is defined here already.
    console.log(this.someProp);
  }
})

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.

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:

  1. 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.
  2. Load the module in your application by adding it as a dependent module:
angular.module('app', [
  'ngCompileExtPreAssignBindings',
  /* ...other modules... */
]);

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.

  1. Finally, use $compileProvider.preAssignBindingsEnabled() to enable bindings pre-assignment:
.config(['$compileProvider', function($compileProvider) {
  // Enable bindings pre-assignment.
  $compileProvider.preAssignBindingsEnabled(true);

  // ...or disable bindings pre-assignment.
  $compileProvider.preAssignBindingsEnabled(false);

  // ...or query the current value of the setting.
  var isBindingsPreAssignmentEnabled = $compileProvider.preAssignBindingsEnabled();
}]);

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:

.component('myComponent', {
  bindings: {
    someProp: '<'
  },
  controller: class Es2015Class {
    constructor() {
      // `someProp` is not defined here, even if bindings pre-assignment is enabled.
      console.log(this.someProp);
    }
  }
})

In order to take advantage of bindings pre-assignment, you need to define component controllers using ES5 constructor functions:

.component('myComponent', {
  bindings: {
    someProp: '<'
  },
  controller: function Es5ConstructorFunction() {
    // With bindings pre-assignment, `someProp` is defined here already.
    console.log(this.someProp);
  }
})

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:

.component('myComponent', {
  bindings: {
    someProp: '<'
  },
  controller: function MyController() {
    // Assign a default value for `someProp`.
    this.someProp = 'default value';

    this.$onInit = function $onInit() {
      // Log the value of `someProp`.
      console.log(this.someProp);
    }
  }
})

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:

<my-component some-prop="'bound value'"></my-component>

Let's see how the component will behave differently with and without bindings pre-assignment:

Without bindings pre-assignment:

  1. someProp is set to default value in the constructor.
  2. AngularJS updates someProp to bound value due to the template binding.
  3. $onInit() logs bound value (which is the current value of someProp).

With bindings pre-assignment:

  1. AngularJS updates someProp to bound value due to the template binding (before calling the constructor).
  2. someProp is set to default value in the constructor.
  3. $onInit() logs default value (which is the current value of someProp).

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.

class MyClass {
  someProp = 'default value';

  constructor(public someOtherProp = 'other default value') {}
}

...will be transpiled to something equivalent to:

function MyClass(someOtherProp) {
  if (someOtherProp === undefined) someOtherProp = 'other default value';

  this.someOtherProp = someOtherProp;
  this.someProp = 'default value';
}