Skip to content

Commit

Permalink
feat: Do not trigger change detection internally on mouseenter (#420)
Browse files Browse the repository at this point in the history
  • Loading branch information
arturovt authored May 9, 2023
1 parent 39387c8 commit e574b0a
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 35 deletions.
8 changes: 4 additions & 4 deletions src/lib/picker/category.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOver)="emojiOver.emit($event)"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClick)="emojiClick.emit($event)"
></ngx-emoji>
Expand Down Expand Up @@ -93,7 +93,7 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOver)="emojiOver.emit($event)"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClick)="emojiClick.emit($event)"
></ngx-emoji>
Expand Down Expand Up @@ -126,12 +126,12 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit {
@Input() emojiBackgroundImageFn?: Emoji['backgroundImageFn'];
@Input() emojiImageUrlFn?: Emoji['imageUrlFn'];
@Input() emojiUseButton?: boolean;
@Output() emojiOver: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();
/**
* Note: the suffix is added explicitly so we know the event is dispatched outside of the Angular zone.
*/
@Output() emojiOverOutsideAngular: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiLeaveOutsideAngular: Emoji['emojiLeave'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();
@ViewChild('container', { static: true }) container!: ElementRef;
@ViewChild('label', { static: true }) label!: ElementRef;
containerStyles: any = {};
Expand Down
27 changes: 27 additions & 0 deletions src/lib/picker/ngx-emoji/emoji.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ import { TestBed } from '@angular/core/testing';
import { EmojiModule } from './emoji.module';

describe('EmojiComponent', () => {
it('should trigger change detection whenever `emojiOver` has observers', () => {
@Component({
template: '<ngx-emoji (emojiOver)="onEmojiOver()"></ngx-emoji>',
})
class TestComponent {
onEmojiOver() {}
}

TestBed.configureTestingModule({
imports: [EmojiModule],
declarations: [TestComponent],
});

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const appRef = TestBed.inject(ApplicationRef);
spyOn(appRef, 'tick');
spyOn(fixture.componentInstance, 'onEmojiOver');

const emoji = fixture.nativeElement.querySelector('span.emoji-mart-emoji');
emoji.dispatchEvent(new MouseEvent('mouseenter'));

expect(appRef.tick).toHaveBeenCalledTimes(1);
expect(fixture.componentInstance.onEmojiOver).toHaveBeenCalled();
});

it('should trigger change detection whenever `emojiLeave` has observers', () => {
@Component({
template: '<ngx-emoji (emojiLeave)="onEmojiLeave()"></ngx-emoji>',
Expand Down
66 changes: 37 additions & 29 deletions src/lib/picker/ngx-emoji/emoji.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export interface EmojiEvent {
#button
type="button"
(click)="handleClick($event)"
(mouseenter)="handleOver($event)"
[attr.title]="title"
[attr.aria-label]="label"
class="emoji-mart-emoji"
Expand All @@ -67,7 +66,6 @@ export interface EmojiEvent {
<span
#button
(click)="handleClick($event)"
(mouseenter)="handleOver($event)"
[attr.title]="title"
[attr.aria-label]="label"
class="emoji-mart-emoji"
Expand Down Expand Up @@ -99,16 +97,19 @@ export class EmojiComponent implements OnChanges, Emoji, OnDestroy {
@Input() sheetRows?: number;
@Input() sheetColumns?: number;
@Input() useButton?: boolean;
@Output() emojiOver: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();
/**
* Note: `emojiLeave` and `emojiLeaveOutsideAngular` are dispatched on the same event, but for different
* purposes. The `emojiLeaveOutsideAngular` would be set up in category component so we don't care
* about zone context the callback is being called in. The `emojiLeave` is for backwards compatibility
* if anyone is listening to this event explicitly in their code.
* Note: `emojiOver` and `emojiOverOutsideAngular` are dispatched on the same event (`mouseenter`), but
* for different purposes. The `emojiOverOutsideAngular` event is listened only in `emoji-category`
* component and the category component doesn't care about zone context the callback is being called in.
* The `emojiOver` is for backwards compatibility if anyone is listening to this event explicitly in their code.
*/
@Output() emojiOver: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiOverOutsideAngular: Emoji['emojiOver'] = new EventEmitter();
/** See comments above, this serves the same purpose. */
@Output() emojiLeave: Emoji['emojiLeave'] = new EventEmitter();
@Output() emojiLeaveOutsideAngular: Emoji['emojiLeave'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();

style: any;
title?: string = undefined;
label = '';
Expand Down Expand Up @@ -139,7 +140,7 @@ export class EmojiComponent implements OnChanges, Emoji, OnDestroy {
private readonly emojiService = inject(EmojiService);

constructor() {
this.setupMouseLeaveListener();
this.setupMouseListeners();
}

ngOnChanges() {
Expand Down Expand Up @@ -238,27 +239,34 @@ export class EmojiComponent implements OnChanges, Emoji, OnDestroy {
this.emojiClick.emit({ emoji, $event });
}

handleOver($event: Event) {
const emoji = this.getSanitizedData();
this.emojiOver.emit({ emoji, $event });
}

private setupMouseLeaveListener(): void {
this.button$
.pipe(
private setupMouseListeners(): void {
const eventListener$ = (eventName: string) =>
this.button$.pipe(
// Note: `EMPTY` is used to remove event listener once the DOM node is removed.
switchMap(button => (button ? fromEvent(button, 'mouseleave') : EMPTY)),
switchMap(button => (button ? fromEvent(button, eventName) : EMPTY)),
takeUntil(this.destroy$),
)
.subscribe($event => {
const emoji = this.getSanitizedData();
this.emojiLeaveOutsideAngular.emit({ emoji, $event });
// Note: this is done for backwards compatibility. We run change detection if developers
// are listening to `emojiLeave` in their code. For instance:
// `<ngx-emoji (emojiLeave)="..."></ngx-emoji>`.
if (this.emojiLeave.observed) {
this.ngZone.run(() => this.emojiLeave.emit({ emoji, $event }));
}
});
);

eventListener$('mouseenter').subscribe($event => {
const emoji = this.getSanitizedData();
this.emojiOverOutsideAngular.emit({ emoji, $event });
// Note: this is done for backwards compatibility. We run change detection if developers
// are listening to `emojiOver` in their code. For instance:
// `<ngx-emoji (emojiOver)="..."></ngx-emoji>`.
if (this.emojiOver.observed) {
this.ngZone.run(() => this.emojiOver.emit({ emoji, $event }));
}
});

eventListener$('mouseleave').subscribe($event => {
const emoji = this.getSanitizedData();
this.emojiLeaveOutsideAngular.emit({ emoji, $event });
// Note: this is done for backwards compatibility. We run change detection if developers
// are listening to `emojiLeave` in their code. For instance:
// `<ngx-emoji (emojiLeave)="..."></ngx-emoji>`.
if (this.emojiLeave.observed) {
this.ngZone.run(() => this.emojiLeave.emit({ emoji, $event }));
}
});
}
}
2 changes: 1 addition & 1 deletion src/lib/picker/picker.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
[emojiBackgroundImageFn]="backgroundImageFn"
[emojiImageUrlFn]="imageUrlFn"
[emojiUseButton]="useButton"
(emojiOver)="handleEmojiOver($event)"
(emojiOverOutsideAngular)="handleEmojiOver($event)"
(emojiLeaveOutsideAngular)="handleEmojiLeave()"
(emojiClick)="handleEmojiClick($event)"
></emoji-category>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/picker/picker.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('PickerComponent', () => {
fixture.detectChanges();
});

it('should update preview on `mouseleave` but should not trigger change detection', fakeAsync(() => {
it('should update preview on `mouseenter` and `mouseleave` but should not trigger change detection', fakeAsync(() => {
const picker = fixture.debugElement.query(By.directive(PickerComponent));
expect(picker.componentInstance.previewEmoji).toEqual(null);

Expand Down

0 comments on commit e574b0a

Please sign in to comment.