TSR
TSR

Reputation: 20416

Click outside component not working | Angular

I made a Stackblitz for my code (link: https://stackblitz.com/edit/angular-iah7up ). This is my custom component that toggles two ng-content based on click in and clicks out.

import {Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output} from '@angular/core';
import {fromEvent, Subject} from "rxjs";
import {untilDestroyed} from "ngx-take-until-destroy";
import {filter, switchMapTo, take} from "rxjs/operators";

@Component({
    selector: 'app-editable-inplace',
    templateUrl: './editable-inplace.component.html',
    styleUrls: ['./editable-inplace.component.css']
})
export class EditableInplaceComponent implements OnInit, OnDestroy {

    @Output() update = new EventEmitter();
    mode: 'view' | 'edit' = 'view';
    editMode = new Subject();
    editMode$ = this.editMode.asObservable();


    ngOnInit(): void {
        this.viewModeHandler();
        this.editModeHandler();
    }

    constructor(private host: ElementRef) {
    }

    // This method must be present, even if empty.
    ngOnDestroy() {
        // To protect you, we'll throw an error if it doesn't exist.
    }

    private viewModeHandler() {
        fromEvent(this.element, 'click').pipe(
            untilDestroyed(this)
        ).subscribe(() => {
            console.log('clicked inside')
            this.editMode.next(true);
            this.mode = 'edit';

        });
    }

    private editModeHandler() {
        const clickOutside$ = fromEvent(document, 'click').pipe(
            filter(({target}) => {
                console.log(this.mode)
                console.log('parent', this.element, 'child', target)

                const ans = this.element.contains(target) === false
                console.log('clickoutside', ans)
                return ans
            }),
            take(1)
        )

        this.editMode$.pipe(
            switchMapTo(clickOutside$),
            untilDestroyed(this)
        ).subscribe(event => {

            this.update.next();
            this.mode = 'view';
        });
    }

    get element() {
        return this.host.nativeElement;
    }

    toViewMode() {
        this.update.next();
        this.mode = 'view';
    }

}

Template:

<div *ngIf="mode==='view'" >
    <ng-content select="[view]"></ng-content>
</div>
<div *ngIf="mode==='edit'" >
    <ng-content select="[edit]"></ng-content>
</div>

Usage:

<app-editable-inplace >
    <div view>
       <h3>Click to edit [not working :-( ]</h3>

    </div>
    <div edit>

              <input placeholder="Click to edit"   >

    </div>
</app-editable-inplace>

But When I click on the view, clickOutside$ is triggered instantly (I don't know why) And also, the line this.element.contains(target) === false is not working because I can see in the console that the host contains the clicked item although always say that I clicked outside (I don't know why too)

enter image description here

Upvotes: 2

Views: 994

Answers (2)

yurzui
yurzui

Reputation: 214047

In browser many events are bubbling(and click event is one of them). It means that event goes from the target element straight up. And if you registered event with the same name somewhere above then it will be fired nevermind what happend in target handler.

 document  =====================================================>  outsideHandler
  body                                                          /\
    ....                                                        ||  
    <app-editable-inplace>                                   bubbling
      <div view>                                                /\
        <h3>Click to edit [not working :-( ]</h3> <===== target || viewClickHandler
      </div>
      ...
    </app-editable-inplace>

So, as you have already guessed by this reason clickOutside$ is triggered instantly.

There are different ways you can use to fix it:

1) The most simple way is to use event.stopPropagation() so that the event won't be bubbled further.

private viewModeHandler() {
    fromEvent(this.element, 'click').pipe(
        untilDestroyed(this)
    ).subscribe((e: any) => {
        console.log('clicked inside');
        e.stopPropagation(); <==================== add this

2) Since the bubbling event is the same in those handlers you can set some flag to prevent handling it in top handler.

 private viewModeHandler() {
    fromEvent(this.element, 'click').pipe(
        untilDestroyed(this)
    ).subscribe((e: any) => {
        console.log('clicked inside');
        e.fromInside = true;


 ...
 const clickOutside$ = fromEvent(document, 'click').pipe(
  filter((e: any) => {
    return !e.fromInside && this.element.contains(e.target) === false
  }),

3) Catch click event in capture phase:

fromEvent(this.element, 'click', { capture: true }).pipe(

... 

const clickOutside$ = fromEvent(document, 'click', { capture: true })

Upvotes: 1

Megh
Megh

Reputation: 135

It is because you have bind your click event to entire document.

due to that your click outside counter increment.

all you have to do is to check click control event and then prevent propagation.

please review this article to understand more about events.

https://medium.com/@vsvaibhav2016/event-bubbling-and-event-capturing-in-javascript-6ff38bec30e

Upvotes: 0

Related Questions