Ionic - Go to the next input field instead of submitting the form


#1

I would like to know if there is a way to go to the next input field instead of submitting the form. I tried it in Android 6.0 and when clicking the right-arrow button (don’t know its name, if it’s the go button, next button or another :S) that is on the bottom right corner of the keyboard the form is submitted. The same occurs in the browser with Enter and I think will also happen in other Android versions and iOS.

I think that this is the default behaviour of browsers for submitting forms, after all you also have the Tab key and the mouse if want to go to the next input. But in an app this is not a wanted behaviour IMHO and I don’t see apps behaving like that.

Maybe this is not an Ionic2 specific question, but considering that Ionic is mainly focused in mobile apps I think that this would be a good place to ask, after all I’m creating an Ionic2 app.

What I want is a way to go to the next input instead of submitting the form, and if possible submitting the form when the current input is the last one. A simple and generic solution (like a directive or something like that) would be the best.

Thanks in advance.


#2

Achieved what I wanted doing the following (the functions in the bottom can be moved to a service or utilitarian class, if wanted):

import { Directive, Input, HostListener, Renderer } from '@angular/core';

const ATTR_NAME = 'myTabindex';

@Directive({
	selector: `[${ATTR_NAME}]`
})
export class TabindexDirective {
	@Input(`${ATTR_NAME}`) myTabindex: string;

	constructor(private renderer: Renderer) { }

	@HostListener('keydown', ['$event']) onInputChange(e) {
		var code = e.keyCode || e.which;

		if (code === 13) {
			let next: HTMLElement = this.getMyNextFocusableElement(e.srcElement);

			if (next) {
				e.preventDefault();
				this.renderer.invokeElementMethod(next, 'focus', []);
			}
		}
	}

	private getMyNextFocusableElement(elem: HTMLElement): HTMLElement {
		let tabindex: number = parseInt(this.myTabindex || '0');
		let next: HTMLElement = MyUtils.getNextFocusableElement(elem, ATTR_NAME, tabindex);
		return next;
	}
}

const MyUtils = (() => {
	const FOCUSABLES = ['input', 'select', 'textarea', 'object'];
	const FOCUSABLES_SELECTOR = FOCUSABLES.join(',');

	function getNextFocusableElement(elem: HTMLElement, attrName: string, tabindex: number): HTMLElement {
		let form = getFormElement(elem);
		let next = null;

		tabindex++;
		next = getElement(form, attrName, tabindex);

		while (next) {
			next = getFocusableElement(next);

			if (next) {
				return next;
			}

			tabindex++;
			next = getElement(form, attrName, tabindex);
		}

		return null;
	}

	function getFormElement(elem: HTMLElement): HTMLElement {
		let form = elem ? <HTMLElement>elem.parentElement : null;

		while (form && (form.tagName.toLowerCase() !== 'form')) {
			form = form.parentElement;
		}

		return form;
	}

	function getElement(form: HTMLElement, attrName: string, tabindex: number): HTMLElement {
		let selector = `[${attrName}="${tabindex}"]`;
		let elem = form ? <HTMLElement>form.querySelector(selector) : null;
		return elem;
	}

	function getFocusableElement(elem: HTMLElement): HTMLElement {
		let tagName = elem.tagName.toLowerCase();
		let focusable = FOCUSABLES.some(
			tagFocusable => tagFocusable === tagName
		);

		if (!focusable) {
			elem = <HTMLElement>elem.querySelector(FOCUSABLES_SELECTOR);
			focusable = !!elem;
		}

		if (focusable) {
			//TODO: verify if elem disabled, readonly, hidden, etc...
			// in which case focusable must be changed to false
		}

		if (focusable) {
			return elem;
		}
	}

	return {
		getNextFocusableElement: getNextFocusableElement
	};
})();

I just use the attribute myTabindex in the html fields (ex: username[myTabindex="1"], password[myTabindex="2"], and so on), and I add the directive to the component, then it works as I wanted.

I’m not focusing buttons because it isn’t working right when I tested with radios and I am not checking visible, disabled, readonly, etc… but for now it solves the problem for me.


#3

Hey Lucas,

Isn’t using $ means jqueary? Can it be used with ionic2/angular2?


#4

Hi @rubmz! No, that’s not jquery. It’s template literals to avoid mistyping the attribute name (look at the const ATTR_NAME at the top of the file). This also avoids forgetting changing all its uses in the file if I want to change the attribute name (I only have to change the const value in 1 place).

But this seems to not work well with AOT (Ahead Of Time) Compilation, so I changed my project removing the const ATTR_NAME and replacing its uses with the corresponding literal string. For instance:

