Change behaviour of enter key in a phone - Angular 5 - html

I am working with inputs but I am not really sure about how is the configuration of the navigation done (I guess that it are predefined behaviours).
I am not in the last input the enter key goes to the next one. This one is working as I want.
Nevertheless, when I am on the last input, when I press enter, it automatically clicks on the next button.
This is what I am trying to avoid. Is there any way to change this behaviour? Just to close the keyboard or to click on another button?
I have tried with keyup.enter and it pseudo works. It calls to the method but also clicks on the next button
HTML
<input
type="text"
class="form-control"
id="validationCustomSurname"
placeholder="e.g. Lopez"
required
(keyup.enter)="onNavigate(1, 'forward')"
[(ngModel)]="values.store.surname"
name="surname"
/>
This method should work on a phone, so I guess that keydown is not an option since $event.code does not give me any code in the phone.

Some time ago I make a directive see stackblitz that you apply in a div (or in a form) in the way
<form [formGroup]="myForm" (submit)="submit(myForm)" enter-tab>
Each input or button add a reference variable #nextTab like
<input name="input1" formControlName="input1" #nextTab/>
<button type="button" #nextTab/>
</form>
The directive use ContentChildren to add a keydown.enter to all the components that have #nextTab to focus to the next control
export class EnterTabDirective {
#ContentChildren("nextTab") controls: QueryList<any>
nextTab
constructor(private renderer: Renderer2, private el: ElementRef) {
}
ngAfterViewInit(): void {
this.controls.changes.subscribe(controls => {
this.createKeydownEnter(controls);
})
if (this.controls.length) {
this.createKeydownEnter(this.controls);
}
}
private createKeydownEnter(querycontrols) {
querycontrols.forEach(c => {
this.renderer.listen(c.nativeElement, 'keydown.enter', (event) => {
if (this.controls.last != c) {
let controls = querycontrols.toArray();
let index = controls.findIndex(d => d == c);
if (index >= 0) {
let nextControl = controls.find((n, i) => n && !n.nativeElement.attributes.disabled && i > index)
if (nextControl) {
nextControl.nativeElement.focus();
event.preventDefault();
}
}
}
})
})
}

Here's a very simple approach, with just a few lines of code:
First, in your Template when you dynamically create your Input elements: 1. populate the tabIndex attribute with a unique number, 2. populate a super-simple custom "Tag" Directive with the same unique number as the tabIndex, and 3. set up a Keydown "Enter" event listener:
Template:
<ng-container *ngFor="let row in data">
<input tabindex ="{{row[tabCol]}}" [appTag]="{{row[tabCol]}}" (keydown.enter)="onEnter($event)" . . . />
</ng-container>
In your component, your super-simple event-listener onEnter():
#ViewChildren(TagDirective) ipt!: QueryList<ElementRef>;
onEnter(e: Event) {
this.ipt["_results"][(<HTMLInputElement>e.target).tabIndex%(+this.ipt["_results"].length-1)+1].el.nativeElement.focus();
}
Note: The modulus (%) operation is just to make sure that if you're at the last Input, you'll get cycled back to the first input.
Super-simple, bare-minimum "Tag" Directive
import { Directive, ElementRef, Input } from '#angular/core';
#Directive({
selector: '[appTag]'
})
export class TagDirective {
#Input('appTag') id: number;
constructor(public el: ElementRef) { }
}
There's probably even a way to get rid of the "Tag" `Directive altogether and make it even more simple, but I haven't had time to figure out how to do that yet . . .

Related

Not mandatory option selection in Autocomplete Angular Material [duplicate]

