Angular 2 Focus on first invalid input after Click/Event - javascript

I have an odd requirement and was hoping for some help.
I need to focus on the first found invalid input of a form after clicking a button (not submit). The form is rather large, and so the screen needs to scroll to the first invalid input.
This AngularJS answer would be what I would need, but didn't know if a directive like this would be the way to go in Angular 2:
Set focus on first invalid input in AngularJs form
What would be the Angular 2 way to do this? Thanks for all the help!

This works for me. Not the most elegant solution, but given the constraints in Angular we are all experiencing for this particular task, it does the job.
scrollTo(el: Element): void {
if(el) {
el.scrollIntoView({ behavior: 'smooth' });
}
}
scrollToError(): void {
const firstElementWithError = document.querySelector('.ng-invalid');
this.scrollTo(firstElementWithError);
}
async scrollIfFormHasErrors(form: FormGroup): Promise <any> {
await form.invalid;
this.scrollToError();
}
This works, allowing you to evade manipulating the DOM. It simply goes to the first element with .ng-invalid on the page through the document.querySelector() which returns the first element in the returned list.
To use it:
this.scrollIfFormHasErrors(this.form).then(() => {
// Run any additional functionality if you need to.
});
I also posted this on Angular's Github page: https://github.com/angular/angular/issues/13158#issuecomment-432275834

Unfortunately I can't test this at the moment, so might be a few bugs, but should be mostly there.
Just add it to your form.
import {Directive, Input, HostListener} from '#angular/core';
import {NgForm} from '#angular/forms';
#Directive({ selector: '[scrollToFirstInvalid]' })
export class ScrollToFirstInvalidDirective {
#Input('scrollToFirstInvalid') form: NgForm;
constructor() {
}
#HostListener('submit', ['$event'])
onSubmit(event) {
if(!this.form.valid) {
let target;
for (var i in this.form.controls) {
if(!this.form.controls[i].valid) {
target = this.form.controls[i];
break;
}
}
if(target) {
$('html,body').animate({scrollTop: $(target.nativeElement).offset().top}, 'slow');
}
}
}
}

If you are using AngularMaterial, the MdInputDirective has a focus() method which allow you to directly focus on the input field.
In your component, just get a reference to all the inputs with the #ViewChildren annotation, like this:
#ViewChildren(MdInputDirective) inputs: QueryList<MdInputDirective>;
Then, setting focus on the first invalid input is as simple as this:
this.inputs.find(input => !input._ngControl.valid).focus()

I don't know if this is valid approach or not but this is working great for me.
import { Directive, Input, HostListener, ElementRef } from '#angular/core';
import { NgForm } from '#angular/forms';
import * as $ from 'jquery';
#Directive({ selector: '[accessible-form]' })
export class AccessibleForm {
#Input('form') form: NgForm;
constructor(private el: ElementRef) {
}
#HostListener('submit', ['$event'])
onSubmit(event) {
event.preventDefault();
if (!this.form.valid) {
let target;
target = this.el.nativeElement.querySelector('.ng-invalid')
if (target) {
$('html,body').animate({ scrollTop: $(target).offset().top }, 'slow');
target.focus();
}
}
}
}
In HTML
<form [formGroup]="addUserForm" class="form mt-30" (ngSubmit)="updateUser(addUserForm)" accessible-form [form]="addUserForm"></form>
I have mixed the approach of angularjs accessible form directive in this.
Improvements are welcomed!!!

I've created an Angular directive to solve this problem. You can check it here ngx-scroll-to-first-invalid.
Steps:
1.Install the module:
npm i #ismaestro/ngx-scroll-to-first-invalid --save
2.Import the NgxScrollToFirstInvalidModule:
import {BrowserModule} from '#angular/platform-browser';
import {NgModule} from '#angular/core';
import {NgxScrollToFirstInvalidModule} from '#ismaestro/ngx-scroll-to-first-invalid';
#NgModule({
imports: [
BrowserModule,
NgxScrollToFirstInvalidModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
3.Use the directive inside a form:
<form [formGroup]="testForm" ngxScrollToFirstInvalid>
<input id="test-input1" type="text" formControlName="someText1">
<button (click)="saveForm()"></button>
</form>
Hope it helps!
:)

