Search code examples
angularjscomponentslifecycle

Angular 1.5 component $postLink gets triggered too soon in when using templateUrl


I followed this post to get acquainted with Angular's 1.5 component postLink event.

I got this working in a plunker. Here's the code for the tabs component:

controller: function () {
this.$onInit = function () {
  console.log("$onInit");
  this.tabs = [];
};
this.addTab = function addTab(tab) {
  console.log("addTab");
  this.tabs.push(tab);
};
this.selectTab = function selectTab(index) {
  for (var i = 0; i < this.tabs.length; i++) {
    this.tabs[i].selected = false;
  }
  this.tabs[index].selected = true;
};
this.$postLink = function () {
  console.log("$postLink. nr of tabs added: " + this.tabs.length);

  this.selectTab(this.selected);
};
}

The console output:

  • $onInit
  • addTab
  • addTab
  • addTab
  • $postLink. nr of tabs added: 3

However, when I try to do the same in typescript, the postLink event gets triggered too soon. It gets triggered before the tabs can be added to the tabs-component.

Here's some of the code: /tabs/tab/tab.component.ts

namespace MainApp {
const mainApp = angular.module("mainApp");

class TabComponent implements ng.IComponentOptions {
    public templateUrl: string | ng.Injectable<(...args: any[]) => string>;
    public controller: any;
    public controllerAs: string;
    public transclude: boolean;
    public bindings: any;
    public require: any;

    constructor() {
        this.templateUrl = ["rootUrl", (rootUrl) => rootUrl +  "app/uitrijregelingBerekening/tabs/tab/tab.html"];
        this.controller = TabController;
        this.transclude = true;
        this.bindings = {
            label: "@",
        };
        this.require = {
            tabs: "^^",
        };
    }
}

mainApp.component("tab", new TabComponent());

}

/tabs/tab/tab.controller.ts

namespace MainApp {
interface ITabBindings {
    label: string;
}

export class TabController implements ITabBindings {
    public label: string;
    private tabs: TabsController;

    public tab: any;

    constructor() {
    }

    public $onInit() {
        this.tab = {
            label: this.label,
            selected: false
        };
        this.tabs.addTab(this.tab);
    }
}
}

/tabs/tabs.component.ts

namespace MainApp {
const mainApp = angular.module("mainApp");

class TabsComponent implements ng.IComponentOptions{
    public templateUrl: string | ng.Injectable<(...args: any[]) => string>;
    public controller: any;
    public controllerAs: string;
    public bindings: any;
    public transclude: boolean;

    constructor() {
        this.templateUrl = ["rootUrl", (rootUrl) => rootUrl +  "app/uitrijregelingBerekening/tabs/tabs.html"];
        this.controller = TabsController;
        this.bindings = {
            selected:"@",
        };
        this.transclude = true;
    }
}

mainApp.component("tabs", new TabsComponent());

}

/tabs/tabs.controller.ts

namespace MainApp {
export interface ITabsBindings {
    selected: number;
}

export class TabsController implements ITabsBindings {
    public selected: number;
    public tabs: Array<any>;

    private scope: any;

    static $inject = ["$scope"];
    constructor($scope: ng.IScope) {
        this.scope = $scope;

    }

    public $onInit() {
        console.log("$onInit");
        this.tabs = new Array<any>();
    }

    public addTab(tab: any) {
        console.log("addTab");

        this.tabs.push(tab);
    }

    public selectTab(index: number) {
        for (var i = 0; i < this.tabs.length; i++) {
            this.tabs[i].selected = false;
        }
            this.tabs[index].selected = true;
    }

    public $postLink() {
        console.log("$postLink. nr of tabs added: " + this.tabs.length);

        this.selectTab(this.selected);
    }

}
}

The templates are the same.

Now the console output is:

  • $onInit
  • $postLink. nr of tabs added: 0
  • angular.js:13920 TypeError: Cannot set property 'selected' of undefined
  • addTab
  • addTab
  • addTab

Am I missing something here?


Solution

  • Well, you are using a different approach now. Before you just pushed it into an array in one controller. Now you have two components and controllers.

    From the Typescript docs, this is your problem.

        /**
         * 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 2. Since the compilation process is rather
         * different in Angular 1 there is no direct mapping and care should be taken when upgrading.
         */
        $postLink?(): void;
    

    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.

    Instead of using require you should maybe bind the tabs array to the children.