pjlamb12
pjlamb12

Reputation: 2422

Custom Angular Directive not Working with Interpolation

I have a custom Angular directive that takes a couple inputs. Its purpose is to highlight the matching parts of the element to which the directive is added with the input matchTerm. It's supposed to work with a typeahead results list, so as the user types the results that come back have the matching strings highlighted.

Here is the entire directive:

import { Directive, Input, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
import { highlightStringMatches } from './typeahead.util';

@Directive({
    selector: '[hsaTypeaheadResult]',
})
export class TypeaheadResultDirective implements OnChanges {
    @Input() matchTerm: string = '';
    @Input() highlightMatches: boolean = true;
    @Input() caseInsensitiveMatch: boolean = true;

    constructor(private _element: ElementRef) {
        console.log({ element: this._element });
    }

    ngOnChanges(changes: SimpleChanges) {
        console.log({ changes });
        if (changes.matchTerm && this.highlightMatches) {
            this.markStringMatches(this._element);
        }
    }

    public markStringMatches(ref: ElementRef) {
        const itemString = ref.nativeElement.textContent.trim();

        console.log({
            itemString,
            matchTerm: this.matchTerm,
            highlightMatches: this.highlightMatches,
            caseInsensitiveMatch: this.caseInsensitiveMatch,
        });

        ref.nativeElement.innerHTML =
            this.highlightMatches && this.matchTerm
                ? highlightStringMatches(itemString, this.matchTerm, this.caseInsensitiveMatch)
                : itemString;
    }
}

The directive works if I do the following:

<ul>
    <li hsaTypeaheadResult matchTerm="res">Result 1</li>
    <li hsaTypeaheadResult matchTerm="res">Result 2</li>
    <li hsaTypeaheadResult matchTerm="res">Result 3</li>
</ul>

The "Res" of each li is bolded. But it doesn't work if I do either of the following:

<ul>
    <li hsaTypeaheadResult matchTerm="res" *ngFor="let result of resultsArr">{{ result }}</li>
</ul>
<ul>
    <li hsaTypeaheadResult matchTerm="res">{{ results[0] }}</li>
    <li hsaTypeaheadResult matchTerm="res">{{ results[1] }}</li>
    <li hsaTypeaheadResult matchTerm="res">{{ results[2] }}</li>
</ul>

Any time a variable is interpolated in the curly brackets for an element where the directive is used, the value of the variable doesn't show up on the screen:

output results

Just in case it was something with having the structural *ngFor directive on the li element as well, I tried putting the ngFor on an ng-template tag, but it still didn't work.

You can see a full example on Stackblitz here. I'm not sure why it doesn't work with interpolation. In my tests, which you can see on the Stackblitz project, I used an ngFor loop once I found out that it wasn't working with interpolation in my apps and the tests still pass.

I've tried this in a brand new Angular CLI project as well as a Stackblitz and neither of those projects had any other dependencies that should affect it negatively. Any help would be greatly appreciated.

Upvotes: 1

Views: 574

Answers (1)

pjlamb12
pjlamb12

Reputation: 2422

After thinking about this for some time, I realized that the ElementRef.nativeElement.textContent was empty in the directive's constructor when using interpolation. So I realized that must mean I was running the function in the directive too soon. To test, I used a setTimeout and waited 2 seconds before running the function to highlight the match. When I did that, the directive worked as expected.

After that it was just a matter of finding a lifecycle method that would run after the view was ready, and AfterViewInit worked perfect. The directive code is as follows now:

import { Directive, Input, ElementRef, OnChanges, SimpleChanges, AfterViewInit } from '@angular/core';
import { highlightStringMatches } from './typeahead.util';

@Directive({
    selector: '[hsaTypeaheadResult]',
})
export class TypeaheadResultDirective implements OnChanges, AfterViewInit {
    @Input() matchTerm: string = '';
    @Input() highlightMatches: boolean = true;
    @Input() caseInsensitiveMatch: boolean = true;

    constructor(private _element: ElementRef) {}

    ngOnChanges(changes: SimpleChanges) {
        if (changes.matchTerm && this.highlightMatches && this._element.nativeElement.textContent) {
            this.markStringMatches(this._element);
        }
    }

    ngAfterViewInit() {
        if (this.matchTerm && this.highlightMatches && this._element.nativeElement.textContent) {
            this.markStringMatches(this._element);
        }
    }

    public markStringMatches(ref: ElementRef) {
        const itemString = ref.nativeElement.textContent.trim();

        ref.nativeElement.innerHTML =
            this.highlightMatches && this.matchTerm
                ? highlightStringMatches(itemString, this.matchTerm, this.caseInsensitiveMatch)
                : itemString;
    }
}

Upvotes: 1

Related Questions