Angular Custom focus Directive. Focus a form's first invalid input - javascript

I have created a directive to focus an input if it's invalid
import { Directive, Input, Renderer2, ElementRef, OnChanges } from '#angular/core';
#Directive({
// tslint:disable-next-line:directive-selector
selector: '[focusOnError]'
})
export class HighlightDirective implements OnChanges {
#Input() submitted: string;
constructor(private renderer: Renderer2, private el: ElementRef) { }
ngOnChanges(): void {
const el = this.renderer.selectRootElement(this.el.nativeElement);
if (this.submitted && el && el.classList.contains('ng-invalid') && el.focus) {
setTimeout(() => el.focus());
}
}
}
I do have a reactive form with two inputs, and I've applied the directive to both inputs
<form>
...
<input type="text" id="familyName" focusOnError />
...
<input type="text" id="appointmentCode" focusOnError />
...
</form>
After submitting the form it works fine, but what I'm struggling to achieve is the following:
Expected result:
- After submitting the form if both inputs are invalid, only the first one should be focused.
Current result:
- After submitting the form if both inputs are invalid, the second one gets focused.
I don't know how to specify "only do this if it's the first child", I've tried with the directive's selector with no luck.
Any ideas?
Thanks a lot in advance.

To control the inputs of a Form, I think the better solution is use ViewChildren to get all elements. So, we can loop over this elements and focus the first.
So, we can has a auxiliar simple directive :
#Directive({
selector: '[focusOnError]'
})
export class FocusOnErrorDirective {
public get invalid()
{
return this.control?this.control.invalid:false;
}
public focus()
{
this.el.nativeElement.focus()
}
constructor(#Optional() private control: NgControl, private el: ElementRef) { }
}
And, in our component we has some like
#ViewChildren(FocusOnErrorDirective) fields:QueryList<FocusOnErrorDirective>
check() {
const fields=this.fields.toArray();
for (let field of fields)
{
if (field.invalid)
{
field.focus();
break;
}
}
}
You can see in action in the stackblitz
UPDATE always the things can improve:
Why not create a directive that applied to the form?
#Directive({
selector: '[focusOnError]'
})
export class FocusOnErrorDirective {
#ContentChildren(NgControl) fields: QueryList<NgControl>
#HostListener('submit')
check() {
const fields = this.fields.toArray();
for (let field of fields) {
if (field.invalid) {
(field.valueAccessor as any)._elementRef.nativeElement.focus();
break;
}
}
}
So, our .html it's like
<form [formGroup]="myForm" focusOnError>
<input type="text" formControlName="familyName" />
<input type="text" formControlName="appointmentCode" />
<button >click</button>
</form>
See the stackblitz
Even more, if we use as selector form
#Directive({
selector: 'form'
})
Even we can remove the focusOnError in the form
<form [formGroup]="myForm" (submit)="submit(myForm)">
..
</form>
Update 2 Problems with formGroup with formGroup. SOLVED
NgControl only take account the controls that has [(ngModel)], formControlName and [formControl], so. If we can use a form like
myForm = new FormGroup({
familyName: new FormControl('', Validators.required),
appointmentCode: new FormControl('', Validators.required),
group: new FormGroup({
subfamilyName: new FormControl('', Validators.required),
subappointmentCode: new FormControl('', Validators.required)
})
})
We can use a form like:
<form [formGroup]="myForm" focusOnError (submit)="submit(myForm)">
<input type="text" formControlName="familyName" />
<input type="text" formControlName="appointmentCode" />
<div >
<input type="text" [formControl]="group.get('subfamilyName')" />
<input type="text" [formControl]="group.get('subappointmentCode')" />
</div>
<button >click</button>
</form>
where in .ts we has
get group()
{
return this.myForm.get('group')
}
Update 3 with Angular 8 you can get the descendants of the children, so it's simply write
#ContentChildren(NgControl,{descendants:true}) fields: QueryList<NgControl>

