16
votes

I am writing a form in Angular 2 where the user submits the form, it is validated and if there are any errors with the inputs, I want to scroll the user's browser to the first element with the class "error"

The problem is, all of my errors use *ngIf like so:

<input type="text" [(ngModel)]="model.first_name">
<div class="error" *ngIf="errors.first_name">
    {{errors.first_name}}
</div>

In my submit function

submit(){
   this.errors = this.validate();
   if(this.errors.any()){
      var errorDivs = document.getElementsByClassName("error");
      if(errorDivs.length > 0){
         errorDivs[0].scrollIntoView();
      }
   }
}

I realize this is because *ngIf removes the div from the DOM completely and the Angular check for changes hasn't been given a chance to run yet. Is there a clever and clean way to do this?

4
you can wrap div.error into one external paren div and scroll to that parent div instead. Also you can use [hidden] instead of *ngIfDDRamone

4 Answers

21
votes

Not sure I fully understand your question.

Using a directive like below would make the element scroll into view when errors.first_name becomes truthy:

<div class="error" *ngIf="errors.first_name" scrollTo>
    {{errors.first_name}}
</div>
@Directive({ selector: '[scrollTo]'})
class ScrollToDirective implements AfterViewInit {
  constructor(private elRef:ElementRef) {}
  ngAfterViewInit() {
    this.elRef.nativeElement.scrollIntoView();
  }
}
11
votes

Here's a way I figured out:

Create a helper class

export class ScrollHelper {
    private classToScrollTo: string = null;

    scrollToFirst(className: string) {
        this.classToScrollTo = className;
    }

    doScroll() {
        if (!this.classToScrollTo) {
            return;
        }
        try {
            var elements = document.getElementsByClassName(this.classToScrollTo);
            if (elements.length == 0) {
                return;
            }
            elements[0].scrollIntoView();
        }
        finally{
            this.classToScrollTo = null;
        }
    }
}

Then create one in the component you wish to use it in

private scrollHelper : ScrollHelper = new ScrollHelper();

then when you find out you have errors

submit(){
   this.errors = this.validate();
   if(this.errors.any()){
        this.scrollHelper.scrollToFirst("error");
   }
}

then postpone the actual scroll until ngAfterViewChecked after *ngIf has been evaluated

ngAfterViewChecked(){
    this.scrollHelper.doScroll();
}
2
votes

This may not be the most elegant solution, but you can try wrapping your error focusing code in a setTimeout

submit(){
   this.erroors = this.validate();
   setTimeout(() => {
     if(this.errors.any()){
        var errorDivs = document.getElementsByClassName("error");
        if(errorDivs.length > 0){
          errorDivs[0].scrollIntoView();
        }
     }
   }, 50);
}

The key isn't so much to delay the focusing code as much as it is about ensuring that the code runs after a tick of the event loop. That will give the change detection time to run, thus ensuring your error divs have time to be added back to the DOM by Angular.

0
votes

My solution, which is quite simple.

Steps

create a member:

scrollAnchor: string;

create a method that will scroll to anchor if the anchor is set.

ngAfterViewChecked(){
  if (this.scrollAnchor) {
    this.scrollToAnchor(this.scrollAnchor);
    this.scrollAnchor = null;
  }
}

and then when you want to scroll to somewhere that is in a div that has a ngIf, simply set the scrollAnchor

this.scrollAnchor = "results";

Very simple! Note you have to write the scroll to anchor method yourself... or scroll to element or whatever. Here is mine

 private scrollToAnchor(anchor: string): boolean {
    const element = document.querySelector("#" + anchor);
    if (element) {
      element.scrollIntoView({block: "start", behavior: "smooth"});
      return true;
    }
    return false;
  }