Plain HTML solution.
If you don't need to be scrolling , just focus on first valid input, I use :
public submitForm() {
if(this.form.valid){
// submit form
} else {
let invalidFields = [].slice.call(document.getElementsByClassName('ng-invalid'));
invalidFields[1].focus();
}
}
This is for template driven form here. We focus on second element of invalidFields cuz first is the whole form which is invalid too.

For Angular Material ,
The below worked for me
#ViewChildren(MatInput) inputs: QueryList <MatInput>;
this.inputs.find(input => !input.ngControl.valid).focus();

I recommend putting this in a service, for me it worked like this:
if (this.form.valid) {
//submit
} else {
let control;
Object.keys(this.form.controls).reverse().forEach( (field) => {
if (this.form.get(field).invalid) {
control = this.form.get(field);
control.markAsDirty();
}
});
if(control) {
let el = $('.ng-invalid:not(form):first');
$('html,body').animate({scrollTop: (el.offset().top - 20)}, 'slow', () => {
el.focus();
});
}
}

#HostListener('submit', ['$event'])
onSubmit(event) {
event.preventDefault();
if (!this.checkoutForm.valid) {
let target;
target = $('input[type=text].ng-invalid').first();
if (target) {
$('html,body').animate({ scrollTop: $(target).offset().top }, 'slow', ()=> {
target.focus();
});
}
}
}

Related

Angular 6 - Back button press trigger more than once