@Input(`${ATTR_NAME}`) myTabindex: string;

was changed to:

@Input('myTabindex') myTabindex: string;

And so on…

It works well with Ionic2, without needing external libraries.


#5

Is this the only/best way?

Is there a way to just change the button on the keyboard?

I’m struggling with this. The default action should never be submit, otherwise the users may submit the form before filling all of it by mistake.


#6

@alexandretok I don’t know if this is the only way (probably not), but seeing that this worked for me I’m using it anyway :slight_smile:.

I don’t know about changing the button on the keyboard though (I searched about it back then, but haven’t found anything that helped).

I also found it weird that the submit was called on every input (although this is the default HTML form submit behaviour, but especially in mobile this seems not right, and in apps I use I don’t see this behaviour, but going to the next input, if there is one). That is probably the main reason that I created this directive.


#7

Thanks for providing your code as it was exactly what I was looking for! I had issues where the focus on the next ion-input wasn’t bringing up the keyboard.

After plenty of experimentation, I updated your code with a different focusing mechanism that ended up shortening the code quite a bit:

@Directive({
    selector: '[myTabIndex]'
})

export class TabindexDirective {

  inputRef: TextInput;
  constructor(inputRef: TextInput) {
    this.inputRef = inputRef;
  }

  @HostListener('keydown', ['$event']) onInputChange(e) {
    var code = e.keyCode || e.which;
    if (code === 13) {
      e.preventDefault();
      this.inputRef.focusNext();
    }
  }
}

#8

how to use myTabIndex in html file please can you give demo of both js and html in ionic 2?


#9

@hardikslk You can create the following files:

tabindex.directive.ts:

import { Directive, Input, HostListener, Renderer } from '@angular/core';

@Directive({
	selector: '[myTabindex]'
})
export class TabindexDirective {
	@Input('myTabindex') myTabindex: string;

	constructor(private renderer: Renderer) { }

	@HostListener('keydown', ['$event']) onInputChange(e) {
		var code = e.keyCode || e.which;

		if (code === 13) {
			let next: HTMLElement = this.getMyNextFocusableElement(e.srcElement);

			if (next) {
				e.preventDefault();
				this.renderer.invokeElementMethod(next, 'focus', []);
			}
		}
	}

	private getMyNextFocusableElement(elem: HTMLElement): HTMLElement {
		let tabindex: number = parseInt(this.myTabindex || '0');
		let next: HTMLElement = MyUtils.getNextFocusableElement(elem, 'myTabindex', tabindex);
		return next;
	}
}

const MyUtils = (() => {
	const FOCUSABLES = ['input', 'select', 'textarea', 'button', 'object'];
	const FOCUSABLES_SELECTOR = FOCUSABLES.join(',');

	function getNextFocusableElement(elem: HTMLElement, attrName: string, tabindex: number): HTMLElement {
		let form = getFormElement(elem);
		let next = null;

		tabindex++;
		next = getElement(form, attrName, tabindex);

		while (next) {
			next = getFocusableElement(next);

			if (next) {
				return next;
			}

			tabindex++;
			next = getElement(form, attrName, tabindex);
		}

		return null;
	}

	function getFormElement(elem: HTMLElement): HTMLElement {
		let form: HTMLFormElement = elem ? (<HTMLInputElement>elem).form : null;
		return form;
	}

	function getElement(form: HTMLElement, attrName: string, tabindex: number): HTMLElement {
		let selector = `[${attrName}="${tabindex}"]`;
		let elem = form ? <HTMLElement>form.querySelector(selector) : null;
		return elem;
	}

	function getFocusableElement(elem: HTMLElement): HTMLElement {
		let tagName = elem.tagName.toLowerCase();
		let focusable = FOCUSABLES.some(
			tagFocusable => tagFocusable === tagName
		);

		if (!focusable) {
			elem = <HTMLElement>elem.querySelector(FOCUSABLES_SELECTOR);
			focusable = !!elem;
		}

		if (focusable) {
			//TODO: verify if elem disabled, readonly, hidden, etc...
			// in which case focusable must be changed to false
		}

		if (focusable) {
			return elem;
		}
	}

	return {
		getNextFocusableElement: getNextFocusableElement
	};
})(); 

Remember to include it in your module:

import { TabindexDirective } from '../directives/tabindex.directive';
import { Page1 } from '../pages/page1/page1';
import { Page2 } from '../pages/page2/page2';
import { MyApp } from './app.component';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';

