Reputation: 13002
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
Upvotes: 5
Views: 1704
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
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