0

I am making a data table component. For accessibility, I want each table header to have a tooltip via a title attribute. The tooltip value can be explicitly set (in case it is different than the text in the header), but by default I want it to use whatever the inner text is.

I have a half-working solution, but it's not quite working and I don't know if I'm breaking some Angular rules by doing it this way.

Here's my abbreviated component html:

<div #headerContent [attr.title]="title"><ng-content></ng-content></div>

I am tagging the div with headerContent so I can reference it later.

Ok, now here's the abbreviated component class:

@Component({ ... })
export class TableHeaderComponent implements AfterContentInit {
    @ViewChild('headerContent') headerContent: ElementRef;
    @Input() title: string;

    ngAfterContentInit() {
        if (!this.title) {
            this.title = this.headerContent.nativeElement.textContent.trim();
        }
    }
}

The idea is that if no title was specified, look at the div and grab its text content, using that for the title.

This works fine when I test it in the browser.

Example usage:

<th table-header>Contact</th>

Here, since I didn't specify a title, it should use Contact as the title.

But when I write unit tests for this component, it blows up with:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Contact'

I'm not sure what I'm doing wrong here.

EDIT: Injecting the ChangeDetectorRef into my component class and calling detectChanges() after updating the title property seems to have fixed the problem. It works in the browser and also the unit tests pass.

Still wondering if this is breaking any rules in Angular to do it this way.

Joe Attardi
  • 4,381
  • 3
  • 39
  • 41
  • 1
    This should be done in the `ngOnInit`, you can get more information [in this question](https://stackoverflow.com/questions/43375532/expressionchangedafterithasbeencheckederror-explained) as to why :) – 0mpurdy Aug 07 '17 at 19:40
  • But in `ngOnInit`, wouldn't the `@ViewChild` not have been initialized yet? Doesn't that happen in `ngAfterViewInit`? I just tried this, and in `ngOnInit()` `this.headerContent` is `undefined`. – Joe Attardi Aug 07 '17 at 19:41
  • 1
    I must admit I avoid using `@ViewChild` as much as possible, I usually try to find another way of accomplishing the task, I'll try and create a plunker now to test this issue :) – 0mpurdy Aug 07 '17 at 19:43
  • 1
    Ok, I have created [this plunker](https://plnkr.co/edit/z3KdsraOMiB7pfc6NQfF?p=preview) and I can't seem to be able to recreate your issue (even with ngAfterContentInit). Can you spot anything I have done differently, or is it the same and it is just not working locally? – 0mpurdy Aug 07 '17 at 19:50
  • Thanks for testing it out. It works fine in the browser, the issue was arising when running the unit tests. Not sure why it was working in one but not the other. But as I mentioned above, once I explicitly triggered change detection after changing the `title` seems to have fixed the problem. – Joe Attardi Aug 07 '17 at 19:52
  • If that is a working solution, I recommend you add it as an answer for future users! (and don't have it in the question as that may cause confusion) – 0mpurdy Aug 07 '17 at 19:58
  • Good idea. I did that. Thanks for your help! – Joe Attardi Aug 07 '17 at 20:17

1 Answers1

1

I was able to solve this by manually triggering change detection after updating the property.

To do this, you first inject the ChangeDetectorRef in the constructor:

constructor(private cd: ChangeDetectorRef) { }

Then after updating the property, call cd.detectChanges().

Joe Attardi
  • 4,381
  • 3
  • 39
  • 41