javascript - Händelsedelegering i Angular2


Jag utvecklar en app i ng2 och jag har problem med något. Jag bygger en kalender där du kan välja ett datumintervall och jag behöver reagera på click & mouseenter/mouseleave-händelser på dagsceller. Så jag har en kod (förenklad) som denna:

calendar.component.html

<month>
    <day *ngFor="let day of days" (click)="handleClick()" 
         (mouseenter)="handleMouseEnter()" 
         (mouseleave)="handleMouseLeave()" 
         [innerHTML]="day"></day>
</month>

Men detta ger mig hundratals separata händelselyssnare i webbläsarens minne (varje dags cell får 3 händelselyssnare, och jag kan ha upp till 12 månader visade samtidigt, så det skulle bli över 1 000 lyssnare).

Så jag ville göra det "på rätt sätt", med metoden som kallas "event delegation". Jag menar, fästa ett klickhändelse på föräldrakomponenten (month) och när den får en klickhändelse, helt enkelt kontrollera om den inträffade på Day-komponenten - och först då skulle jag reagera på detta klick. Något som jQuery gör i sin on()-metod när du skickar med selector-parametern.

Men jag gjorde det genom att referera till DOM-elementen direkt i handlarkodens kod:

månad.component.ts

private handleClick(event) {
    if (event.target.tagName === 'DAY') {
        // handle day click
    } else {
        // handle other cases
    }
}

och mina kollegor avvisade min idé, eftersom - som de sa - "Det måste finnas ett enklare, korrekt sätt att hantera detta i NG2; som det finns i jQuery. Dessutom håller det på att spåra ur här - du reagerar på Days klick i Months kod."

Så, min fråga är, finns det ett bättre sätt? Eller försöker jag lösa ett problem som jag inte borde bry mig om längre, eftersom användarnas enheter får mer och mer minne/processorkraft varje dag?

Tack i förväg!

Svar #1

Intro

Jag snubblade över detta idag, och jag kan verkligen se behovet av denna implementation i många applikationer. Nu kan jag inte garantera att detta är 100% den bästa tekniken, men jag har gjort allt jag kan för att göra detta tillvägagångssätt så Angular-inspirerat som möjligt.

Tillvägagångssättet som jag har kommit på har 2 steg. Både steg 1 och steg 2 kommer att lägga till totalt years * months + years * months * days, så för 1 år kommer du att ha 12 + 365 händelser.


Steg Omfattning

Steg 1: Delegera händelser från när en månad klickas ner till den faktiska dagen som klickades utan att kräva en händelse på dagen.
Steg 2: Sprid den valda dagen tillbaka till månaden.

Strax innan vi går in på detaljer består applikationen av 3 komponenter som är inbäddade i följande ordning: app => month => day

Detta är all HTML som krävs. app.component innehåller ett antal månader, month.component innehåller ett antal dagar och day.component gör inget annat än att visa sin dag som text.

app.component.html

<app-month *ngFor="let month of months" [data-month]="month"></app-month>

månad.component.html

<app-day *ngFor="let day of days" [data-day]="day">{{day}}</app-day>

day.component.html

<ng-content></ng-content>

Det här är ganska standardgrejer.

Steg 1

Låt oss titta på month.component.ts där vi vill delegera vår händelse från.

// obtain a reference to the month(this) element 
constructor(private element: ElementRef) { }

// when this component is clicked...
@HostListener('click', ['$event'])
public onMonthClick(event) {
  // check to see whether the target element was a child or if it was in-fact this element
  if (event.target != this.element.nativeElement) {
    // if it was a child, then delegate our event to it.
    // this is a little bit of javascript trickery where we are going to dispatch a custom event named 'delegateclick' on the target.
    event.target.dispatchEvent(new CustomEvent('delegateEvent'));
  }
}

I både steg 1 och 2 finns det endast 1 förbehåll och det är; om du har nästlade underordnade element inom din day.component.html, måste du antingen implementera bubbling för detta, bättre logik i det if-uttrycket, eller en snabb lösning skulle vara.. i day.component.css :host *{pointer-events: none;}


Nu behöver vi tala om för vår day.component att den ska förvänta sig vår delegateEvent-händelse. Så i day.component.ts behöver du bara göra (på det mest angular-sättet möjligt) ...

@HostListener('delegateEvent', ['$event'])
 public onEvent() {
   console.log("i've been clicked via a delegate!");
 }

Detta fungerar eftersom TypeScript inte bryr sig om huruvida händelsen är inhemsk eller inte, den kommer bara att binda en ny JavaScript-händelse till elementet och tillåter oss därmed att kalla den "nativt" via event.target.dispatchEvent som vi gör ovan i month.component.ts.

Det avslutar steg 1, vi delegerar nu framgångsrikt händelser från vår månad till våra dagar.


Steg 2

Så vad händer om vi säger att vi vill köra lite logik i vårt delegerade event inom day.component och sedan returnera det till month.component - så att det sedan kan fortsätta med sin egen funktionalitet på ett väldigt objektorienterat sätt? Lyckligtvis kan vi implementera detta väldigt enkelt!

I month.component.ts uppdatera till följande. Allt som har ändrats är att vi nu kommer att skicka en funktion med vår händelseanropning och vi definierade vår återuppringningsfunktion.

@HostListener('click', ['$event'])
  public onMonthClick(event) {  
    if (event.target != this.element.nativeElement) {
      event.target.dispatchEvent(new CustomEvent('delegateEvent', { detail: this.eventDelegateCallback}));
    }
  }

  public eventDelegateCallback(data) {
    console.log(data);
  }

Allt som återstår är att anropa denna funktion inom day.component.ts...

public onEvent(event) {
    // run whatever logic you like, 
    //return whatever data you like to month.component
    event.detail(this.day);
}

Tyvärr är vår återuppringningsfunktion lite tvetydigt namngiven här, men TypeScript kommer att klaga på att egenskapen inte är ett definierat objektliteral för CustomEventInit om den ges ett annat namn.


Flera händelsetrattar

Det andra häftiga med detta tillvägagångssätt är att du aldrig bör behöva definiera fler än detta antal händelser eftersom du bara kan kanalisera alla händelser genom denna delegering och sedan köra logik inom day.component.ts för att filtrera efter event.type...

månad.component.ts

@HostListener('click', ['$event'])
@HostListener('mouseover', ['$event'])
@HostListener('mouseout', ['$event'])
  public onMonthEvent(event) {  
    if (event.target != this.element.nativeElement) {
      event.target.dispatchEvent(new CustomEvent('delegateEvent', { detail: this.eventDelegateCallback }));
    }
  }

day.component.ts

private eventDelegateCallback: any;

@HostListener('delegateEvent', ['$event'])
  public onEvent(event) {
    this.eventDelegateCallback = event.detail;
    if(event.type == "click"){
       // run click stuff
       this.eventDelegateCallback(this.day)
    }
  }

Relaterade artiklar