3

I am trying to call this highlight() on the ngOnInit but I am getting this error: ERROR TypeError: Cannot read property 'innerHTML' of null.

In the ngOninit I have

this.annotationSub = this.annotationService
      .getWordUpdateListenerTwo()
      .subscribe((theHardWords: ComplexWord[]) => {
        this.thewords = [];
        this.theHardWords = theHardWords;
        this.theHardWords.map(word => {
          this.thewords.push(word.word);
          this.wordWithAnnotation.push(word);
        });
      });

this.postsSub = this.postsService
     .getPostUpdateListenerTwo()
     .subscribe((posts: Post[]) => {
            this.posts = posts;
            this.posts.map(post => {
              if (post.id === this.id) {
                 this.postIWant = post.fileText;
              }
            });
    });
    this.highlight(this.thewords); 

This picks out the post which then gets displayed shown below:

My HTML:

<div id="scrollable">
    {{ postIWant }}
</div>

This is the highlight function which is giving me the problems, If I call this highlight function after the document has loaded with a button it works fine, but If I call it in the ngOnInit it does not give enough time for the innerHTML to get populated therefore throws an error.

I have tried using ngAfterViewInit(): void {} but still even that does not give it enough time. Below is the highlight function.

highlight(words) {
    const high = document.getElementById('scrollable');
    const paragraph = high.innerHTML.split(' ');
    const res = [];

    paragraph.map(word => {
      let t = word;
      if (words.indexOf(word) > -1) {
        t =
          '<a class="clickable" style="background-color: yellow; text-decoration: underline;">' +
          word +
          '</a>';
      }
      res.push(t);
    });
    high.innerHTML = res.join(' ');
    const elementsToMakeClickable = document.getElementsByClassName(
      'clickable'
    );
    const elementsToMakeClickableArray = Array.from(elementsToMakeClickable);
    elementsToMakeClickableArray.map(element => {
      element.addEventListener('click', this.viewAnnotation.bind(this));
    });
    document.getElementById('btnHighLight').style.visibility = 'visible';
}

As mentioned earlier, it works if I load the page up and press a button to trigger the highlight() but I want it to run that function and highlight the words without me having to click anything. Does anyone have any ideas? Thanks!

(I am using Angular).

Andrew
  • 695
  • 2
  • 9
  • 25
  • 1
    You'll need to have the highlight within your subscribe function so that it executes when the data is loaded or add an observable to check when the data is loaded to then highlight. – Bert Maurau Jul 24 '18 at 19:27
  • I have tried to create an observable to trigger this when the values have been put into the `this.postIWant` but that isn't the issue. The issue seems to be that even if it has the values when the page is refreshing there is no innerHTML text so it's null for the split second and it returns the error. @BertMaurau – Andrew Jul 24 '18 at 19:30
  • Does `scrolable` div depends of your data from `annotaionService` or `postsService` ? – kat1330 Jul 24 '18 at 19:31
  • @kat1330 Yes from the `postsService`: `
    {{ post.header }}
    {{ postIWant }}
    `
    – Andrew Jul 24 '18 at 19:32
  • OK. Then you can say that `postsService ` depends of `annotaionService ` becasue you using `this.thewords`? Correct? – kat1330 Jul 24 '18 at 19:34
  • Yes, I'm guessing so, sorry I'm not an expert, but I am trying my best. If you want I can show the code in a Gist? It's probably much easier to read and understand? Let me know @kat1330 – Andrew Jul 24 '18 at 19:35
  • 1
    And a suggestion ... consider using `ViewChild` instead of the document object and its properties/methods (such as `getElementById`. It works better within Angular. – DeborahK Jul 24 '18 at 19:46

2 Answers2

2

I can see that you are not properly synchronizing you observables. I will rather combine observables from annotationService and postsService. Then subscribe and execute this.highlight(this.thewords);.

Here is example (in RxJS 6):

const annotation$ = this.annotationService.getWordUpdateListenerTwo();
const posts$ = this.postsService.getPostUpdateListenerTwo();


annotation$.pipe(combineLatest(posts$, (annotations, posts) => ({annotations, posts}))).subscribe((annotations, posts) => {

 // Do your logic here and after execute highlight()

 this.highlight(thewords);
});

In example above I combining annotation$and posts$ which means that subscription will be executed on latest observables. Then I am guessing you will have do your logic which you need on on the end you can execute highlight().

But, above approach doesn't ensure that scrolable div will be loaded before combined observable finished. To listen changes on particular DOM element you can use MutationObserver API and create custom directive which can be added to scrollable div. Please see following article Listening to DOM Changes Using MutationObserver in Angular.

You can also try to access to your HTML via ViewChild. Please see more in following stackoverflow question: How can I select an element in a component template?

kat1330
  • 5,134
  • 7
  • 38
  • 61
  • Thankyou @kat1330 for your answer. I am trying to implement this now and see If I can make this work. – Andrew Jul 24 '18 at 19:48
  • Got this error, been trying to change the types but no luck: Argument of type 'Observable<{ annotations: any; posts: any; }>' is not assignable to parameter of type 'OperatorFunction'. Type 'Observable<{ annotations: any; posts: any; }>' provides no match for the signature '(source: Observable): Observable'. – Andrew Jul 24 '18 at 19:58
  • You can cast to `any` or did you imported`combineLatest` from `rxjs/operators`? – kat1330 Jul 24 '18 at 20:00
  • Yeah, I imported it from rxjs/operators, but if this does not make it wait until the DOM is loaded I will still get the same error. So I will have to figure out how to wait until the DOM is loaded. @Kat1330 – Andrew Jul 24 '18 at 20:02
  • Please see my updates. You can implement custom directive with MutationObserver API or use ViewChild to access DOM. – kat1330 Jul 24 '18 at 20:09
2

Your document.getElementById('scrollable'); call is returning null.

This is why you aren't supposed to interact with the DOM in Angular. Angular completely decouples it and gives you an API to interact with it.

ngOnInit is called after the component's Inputs and Outputs have been resolved. ngAfterViewInit is called once the view template is attached and template variables have been resolved.

There is no lifecycle hook in Angular that fires once a component's markup is attached to the DOM.

There's a number of ways you can get the element through Angular and not via DOM queries, but none are necessary here.

Just bind it in the markup:

component.html:

<div id="scrollable">
  <a class="clickable inline-styling-is-bad" style="background-color: yellow; text-decoration: underline;">
    {{ postIWant }} 
  </a>
</div>

If you need a repeating series of elements based on an array...it seems obvious, but just use *ngFor.

Your Observable streams have some issues as well, but that's out of scope for this topic.

joh04667
  • 7,159
  • 27
  • 34
  • I am not too sure what you have done there with the a tag? I have a post and a set of words which I pull from the database and the function gets those words and highlights them for me and puts them into a clickable a tag. This does not solve that? Maybe I'm not understanding it though as i'm fairly new. – Andrew Jul 24 '18 at 19:50
  • If the text you want in the post is a string, you can bind it to the markup via template binding here. – joh04667 Sep 12 '18 at 13:52