I have the following code to detect the back button press using angular 6.
import { Location } from '#angular/common';
export class ProductsComponent implements OnInit {
constructor( private location: Location){
this.handleBackButtonPress();
}
handleBackButtonPress() {
this.subscribed = true;
this.location.subscribe(redirect => {
if (redirect.pop === true) {
alert('this is a backbutton click');
}
});
}
}
This is working and we got alert on back button press. The problem is If we visit the same page more than once it will trigger the alert with the number of time we visited the route with the same component.
Note:
I have checked for a solution like this.location.unsubscribe(), But failed to find a function like that for location.
You just need to unsubscribe when the component is destroyed by the ngOnDestroy lifecycle hook.
import { Location } from '#angular/common';
import { SubscriptionLike } from 'rxjs';
export class ProductsComponent implements OnInit, OnDestroy {
public subscription: SubscriptionLike;
constructor(private location: Location){
this.handleBackButtonPress();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
handleBackButtonPress() {
this.subscription = this.location.subscribe(redirect => {
if (redirect.pop === true) {
alert('this is a backbutton click');
}
});
}
}
As mentioned by briosheje in the comments the lifecycle hook does not run on browser refreshes. For that you'll need to handle the unsubscription on the document's onbeforereload event.
The problem, I analyzed here is, every time whenever constructor will run. It will call your function for sure. So you have to check whether this function has been run previously or not.
Simplest answer is
constructor( private location: Location){
const PopCalled = localStorage.getItem('PopCalled')
if(!PopCalled)
this.handleBackButtonPress();
}
handleBackButtonPress() {
this.subscribed = true;
this.location.subscribe(redirect => {
if (redirect.pop === true) {
localStorage.setItem('PopCalled', true);
alert('this is a backbutton click');
}
});
}
Basically, you have to manage the state of PopCalled its up to you which way you want to choose, as per my knowledge this is the simplest way.

jQuery scrollTo() not working in angular 4

I'm using a library called ng2-pdf-viewer for some reason the default way to stick to a page isn't working, so what I've done is I've used jQuerys scrollTo() method to scroll to the the .page class in the PDF so if I wanted the page to be scrolled to page 2 it would be page .page:nth-child(2) now I've got this working.. but only after you refresh the page and not when you first land on the page so If I follow a link to my pdf-view page it doesn't scroll but then when I refresh the page it does.. now I'm not sure if using jQuery scrollTo is the best method but its the only way I've been able to kind of get it to work
HTML
<pdf-viewer [src]="pdfSrc"
[render-text]="true"
[autoresize]="true"
[original-size]="false"
style="display: block;" (after-load-complete)="callBackFn($event)">
</pdf-viewer>
COMPONENT.TS
import { Component, OnInit, Output, EventEmitter, AfterViewInit} from '#angular/core';
import { AppConsts } from '#shared/AppConsts';
import { AppComponentBase } from '#shared/common/app-component-base';
import { ActivatedRoute } from '#angular/router';
import { HeaderTitleService } from '#shared/service/headerTitle.service';
declare var jQuery: any;
const $ = jQuery;
#Component({
selector: 'app-pdfview',
templateUrl: './pdfview.component.html',
styleUrls: ['./pdfview.component.less']
})
export class PdfviewComponent implements OnInit {
pdfSrc: string;
pdf: string;
pageNum: number = 2;
botString: string;
pageNumberVar: string;
#Output() notify: EventEmitter<String> = new EventEmitter<String>();
constructor(
private route: ActivatedRoute,
private headerTitleService: HeaderTitleService
) {
this.pdf = route.snapshot.params['pdfId'];
if (this.pdf === '1') {
this.pdfSrc = '../../../assets/pdf/emeds1.pdf';
this.botString = 'Admission Reconciliation loaded on Page 2 - matching the word ‘reconcile’ for you.';
this.pageNumberVar = '2';
} else if (this.pdf === '2') {
this.pdfSrc = '../../../assets/pdf/medrec.pdf';
this.botService.sendMessage('I have loaded the document on page 21 showing "Medication Reconciliation"');
setTimeout(() => {
this.botService.sendMessage('That saved you hours of reading :)');
}, 2000);
setTimeout(() => {
$('html, body').animate({
scrollTop: $('.page:nth-child(29)').offset().top
}, 300);
}, 1000);
}
}
callBackFn(pdf: PDFDocumentProxy) {
this.botService.sendMessage(this.botString);
}
ngAfterViewInit() {
setTimeout(function(){
$('html, body').animate({
scrollTop: $('.page:nth-child(' + this.pageNumberVar + ')').offset().top
}, 300);
}, 1000);
}
}
as you can see above ive tried putting the scrollTo function in ngAfterViewInit but that didnt fix it either
Generally speaking, I believe that it is not a good idea to use jQuery with Angular, since it accesses the browser's DOM directly, bypassing Angular's (emulated) Shadow DOM.
In this case, I think that you can probably do away with your scrollTo fix, and just get the pdf viewer working properly.
I initially had problems with showing the correct page too, but after trying a few things, came up with this working configuration:
<pdf-viewer [src]="pdfSrc"
[(page)]="page"
[show-all]="false"
[rotation]="0"
[zoom]="0.9"
>
</pdf-viewer>
I would start with this, making sure that you get the proper page rendered, and then add your other properties, one at a time, to see what works and what breaks.

Accessing Style of Ionic 2 DOM Elements

I'm trying to access the DOM elements of one of my pages with the following:
ionViewDidEnter() {
this.platform.ready().then(() => {
let elm = <HTMLElement>document.querySelector("ion-navbar.toolbar.toolbar-ios.statusbar-padding");
console.log(elm.style);
})
}
However, it appears this element has no style - I have tried various combinations to access it but no luck.
Specifically, I'm looking for the height of the ion-navbar. Is this possible?
You can get the actual height of an element with element.offsetHeight.
As for the style property, it will give you only the attributes defined in the element's inline style attribute (e.g. <div style="height: 20px;">...</div>), not the ones applied by the CSS using selector rules. See this MDN article for more details.
This is my workaround for that.
let tabs = document.querySelectorAll('.show-tabbar');
if (tabs !== null) {
Object.keys(tabs).map((key) => {
tabs[key].style.transform = 'translateY(56px)';
});
}
I always have found it terribly useful to simply use
ionic serve
inspect the element in chrome and easily see the style for the given device.
To access the DOM element in angular I have used the #id route. The following snippet was used to verify the appropriate class was applied by ionic.
html- home.html
<ion-spinner #testspinner name="crescent" paused="{{isPaused}}"></ion-spinner>
ts- home.ts
import {Component} from '#angular/core';
import {ViewChild} from '#angular/core';
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
isPaused: boolean;
#ViewChild('testspinner') testspinner;
constructor() {
this.isPaused = false; // set to true will cause to never have animation state running.
}
containsPausedClass(list: string[]) {
return (list.indexOf('spinner-paused') != -1);
}
ngAfterViewInit() {
// if the spinner is allows to load without 'spinner-paused' then safari will work.
setTimeout(() => {
this.isPaused = true;
}, 0);
console.log('test spinner is paused ',
this.containsPausedClass(this.testspinner._elementRef.nativeElement.classList.value));
}
togglePause() {
this.isPaused = !this.isPaused;
setTimeout(() => {
console.log('test spinner is paused ',
this.containsPausedClass(this.testspinner._elementRef.nativeElement.classList.value));
}, 100);
}
}

