Search code examples
angularjsangularjs-components

Pitfalls of the New AngularJS ng-ref Directive


The release of AngularJS V1.7.1* introduces the new ng-ref directive. While this new directive enables users to easily do certain things, I see great potential for abuse and problems.

The ng-ref attribute tells AngularJS to publish the controller of a component on the current scope. This is useful for having a component such as an audio player expose its API to sibling components. Its play and stop controls can be easily accessed.

The first problem is the player controls are undefined inside the $onInit function of the controller.

Initial vm.pl = undefined  <<<< UNDEFINED
Sample = [true,false]

For code that depends on data being available, how do we fix this?

The DEMO

angular.module("app",[])
.controller("ctrl", class ctrl {
  constructor() {
    console.log("construct")
  }
  $onInit() {
    console.log("onInit", this.pl);
    this.initPL = this.pl || 'undefined';
    this.sample = this.pl || 'undefined';
    this.getSample = () => {
      this.sample = `[${this.pl.box1},${this.pl.box2}]`;
    }
  }
})
.component("player", {
  template: `
    <fieldset>
      $ctrl.box1={{$ctrl.box1}}<br>
      $ctrl.box2={{$ctrl.box2}}<br>
      <h3>Player</h3>
    </fieldset>
  `,
  controller: class player {
    constructor() {
      console.log("player",this);
    }
    $onInit() {
      console.log("pl.init", this)
      this.box1 = true;
      this.box2 = false;
    }
  },
})
<script src="//unpkg.com/[email protected]/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
    Initial vm.pl = {{vm.initPL}}<br>
    Sample = {{vm.sample}}<br>
    <button ng-click="vm.getSample()">Get Sample</button>
    <br>
    <input type="checkbox" ng-model="vm.pl.box1" />
      Box1 pl.box1={{vm.pl.box1}}<br>
    <input type="checkbox" ng-model="vm.pl.box2" />
      Box2 pl.box2={{vm.pl.box2}}<br>
    <br>
    <player ng-ref="vm.pl"></player>
</body>


Solution

  • Getting ref to components controller isn't new, directives back in the day allowed it and that wasn't a problem at all, it's necessary to have such feature, ng-ref is just a helper for you to do this from the template side (the same way angular 2+ does).

    Nevertheless, if you need the child components ready you should use $postLink() instead of $onInit. $postLink is called after the component is linked with his children, which means, the ng-ref will be ready when it gets called.

    So all you have to do is change your onInit like so:

    ̶$̶o̶n̶I̶n̶i̶t̶(̶)̶ ̶{̶
    $postLink() {
        console.log("onInit", this.pl);
        this.initPL = this.pl || 'undefined';
        this.sample = this.pl || 'undefined';
        this.getSample = () => {
          this.sample = `[${this.pl.box1},${this.pl.box2}]`;
        }
    }
    

    $postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs. This hook can be considered analogous to the ngAfterViewInit and ngAfterContentInit hooks in Angular. Since the compilation process is rather different in AngularJS there is no direct mapping and care should be taken when upgrading.

    Ref.: Understanding Components

    The full working snippet can be found bellow (I removed all console.log to make it clearer):

    angular.module("app",[])
    .controller("ctrl", class ctrl {
      constructor() {
        //console.log("construct")
      }
      $postLink() {
        //console.log("onInit", this.pl);
        this.initPL = this.pl || 'undefined';
        this.sample = this.pl || 'undefined';
        this.getSample = () => {
          this.sample = `[${this.pl.box1},${this.pl.box2}]`;
        }
      }
    })
    .component("player", {
      template: `
        <fieldset>
          $ctrl.box1={{$ctrl.box1}}<br>
          $ctrl.box2={{$ctrl.box2}}<br>
        </fieldset>
      `,
      controller: class player {
        constructor() {
          //console.log("player",this);
        }
        $onInit() {
          //console.log("pl.init", this)
          this.box1 = true;
          this.box2 = false;
        }
      },
    })
    <script src="//unpkg.com/[email protected]/angular.js"></script>
    <body ng-app="app" ng-controller="ctrl as vm">
        Initial vm.pl = {{vm.initPL}}<br>
        Sample = {{vm.sample}}<br>
        <button ng-click="vm.getSample()">Get Sample</button>
        <br>
        <input type="checkbox" ng-model="vm.pl.box1" />
          Box1 pl.box1={{vm.pl.box1}}<br>
        <input type="checkbox" ng-model="vm.pl.box2" />
          Box2 pl.box2={{vm.pl.box2}}<br>
        <player ng-ref="vm.pl"></player>
      </body>