well, just for funny stackblitz. If we has a formControl, we can inject ngControl that it's the control itself. So we can get the formGroup. I control the "submited" making a work-around in the app.component
<button (click)="check()">click</button>
check() {
this.submited = false;
setTimeout(() => {
this.submited = true;
})
}
The directive is like
export class FocusOnErrorDirective implements OnInit {
#HostListener('input')
onInput() {
this._submited = false;
}
//I used "set" to avoid ngChanges, but then I need the "ugly" work-around in app.component
#Input('focusOnError')
set submited(value) {
this._submited = value;
if (this._submited) { ((is submited is true
if (this.control && this.control.invalid) { //if the control is invalid
if (this.form) {
for (let key of this.keys) //I loop over all the
{ //controls ordered
if (this.form.get(key).invalid) { //If I find one invalid
if (key == this.control.name) { //If it's the own control
setTimeout(() => {
this.el.nativeElement.focus() //focus
});
}
break; //end of loop
}
}
}
else
this.el.nativeElement.focus()
}
}
}
private form: FormGroup;
private _submited: boolean;
private keys: string[];
constructor(#Optional() private control: NgControl, private el: ElementRef) { }
ngOnInit() {
//in this.form we has the formGroup.
this.form = this.control?this.control.control.parent as FormGroup:null;
//we need store the names of the control in an array "keys"
if (this.form)
this.keys = JSON.stringify(this.form.value)
.replace(/[&\/\\#+()$~%.'"*?<>{}]/g, '')
.split(',')
.map(x => x.split(':')[0]);
}
}

Related

Invalid input focus is not working with FormArray

So everything works fine with normal FormGroup but when it comes to FormArray it doesn't focus the invalid input.
My form initialization is below
initForm() {
this.parentForm= this.fb.group({
childFormArray: this.fb.array([this.createchildForm()])
});
}
after this, I initialize formarray like below
createChildForm(data?: any): FormGroup {
var childForm = this.fb.group({
name: [data?.name? data?.name: '']
});
childForm .valueChanges.subscribe(value => {
var fieldWithValue = Object.keys(value).filter(key => value[key] == '');
fieldWithValue.forEach(conName => {
childForm .get(conName)?.addValidators([Validators.required]);
});
});
return childForm ;
}
My method to set errors after clicking submit (requirement);
assignError(){
this.parentForm.controls.childFormArray.value.forEach((v: any, index: number) => {
var array = this.parentForm.controls.childFormArrayas FormArray;
var item = array.at(index);
var emptyItems = Object.keys(v).filter(key => v[key] == '');
emptyItems.forEach(ele => {
if (ele != "section") {
item.get(ele)?.updateValueAndValidity({ emitEvent: false });
}
});
});
}
and after this I have made my validator which will check for invalid input and focus it.
import { Directive, HostListener, ElementRef } from '#angular/core';
#Directive({
selector: '[focusInvalidInput]'
})
export class FormDirective {
constructor(private el: ElementRef) { }
#HostListener('submit')
onFormSubmit() {
const invalidControl = this.el.nativeElement.querySelector('.ng-invalid');
if (invalidControl) {
invalidControl.focus();
}
}
}
after this I have used its selector in my corresponding form
focusInvalidInput (ngSubmit)="saveDetails()"
and inside submit method I call my error adding method which is
saveDetails(){
assignError();
}
After doing all this I am able to focus invalid input but somehow its not working for formarray.
and when I console invalidControl its prints all the invalid input which should not happen maybe bcz there are many invalid input and whome should it focus so I tried using .first() method but it gives error saying first is not a method
The actual reason was focus doesn't work on div and my input which were using formArray's controls were wrapped inside a div which is
<div id="resp-table-body" *ngFor="let item of getParentFormControls(); let i = index"
[formGroupName]="i">
<div class="table-body-cell">
<input type="text" class="form-control no_shadow_input" id="name"
placeholder="Enter Here" formControlName="name" autocomplete="off">
<span *ngIf="item.get('name')?.hasError('required')"
class="text-danger">
Name is required
</span>
</div>
</div>
So all I had to change is add input.ng-invalid in my directive
const invalidControl = this.el.nativeElement.querySelector('input.ng-invalid');
Now everything is working fine

how to pass data to a form in another component in Angular

I have a form in a seperate component which looks like this:
<form [formGroup]="form">
<div class="card" style="margin-bottom: 10px" >
<input
formControlName="name"
type="text"
placeholder="please enter"
/>
</div>
</form>
the ts file looks like this (formcontrol part)
form = this.builder.group({
name: new FormControl(),
})
this is how I call it from the other component:
component B:
<form></form>
What I want to achieve now is the following I am filling some values from my backend which works, I know want to asign these values to my form from the form component. I know I have to use #Input I am kinda stuck against a wall and dont know how to connect them. Can someone give me a small example how I can fill my form from values from component b?
To my knowledge I have posted the most important part of this question, if anything else is needed feel free to ask
UPDATE_
Form.ts
#Input()
myObject: { name: string };
form = this.builder.group({
name: new FormControl()
});
ngOnChanges(changes: SimpleChanges): void {
if (changes.myObject?.currentValue) {
this.form.patchValue(this.myObject);
}
}
UPDATE2:
my form ts looks like this now:
#Input()
myObject: { name: string };
form = this.builder.group({
name: new FormControl()
});
ngOnChanges(changes: SimpleChanges): void {
if (changes.myObject && changes.myObject.currentValue) {
this.form.patchValue(this.myObject);
}
}
My form.hml like this:
<form [formGroup]="form">
<div class="card" style="margin-bottom: 10px" >
<input
formControlName="name"
type="text"
placeholder="please enter"
/>
</div>
</form
my calling component ts like this:
this.form.controls.legitimiertePersonNameField.setValue(response.vorname)
this.someName = this.form.controls.legitimiertePersonNameField.value -> gets the data from formcontrol in parent/calling
html from caling looks like this:
<gwg-form [myObject]="someName"></gwg-form>
The someName value is correct I could see it in a console.log but it does not set the value for the fomrCOntrol of the form.html it is just empty input
form.component.ts
#Component({
selector: 'my-form'
...
})
export class FormComponent implements OnChanges {
#Input()
myObject: { name: string };
form = this.builder.group({
name: new FormControl()
});
ngOnChanges(changes: SimpleChanges): void {
if (changes.myObject?.currentValue) {
this.form.patchValue(this.myObject);
}
}
}
form.component.html
<form [formGroup]="form">
<div class="card" style="margin-bottom: 10px" >
<input
formControlName="name"
type="text"
placeholder="please enter" />
</div>
</form>
calling.component.ts
#Component({ ... })
export class CallingComponent {
someObject: { name: string };
loadData(): void {
this.someObject = ...
}
}
calling.component.html
<my-form [myObject]="someObject">
</my-form>

FormControl validator always invalid

Following my previous question, I'm trying to create a custom validator that allow the users to type only specific values in an input of text.
app.component.ts:
export class AppComponent implements OnInit {
myForm: FormGroup;
allowedValuesArray = ['Foo', 'Boo'];
ngOnInit() {
this.myForm = new FormGroup({
'foo': new FormControl(null, [this.allowedValues.bind(this)])
});
}
allowedValues(control: FormControl): {[s: string]: boolean} {
if (this.allowedValuesArray.indexOf(control.value)) {
return {'notValidFoo': true};
}
return {'notValidFoo': false};
}
}
app.component.html:
<form [formGroup]="myForm">
Foo: <input type="text" formControlName="foo">
<span *ngIf="!myForm.get('foo').valid">Not valid foo</span>
</form>
The problem is that the foo FormControl is always false, (the myForm.get('foo').valid is always false).
What wrong with my implementation?
you just need to return null when validation is ok. and change that method like below
private allowedValues: ValidatorFn (control: FormControl) => {
if (this.allowedValuesArray.indexOf(control.value) !== -1) {
return {'notValidFoo': true};
}
return null;
}

Angular .removeAt(i) at FormArray does not update in DOM - Angular

I thought it was an issue with my implementation but seems that my code for creating dynamic FormArray should be functional based from this question I raised. When I integrate it to my project, the remove function does delete the element from the FormArray, but it does not reflect in the interface/ does not remove object from the DOM. What could be causing this?
import {
Component,
VERSION
} from '#angular/core';
import {
FormGroup,
FormControl,
FormArray,
Validators,
FormBuilder
} from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
objectProps: any[];
public dataObject = [{
"label": "Name",
"name": "name",
"type": "text",
"data": ""
},
{
"label": "Contacts",
"name": "contacts",
"type": "inputarray",
"array": []
}
]
form: FormGroup;
constructor(private _fb: FormBuilder) {}
ngOnInit() {
const formGroup = {};
for (let field of this.dataObject) {
if (field.type == "inputarray") {
console.log("THIS IS " + field.type)
formGroup[field.name] = this._fb.array([''])
} else {
console.log("THIS IS " + field.type)
formGroup[field.name] = new FormControl(field.data || '') //, this.mapValidators(field.validation));
}
}
this.form = new FormGroup(formGroup);
}
addFormInput(field) {
const form = new FormControl('');
( < FormArray > this.form.controls[field]).push(form);
}
removeFormInput(field, i) {
( < FormArray > this.form.controls[field]).removeAt(i);
}
}
<form *ngIf="form" novalidate (ngSubmit)="onSubmit(form.value)" [formGroup]="form">
<div *ngFor="let field of dataObject">
<h4>{{field.label}}</h4>
<div [ngSwitch]="field.type">
<input *ngSwitchCase="'text'" [formControlName]="field.name" [id]="field.name" [type]="field.type" class="form-control">
<div *ngSwitchCase="'inputarray'">
<div formArrayName="{{field.name}}" [id]="field.name">
<div *ngFor="let item of form.get(field.name).controls; let i = index;" class="array-line">
<div>
<input class="form-control" [formControlName]="i" [placeholder]="i">
</div>
<div>
<button id="btn-remove" type="button" class="btn" (click)="removeFormInput(field.name, i)">x</button>
</div>
</div>
</div>
<div>
<button id="btn-add" type="button" class="btn" (click)="addFormInput(field.name)">Add</button>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-danger btn-block" style="float: right; width:180px" [disabled]="!form.valid">Save</button>
This is not a great solution but I solved my problem by manipulating value and after removing the control.
I simply moved the item that I wanted to remove to the end of the array and then I removed the last item.
removeItem(index: number): void {
const value = this.formArray.value;
this.formArray.setValue(
value.slice(0, index).concat(
value.slice(index + 1),
).concat(value[index]),
);
this.formArray.removeAt(value.length - 1);
}
I hope it helps someone struggling with this issue in the future.
Maybe you can try to force change detection using the reference to application. To do that inject the ApplicationRef in the constructor an call the tick(); on your removeFormInput method.
constructor(private _fb: FormBuilder, private appRef: ApplicationRef) {}
And in removeFormInput
removeFormInput(field, i) {
(<FormArray>this.form.controls[field]).removeAt(i);
this.appRef.tick();
}
Take a look at angular documentation: API > #angular/core /ApplicationRef.tick()
replace below function, you are not removing the row object from 'dataObject'.
removeFormInput(field, i) {
( < FormArray > this.form.controls[field]).removeAt(i);
this.dataObject.splice(this.dataObject.indexOf(field),1);
}
Take a look here Add and Remove form items I build on stackblitz, for me its working fine, adding and removing items... Take a look.
working version
I was facing this problem as well. The solution for me was to get rid of/fix the trackBy function in NgFor*. I think you need to introduce a proper trackBy function and it might solve your error.
sauce: How to use `trackBy` with `ngFor`