Textarea to ignore enter but need to trigger Save button

A bit tricky situation. For the code below, I have added (keydown.enter)="false" to ignore the break line/enter button in textarea
This is causing a user issue and would like the existing behaviour where Pressing enter should automatically trigger the "Save button"
Any idea how to trigger the Save button when still focusing in textArea but ignore the breakline?
<textarea #textArea
style="overflow:hidden; height:auto; resize:none;"
rows="1"
class="form-control"
[attr.placeholder]="placeholder"
[attr.maxlength]="maxlength"
[attr.autofocus]="autofocus"
[name]="name"
[attr.readonly]="readonly ? true : null"
[attr.required]="required ? true : null"
(input)="onUpdated($event)"
[tabindex]="skipTab ? -1 : ''"
(keydown.enter)="false"
[(ngModel)]="value">
</textarea >
Extending the answer by #Pengyy
You can bind the bind the enter key to a pseudoSave function, and preventDefault inside of that, thus preventing both the Save function and the newline. Then you can either call the save function from there(assuming it is accessible such as a service) or you can emit an EventEmitter, and have that emit get caught to trigger the Save function.
you can bind the same function of Save button to keydown.enter of texterea, and call $event.preventDefault to avoid the newline.
sample plunker.
Assuming that your textarea is inside a form element.
{Plunker Demo}
You can achieve it by using a hidden submit input, like this
#Component({
selector: 'my-app',
template: `
<form (submit)="formSubmitted($event)">
<input #proxySubmitBtn type="submit" [hidden]="true"/>
<textarea #textArea (keydown.enter)="$event.preventDefault(); proxySubmitBtn.click()">
</textarea>
</form>
`,
})
export class App {
formSubmitted(e) {
e.preventDefault();
alert('Form is submitted!');
}
}
You can create a service which can send a notification to other components that will handle the command. The service could look like this:
import { Injectable } from "#angular/core";
import { Subject } from "rxjs/Subject";
#Injectable()
export class DataSavingService {
private dataSavingRequested = new Subject<void>();
public dataSavingRequested$ = this.dataSavingRequested.asObservable();
public requestDataSaving(): void {
this.dataSavingRequested.next();
}
}
... and should be registered in the providers section of the module. Note: if data must be passed in the notification, you can declare a non-void parameter type for the dataSavingRequested Subject (e.g. string).
The service would be injected in the component with the textarea element and called in the handler of the Enter keypress event:
import { DataSavingService } from "./services/data-saving.service";
...
#Component({
template: `
<textarea (keypress.enter)="handleEnterKeyPress($event)" ...></textarea>
`
})
export class ComponentWithTextarea {
constructor(private dataSavingService: DataSavingService, ...) {
...
}
public handleEnterKeyPress(event: KeyboardEvent): void {
event.preventDefault(); // Prevent the insertion of a new line
this.dataSavingService.requestDataSaving();
}
...
}
The component with the Save button would subscribe to the dataSavingRequested$ notification of the service and save the data when notified:
import { Component, OnDestroy, ... } from "#angular/core";
import { Subscription } from "rxjs/Subscription";
import { DataSavingService } from "../services/data-saving.service";
...
#Component({
...
})
export class ComponentWithSaveButton implements OnDestroy {
private subscription: Subscription;
constructor(private dataSavingService: DataSavingService, ...) {
this.subscription = this.dataSavingService.dataSavingRequested$.subscribe(() => {
this.saveData();
});
}
public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
private saveData(): void {
// Perform data saving here
// Note: this method should also be called by the Save button
...
}
}
The code above assumes that the saving must be performed in the component with the Save button. An alternative would be to move that logic into the service, which would expose a saveData method that could be called by the components. The service would need to gather the data to save, however. It could be obtained with a Subject/Observable mechanism, or supplied directly by the components as a parameter to saveData or by calling another method of the service.
it could be 2 solutions:
Use javascript to handle enter event and trigger Save function in it
or
Use Same thing from Angular side as describe in this.
This may also help you

