Click outside component not working | Angular

Viewed 704

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

2 Answers

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 })
Related