I'm trying to implement the autocomplete component from Angular Material:
https://material.angular.io/components/autocomplete/overview
It works well for letting the user select a particular item from the suggested list but I also want to allow the user to add items not in the list.
So lets say the suggested list has the following items:
Cats
Birds
Dogs
And the user starts typing "Do" and the autocomplete shows "Dogs" as the suggested option (because I'm also filtering the list based on what they type). But then the user continues typing "Dolls" and now nothing is displayed in the autocomplete suggestions. Then the user hits enter and it gets added to the list.
Current behavior is that if what the user typed doesn't exist in the list then they are unable to add the item.
If you add an enter key listener to the input field, you can process the entered value and add it to the options if it doesn't exist. You can also dynamically add whatever the user enters to the list of filtered options as an "add new item" option, or add an "add" icon to the field (e.g. as a matSuffix). Or you can do all three:
Stackblitz
HTML
<form class="example-form">
<mat-form-field class="example-full-width">
<input matInput placeholder="Item" aria-label="Item" [matAutocomplete]="auto" [formControl]="itemCtrl" (keyup.enter)="addOption()">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="optionSelected($event.option)">
<mat-option *ngFor="let item of filteredItems | async" [value]="item">
<span>{{ item }}</span>
</mat-option>
</mat-autocomplete>
<button *ngIf="showAddButton && itemCtrl.value" matSuffix mat-button mat-icon-button (click)="addOption()"><mat-icon matTooltip='Add "{{itemCtrl.value}}"'>add</mat-icon></button>
</mat-form-field>
</form>
TS
import { Component } from '#angular/core';
import { FormControl } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { startWith } from 'rxjs/operators/startWith';
import { map } from 'rxjs/operators/map';
/**
* #title Autocomplete with add new item option
*/
#Component({
selector: 'autocomplete-overview-example',
templateUrl: 'autocomplete-overview-example.html',
styleUrls: ['autocomplete-overview-example.css']
})
export class AutocompleteOverviewExample {
itemCtrl: FormControl;
filteredItems: Observable<any[]>;
showAddButton: boolean = false;
prompt = 'Press <enter> to add "';
items: string[] = [
'Cats',
'Birds',
'Dogs'
];
constructor() {
this.itemCtrl = new FormControl();
this.filteredItems = this.itemCtrl.valueChanges
.pipe(
startWith(''),
map(item => item ? this.filterItems(item) : this.items.slice())
);
}
filterItems(name: string) {
let results = this.items.filter(item =>
item.toLowerCase().indexOf(name.toLowerCase()) === 0);
this.showAddButton = results.length === 0;
if (this.showAddButton) {
results = [this.prompt + name + '"'];
}
return results;
}
optionSelected(option) {
if (option.value.indexOf(this.prompt) === 0) {
this.addOption();
}
}
addOption() {
let option = this.removePromptFromOption(this.itemCtrl.value);
if (!this.items.some(entry => entry === option)) {
const index = this.items.push(option) - 1;
this.itemCtrl.setValue(this.items[index]);
}
}
removePromptFromOption(option) {
if (option.startsWith(this.prompt)) {
option = option.substring(this.prompt.length, option.length -1);
}
return option;
}
}
It's weird that the user can add an item in the suggested list. The list is suggested to the user by someone who knows what to suggest. But anyway...
The user can type anything in the field and ignore the suggestions. By ignoring the suggested Dogs and typing Dolls, user can press an "Add" button which will add whatever is typed in (Dolls) to the options array.
For example, you can do it by listening to the submit event on the form:
(ngSubmit)="options.push(myControl.value); myControl.reset()"
Here's the complete demo as well.

When select dropdown directive for submit on press 'Enter' no more work

All example of code is here:
https://stackblitz.com/edit/ngx-modal-cegwyu?file=src%2Fapp%2Fenter.directive.ts
When I fill input and press enter on keyboard I got alert. ( try to stackblitz )
But problem is when I fill input and select dropdown then press enter - nothing.
I lose my event I don't know where.... and why?
#Directive({ selector: '[entersubmit]' })
export class EnterEventDirective {
constructor(private el: ElementRef) {}
#HostListener('keyup.enter', ['$event'])
onFormKeyUp(event: KeyboardEvent) {
alert('submitted'); //when select from dropdown I lose
//but when fill only input this is work good
}
}

Get selected tab to switch icons Ionic 5

