Why is toggling a search box in a header so difficult in Angular 2?

In Angular 2 I have the following pseduocode…

<header>
<form class="searchContainer" [ngClass]="{'visible': showSearch, 'invisible': !showSearch}">
...
</form>
</header>

As you can see I have a header with a search form container that has the class ‘visible’ when the variable ‘showSearch’ is true, and ‘invisible’ otherwise. As a component by itself with this html, this works fine. When the user clicks the search icon showSearch is set to true and the form shows.

Now the issue comes when I want to hide this container again. I want to set showSearch to false and thus hide the container when the user clicks anywhere that isn’t the header (ie. the body of the page).
Now in jQuery, this would be easy. On click of the body, you would find the element with ‘.searchContainer’ and removeClass(‘visible’).addClass(‘invisible’) right? Easy.

Except this is Angular 2. We don’t have jQuery (or want to use it) here. Normally what I would do in this case, is to have something like this in the parent component that contains the header component:

<app-header [showSearch]="showSearch"></app-header>
<body>
..
</body>

That’s the HTML. I pass the showSearch variable down from the parent to the child and toggle it with the parent right? That’s how normally it works. One way binding. (If the child needs to talk to the parent it uses event emitters instead).


@HostListener('click', ['$event'])
closeSearchBox(event) {
const toElement = event.toElement;
let insideHeader = false;
let node = toElement;
while (node != null && node.classList !== undefined) {
if (node.classList.contains('header-container')) {
insideHeader = true;
}
node = node.parentNode;
}

if (!insideHeader) {
const searchBox = this.elementRef.nativeElement.querySelector('.search-container');
const searchMenu = this.elementRef.nativeElement.querySelector('.header-menu-list');
if (searchBox.classList.contains('visible')) {
this.showSearch = false;
}
}
}

So this should work right? I have an HostListener on the parent (that’s really the whole body of my app) so if the user clicks anywhere it will check to see if the element is outside the header (because we dont want to close it if its inside the header), and close the search container if it is.

Problem is… it didn’t work. I don’t know why. I searched StackOverflow, Angular Docs, scoured Google etc nothing. I even tried using EventEmitter but thats usually only from child to parent and not parent to child. Two-way bindings? nope doesn’t work either. It *should* just work inherently as part of Angular’s single way binding. Maybe the HostListener is outside the Angular Zone and I can use ChangeDetectorRef to detectChanges? Nope. That didn’t work either.
I *could* use a service and just subscribe to that service from the child component … but thats kind of overkill for ONE variable change to toggle a search box.

So finally. I had to make a solution that is not so elegant, and doesn’t use any variables or bindings. But it works.

<header>
<form class="searchContainer invisible">
...
</form>
</header>

So I took a jQuery-like approach of just toggling the class manually with ‘elementRef.nativeElement.classList’ instead. Its not as pretty as using bindings, but at least this way it works.


toggleSearchHeader(showHeader) {

if (showHeader) {
this.searchHeaderMenu.nativeElement.classList.remove('visible');
this.searchHeaderMenu.nativeElement.classList.add('invisible');
this.searchHeaderBox.nativeElement.classList.remove('invisible');
this.searchHeaderBox.nativeElement.classList.add('visible');
} else {
this.searchHeaderMenu.nativeElement.classList.remove('invisible');
this.searchHeaderMenu.nativeElement.classList.add('visible');
this.searchHeaderBox.nativeElement.classList.remove('visible');
this.searchHeaderBox.nativeElement.classList.add('invisible');
}

}

I have to substitute changing one variable showSearch with a whole method that does janky DOM crap and..


@HostListener('click', ['$event'])
closeSearchBox(event) {
const toElement = event.toElement;
let insideHeader = false;
let node = toElement;
while (node != null && node.classList !== undefined) {
if (node.classList.contains('header-container')) {
insideHeader = true;
}
node = node.parentNode;
}

if (!insideHeader) {
const searchBox = this.elementRef.nativeElement.querySelector('.search-container');
const searchMenu = this.elementRef.nativeElement.querySelector('.header-menu-list');
if (searchBox.classList.contains('visible')) {
searchBox.classList.remove('visible');
searchBox.classList.add('invisible');
searchMenu.classList.remove('invisible');
searchMenu.classList.add('visible');
}
}
}

So yeah as you can see those are the changes I needed to get it to work. For some reason, using a variable inside `HostListener` and passing that down to the child component doesn’t reflect the bindings. So I have to use a more archaic way instead. Not only that but its much more difficult to test with unit tests now that it relies on the DOM instead of an Angular variable. Bleh. If anyone knows why this is, let me know please! comment on this post..