Angular2 starting host listeners on button click

I need to call the hosts after a specific button is clicked. Host listeners then highlight any hovering element on the page and listen for mouse clicks that open the modal. The problem is that when I start listening to mouse clicks and click, the modal sometimes won't open until I click the button that starts the host listeners. Also, the selected items "get stuck" and remain selected after clicking the mouse while trying to open the modal.

Is this an asynchronous problem? Any idea how to fix this please?

Highlight.directive

import { Directive, ElementRef, HostListener, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';

import { FeedbackService } from './feedback.service';

import {Observable} from 'rxjs/Rx';

@Directive({
  selector: 'a, abbr, address, article, body, br, button, div, form, h1, h2, h3, h4, h5, h6, header, hr, i, iframe, img, ' +
  'input, label, li, link, meta, nav, object, ol, option, output, p, param, pre, section, select, small, source, span,' +
  'summary, table, tbody, td, textarea, tfoot, th, thead, time, title, tr, u, ul, video'
})
export class HighlightDirective {
    elementsArray: string[];
    listening: boolean = false;
    allowClick: boolean = false;

    @Output() notifyParent: EventEmitter<any> = new EventEmitter();

    @Input() start: boolean;

    constructor(private el: ElementRef, private feedbackService: FeedbackService) {
        this.elementsArray = ["a", 'abbr', 'address', 'article', 'body', 'br', 'button', 'div', 'form', 'h1', 'h2', 'h3', 'h4', 'h5'
        , 'h6', 'header', 'hr', 'i', 'iframe', 'img', 'input', 'label', 'li', 'link', 'meta', 'nav', 'object', 'ol', 'option'
        , 'output', 'p', 'param', 'pre', 'section', 'select', 'small', 'source', 'span', 'summary', 'table', 'tbody', 'td'
        , 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'u', 'ul', 'video'];

        feedbackService.myBool$.subscribe((newBool: boolean) => { this.listening = newBool; });
    }

    //check: boolean = false;

    ngOnChanges(changes: SimpleChanges) {
        console.log(changes);
        this.listening = true;
    }

    public getElement(): ElementRef {
        return this.el;
    }

    public startFeedback(): void {
        this.listening = true;
    }

    ngOnInit() {
        //Observable.fromEvent(document, 'mouseenter').subscribe(data => {});
    }

    //@HostListener('click') onClick() {
    @HostListener('document:click', ['$event.target']) onClick(targetElement) {
        //if (this.listening && this.allowClick) {
        if (this.feedbackService.boolSubject.getValue() == true && this.allowClick) {
            //document.getElementById('feedbackButton').click();

            console.log(11);
            this.notifyParent.emit(targetElement);

            //this.feedbackService.boolSubject.next(false);

            this.el.nativeElement.style.boxShadow = null;
            this.listening = false;
            this.start = false;
            this.allowClick = false;
        }
    }

    @HostListener('mouseenter', ['$event.target']) onMouseEnter(targetElement) {
        //if(this.listening) {
        if (this.feedbackService.boolSubject.getValue() == true) {

            if(!this.allowClick)
                this.allowClick = true;

            targetElement.parentNode.style.boxShadow = null;  
            if(targetElement.className != 'container')
                targetElement.style.boxShadow = '0 0 0 5px yellow';
        }
    }

    @HostListener('mouseleave', ['$event.target']) onMouseLeave(targetElement) {
        //if(this.listening) {
        if (this.feedbackService.boolSubject.getValue()) {
            targetElement.style.boxShadow = null;
            if(targetElement.parentNode.className != 'container')
                targetElement.parentNode.style.boxShadow = '0 0 0 5px yellow';

            let check = false;

            for (let entry of this.elementsArray) {
                if (targetElement.parentNode.nodeName == entry.toUpperCase()) {
                    check = true;
                    break;
                }
            }

            if (!check)
                targetElement.parentNode.style.boxShadow = null;
        }
    }
}

      

Feedback.service

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import {AsyncSubject} from 'rxjs/Rx';

@Injectable()
export class FeedbackService {
    myBool$: Observable<boolean>;

    public boolSubject: BehaviorSubject<boolean>;

    private checkValue: boolean = false;

    constructor() {
        this.boolSubject = new BehaviorSubject<boolean>(false);
        this.myBool$ = this.boolSubject.asObservable();
    }

    startFeedback(): void {
        this.boolSubject.next(true);
        //this.checkValue = true;
    }

    endFeedback(): void {
        this.boolSubject.next(false);
        //this.checkValue = false;
    }

    getValue(): boolean {
        //this.boolSubject.next(true);
        return this.checkValue;
    }
}

      

App.component

import { Component, ViewChild, ElementRef, QueryList, Input, ViewChildren, ContentChildren } from '@angular/core';
import { AuthService } from './auth.service';

import { HighlightDirective } from './highlight.directive';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import { ModalComponent } from './modal.component';
import { FeedbackModalComponent } from './feedbackModal.component';
import { FeedbackService } from './feedback.service';


@Component({
  moduleId: module.id,
  selector: 'my-app',
  template: `
  <div class="container">
    <h1 myHighlight="orange">{{title}}</h1>
    <nav>
      <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
      <a routerLink="/secret-heroes" *ngIf="authService.loggedIn()" routerLinkActive="active">Secret Heroes</a>
      <a (click)=authService.login() *ngIf="!authService.loggedIn()">Log In</a>
      <a (click)=authService.logout() *ngIf="authService.loggedIn()">Log Out</a>
      <a (click)=giveFeedback() (notifyParent)="getNotification($event)">Give Feedback</a>

    <my-feedback-modal>
    </my-feedback-modal>

      </nav>
      <router-outlet></router-outlet>
    </div>
  `,
  styleUrls: ['app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
  startFeedback = false;
  feedbackElement: ElementRef;

  @ViewChildren(HighlightDirective) highlightDirs: QueryList<HighlightDirective>;

  @ViewChild(FeedbackModalComponent) feedbackModal: FeedbackModalComponent;

  constructor(private authService: AuthService, private el: ElementRef, private feedbackService: FeedbackService) {  }

  clickedElement:BehaviorSubject<ElementRef> = new BehaviorSubject(this.el);

  ngAfterViewInit() {
    //this.clickedElement.next(this.highlightDir.getElement().nativeElement.nodeName);
  }

  getNotification(evt) {
        // Do something with the notification (evt) sent by the child!
      console.log(evt);
      this.feedbackModal.show(evt);
    }

  giveFeedback(): void {
      //this.startFeedback = true;
//    this.highlightDirs.forEach((highlightDir: HighlightDirective) => {
//       highlightDir.startFeedback();
//    });
    this.feedbackService.startFeedback();
  }
}

      

FeedbackModal.component

import { Component, ViewChild, ElementRef, Input } from '@angular/core';

import { ModalComponent } from './modal.component';

import { Hero } from './hero';
import { HeroService } from './hero.service';
import { Router } from '@angular/router';

import { FeedbackService } from './feedback.service';

@Component({
  moduleId: module.id,
  selector: 'my-feedback-modal',
  template: `
  <div class="modal fade" tabindex="-1" [ngClass]="{'in': visibleAnimate}"
      [ngStyle]="{'display': visible ? 'block' : 'none', 'opacity': visibleAnimate ? 1 : 0}">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          Element <{{ elementName }}>
        </div>
        <div class="modal-body">
            <div id="first-row"></div><br>
          <form action="">
            <label for="rating">Rating</label>
            <div class="row" >
             <div class="col-xs-12">
                <label class="radio-inline">
                <input type="radio" name="inlineRadioOptions" id="inlineRadio1" value="option1"> 1
                </label>
                <label class="radio-inline">
                <input type="radio" name="inlineRadioOptions" id="inlineRadio2" value="option2"> 2
                </label>
                <label class="radio-inline">
                <input type="radio" name="inlineRadioOptions" id="inlineRadio3" value="option3"> 3
                </label>
                <label class="radio-inline">
                <input type="radio" name="inlineRadioOptions" id="inlineRadio4" value="option4"> 4
                </label>
                <label class="radio-inline">
                <input type="radio" name="inlineRadioOptions" id="inlineRadio5" value="option5"> 5
                </label>
                </div>
            </div>
            <br>
            <label for="comment">Comment</label>
            <textarea class="form-control" [(ngModel)]="hero.name" placeholder="name" name="name" rows="3"></textarea><br>
          </form>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" (click)="hide()">Close</button>
            <button type="button" class="btn btn-primary" (click)="save()">Save changes</button>
        </div>
      </div>
    </div>
  </div>
  `,
  styleUrls: ['modal.component.css']
})

export class FeedbackModalComponent {
  public visible = false;
  private visibleAnimate = false;
  private elementName: any;
  private appendingElement: any;

    @Input() hero: Hero;
    error: any;

  @ViewChild(ModalComponent) modal: ModalComponent;

  constructor(private el: ElementRef, private heroService: HeroService,
    private router: Router, private feedbackService: FeedbackService) {  }

  ngAfterViewInit() {
    //this.clickedElement.next(this.highlightDir.getElement().nativeElement.nodeName);
  }

  ngOnInit(): void {
      this.hero = new Hero();
  }

  public show(el: any): void {
      this.feedbackService.boolSubject.next(false);
    //this.feedbackService.endFeedback();

    this.el = el;
    this.elementName = el.nodeName;

//    this.appendingElement = document.getElementById('first-row');
//    let cloneEl = el.cloneNode(true);
//    this.appendingElement.appendChild(cloneEl);

    this.visible = true;
    setTimeout(() => this.visibleAnimate = true);
  }

  public hide(): void {
//      this.appendingElement.removeChild(this.appendingElement.firstChild);

    this.visibleAnimate = false;
    setTimeout(() => this.visible = false, 300);
  }

  save(): void {
    this.heroService
        .save(this.hero)
        .then(hero => {
          this.hero = hero; // saved hero, w/ id if new
          if(this.router.url == '/heroes')
            window.location.reload();
          else
            this.hide();
        })
        .catch(error => this.error = error); // TODO: Display error message
  }
}

      

Any help would be greatly appreciated.

+1


source to share


1 answer


I managed to figure it out. The only working solution I've found seems to reverse all service and directives and recycle app.component by adding Renderers that listen for any changes to the document. It also makes the code tidier, but I don't know if that's good practice.

App.component updated



import { Component, ViewChild, ElementRef, Renderer, OnDestroy } from '@angular/core';
import { AuthService } from './auth.service';

import { FeedbackModalComponent } from './feedbackModal.component';
import { Router } from '@angular/router';
import { NotificationsService } from 'angular2-notifications';


@Component({
    moduleId: module.id,
    selector: 'my-app',
    template: `
    <div class="container">
        <h1 myHighlight="orange">{{title}}</h1>
        <nav>
            <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
            <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
            <a routerLink="/secret-heroes" *ngIf="authService.loggedIn()" routerLinkActive="active">Secret Heroes</a>
            <a (click)=authService.login() *ngIf="!authService.loggedIn()">Log In</a>
            <a (click)=authService.logout() *ngIf="authService.loggedIn()">Log Out</a>
            <a (click)=giveFeedback() (notifyParent)="getNotification($event)">Give Feedback</a>

            <my-feedback-modal>
            </my-feedback-modal>

            </nav>
            <router-outlet></router-outlet>

            <simple-notifications [options]="options"></simple-notifications>
      </div>
    `,
    styleUrls: ['app.component.css']
})
export class AppComponent {
    title = 'Tour of Heroes';
    public options = {
        timeOut: 0,
        showProgressBar: true,
        pauseOnHover: false,
        clickToClose: true,
        maxLength: 50,
        animate: "fromRight"
    };

    lastEvent: any;
    listening: boolean = false;
    allowClick: boolean = false;
    globalListenFunc: Function;
    globalListenFunc2: Function;

    @ViewChild(FeedbackModalComponent) feedbackModal: FeedbackModalComponent;

    constructor(private authService: AuthService, private el: ElementRef, private router: Router, 
        private notificationsService: NotificationsService, private renderer: Renderer) 
    {  
        this.lastEvent = el;
        this.lastEvent.style = [];
        this.lastEvent.style.boxShadow = null;
    }

    addListeners() {
        this.globalListenFunc = this.renderer.listenGlobal('document', 'click', (event) => {
            if (this.listening == true && this.allowClick) {
                this.notificationsService.remove();
                this.feedbackModal.show(event.srcElement);

                // FIND A BETTER FIX
                this.router.navigateByUrl('/dashboard');

                this.el.nativeElement.style.boxShadow = null;
                this.listening = false;
                this.allowClick = false;

                this.removeListeners();
            }
        });

        this.globalListenFunc2 = this.renderer.listenGlobal('document', 'mousemove', (event) => {
            if (this.listening == true && this.lastEvent != event.srcElement) {
                if(!this.allowClick)
                    this.allowClick = true;

                this.lastEvent.style.boxShadow = '';
                if(event.srcElement.className != 'container') {
                    event.srcElement.style.boxShadow = '0 0 0 5px yellow';
                }
                this.lastEvent = event.srcElement;
            }
        });
    }

    removeListeners() {
        this.globalListenFunc();
        this.globalListenFunc2();
    }

    ngOnDestroy() {
        // Remove the listeners!
        this.globalListenFunc();
        this.globalListenFunc2();
    }

    giveFeedback(): void {
        this.notificationsService.info(
            'Feedback',
            "Choose an element you would like to rate.",
            this.options
        )
        this.listening = true;
        this.addListeners();
    }
}

      

0


source







All Articles