@NgModule({
  declarations: [
    MyApp,
    Page1,
    Page2,
	TabindexDirective
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    Page1,
    Page2
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

Then just use it in any component that you want. For example:

page1.ts:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
	selector: 'page-page1',
	templateUrl: 'page1.html'
})
export class Page1 {
	public model: { email: string, password: string } = {
		email: '',
		password: ''
	};

	constructor() { }

	public onLogin() {
		//TODO do login stuff here...
		console.log('login: ', this.model);
	}
}

page1.html:

<ion-header>
	<ion-navbar>
		<button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
		<ion-title>Page One</ion-title>
	</ion-navbar>
</ion-header>

<ion-content padding>
	<h3>Tabindex Example</h3>

	<form (ngSubmit)="onLogin()" novalidate="novalidate" #myForm>
		<ion-item>
			<ion-input 
				name="email" 
				type="email" 
				[(ngModel)]="model.email" 
				#email
				myTabindex="1"
			></ion-input>
		</ion-item>

		<ion-item>
			<ion-input 
				name="password" 
				type="password" 
				[(ngModel)]="model.password" 
				#password
				myTabindex="2"
			></ion-input>
		</ion-item>

		<button ion-button block>Login</button>
	</form>
</ion-content>

Update

Like @KyleHoskins said, you can do this to make the directive significantly simpler:

tabindex.directive.ts:

import { Directive, HostListener, Input } from '@angular/core';
import { TextInput } from 'ionic-angular';

@Directive({
	selector: '[myTabindex]'
})
export class TabindexDirective {

	constructor(private inputRef: TextInput) { }

	@HostListener('keydown', ['$event']) onInputChange(e) {
		var code = e.keyCode || e.which;

		if (code === 13) {
			e.preventDefault();
			this.inputRef.focusNext();
		}
	}
}

page1.html:

<ion-header>
	<ion-navbar>
		<button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
		<ion-title>Page One</ion-title>
	</ion-navbar>
</ion-header>

<ion-content padding>
	<h3>Tabindex Example</h3>

	<form (ngSubmit)="onLogin()" novalidate="novalidate" #myForm>
		<ion-item>
			<ion-input 
				name="email" 
				type="email" 
				[(ngModel)]="model.email" 
				#email
				myTabindex
			></ion-input>
		</ion-item>

		<ion-item>
			<ion-input 
				name="password" 
				type="password" 
				[(ngModel)]="model.password" 
				#password
			></ion-input>
		</ion-item>

		<button ion-button block>Login</button>
	</form>
</ion-content>

Take note that the number in front of the myTabindex is unnecessary now, and I removed it from the last input otherwise it would return to the first, instead of triggering the submit action.

Also note that in this simplified way, TextInput is from the ionic-angular package and is related to an Ionic2 input, so you probably cannot use the directive in a custom component (you would receive some exception saying that there is no provider for TextInput or something in these lines). There is also the fact that focusNext() is labeled with @private so I don’t know if it is a good idea to use it.


And that’s it! Hope it helps.


#10

@lucasbasquerotto thanks for fast reply


#11

@hardikslk Don’t worry :smile: Take a look at the update section also.


#12

@lucasbasquerotto thanks really its helpful section because I put first directive file content which you mention in first post.
:slight_smile:thanks again


#13

@lucasbasquerotto I have one question can you help me regarding Ionic 2?


#14

@hardikslk You can create a new topic about the question and post the thread url here, and I will take a look to see if I know how to solve it.


#15

this is cool! thanks!


#16

Thanks a lot!
Works perfectly in Android, but in iOS it hides the keyboard. Any idea why?


#17

@ycanales Nope, I don’t have an iOS device :confused:


#18

I used your updated version, and it works perfectly except:

I apply a function to my (blur) event on every input, to show an appropriate error if validation fails. The line e.preventDefault() in the directive was messing that up, so I simply removed that line, and everything is 100’s.

Cheers


#19

Its working good…
But it is only for input type …field…
How can I focus on dropdown field and checkbox field???

Is there any way???


#20

@anbucse I don’t know if there is some solution for dropdown and checkbox fields, but in this case what would be the expected behaviour?

Maybe the same behaviour as pressing Tab…

But if you press Enter in a checkbox it is selected/unselected. On the other hand, if you press Tab you go to next input; so I don’t know if there is a solution for checkbox (for leaving it).

About dropdown, normally ionic dropdown opens a modal/popup to show the options when selected, I don’t know if there is a way to select it doing nothing (without showing the popup).

I tried to find some common superclass of TextInput that works for ionic inputs, checkboxes, dropdown, datetime, etc., with a focusNext() common method but haven’t found it. I don’t know if the Ionic team will create one in the future or not.