Jan Vladimir Mostert
Jan Vladimir Mostert

Reputation: 13002

Stop ngAfterContentInit from executing twice

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

Upvotes: 5

Views: 1704

Answers (2)

matanlurey
matanlurey

Reputation: 8614

This doesn't seem like a bug.

Here is the generated code for demo.dart: https://gist.github.com/matanlurey/f311d90053e36cc09c6e28f28ab2d4cd

void detectChangesInternal() {
  bool firstCheck = identical(this.cdState, ChangeDetectorState.NeverChecked);
  final _ctx = ctx;
  if (!import8.AppViewUtils.throwOnChanges) {
    dbg(0, 0, 0);
    if (firstCheck) {
      _Demo_0_2.ngAfterContentInit();
    }
  }
  _compView_0.detectChanges();
}

I was suspicious, so I changed your print message to include the hashCode:

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

Then I saw the following:

ngAfterContentInit: Demo 516575718

ngAfterContentInit: Demo 57032191

Which means two diffferent instances of demo were alive, not just one emitting twice. I then added StackTrace.current to the constructor of demo to get a stack trace:

Demo() {
  print('Created $this:$hashCode');
  print(StackTrace.current);
}

And got:

0 Demo.Demo (package:bugdemo/demo.dart:11:20) 1 ViewBuggyApp1.build (package:bugdemo/buggy-app.template.dart:136:21) 2 AppView.create (package:angular2/src/core/linker/app_view.dart:180:12) 3 DebugAppView.create (package:angular2/src/debug/debug_app_view.dart:73:26) 4 TemplateRef.createEmbeddedView (package:angular2/src/core/linker/template_ref.dart:27:10) 5 ViewContainer.createEmbeddedView (package:angular2/src/core/linker/view_container.dart:86:43)

Which in turn said your component was being created by BuggyApp's template:

<standard-layout>
  <demo *contentItem></demo>
</standard-layout>

Closer. Kept digging.

I commented out ContentItemDirective, and saw Demo was now created once. I imagine you expected it not to be created at all due to *contentItem, so kept digging - and finally figured it out.

Your StandardLayout component creates templates:

<div *ngFor="let item of contentItems ?? []">
  <template [ngTemplateOutlet]="item.template"></template>
</div>

But so does your ContentItemDirective via ComponentResolver. I imagine you did not intend this. So I'd figure out what you were trying to do and address your issue by removing creating the component in one of these places.

Upvotes: 3

No&#233;mi Sala&#252;n
No&#233;mi Sala&#252;n

Reputation: 5036

Maybe related to In Angular2, why are there 2 times check for content and view after setTimeout?

Are you in dev mode ? The console tells you when the app bootstrap. In dev mode, Angular perfom the changeDetection twice, so it can detect side effects.

Upvotes: 2

Related Questions