The ngAfterViewInit lifecycle hook is not being called for a Component that is transcluded into another component using <ng-content> like this:
<app-container [showContent]="showContentContainer">
<app-input></app-input>
</app-container>
However, it works fine without <ng-content>:
<app-input *ngIf="showContent"></app-input>
The container component is defined as:
#Component({
selector: 'app-container',
template: `
<ng-container *ngIf="showContent">
<ng-content></ng-content>
</ng-container>
`
})
export class AppContainerComponent {
#Input()
showContentContainer = false;
#Input()
showContent = false;
}
The input component is defined as:
#Component({
selector: 'app-input',
template: `<input type=text #inputElem />`
})
export class AppInputComponent implements AfterViewInit {
#ViewChild("inputElem")
inputElem: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
console.info("ngAfterViewInit fired!");
this.inputElem.nativeElement.focus();
}
}
See a live example here: https://stackblitz.com/edit/angular-playground-vqhjuh
There are two issues at hand here:
Child components are instantiated along with the parent component, not when <ng-content> is instantiated to include them. (see https://github.com/angular/angular/issues/13921)
ngAfterViewInit does not indicate that the component has been attached to the DOM, just that the view has been instantiated. (see https://github.com/angular/angular/issues/13925)
In this case, the problem can be solved be addressing either one of them:
The container directive can be re-written as a structural directive that instantiates the content only when appropriate. See an example here: https://stackblitz.com/edit/angular-playground-mrcokp
The input directive can be re-written to react to actually being attached to the DOM. One way to do this is by writing a directive to handle this. See an example here: https://stackblitz.com/edit/angular-playground-sthnbr
In many cases, it's probably appropriate to do both.
However, option #2 is quite easy to handle with a custom directive, which I will include here for completeness:
#Directive({
selector: "[attachedToDom],[detachedFromDom]"
})
export class AppDomAttachedDirective implements AfterViewChecked, OnDestroy {
#Output()
attachedToDom = new EventEmitter();
#Output()
detachedFromDom = new EventEmitter();
constructor(
private elemRef: ElementRef<HTMLElement>
) { }
private wasAttached = false;
private update() {
const isAttached = document.contains(this.elemRef.nativeElement);
if (this.wasAttached !== isAttached) {
this.wasAttached = isAttached;
if (isAttached) {
this.attachedToDom.emit();
} else {
this.detachedFromDom.emit();
}
}
}
ngAfterViewChecked() { this.update(); }
ngOnDestroy() { this.update(); }
}
It can be used like this:
<input type=text
(attachedToDom)="inputElem.focus()"
#inputElem />
If you check the console of your stackblitz, you see that the event is fired before pressing any button.
I can only think of that everything projected as will be initialized/constructed where you declare it.
So in your example right between these lines
<app-container [showContent]="showContentContainer">
{{test()}}
<app-input></app-input>
</app-container>
If you add a test function inside the app-container, it will get called immediatly. So <app-input> will also be constructed immediatly. Since ngAfterVieWInit will only get called once (https://angular.io/guide/lifecycle-hooks), this is where it will be called already.
adding the following inside AppInputComponent is a bit weird however
ngOnDestroy() {
console.log('destroy')
}
the component will actually be destroyed right away and never initialized again (add constructor or onInit log to check).
Related
Here is a stackBlitz demo.
I have a d3.js service file that builds my svg layout. Its a force directed d3 graph that has nodes. Each node carries its own data.
I have extracted that data into an array, capturing the ids of the nodes when selected. In my example, to select a node and capture the ID a user needs to hold/press Ctrl then click on a node.
This is done in a d3 .on click function within my angular service file.
Service.ts
export class DirectedGraphExperimentService {
public idArray = []
_update(_d3, svg, data): any {
...
svg.selectAll('.node-wrapper').on('click', function () {
if (_d3.event.ctrlKey) {
d3.select(this).classed(
'selected',
!d3.select(this).classed('selected')
);
const selectedSize = svg.selectAll('.selected').size();
if (selectedSize <= 2) {
svg
.selectAll('.selected')
.selectAll('.nodeText')
.style('fill', 'blue');
this.idArray = _d3.selectAll('.selected').data();
return this.idArray.filter((x) => x).map((d) => d.id);
}
}
});
...
}
}
My global variable this.idArray = [] does not update with the id strings therefore cant pass the array to the component like this this.directedGraphExperimentService.idArray its always [] empty.
component.ts
import { Component, OnInit, ViewChild, ElementRef, Input } from '#angular/core';
import { DirectedGraphExperimentService } from './directed-graph-experiment.service';
#Component({
selector: 'dge-directed-graph-experiment',
template: `
<style>
.selected .nodeText{
fill:red;
}
</style>
<body>
<button (click)="passValue()">Pass Value</button>
<svg #svgId width="500" height="700"><g [zoomableOf]="svgId"></g></svg>
</body>
`,
})
export class DirectedGraphExperimentComponent implements OnInit {
#ViewChild('svgId') graphElement: ElementRef;
constructor(
private directedGraphExperimentService: DirectedGraphExperimentService
) {}
ngOnInit() {}
#Input()
set data(data: any) {
this.directedGraphExperimentService.update(
data,
this.graphElement.nativeElement
);
}
passValue() {
console.log(this.directedGraphExperimentService.idArray); // returns []
}
}
I've also tried emitting an event with the value via the <svg></svg> container in the component template.
Is there another way I can get the updated this.array values into the parent component? A way to subscribe to the value in the function? Perhaps with a behaviourSubject from rxjs?
Here is a stackBlitz demo. In this demo you will see I have added a button that I press to trigger the update to my component file, obviously its only passing the empty global variable on my service. To add to the array you will need to press Ctrl and click on a node, you will see in console the array filling up(max of 2 string).
I have two components : TileComponent.ts and FullScreenComponent.ts.
On clicking the TileComponent, the FullScreenComponent opens up. In the TileComponent,I have the following code. ngOnInit() method gets triggered whenever the TileComponent loads.
TileComponent.ts:
ngOnInit() {
console.log("TileCompnent :ngOnInit");
this.crossDomainService.globalSelectors.subscribe(selectors => {
globalCountries = selectors.jurisdiction || [];
this.getArticles(globalCountries);
});
// Multiple Language
this.crossDomainService.globalLanguage.subscribe(() => {
console.log("TileCompnent :ngOnInit : crossDomainService");
this.getArticles(globalCountries || countries);
});
}
Now on closing the FullScreenComponent leads to the loading of the TileComponent but this time I see that ngOnInit() method is not getting triggered.
Can anyone help me to know any reason this is not working?
tile.component.html:
<div class="carousel-inner">
<a
(click)="openFullScreen(article)"
*ngFor="let article of articles"
[ngClass]="getItemClassNames(article)"
class="item"
>
</div>
tile.component.ts
ngOnInit() {
console.log("TileCompnent :ngOnInit");
const countries =
this.crossDomainService.initialGlobalSelectors &&
this.crossDomainService.initialGlobalSelectors.jurisdiction.length
? this.crossDomainService.initialGlobalSelectors.jurisdiction
: [];
this.getArticles(countries);
let globalCountries;
this.crossDomainService.globalSelectors.subscribe(selectors => {
globalCountries = selectors.jurisdiction || [];
this.getArticles(globalCountries);
});
// Multiple Language
this.crossDomainService.globalLanguage.subscribe(() => {
console.log("TileCompnent :ngOnInit : crossDomainService");
this.getArticles(globalCountries || countries);
});
}
openFullScreen(article: ArticlePreview) {
this.crossDomainService.openFullScreen(article);
}
full-screen.component.html:
<div class="layout-center-wrapper" [hidden]="isPolicyShown">
<app-header></app-header>
<div class="row wrapper">
<app-sidebar></app-sidebar>
<router-outlet></router-outlet>
</div>
</div>
<app-policy [hidden]="!isPolicyShown"></app-policy>
header.component.html:
<header class="row header">
<p class="header__title">
Application Name
<a (click)="closeFullScreen()" class="header__close">
<span class="icon icon_close"></span>
</a>
</p>
</header>
header.component.ts:
import { Component } from '#angular/core';
import { CrossDomainService } from '../../core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.less']
})
export class HeaderComponent {
constructor(private crossDomainService: CrossDomainService, private analyticsService: AnalyticsService) {}
closeFullScreen() {
this.crossDomainService.closeFullScreen();
}
}
ngOnInit lifecycle is only run when the view of the component is first rendered.
Since the old Tile Component is not destroyed and is always in the background even when the FullScreenComponent is displayed, the lifecycle hook never gets triggered even when you close the component.
( I am assuming you are not using the router to navigate, but use it as a popup since there is a close button as shown in the question )
Cannot help you isolate the issue or help you with suggestions unless you share some code. But the reason for ngOnInit not firing as per the question is because the component is not re-created.
Update :
I still can't realise why you need to trigger the ngOnInit ? If you just want to execute the code inside, make it a separate function say initSomething then call it inside ngOnInit to execute it the first time. Now if you just invoke this function on crossDomainService.closeFullScreen you get the desired effect.
To trigger the function whenever the closeFullScreen is called, you can create a Subject in the crossDomainService Service, and subscribe this subject it inside the ngOnInit(), and run the initSomething function mentioned above everytime it emits a value. Inside the closeFullScreen function, all you have to now do is do a Subject.next()
Pardon the brevity since I am away from my desk and typing on mobile, though the explanation should be enough to develop the code on your own.
One of the simple workaround would be to use changedetectorRef to hook up the initial state of component.
`import { Component, OnInit, ChangeDetectorRef } from '#angular/core';`
and insert it in constructor and you can keep OnInit function blank
constructor(){
this.changeDetectorRef.detectChanges();}
I am producing some divs using ngFor:
<div *ngFor='let img of images'>
I want to pass the div just produced to a function. At first, I tried to just pass a number to the function
<div *ngFor='let img of images' (being)="ToRunEachTime(img)">
where being is a directive like:
#Directive({
selector: '[being]'
})
export class beingDirective {
#Output() being: EventEmitter<any> = new EventEmitter<any>();
constructor() {}
ngOnInit() {
this.being.emit('anything');
}
}
This code works well and I am able to pass img (which is a number) each time to the function ToRunEachTime. However, I wish to pass the whole div which is just created by *ngFor to this function. I tried:
<div *ngFor='let img of images' (being)="ToRunEachTime($event)">
but the function
ToRunEachTime(event: Event){
var currentdiv= <HTMLDivElement>event.target;
}
gives me undefined. Any Help is appreciated!
In your directive you are emitting a string which does not contain the target property
export class beingDirective {
#Output() being: EventEmitter<any> = new EventEmitter<any>();
constructor(private el:ElementRef,private renderer:Renderer2) {}
ngOnInit() {
this.being.emit(this.el.nativeElement);
}
}
If you are looking to emit any Browser event you should be adding a HostListener as below and emit that event
#HostListener('mouseover', ['$event']) onClick(event) {
this.being.emit(event);
}
This will give you the target element. Updated the demo.
LIVE DEMO
I need to use the swipeup/swipedown gestures in an Ionic 2 application. When I do
<div (swipe)='someFunction($event)'></div>
Then my someFunction(e) is being called, but only on horizontal slides -- therefore I'm unable to listen to swipes in up and down directions. (swipeup) and (swipedown) seem not to do anything at all. Do you have any idea whether this is possible at all with the Ionic beta?
Ionic 2 makes use hammerjs library to handle its gestures.
They’ve also built their own Gesture class that effectively acts as a wrapper to hammerjs: Gesture.ts.
So you can do your own directive like:
import {Directive, ElementRef, Input, OnInit, OnDestroy} from 'angular2/core'
import {Gesture} from 'ionic-angular/gestures/gesture'
declare var Hammer: any
/*
Class for the SwipeVertical directive (attribute (swipe) is only horizontal).
In order to use it you must add swipe-vertical attribute to the component.
The directives for binding functions are [swipeUp] and [swipeDown].
IMPORTANT:
[swipeUp] and [swipeDown] MUST be added in a component which
already has "swipe-vertical".
*/
#Directive({
selector: '[swipe-vertical]' // Attribute selector
})
export class SwipeVertical implements OnInit, OnDestroy {
#Input('swipeUp') actionUp: any;
#Input('swipeDown') actionDown: any;
private el: HTMLElement
private swipeGesture: Gesture
private swipeDownGesture: Gesture
constructor(el: ElementRef) {
this.el = el.nativeElement
}
ngOnInit() {
this.swipeGesture = new Gesture(this.el, {
recognizers: [
[Hammer.Swipe, {direction: Hammer.DIRECTION_VERTICAL}]
]
});
this.swipeGesture.listen()
this.swipeGesture.on('swipeup', e => {
this.actionUp()
})
this.swipeGesture.on('swipedown', e => {
this.actionDown()
})
}
ngOnDestroy() {
this.swipeGesture.destroy()
}
}
This code allows you to do something like this:
<div swipe-vertical [swipeUp]="mySwipeUpAction()" [swipeDown]="mySwipeDownAction()">
Just an update, Ionic now has gesture controls. see
http://ionicframework.com/docs/v2/components/#gestures
gestures return an $event object. You can probably use this data to check whether if it's a swipeup/swipedown event.
See $event screenshot (since I can't attach images yet ;) )
In Angular 2 I am trying to animated in new components via the Router onActivate method.
I have set up a Plunk with a demonstration of the issue here:
http://plnkr.co/FikHIEPONMYhr6COD9Ou
An example of the onActivate method in one of the page components:
routerOnActivate(next: ComponentInstruction, prev: ComponentInstruction) {
document.getElementsByTagName("page3")[0].className='animateinfromright';
}
The issue that I'm having is that I want the new components to animate in on top of the existing component, but the old component is removed from the DOM before the new component is added.
Is there any way to delay the removal of the previous page while the new one animates in?
I found this similar issue: Page transition animations with Angular 2.0 router and component interface promises
but the technique just delays the removal of the previous component before the new one is added.
Eventually I will have different animations depending on which page we are moving from / to, hence having the onActivate in each of the page components.
Many thanks for any help!
You could add an "EchoComponent" where your <router-outlet> is, create a <canvas> in it and drawImage() on routerOnDeactivate()... Something like:
#Component({
template: `<canvas #canvas *ngIf="visible"></canvas>`
})
class EchoComponent {
#ViewChild("canvas") canvas;
public visible = false;
constructor(private _shared: SharedEmitterService) {
this._shared.subscribe(el => el ? this.show(el) : this.hide(el));
}
show(el) {
this.canvas.drawImage(el);
this.visible = true;
}
hide() {
this.visible = false;
}
}
#Component({...})
class PrevRoute {
constructor(private _eref: ElementRef,
private _shared: SharedEmitterService) {}
routerOnDeactivate {
this._shared.emit(this._eref.nativeElement);
}
}
#Component({...})
class NextRoute {
constructor(private _eref: ElementRef,
private _shared: SharedEmitterService) {}
routerOnActivate {
this._shared.emit(false);
}
}
This is just a pseudo code (writing it from memory), but it should illustrate what would you need for this approach.