I'm trying to change my tab icons from filled to outline when someone selects it (filled when selected, outline when not selected).
On the Ionic 5 Tabs Doc there's a getSelected() method but no examples on how to use this.
My idea was to use ionTabsDidChange to detect when someone clicked a tab, then use getSelected() and set the icon from 'home' to 'home-outline'.
Tabs.html
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button class="tab-btn" tab="home">
<ion-icon name="{{ homeIcon }}"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button class="tab-btn" tab="price">
<ion-icon name="{{ priceIcon }}"></ion-icon>
<ion-label>Price Search</ion-label>
</ion-tab-button>
<ion-tabs>
Tabs.ts
export class TabsPage {
public homeIcon = 'home';
private homeFilled = 'home';
private homeOutline = 'home-outline'
public priceIcon = 'search-outline';
private priceFilled = 'search';
private priceOutline = 'search-outline';
ionTabsDidChange() {
let selectedTabName = getSelected();
// Stuff to switch icon from filled to outline and vice versa
}
}
The issue is that I don't know how to use getSelected(), I've tried adding ViewChild like this stackoverflow, but getSelected() is not a function (Changed to IonTabs because Tabs don't exist in Ionic 5.
At this point, the only solution I can think of is saving the tabs state and adding click functions for everything.
You are heading the right direction, there are still few missing points. In the Ionic doc you are reading the "events" are not directly accessible in the page without binding them to the component itself and in order to use ViewChild you also need to give the component an id:
Tabs.html
<ion-tabs #tabs (ionTabsDidChange)="setCurrentTab()">
"tabs" will be the id of the component and whenever ionTabsDidChange event gets triggered it will call setCurrentTab method, it should be declared on your page.
Then in the page, as you have already mentioned you'll need to add a ViewChild (now possible with the id) and use getSelected() method.
Tabs.ts
export class TabsPage {
#ViewChild('tabs', { static: false }) tabs: IonTabs;
...
setCurrentTab() {
this.selectedTab = this.tabs.getSelected();
}
}
And voila, that should be it :-)
There's another really easy way to do this. First add the ionTabsDidChange attribute to your ion-tabs element as #andriyleu suggests but this time make sure to pass $event details in.
<ion-tabs (ionTabsDidChange)="setCurrentTab($event)">
Then you can define the function in your ts file like this.
current_tab = "home"; // Set this as you default page name
setCurrentTab(ev: any){
this.current_tab = ev.tab;
}
Finally, in your html you can use a very efficient piece of Javascript to determine which icon to show. Perhaps like me, you're switching between a filled and outline version.
<ion-icon [name]="current_tab == 'home' ? 'home' : 'home-outline'"></ion-icon>
Thanks to the everyone who answered this and helped me figure it out!
Inside the ion-tabs tag, ionTabsDidChange passes an event which has the selected tab. You can get that event by doing the following then it should give you the clicked tab:
tabs.html
<ion-tabs (ionTabsDidChange)="tabClicked($event)">
tabs.ts
tabClicked(e) {
this.selectedTab = e.tab
}
Doesn't work in React.
const log = (e: any): any => {
console.log(e);
}
<IonTabs ionTabsDidChange={log}>
Type '{ children: Element[]; ionTabsDidChange: (e: any) => any; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<IonTabs> & Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
[react-scripts] Property 'ionTabsDidChange' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<IonTabs> & Readonly<Props> & Readonly<{ children?: ReactNode; }>'. TS2322
[react-scripts] 47 | <IonApp>
Any thoughts?
Ionic will add "tab-selected" class to the selected tab. You can use that class to style the tab icon.
This post is old and answered but I'm going to expand Shrishail's answer, because I think it's the better answer and it didn't get enough attention. when you just want to change looks, like changing icon, it's better to use css. here is a working example based on tab-selected class:
<ion-tab-button>
<ion-icon name="home" class="selected"></ion-icon>
<ion-icon name="home-outline" class="unselected"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
and in css:
.tab-selected .unselected {
display: none;
}
.tab-selected .selected {
display: initial !important;
}
.selected {
display: none;
}
For people that use react.js, this solution was effective for me.
const App: React.FC = () => {
const [selectedTab, setSelectedTab] = useState<string>();
const handleTabsDidChange = async (event: CustomEvent) => {
setSelectedTab(event.detail.tab);
};
return (
<IonApp>
<IonReactRouter>
<IonTabs onIonTabsDidChange={handleTabsDidChange}>
<IonRouterOutlet>
<Route exact path="/status">
<Status />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="status" href="/status">
<IonIcon icon={selectedTab === 'status' ? disc : discOutline} />
<IonLabel>Status</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
};

Angular - Prevent click event on disabled buttons

I'm trying to prevent click event on disabled buttons, in other words, prevent some user who removes the disabled attribute to call some action.
For now, I have the following code to do this:
<button [disabled]="someCondition" (click)="executeAction()">Execute action</button>
executeAction(): void {
if (this.someCondition) return;
// ...
}
Works, but it isn't a good solution as I have to do it for ALL buttons in my app (and believe me, it's easy to forgot to do this and even a Linter can't help me here).
Looking for a more robust solution, I thought that directive could help me:
import { Directive, HostListener, Input, Renderer2, ElementRef } from '#angular/core';
#Directive({
selector: 'button'
})
export class ButtonDirective {
#Input() set disabled(value: boolean) {
this._disabled = value != null;
this.renderer2.setAttribute(this.elementRef.nativeElement, 'disabled', `${this._disabled}`);
}
private _disabled: boolean;
constructor(
private readonly elementRef: ElementRef,
private readonly renderer2: Renderer2
) { }
#HostListener('click', ['$event'])
onClick(mouseEvent: MouseEvent) {
// nothing here does what I'm expecting
if (this._disabled) {
mouseEvent.preventDefault();
mouseEvent.stopImmediatePropagation();
mouseEvent.stopPropagation();
return false; // just for test
}
}
}
<button [disabled]="someCondition" (click)="executeAction()">Execute action</button>
executeAction(): void {
console.log('still being called');
}
...however it does absolutely nothing. It doesn't prevent the click event. Is there any solution that I don't have to control the action itself in its call?
STACKBLITZ
This is a workaround with CSS which cheaper than scripts.
You easily could use
pointer-events: none;
In this case, the button will not be clickable.
As a UX enhance you could also wrap your button inside a div and give this div a CSS property
cursor: not-allowed;
Which will show the blocked circle icon instead of normal mouse view when hover.
In your directive, you can do something like this. You can achieve it by adding an event listener to parent in the capturing phase.
ngOnInit() {
this.elementRef.nativeElement.parentElement.addEventListener('click',(e) => {
if(this._disabled && e.target.tagName === 'BUTTON') {
e.stopImmediatePropagation();
e.stopPropagation();
}
}, true);
}
You can remove the listener in onDestroy
Prevent click event on disabled buttons
If the disabled attribute is there the click will not happen.
When user decides to use devtools
However if the user edits the HTML and removes the disabled attribute manually, then click will happen. You can try and do the check as you have suggested, but the browser is an unsafe environment. The user will still be able to execute any code on the webpages behalf irrespective of any frontend checks you might put in.

