Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 445
Jan Vladimir Mostert Avatar asked Jun 29 '17 18:06

Jan Vladimir Mostert


People also ask

How many times ngAfterContentInit is called?

According to docs ngAfterContentInit is called only once after the first NgDoCheck, but in my case is executed twice and I can't find the reason why this is happening. Any ideas about why this hook could be executed twice or any known side effects that can cause that? angular.

What is the difference between ngAfterContentInit and ngAfterViewInit?

ngAfterContentInit : This is called after components external content has been initialized. ngAfterViewInit : This is called after the component view and its child views has been initialized.

What is the difference between ngOnInit and ngAfterViewInit?

ngOnInit() is called right after the directive's data-bound properties have been checked for the first time, and before any of its children have been checked. It is invoked only once when the directive is instantiated. ngAfterViewInit() is called after a component's view, and its children's views, are created.

When should I use Afterviewchecked?

When should you use ngAfterViewChecked? ngAfterViewChecked is useful when you want to call a lifecycle hook after all child components have been initialized and checked.


1 Answers

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.

like image 126
matanlurey Avatar answered Sep 30 '22 10:09

matanlurey