Custom structural directive inside ngFor updates before ngFor
Asked Answered
D

1

0

I am creating a small application, which display a list of People with Name, Surname and Age fields using ngFor. The application has search field, where one can enter a query, and then the list will get replaced with new Entities from the server, based on that query.

I created a directive, that highlights letters from the query inside the ngFor row.

For example, if I have a person in database that its name is David, and I enter "Dav" inside my query, only entities conatining "Dav" will be loaded from the server in ngFor, and "Dav" letters will be highlighted and "id" will be not. If I have David and Davin, both entities will be highlighted.

The directive works as expected only if I am using an artifical setTimeout(), to make sure that the new list will load before the Directive takes action. Is there any other way to make this work?

DIRECTIVE:

import { Directive, Input, ElementRef } from '@angular/core';
import { SimpleChanges, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appQueryHighlight]'
})
export class QueryHighlightDirective {

  @Input('appQueryHighlight') query: string;
  queryPos: number;
  paragraphElement: HTMLParagraphElement;

  constructor(private element: ElementRef, private renderer: Renderer2) {
    this.paragraphElement = (<HTMLParagraphElement>this.element.nativeElement);
  }

  ngOnChanges(changes: SimpleChanges){
    // Temporary timeout solution
    setTimeout(()=>{

      var childCount = this.paragraphElement.childElementCount;
      var text: string = "";

      // If paragraph contain SPANS, we need to flat them to innerHTML
      if(childCount > 1) {
        for(var i = 0; i < childCount; i++) {
          text += (<HTMLSpanElement>this.paragraphElement.childNodes[0]).innerHTML;
          console.log("SPAN" + (<HTMLSpanElement>this.paragraphElement.childNodes[0]).innerHTML);
          this.paragraphElement.removeChild(this.paragraphElement.childNodes[0]);
        }
        console.log("Text=" + text)
        this.paragraphElement.innerHTML = text;
      }

      console.log('Directive ngOnChanges: query=' + this.query + ", paragraph=" + this.paragraphElement.innerHTML);

      this.queryPos = this.paragraphElement.innerHTML.toUpperCase().indexOf(this.query.toUpperCase());
      if(this.query!="" && this.queryPos >= 0) {
        //this.paragraphElement.style.backgroundColor = 'yellow';

        //First span, containing pre-colored text
        var span1 = this.renderer.createElement('span');
        var text1 = this.renderer.createText(this.paragraphElement.innerHTML.substring(0,this.queryPos));
        this.renderer.appendChild(span1, text1);

        //Colored text span, containing query
        var span2 = this.renderer.createElement('span');
        var text2 = this.renderer.createText(this.paragraphElement.innerHTML.substr(this.queryPos, this.query.length));
        this.renderer.setStyle(span2, 'color', "red");
        this.renderer.setStyle(span2, 'text-decoration', "underline");
        this.renderer.appendChild(span2, text2);

        //Third span, containing text after query
        var span3 = this.renderer.createElement('span');
        var text3 = this.renderer.createText(this.paragraphElement.innerHTML.substring(this.queryPos + this.query.length));
        this.renderer.appendChild(span3, text3);

        this.paragraphElement.innerHTML = "";
        this.renderer.appendChild(this.paragraphElement, span1);
        this.renderer.appendChild(this.paragraphElement, span2);
        this.renderer.appendChild(this.paragraphElement, span3);
      }
      else {
        //this.paragraphElement.style.color = 'black';
      }
    }, 15);
  }
}

LIST-COMPONENT.TS:

ngOnChanges(changes: SimpleChanges) { 
  this.debtsService.getFilteredDebts(this.query)
    .subscribe(
      (data) => {
        this.debtsList = data;
        this.afterFilteringQuery = this.query;
      },
      (err) => console.log("Error occured: " + err)
    );
}

LIST-COMPONENT.HTML:

  <app-person-item 
    *ngFor="let person of personList;" 
    [query]="afterFilteringQuery">
  </app-person-item>
Darden answered 14/1, 2018 at 1:21 Comment(2)
the fact that you manipulate so many DOM elements in the directive looks like a code smell to meDemineralize
You mean it is not a good approach? What would be better then?Darden
F
0

Instead of ngOnChanges approach you may try to apply BehaviorSubject approach . I'm not sure but Observable's .next() call should guarantee an additional event loop cycle which is necessary in your case as we can see per working zero setTimeout call.

import { Directive, Input, ElementRef, Renderer2 } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Directive({
  selector: '[appQueryHighlight]'
})
export class QueryHighlightDirective {

  private _query = new BehaviorSubject<string>('');

  @Input('appQueryHighlight')
  set query(value: string) {
    this._query.next(value);
  };
  get query(): string {
    return this._query.getValue();
  }

  ngOnInit() {
    this._query.subscribe((query: string) => {
      // on query change handler
      // ... ngOnChanges-setTimeout previous code with 'query' instead of 'this.query'
    });
  }

}
Firebrat answered 14/1, 2018 at 3:59 Comment(2)
So setTimeout is also a good approach here? As I understand from your answer, we need additional event loop cycle to make this work? But how can we be sure what will be first - list propagation or directive execution?Darden
@Darden I'm afraid it is not possible (at least for me) to make, say, complete conclusions having that parts of project sources that you put in the Question. This is something with Angular Change Detection flow and how you do a component-directive interaction (passing "query" I mean). By delaying the change-reaction, you guarantee the sequence. And BehaviorSubject approach could still be treated as a workaround, but it's not so hacky as setTimeout, it is more naturally for the Angular App.Firebrat

© 2022 - 2024 — McMap. All rights reserved.