I'm writing Angular 2 application and inside it I have dropdown menu written on Bootstrap
<li class="dropdown" dropdown>
<a class="dropdown-toggle" data-toggle="dropdown">
User <span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="download">
<li><a routerLink="/user/profile">My Profile</a></li>
<li><a (click)="logout()">Log Out</a></li>
</ul>
</li>
All what I want is to write down a small directive for toggling menu. End here is it:
#Directive({
selector: "[dropdown]"
})
export class DropdownDirective implements OnInit {
private isOpen = false;
private defaultClassName: string;
#HostListener('click') toggle() {
let that = this;
if (!this.isOpen) {
this.elRef.nativeElement.className = this.defaultClassName + " open";
document.addEventListener("click", () => {
that.elRef.nativeElement.className = that.defaultClassName;
that.isOpen = false;
document.removeEventListener("click");
});
this.isOpen = !this.isOpen;
}
}
constructor(private elRef: ElementRef) {
}
ngOnInit(): void {
this.defaultClassName = this.elRef.nativeElement.className;
}
}
Looks good. But doesn't work. After short debug I found that event listener, which was added to the document, fires just after it has been assigned.
document.addEventListener("click", () => {
that.elRef.nativeElement.className = that.defaultClassName;
that.isOpen = false;
document.removeEventListener("click");
});
As a fact menu closing just after it has been opened. How to fix it and why this happening?
I've solved this same situation with a #HostListener(). On the component holding the dropdown:
#HostListener('document:click', ['$event'])
private clickAnywhere(event: MouseEvent): void {
if (this.IsSelected && !this.elementRef.nativeElement.contains(event.target)) {
this.IsSelected = false;
}
}
this.IsSelected is the binding property I use to show the dropdown.
The condition in the if() is checking whether the user has clicked on the menu or the document body in general.
Make sure to inject elementRef into the constructor so you can access the rendered HTML to check if that is what was clicked:
public constructor(private elementRef: ElementRef) { }
You can find out more about HostListener here.
Related
How can I add an eventlistener on a <div> or other element, to hide something I am displaying via an *ngIf - using Angular, when I click away from that element?
Explanation: I am showing a custom CSS dropdown via *ngIf when you click on <label>Filter</label>, and I want the user to be able to click as many times as they wish in the custom dropdown, but when they click outside the custom dropdown, I would like to hide the custom dropdown via the *ngIf again.
The method called when a user clicks on the label is showHideSectionOptions(), which toggles the showHide variable to true or false.
This is my HTML code:
showHide = false;
<div class="form-row">
<div class="form-group" id="showAndHideSections">
<label (click)="showHideSectionOptions()">
<img src="../../../assets/icons/Filter.png" alt="" class="mr-3">Filter</label>
<div *ngIf="showHide" class="section-options">
// show or hide content
</div>
</div>
</div>
This is my component code:
showHideSectionOptions() {
this.showHide = !this.showHide;
}
I have tried adding an eventlistener as per the below, but I cannot set the value of my showHide variable, as I get the following error: Property 'showHide' does not exist on type 'HTMLElement'.ts(2339):
body.addEventListener('click', function() {
alert('wrapper');
}, false);
except.addEventListener('click', function(ev) {
alert('except');
ev.stopPropagation();
}, false);
Thanks in advance!
First of all, this already has an answer here
However, if you want an Angular solution, you can use a custom directive:
#Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
#Output()
readonly clickOutside = new EventEmitter<MouseEvent>();
#Input()
include?: HTMLElement;
constructor(private el: ElementRef<HTMLElement>) {}
#HostListener('window:click', [ '$event' ])
onClick(event: MouseEvent): void {
if (this.isEventOutside(event)) {
this.clickOutside.emit(this.event);
}
}
private isEventOutside(event: MouseEvent): boolean {
const target = event.target as HTMLElement;
return !this.el.nativeElement.contains(target) &&
(!this.include || !this.include.contains(target))
}
}
Which you can use like this:
<div class="form-group" id="showAndHideSections">
<label (click)="showHideSectionOptions()" #label>
<img src="../../../assets/icons/Filter.png" alt="" class="mr-3">
Filter
</label>
<div *ngIf="showHide" class="section-options"
[include]="label" (clickOutside)="showHide = false">
// show or hide content
</div>
</div>
A more performant one would be one running outside of the ngZone. Because the subscribe happens outside of the directive it will be inside the ngZone when subscribing to the Output
#Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
#Input()
include?: HTMLElement;
#Output()
readonly clickOutside = this.nz.runOutsideAngular(
() => fromEvent(window, 'click').pipe(
filter((event: MouseEvent) => this.isEventOutside(event))
)
);
constructor(private el: ElementRef<HTMLElement>, private nz: NgZone) {}
private isEventOutside(event: MouseEvent): boolean {
const target = event.target as HTMLElement;
return !this.el.nativeElement.contains(target) &&
(!this.include || !this.include.contains(target))
}
}
working stack
I have 3 components 'starter-left-side', 'starter-content', 'camera'. 1 service 'generalParameters'. In the generalParameters, I have 2 properties; 'contentHeader' & contentDescription which has default string values respectively.
Upon initialization of the starter-content, I get these values from the generalParameters and render it in starter-content.html. When I want to go to the camera.component, I just click on the link to the camera.component via the starter-left-side also, I have a method in the starter-left-side that sets the property value of the generalProperties as soon as the link is clicked so it can be used by the starter-content again.
I can successfully change the values in the generalProperties but the problem is, it is not rendered in the starter-component anymore. I do not know on which time of the life cycle hooks should I get the values from the generalProperties again so it can be rendered in the starter-content.html.
generaParameters.service.ts
contentHeader: string;
contentDescription: string;
constructor() {
this.contentHeader = "Dashboard";
this.contentDescription = "This is your dashboard";
}
starter-content.component.html
<h1>
{{pageHeader}}
<small>{{description}}</small>
</h1>
starter-content.component.ts
pageHeader: string;
description: string;
constructor(
private gp: GeneralparametersService
) { }
ngOnInit() {
this.pageHeader = this.gp.contentHeader;
this.description = this.gp.contentDescription;
}
starter-left-side.component.ts
setContent(header, description) {
this.gp.contentHeader = header;
this.gp.contentDescription = description;
}
starter-left-side.component.html
<li class="active"><i class="fa fa-link"></i> <span>Camera</span></li>
Thank you very much for your help.
Since you are communicating using a service you can propagate your changes using an Subject
When you make changes to your subject via the gp.setContent since your other component is observing the changes they will be automatically updated.
I used pluck so that we can only take the properties we need and render them separately.
See my implementation. Hope it helps!!!
starter-left-side.component.html
<li class="active"><i class="fa fa-link"></i> <span>Camera</span></li>
generaParameters.service.ts
import { Subject } from 'rxjs';
private mycontent$ = new Subject();
public content$ = this.mycontent$.asObservable();
setContent(header, description) {
this.content$.next({header, description});
}
starter-content.component.ts
import { pluck } from 'rxjs/operators';
ngOnInit(): void {
this.pageHeader$ = this.gp.content$.pipe(pluck('header'));
this.pageDescription$ = this.gp.content$.pipe(pluck('description'));
}
starter-content.component.html
<h1>
{{pageHeader$ | async }}
<small>{{pageDescription$ | async}}</small>
</h1>
Use a Subject or BehaviorSubject in your Service. Thus, all components get updated when the value changes:
generaParameters.service.ts
import {BehaviorSubject, Observable} from 'rxjs';
contentHeader: BehaviorSubject<string> = new BehaviorSubject('Dashboard');
contentDescription: BehaviorSubject<string> = new BehaviorSubject('This is your dashboard');
constructor() {}
public getContentHeader(): Observable<string> {
return this.contentHeader.asObservable();
}
public setContentHeader(value: string): void {
this.contentHeader.next(value);
}
public getContentDescription(): Observable<string> {
return this.contentDescription.asObservable();
}
public setContentDescription(value: string): void {
this.contentDescription.next(value);
}
starter-content.component.html
<h1>
{{pageHeader}}
<small>{{description}}</small>
</h1>
starter-content.component.ts
pageHeader: string;
description: string;
constructor(
private gp: GeneralparametersService
) { }
ngOnInit() {
this.gp.getContentHeader().subscribe(value => {
this.pageHeader = value;
});
this.gp.getContentDescription().subscribe(value => {
this.contentDescription = value;
});
}
starter-left-side.component.ts
ngOnInit() {
this.gp.getContentHeader().subscribe(value => {
this.pageHeader = value;
});
this.gp.getContentDescription().subscribe(value => {
this.contentDescription = value;
});
}
setContent(header, description) {
this.gp.setContentHeader(header);
this.gp.setContentDescription(description);
}
starter-left-side.component.html
<li class="active"><i class="fa fa-link"></i> <span>Camera</span></li>
I have a navigation: Log in, Sign up, etc.
I have implemented sign up with Google in angular 2 and after I go through Google I want that my navigation dynamically changed on Logout, etc.
My nav in app.component.html
<ul id="navigation-menu">
<li routerLinkActive="active"><a routerLink="/about">About</a></li>
<li routerLinkActive="active"><a routerLink="/contact_us">Contact us</a></li>
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}" *ngIf="logged">
<a routerLink="/login" class="loginLink">Log in</a>
</li>
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}" *ngIf="logged">
<a routerLink="/signin" class="signLink">Sign up</a>
</li>
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}" *ngIf="!logged">
<a routerLink="/uprofile">Profile</a>
</li>
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}" *ngIf="!logged">
<a routerLink="/bprofile">BProfile</a>
</li>
<li *ngIf="!logged"><a routerLink="/login" class="loginLink" (click)="logout()">Logout</a></li>
</ul>
In my app.component.ts I use lifecycle hook ngDoCheck and check localStorage. If it is not empty, I change navigation.
My app.component.ts
export class AppComponent implements DoCheck {
logged: boolean = true;
changeMenuLink() {
if (localStorage.getItem("currentUser")) {
this.logged = false;
}
}
ngDoCheck() {
this.changeMenuLink();
}
When I enter via Google, page redirect to the search page, but nav doesn't change. Menu changes only after clicking on the logo or on another menu item.
fb-gplus-api.component.ts
public auth2: any;
public googleInit() {
gapi.load('auth2', () => {
this.auth2 = gapi.auth2.init({
client_id: 'APP_ID.apps.googleusercontent.com', // your-app-id
cookiepolicy: 'single_host_origin',
scope: 'profile email'
});
this.attachSignin(document.getElementById('googleBtn'));
});
}
public attachSignin(element) {
this.auth2.attachClickHandler(element, {},
(googleUser) => {
let profile = googleUser.getBasicProfile();
let userToken: SocialLogin = new SocialLogin();
userToken.uid = profile.getId();
userToken.token = googleUser.getAuthResponse().id_token;
this.httpToken.postToken(userToken)
.toPromise()
.then(resp => {
if (resp.status === 'OK') {
this.checkStatus(userToken);
}
})
},
(error) => {
alert(JSON.stringify(error, undefined, 2));
}
);
}
checkStatus(user) {
let token = this.randomToken.generateToken(40);
localStorage.setItem('currentUser', JSON.stringify({uid: user.uid, token: token}));
alert("Login success! Have a nice day!");
this.router.navigate(['/search']);
}
ngAfterViewInit(){
this.googleInit();
}
I think the problem with the change of menu starts after use ngAfterViewInit(). I really don't understand how to solve this problem. How can I do this?
Regards
That's happen because you are doing some action outside of ngZone. To solve this issue first import ngZone:
import {NgZone} from "#angular/core";
then inject it into component that doing async call for google login:
constructor(private zone: NgZone)
finally run the handling of all angular2 variables that you doing in callback inside ngzone:
(googleUser) => {
this.zone.run( () => {
....
});
}
I have Component which has a member array variable. This array is bind to DOM with *ngFor. When I add new variable to array my view changes accordingly. Array holds tab names and initially it is set to have only 1 tab. When I refresh page array reinitialized which is what I was expecting. But when I logout and then log back in(router navigation) I see all previous tabs. It is weird to me, because if I console.log(myTabs) array has only 1 element(homeTab).
UPDATE:
.html
<div style="display: table-caption" id="notify-tabs">
<ul class="nav nav-tabs" role="tablist" id="nav-bar">
<li role="presentation" data-toggle="tab" id="homeTab" [class.active]="activeTab==='homeTab'"><a (click)="setValues('home')">Home</a>
<li role="presentation" *ngFor="let tab of myTabs" data-toggle="tab" id={{tab}} [class.active]="activeTab===tab.toString()"><a (click)="setValues(tab)">{{tab}}</a>
</ul>
</div>
.component.ts
#Component({
selector: 'notify-homepage',
templateUrl: 'app/home/home.component.html',
styleUrls: ['styles/css/bootstrap.min.css', 'styles/home.css'],
directives: [DynamicComponent, TileComponent, MapComponent, HeaderComponent, ConversationComponent, ROUTER_DIRECTIVES]
})
export class HomeComponent{
public myTabs: number[] = [21442];
public activeTab: string = 'homeTab';
ngOnInit() {
//Assume fully operating MapService here
this.subscription = this.mapService.conversationId.subscribe(
(id: number) => {
this.myTabs.push(id);
this.setValues(id);
this.activeTab = id.toString();
})
}
ngOnDestroy() {
this.subscription.unsubscribe();
...
}
}
map.service.ts
#Injectable()
export class MapService {
private conversationIdSource = new ReplaySubject<number>();
public conversationId = this.conversationIdSource.asObservable();
...
showConversation(id: number) {
this.conversationIdSource.next(id);
}
}
The answer of #Andrei works, but in my opinion there's a better and more elegant solution.
Just use a combination of #ViewChild() and setters.
For example:
// component.html
<ng-el ... #myElement>
// component.ts
#ViewChild('myElement') set(el) {
if (el) {
console.log('element loaded!');
}
}
Check Lifecycle hooks:
OnChanges https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#onchanges
DoCheck https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#docheck
They help tracking changing in Input and local variables.
OnChanges for Input variables:
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
for (let propName in changes) {
let chng = changes[propName];
let cur = JSON.stringify(chng.currentValue);
let prev = JSON.stringify(chng.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
DoCheck for everything:
ngDoCheck() {
if (this.hero.name !== this.oldHeroName) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
this.oldHeroName = this.hero.name;
}
}
I'm working on an Application with a lot of dropdowns, I would like to be able to close the dropdown whenever a click happens outside of this one.
I found some good solutions, but none of them handle the case of having a ngFor in it, when I log the click event target in the ngFor, I get the element but this one doesn't have any parent. I can not detect it with 'find' or 'contains' neither.
Does someone have a solution to detect if this target is part of the dropdown ?
the directive
import {
Directive,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
SimpleChange
} from '#angular/core';
#Directive({selector: '[clickOutside]'})
export class ClickOutside implements OnInit {
#Output() clickOutside:EventEmitter<Event> = new EventEmitter<Event>();
constructor(private _el:ElementRef) {
this.onClickBody = this.onClickBody.bind(this);
}
ngOnInit() {
document.body.addEventListener('click', this.onClickBody);
}
private onClickBody(e:Event) {
if (!this.isClickInElement(e)) {
this.clickOutside.emit(e);
}
}
private isClickInElement(e:any):boolean {
var current = e.target;
do {
console.log(current);
if ( current === this._el.nativeElement ) {
return( true );
}
current = current.parentNode;
} while ( current );
return false;
}
}
Example of where I call the directive
<div (clickOutside)="onClickedOutside($event)">
<ul>
<li *ngFor="let item of itemsList" (click)="selectItem(item)">
<span class="item">
{{item.name}}
</span>
</li>
</ul>
</div>
When I click on item.name, console.log(current); returns me two lines
<span>Item</span>
<li>
<span>Item</span>
</li>
#Directive({selector: '[clickOutside]'})
export class ClickOutside implements OnInit {
#Output() clickOutside:EventEmitter<Event> = new EventEmitter<Event>();
constructor(private _eref: ElementRef) { }
#HostListener('window:click')
private onClickBody(e:Event) {
if (!this.isClickInElement(e)) {
this.clickOutside.emit(e);
}
}
private isClickInElement(e:any):boolean {
return this._eref.nativeElement.contains(event.target);
}
}
See also https://stackoverflow.com/a/35713421/217408
This solution works with Chrome but unfortunately not with IE. I'm still looking for another way to do it
private isClickInElement(e:any):boolean {
var current = e.target;
if(current == this._el.nativeElement) {
return true;
}
for(let parentKey in e.path) {
if(e.path[parentKey] == this._el.nativeElement) {
return true;
}
}
return false;
}