Using Observables as Outputs

tldr;

We constantly write components or directives that use @Outputs to emit data to the host component. Traditionally, that’s done by using EventEmitter from @angular/core. But you can also emit changes using an Observable, and luckily you can do just that without any issues. In this article we’ll look at a use case for this situation, and how to implement and test the solution.

Setup

I was recently working on a directive that would let the host component know if an arrow key or enter key was pressed in an input element. The purpose was to be able to cycle through and select items from a typeahead menu. I initially created the directive about a year ago, and have been using it since. I was implementing it in a new project, though, and realized I could make some improvements.

First of all, the functionality was based on using fromEvent, and listening for keyup events produced by the input element. Here’s an example of what that looked like:

fromEvent(this.searchInput.nativeElement, “keyup”)
.pipe(
// do stuff with the stream
takeUntil(this.destroy$)
)
.subscribe();

In the pipe, I would use the tap operator and emit values using traditional EventEmitter @Outputs. But for this to work, the fromEvent Observable needs to be subscribed to. You can see that I’ve done that in the above example. To prevent any memory leak issues, I also need to provide the takeUntil operator and destroy the stream when the directive is destroyed. That’s not a huge deal, if you remember to do it, but if you don’t remember to do that you can negatively impact the performance of your app.

The other problem is that with this method, that one Observable handles any keyboard events that I want to emit: arrow events, enter, escape, etc. They’re all managed in the one observable stream, and emitted individually. Again, that’s not a huge deal, but it does muddy up the code and make it a little more confusing about what’s emitted and when.

I wanted a more clean way to handle this situation and alert the host component that one of these events had occurred.

Using an Observable as the Output

Luckily, there’s an easy way to do what I wanted to do. All you need to do is to assign the value of the @Output as an observable. @Outputs are traditionally EventEmitters, extend the RxJS Subject, so making the @Output an observable works flawlessly. One upside to this is that the directive no longer needs to explicitly subscribe to the fromEvent observable. Another is that we can compose the @Outputs individually, making it clear what each @Output is looking for and what it will emit. Here’s an example of the directive after my refactor:

export class TypeaheadInputDirective {
@Input() useDirectionArrowsForNavigation = false;

private fromEvent$: Observable<KeyboardEvent> = fromEvent<KeyboardEvent>(
this.searchInput.nativeElement,
‘keyup’,
).pipe(
tap((event: KeyboardEvent) => {
if (event.key === TypeaheadKeys.ESC) {
this.searchInput.nativeElement.blur();
}
}),
);

private directionArrowPressed$: Observable<TypeaheadKeys> = this.fromEvent$.pipe(
filter((event: KeyboardEvent) => {
return (
event.key === TypeaheadKeys.DOWN ||
event.key === TypeaheadKeys.UP ||
event.key === TypeaheadKeys.LEFT ||
event.key === TypeaheadKeys.RIGHT
);
}),
map((event: KeyboardEvent) => event.key as TypeaheadKeys),
);
@Output() directionArrowPressed: Observable<TypeaheadKeys> = this.directionArrowPressed$;

private enterPressed$: Observable<TypeaheadKeys> = this.fromEvent$.pipe(
filter((event: KeyboardEvent) => {
return event.key === TypeaheadKeys.ENTER;
}),
map((event: KeyboardEvent) => event.key as TypeaheadKeys),
);
@Output() enterKeyPressed: Observable<TypeaheadKeys> = this.enterPressed$;

constructor(private searchInput: ElementRef<HTMLInputElement>) {}
}

In this updated implementation, I have a private fromEvent$ variable that creates the observable and which will be reused as the base for the two different @Outputs. In my case, I’m using fromEvent from RxJS, and then checking if the escape key is pressed. After this, fromEvent$ is used to create an observable that emits a value if an arrow key is pressed, and a second one that emits if the enter key is pressed. Those observables, enterPressed$ and directionArrowPressed$, are assigned to the @Outputs: directionArrowPressed and enterKeyPressed respectively.

This implementation of the directive works the same as the previous implementation, but I think it’s easier to tell what the directionArrowPressed @Output will provide, and same for enterKeyPressed. I also like that I don’t have to explicitly subscribe to an observable and make sure to clean it up.

You can test out this method of assigning Observables to @Outputs here in this StackBlitz.

Testing this Directive

Obviously an important part of creating components and directives is writing unit tests to make sure that they work as intended. The easiest way to test the directive above is by using the TestBed from Angular. Here’s an example of the unit tests for this directive:

@Component({
selector: ‘sha-test-host’,
template: `
<input
shaTypeaheadInput
(directionArrowPressed)=”handleArrowPressed($event)”
(enterKeyPressed)=”handleEnterPressed($event)”
[useDirectionArrowsForNavigation]=”true”
/>
`,
})
class TestHostComponent {
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
handleArrowPressed(key: TypeaheadKeys) {}
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
handleEnterPressed(key: TypeaheadKeys) {}
}

describe(‘TypeaheadInputDirective’, () => {
let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;

beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [TestHostComponent, TypeaheadInputDirective],
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it(‘should create an instance’, () => {
expect(component).toBeTruthy();
});

it(‘should emit directionArrowPressed event when arrow key is pressed’, fakeAsync(() => {
const spy = jest.spyOn(component, ‘handleArrowPressed’);
const input = fixture.debugElement.nativeElement.querySelector(‘input’);
const downArrowEvent = new KeyboardEvent(‘keyup’, { key: TypeaheadKeys.DOWN });
input.dispatchEvent(downArrowEvent);
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(TypeaheadKeys.DOWN);
}));

it(‘should emit enterKeyPressed event when enter key is pressed’, fakeAsync(() => {
const spy = jest.spyOn(component, ‘handleEnterPressed’);
const input = fixture.debugElement.nativeElement.querySelector(‘input’);
const enterKeyEvent = new KeyboardEvent(‘keyup’, { key: TypeaheadKeys.ENTER });
input.dispatchEvent(enterKeyEvent);
fixture.detectChanges();
expect(spy).toHaveBeenCalledWith(TypeaheadKeys.ENTER);
}));

});

In this test, a test host component is created, and the directive is added to the template of that test host component. Then we manually dispatch keyup events and spy on the test host component’s methods to see if they were called with the correct data.

You can add other tests in here for the other arrow keys, or if you add any other @Outputs you can also test those as well.

Conclusion

If you find yourself subscribing to an observable solely for the purpose of emitting a value for a traditional EventEmitter @Output, this is a great alternative. There’s no subscription management, and the event will automatically be emitted to the host component.

Using Observables as Outputs was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

Your email address will not be published. Required fields are marked *