how can I listen to changes in code in angular 2?

I'm using angular 2. I have a component with an input.
I want to be able to write some code when the input value changes.
The binding is working, and if the data is changed (from outside the component) I can see that there is change in the dom.
#Component({
selector: 'test'
})
#View({
template: `
<div>data.somevalue={{data.somevalue}}</div>`
})
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
dataChagedListener(param) {
// listen to changes of _data object and do something...
}
}
You could use the lifecycle hook ngOnChanges:
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
ngOnChanges([propName: string]: SimpleChange) {
// listen to changes of _data object and do something...
}
}
This hook is triggered when:
if any bindings have changed
See these links for more details:
https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
As mentioned in the comments of Thierry Templier's answer, ngOnChanges lifecycle hook can only detect changes to primitives. I found that by using ngDoCheck instead, you are able to check the state of the object manually to determine if the object's members have changed:
A full Plunker can be found here. But here's the important part:
import { Component, Input } from '#angular/core';
#Component({
selector: 'listener',
template: `
<div style="background-color:#f2f2f2">
<h3>Listener</h3>
<p>{{primitive}}</p>
<p>{{objectOne.foo}}</p>
<p>{{objectTwo.foo.bar}}</p>
<ul>
<li *ngFor="let item of log">{{item}}</li>
</ul>
</div>
`
})
export class ListenerComponent {
#Input() protected primitive;
#Input() protected objectOne;
#Input() protected objectTwo;
protected currentPrimitive;
protected currentObjectOne;
protected currentObjectTwo;
protected log = ['Started'];
ngOnInit() {
this.getCurrentObjectState();
}
getCurrentObjectState() {
this.currentPrimitive = this.primitive;
this.currentObjectOne = _.clone(this.objectOne);
this.currentObjectTwoJSON = JSON.stringify(this.objectTwo);
}
ngOnChanges() {
this.log.push('OnChages Fired.')
}
ngDoCheck() {
this.log.push('DoCheck Fired.');
if (!_.isEqual(this.currentPrimitive, this.primitive)){
this.log.push('A change in Primitive\'s state has occurred:');
this.log.push('Primitive\'s new value:' + this.primitive);
}
if(!_.isEqual(this.currentObjectOne, this.objectOne)){
this.log.push('A change in objectOne\'s state has occurred:');
this.log.push('objectOne.foo\'s new value:' + this.objectOne.foo);
}
if(this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)){
this.log.push('A change in objectTwo\'s state has occurred:');
this.log.push('objectTwo.foo.bar\'s new value:' + this.objectTwo.foo.bar);
}
if(!_.isEqual(this.currentPrimitive, this.primitive) || !_.isEqual(this.currentObjectOne, this.objectOne) || this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)) {
this.getCurrentObjectState();
}
}
It should be noted that the Angular documentation provides this caution about using ngDoCheck:
While the ngDoCheck hook can detect when the hero's name has changed,
it has a frightful cost. This hook is called with enormous frequency —
after every change detection cycle no matter where the change
occurred. It's called over twenty times in this example before the
user can do anything.
Most of these initial checks are triggered by Angular's first
rendering of unrelated data elsewhere on the page. Mere mousing into
another input box triggers a call. Relatively few calls reveal actual
changes to pertinent data. Clearly our implementation must be very
lightweight or the user experience will suffer.

Categories

Resources