19

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?

spectacularbob
  • 3,080
  • 2
  • 20
  • 41
  • 3
    you can wrap div.error into one external paren div and scroll to that parent div instead. Also you can use `[hidden]` instead of `*ngIf` – DDRamone Mar 15 '17 at 20:09

4 Answers4

26

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();
  }
}
urish
  • 8,943
  • 8
  • 54
  • 75
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 2
    Pretty cool solution, but is there a way to have multiple error fields on the same page so that it will only scroll to the first one in the DOM? – spectacularbob Mar 15 '17 at 20:57
  • @Gunter what if when we use hidden how will it fare out then – Rahul Singh Oct 02 '17 at 07:25
  • 3
    @RahulSingh that would require a redesign of my directive, because `ngAfterViewInit()` wouldn't be called when the condition becomes true. You can pass the condition to the directive explicitely like `
    ` and in `ScrollToDirective` instead of `ngAfterViewInit` you could add `@HostBinding('hidden') isError:boolean = false; @Input() set scrollTo(bool cond) { this.isError = cond; if(cond) { this.elRef.nativeElement.scrollIntoView(); } }`
    – Günter Zöchbauer Oct 02 '17 at 07:36
  • @GünterZöchbauer thanks , now i need to look at how to get that to work with multiple errors thanks – Rahul Singh Oct 02 '17 at 07:38
11

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();
}
spectacularbob
  • 3,080
  • 2
  • 20
  • 41
2

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.

snorkpete
  • 14,278
  • 3
  • 40
  • 57
2

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;
  }
fergal_dd
  • 1,486
  • 2
  • 16
  • 27