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:
but when looking at the terminal:
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.
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
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.
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.
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 you use ngAfterViewChecked? ngAfterViewChecked is useful when you want to call a lifecycle hook after all child components have been initialized and checked.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With