How to use onBlur event on Angular2?

How do you detect an onBlur event in Angular2?
I want to use it with
<input type="text">
Can anyone help me understand how to use it?
Use (eventName) while binding event to DOM, basically () is used for event binding. Also, use ngModel to get two-way binding for myModel variable.
Markup
<input type="text" [(ngModel)]="myModel" (blur)="onBlurMethod()">
Code
export class AppComponent {
myModel: any;
constructor(){
this.myModel = '123';
}
onBlurMethod(){
alert(this.myModel)
}
}
Demo
Alternative 1
<input type="text" [ngModel]="myModel" (ngModelChange)="myModel=$event">
Alternative 2 (not preferable)
<input type="text" #input (blur)="onBlurMethod($event.target.value)">
Demo
For a model-driven form to fire validation on blur, you could pass updateOn parameter.
ctrl = new FormControl('', {
updateOn: 'blur', //default will be change
validators: [Validators.required]
});
Design Docs
You can also use (focusout) event:
Use (eventName) for while binding event to DOM, basically () is used for event binding. Also you can use ngModel to get two way binding for your model. With the help of ngModel you can manipulate model variable value inside your component.
Do this in HTML file
<input type="text" [(ngModel)]="model" (focusout)="someMethodWithFocusOutEvent($event)">
And in your (component) .ts file
export class AppComponent {
model: any;
constructor(){ }
/*
* This method will get called once we remove the focus from the above input box
*/
someMethodWithFocusOutEvent() {
console.log('Your method called');
// Do something here
}
}
you can use directly (blur) event in input tag.
<div>
<input [value]="" (blur)="result = $event.target.value" placeholder="Type Something">
{{result}}
</div>
and you will get output in "result"
HTML
<input name="email" placeholder="Email" (blur)="$event.target.value=removeSpaces($event.target.value)" value="">
TS
removeSpaces(string) {
let splitStr = string.split(' ').join('');
return splitStr;
}
/*for reich text editor */
public options: Object = {
charCounterCount: true,
height: 300,
inlineMode: false,
toolbarFixed: false,
fontFamilySelection: true,
fontSizeSelection: true,
paragraphFormatSelection: true,
events: {
'froalaEditor.blur': (e, editor) => { this.handleContentChange(editor.html.get()); }}
This is the proposed answer on the Github repo:
// example without validators
const c = new FormControl('', { updateOn: 'blur' });
// example with validators
const c= new FormControl('', {
validators: Validators.required,
updateOn: 'blur'
});
Github : feat(forms): add updateOn blur option to FormControls
Try to use (focusout) instead of (blur)
Another possible alternative
HTML Component file:
<input formControlName="myInputFieldName" (blur)="onBlurEvent($event)">
TypeScript Component file:
import { OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
export class MyEditComponent implements OnInit {
public myForm: FormGroup;
constructor(private readonly formBuilder: FormBuilder) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
myInputFieldName: ['initial value', { validators: [Validators.required, Validators.maxLength(100), anotherValidator], updateOn: 'blur' }],
});
}
onBlurEvent(event) {
// implement here what you want
if (event.currentTarget && event.currentTarget.value && event.currentTarget.value !== '') { }
}
}
I ended up doing something like this in Angular 14
<form name="someForm" #f="ngForm" (ngSubmit)="onSubmit(f)"> ...
<input
matInput
type="email"
name="email"
autocomplete="email"
placeholder="EMAIL"
[ngModel]="payload.email"
#email="ngModel"
(blur)="checkEmailBlur(f)"
required
email
tabindex="1"
autofocus
/>
then ... in .ts
checkEmailBlur(f: NgForm) {
const email = f.value.email;

Categories

Resources