Stop ngAfterContentInit from executing twice

Viewed 1316

I'm a bit confused as to why ngAfterContentInit is executing twice in this scenario. I've created a stripped-down version of our application to reproduce the bug. In short, I make use of a *contentItem to tag components which is then picked up by the standard-layout component for rendering. As soon as I follow this pattern, demo's ngAfterContentInit is executed twice.

I placed the demo app on github which will reproduce the error: https://github.com/jVaaS/stackoverflow/tree/master/ngaftercontentinit

Otherwise here are the important bits:

buggy-app.dart:

@Component(
    selector: "buggy-app",
    template: """       
        <standard-layout>
            <demo *contentItem></demo>
        </standard-layout>
    """,
    directives: const [
        ContentItemDirective,
        StandardLayout,
        Demo
    ]
)
class BuggyApp implements AfterContentInit {

    @override
    ngAfterContentInit() {
        print(">>> ngAfterContentInit: BuggyApp");
    }
}

standard-layout.dart:

////////////////////////////////////////////////////////////////////////////////
///
/// Standard Layout Component
/// <standard-layout></standard-layout>
///
@Component(
    selector: "standard-layout",
    template: """
        <div *ngFor="let item of contentItems ?? []">
            <template [ngTemplateOutlet]="item.template"></template>
        </div>
    """,
    directives: const [ROUTER_DIRECTIVES, ContentItem])
class StandardLayout implements AfterContentInit {

    @ContentChildren(ContentItemDirective)
    QueryList<ContentItemDirective> contentItems;

    @override
    ngAfterContentInit() {
        print(">>> ngAfterContentInit: StandardLayout");
    }

}

////////////////////////////////////////////////////////////////////////////////
///
/// Content Item Directive
/// *contentItem
///
@Directive(selector: '[contentItem]')
class ContentItemDirective implements AfterContentInit {
    final ViewContainerRef vcRef;
    final TemplateRef template;
    final ComponentResolver componentResolver;

    ContentItemDirective(this.vcRef, this.template, this.componentResolver);

    ComponentRef componentRef;

    @override
    ngAfterContentInit() async {
        final componentFactory =
        await componentResolver.resolveComponent(ContentItem);
        componentRef = vcRef.createComponent(componentFactory);
        (componentRef.instance as ContentItem)
            ..template = template;
    }
}

////////////////////////////////////////////////////////////////////////////////
///
/// Content Item Generator
///
@Component(
    selector: "content-item",
    host: const {
        '[class.content-item]': "true",
    },
    template: """
        <template [ngTemplateOutlet]="template"></template>
    """,
    directives: const [NgTemplateOutlet]
)
class ContentItem {
    TemplateRef template;
}
////////////////////////////////////////////////////////////////////////////////

and finally demo.dart:

@Component(
    selector: "demo",
    template: "Hello World Once, but demo prints twice!")
class Demo implements AfterContentInit {

    @override
    ngAfterContentInit() {
        print(">>> ngAfterContentInit: Demo");
    }

}

main.dart doesn't have much in it:

void main() {
    bootstrap(BuggyApp);
}

When I run this, Hello World prints once as expected: enter image description here

but when looking at the terminal:

enter image description here

So the demo component renders exactly once, but its ngAfterContentInit is being executed twice which causes havoc when your assumption is that it only gets executed once.

I've tried adding a hacky workaround, but it seems that component actually gets re-rendered twice:

int counter = 0;

@override
ngAfterContentInit() {

    if (counter == 0) {
        print(">>> ngAfterContentInit: Demo");
        counter++;
    }

}

Is this a bug in Angular or is there something I can do to prevent this?

pubspec.yaml in case it's needed:

name: bugdemo
version: 0.0.0
description: Bug Demo
environment:
  sdk: '>=1.13.0 <2.0.0'
dependencies:
  angular2: 3.1.0
  browser: ^0.10.0
  http: any
  js: ^0.6.0
  dart_to_js_script_rewriter: ^1.0.1
transformers:
- angular2:
    platform_directives:
    - package:angular2/common.dart#COMMON_DIRECTIVES
    platform_pipes:
    - package:angular2/common.dart#COMMON_PIPES
    entry_points: web/main.dart
- dart_to_js_script_rewriter
- $dart2js:
    commandLineOptions: [--enable-experimental-mirrors]

and index.html for good measure:

<!DOCTYPE html>
<html>
<head>
    <title>Strange Bug</title>
</head>
<body>

    <buggy-app>
        Loading ...
    </buggy-app>

<script async src="main.dart" type="application/dart"></script>
<script async src="packages/browser/dart.js"></script>
</body>
</html>

Update

So it was suggested that it might be that it only happens twice in dev-mode. I've done a pub build and ran the index.html which now contains main.dart.js in regular Chrome.

enter image description here

It's still executing twice, tried with ngAfterViewInit and that too executes twice.

Logged a bug in the meantime: https://github.com/dart-lang/angular/issues/478

2 Answers
Related