Angular 5 FormGroup reset doesn't reset validators

I have a form on my page and when I call FormGroup.reset() it sets the forms class to ng-pristine ng-untouched but FormControl.hasError(...) still returns truthy. What am I doing wrong here?
Template
<form [formGroup]="myForm" (ngSubmit)="submitForm(myForm)">
<mat-form-field>
<input matInput formControlName="email" />
<mat-error *ngIf="email.hasError('required')">
Email is a required feild
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="password.hasError('required')">
Password is a required feild
</mat-error>
</mat-form-field>
<button type="submit">Login</button>
</form>
Component
export class MyComponent {
private myForm: FormGroup;
private email: FormControl = new FormContorl('', Validators.required);
private password: FormControl = new FormControl('', Validators.required);
constructor(
private formBuilder: FormBuilder
) {
this.myForm = formBuilder.group({
email: this.email,
password: this.password
});
}
private submitForm(formData: any): void {
this.myForm.reset();
}
}
Plunker
https://embed.plnkr.co/Hlivn4/
It (FormGroup) behaves correctly. Your form requires username and password, thus when you reset the form it should be invalid (i.e. form with no username/password is not valid).
If I understand correctly, your issue here is why the red errors are not there at the first time you load the page (where the form is ALSO invalid) but pop up when you click the button. This issue is particularly prominent when you're using Material.
AFAIK, <mat-error> check the validity of FormGroupDirective, not FormGroup, and resetting FormGroup does not reset FormGroupDirective. It's a bit inconvenient, but to clear <mat-error> you would need to reset FormGroupDirective as well.
To do that, in your template, define a variable as such:
<form [formGroup]="myForm" #formDirective="ngForm"
(ngSubmit)="submitForm(myForm, formDirective)">
And in your component class, call formDirective.resetForm():
private submitForm(formData: any, formDirective: FormGroupDirective): void {
formDirective.resetForm();
this.myForm.reset();
}
GitHub issue: https://github.com/angular/material2/issues/4190
In addition to Harry Ninh's solution, if you'd like to access the formDirective in your component without having to select a form button, then:
Template:
<form
...
#formDirective="ngForm"
>
Component:
import { ViewChild, ... } from '#angular/core';
import { NgForm, ... } from '#angular/forms';
export class MyComponent {
...
#ViewChild('formDirective') private formDirective: NgForm;
constructor(... )
private someFunction(): void {
...
formDirective.resetForm();
}
}
After reading the comments this is the correct approach
// you can put this method in a module and reuse it as needed
resetForm(form: FormGroup) {
form.reset();
Object.keys(form.controls).forEach(key => {
form.get(key).setErrors(null) ;
});
}
There was no need to call form.clearValidators()
Add the property -
#ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
and use this instead of this.myForm.reset();
this.formGroupDirective.resetForm();
This will reset the error display and also do the job of form.reset(). But the form, along with the fields, will still show ng-invalid class
Check this answer for more details - https://stackoverflow.com/a/56518781/9262627
The below solution works for me when trying to reset specific form controller in form group -
this.myForm.get('formCtrlName').reset();
this.myForm.get('formCtrlName').setValidators([Validators.required, Validators.maxLength(45), Validators.minLength(4), Validators.pattern(environment.USER_NAME_REGEX)]);
this.myForm.get('formCtrlName').updateValueAndValidity();
form.reset() won't work on custom form control like Angular Material that's why the function is not working as expected.
My workaround for this is something like this
this.form.reset();
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
this.form.reset() the issue with this is that it will reset your formcontrol values but not the errors so you need to reset them individually by this line of code
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
With this you don't need to use FormGroupDirective which is a cleaner solution for me.
Github issue: https://github.com/angular/angular/issues/15741
I found that after calling resetForm() and reset(), submitted was not being reset and remained as true, causing error messages to display. This solution worked for me. I found it while looking for a solution to calling select() and focus() on an input tag, which also wasn't working as expected. Just wrap your lines in a setTimeout(). I think setTimeout is forcing Angular to detect changes, but I could be wrong. It's a bit of a hack, but does the trick.
<form [formGroup]="myFormGroup" #myForm="ngForm">
…
<button mat-raised-button (click)="submitForm()">
</form>
submitForm() {
…
setTimeout(() => {
this.myForm.resetForm();
this.myFormGroup.reset();
}, 0);
}
resetForm() {
this.myFormGroup.reset();
this.myFormGroup.controls.food.setErrors(null);
this.myFormGroup.updateValueAndValidity();
}
UPDATE FROM 2021 - ANGULAR 11.2
The fact to use a [formGroup]="form and a #formDirective="ngForm" directly into the HTML function is not a good practise. Or maybe you would prefer to use #ViewChild, and do it directly from your .ts. Actually, the problem don't come from Angular, but Material.
If you take a look at their GitHub, you will see this :
/** Provider that defines how form controls behave with regards to displaying error messages. */
#Injectable({providedIn: 'root'})
export class ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.invalid && (control.touched || (form && form.submitted)));
}
}
The form will keep its submitted state. So you just have to delete the last part of the function.
Here is my solution (tested and working). I have a Material Module, into I've implemented this :
export class ShowOnInvalidTouchedErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl): boolean {
return !!(control && control.invalid && control.touched);
}
}
#NgModule({
providers: [
{
provide: ErrorStateMatcher, useClass: ShowOnInvalidTouchedErrorStateMatcher
}
],
exports: [
MatSnackBarModule,
MatTabsModule,
...
]
});
If you want to use this ErrorStateMatcher on only one form, it's possible. Please see this Material example. This is the same principle.
I had no luck with resetting the form directive. But You can also change the input state to pending to do that as well.
this.myForm.get("email").reset();
this.myForm.get("password").reset();
To anyone whom this may help, I am running Angular 9.1.9 and I didn't want to reset the form/controls just the overall validity of the form so I just ran:
this.registerForm.setErrors(null);
...where registerForm: FormGroup and that reset the form errors, leading to:
this.registerForm.valid
...returning true.
The same can be done for controls:
this.registerForm.get('email').setErrors(null)
As soon as the form is touched, these errors are re-evaluated anyway so if that's not good enough, you may need to have a boolean flag to further pin-down exactly when you want to start showing/hiding error UI.
I did not need to touch the directive in my case.
I was also having the same set of problems. My problem was that i was using mat-form-field and formGroup. After resetting the form submitted flag was not resetting.
So, the solution that worked for me is, putting a directive of ngForm along with formGroup and passing onSubmit(form). Added
#ViewChild('form') form;
in component and then I used
this.form.resetForm();
Nothing from above worked for me (Angular 7.2, Angular Material 7.3.7).
Try to pass with submit method an event on view:
<form [formGroup]="group" (ngSubmit)="onSubmit($event)">
<!-- your form here -->
</form>
Then use it to reset currentTarget and your form afterwards:
public onSubmit(event): void {
// your code here
event.currentTarget.reset()
this.group.reset()
}
Simple fix: use button with type="reset" and function submitForm() together
<form [formGroup]="MyForm" (ngSubmit)="submitForm()">
<input formControlName="Name">
<mat-error>
<span *ngIf="!tunersForm.get('Name').value && tunersForm.get('Name').touched"></span>
</mat-error>
<button type="reset" [disabled]="!MyForm.valid" (click)="submitForm()">Save</button>
</form>