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
Related
I'm looping through the values and displaying checkboxes which I'm showing, now in addition there is an search box filter on the top of list of checkboxes basically to filter out checkbox list.
HTML
<input [(ngModel)]="searchText" class="lmn-input lmn-select-input-inner mt-input" placeholder="Search by key word" id="mngd-seg"/>
<div formArrayName="checkBoxValueList"
*ngFor="let dataConceptList of scopeSetDetails.controls['checkBoxValueList'].controls | searchFilter : searchText; let i = index">
<label class="lmn-selection-control">
<span class="lmn-checkbox">
<input class="lmn-control-input" type="checkbox" [formControlName]="i">
<i class="lmn-control-icon lmnicon lmnicon-check"></i>
</span>
<span>Data Concept </span>
<span style="padding-left: 150px">{{ checkBoxValueList[i].scopeSets }}</span>
</label>
</div>
Therefore I've created an custom pipe 'searchFilter' and using this pipe above in *ngFor
PIPE
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'searchFilter',
})
export class FilterPipe implements PipeTransform {
transform(items: any, searchText: string): any {
if (!items) return [];
if (!searchText) return items;
searchText = searchText.toLowerCase();
return items.filter((it) => {
return it.name.toLowerCase().includes(searchText);
});
}
}
In TS I'm initializing the checkboxes to false (with checkboxes, the Angular form control value is bound to the "checked" state (not the "value" attribute), so it should be either true or false) since it needs to be unchecked initially.
TS
const formControls = this.checkBoxValueList.map(
(control) => new FormControl(false)
);
const selectAllControl = new FormControl(false);
this.scopeSetDetails = this.fb.group({
search_text: this.fb.control(''),
checkBoxValueList: new FormArray(formControls, this.minSelectedScopeSet(1)),
selectAll: selectAllControl,
});
Now issue is in filter pipe I'm getting items as an array of FormControl whose value property is 'false'. I need the checkbox text somehow and initially all checkboxes should be unchecked. how can I achieve this?
I am building my own custom WYSIWYG text editor in Angular using a content editable div. I am extracting it as a component. I want to be able to type some text and format it and then when I click a button from the parent component it should either change the text in the editor or update it.
My parent component looks as follows:
#Component({
selector: 'app-parent',
templateUrl: `
<form [formGroup]="form">
<app-default-editor
formControlName="body" >
</app-default-editor>
</form>
<button (click)="changeValue()">Change Value</button>
<p *ngIf="!form.valid">Counter is invalid!</p>
<pre>{{ form.value | json }}</pre>
`
})
export class ParentComponent {
initialText = '<H1>This is the heading</H1>';
form = this.fb.group({
body: '<H1>This is the heading</H1>'
});
changeValue(){
this.form.patchValue({ body: 'This is my new text' });
}
}
My editor component looks as follows:
#Component({
selector: 'app-parent',
templateUrl: `
<button (click)="executeCommand('bold')" ><i class="fas fa-bold"></i></button>
<button (click)="executeCommand('italic')" ><i class="fas fa-italic"></i></button>
<button (click)="executeCommand('underline')" ><i class="fas fa-underline"></i></button>
<div
#htmlEditor
id="htmlEditor"
contenteditable="true"
(input)="onTextChanged()"
[innerHTML]="initialText">
</div>
`
})
export class EditorComponent implements ControlValueAccessor, OnChanges {
#Input()
initialText = "";
#Input()
_textValue = "";
#Input()
minValue = 0;
#ViewChild('htmlEditor') htmlEditor;
ngAfterViewInit() {
this.htmlEditor.nativeElement.innerHTML = this.initialText;
}
get textValue() {
return this._textValue;
}
set textValue(val) {
this._textValue = val;
this.propagateChange(this._textValue);
}
writeValue(value: any) {
if (value !== undefined) {
this.textValue = value;
}
}
propagateChange = (_: any) => { };
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched() { }
validateFn: Function;
ngOnChanges(changes) {
this.validateFn = createEditorRequiredValidator();
}
onTextChanged() {
this.textValue = this.htmlEditor.nativeElement.innerHTML;
}
executeCommand(cmd) {
document.execCommand(cmd, false, this.htmlEditor.nativeElement);
}
}
export function createEditorRequiredValidator() {
return function validateEditor(c: FormControl) {
let err = {
requiredError: {
given: c.value,
required: true
}
};
return (c.value.length == 0) ? err : null;
}
}
With the current code, I can format text to be bold, italic etc and the form in the parent component can display the value in the tags. When I click the button to change the text to a predefined value the form updates but the editor doesn't show any changes.
There are two little mistakes that prevent your code from working as it should.
1. No provider for NG_VALUE_ACCESSOR
This one should have given you errors in your console. To make sure you can use formControlName directive the component needs to provide a value accessor. You achive that by adding the providers when defining the editor component. Like this:
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EditorComponent), multi: true}
]
When you did this the writeValue function within your editor component is called everytime the form control for the field body changes. Lets go to little mistake number two...
2. innerHtml bound to initialText
You update the text property within writeValue but that property is never bound to the template. The innerHtml binding only looks for the initialText property - and that hasnĀ“t changed.
Change your binding to look like this:
<div #htmlEditor
id="htmlEditor"
contenteditable="true"
(input)="onTextChanged()"
[innerHTML]="textValue">
You can utilize the initial text within the getter and return it in case of an empty text value.
I ended up changing the changeValue method in the parent component to:
changeValue(){
this.form.patchValue({ body: [WHATEVER THE TEXT MUST BE] });
this.initialText = [WHATEVER THE TEXT MUST BE]
}
This seems to work but I don't think it is the best answer.
I have a form where a user could add one/more div of Address on click of add button.
I want if user select options=5 from the dropdown, want to show and hide textbox in that particular address Div.
Component Code
get contactFormGroup() {
return this.form.get('Array') as FormArray;
}
ngOnInit() {
this.form= this.fb.group({
Array: this.fb.array([])
});
}
createContact(): FormGroup {
return this.fb.group({
ABC: [null, Validators.compose([Validators.required])],
Test: [null, Validators.compose([Validators.required])]
});
}
addContact() {
this.Group.push(this.createContact());
}
showValue(event) {
const selectedValue = event;
if (selectedValue === '5') {
this.showValuetxtbox = true;
} else {
this.showValuetxtbox = false;
}
}
As you are looping to add the divs, you could use a template reference variable on the drop down. e.g #select then refer to that in the *ngIf:
<form [formGroup]="addExpressionform">
<div formArrayName="expressionArray">
<div *ngFor="let item of contactFormGroup.controls; let i = index;" [formGroupName]="i">
<carbon-dropdown #select
(optionSelected)="showValue($event)"
[formControlN]="'UOM'"
[options]="listOptions" [formGroup]="item"
name="UOM"
>
</carbon-dropdown>
<carbon-text-input *ngIf="select.value == 5"
[formControlN]="'Value'"
[formGroup]="item"
name="Value"
>
</carbon-text-input>
<carbon-button type="primary" (click)="submit()" id="save-parameter">Save</carbon-button>
</div>
</div>
</form>
Simplified StackBlitz demo.
Take a look at this Stackblitz, it's referred to in the Angular docs and could serve as boilerplate to what you are trying to achieve.
You should isolate every possible type of question by creating a different class for each one, so you can shape the data and then use ngSwitch to dynamically create the HTML accordingly.
Question base class:
export class QuestionBase<T> {
controlType: string;
value: T;
key: string;
label: string;
// etc
constructor(options) {
// constructor logic
}
}
Some special class that inherents from base class
import { QuestionBase } from './question-base';
export class SpecialQuestion extends QuestionBase<string> {
controlType = 'specialQuestion';
type: string;
// special Question
specialValue: string;
constructor(options) {
super(options);
this.type = options['type'] || '';
}
}
Then, a question component:
<div [formGroup]="form">
<label>{{question.label}}</label>
<div [ngSwitch]="question.controlType">
// controls logic
<input *ngSwitchCase="'textbox'" >
<select *ngSwitchCase="'specialQuestion'"></select>
</div>
</div>
Then you throw this into a container component where you loop through the entire questions array.
This way your code will be future proof and reusable as you add/change functionality to your forms down the road. You won't have to create spaghetti to meet edge case requirements like an extra input field.
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]);
}
}
I am making angular 6 application where i am using angular reactive form.
Html:
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" *ngFor="let creds of form.controls.credentials?.value; let i = index">
<div [formGroupName]="i" style="display: flex">
<select (ngModelChange)="changeAction($event)" formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}"> {{option.value}} </option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="showLabel">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
<pre>
{{ form ?.value | json }}
</pre>
Ts:
form: FormGroup;
showLabel: boolean = false;
options : any = [
{ "key" : "set", "value": "Set" },
{ "key" : "wait", "value": "Wait" },
{ "key" : "go", "value": "Go" }
]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
credentials: this.fb.array([]),
});
}
addCreds() {
const creds = this.form.controls.credentials as FormArray;
creds.push(this.fb.group({
action: '',
name: '',
label: ''
}));
}
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
Working stackblitz: https://stackblitz.com/edit/angular-form-array-example-yksojj
In this given example there will be an add button, upon clicking that button you will get a select-box with values as set,wait,go and an input called name.. Upon click over add button the same row will be added and forms each object inside array.
Also you can see an if condition inside html for label,
<div *ngIf="showLabel">
<input placeholder="Label" formControlName="label">
</div>
The thing i am in the need was in the select box if i choose set or go then the label needs to displayed otherwise it should not be displayed for which i have written,
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
To be clear enough If the user clicks three times add button then the dropdown and name field alone needs to be displayed for three times whereas if the user selects the value from dropdown as set or go then the label input needs to be displayed to that particular row alone where the dropdown has the value set and go.. If the selection was wait then there should not be label box for the row which has dropdown value as wait.
Kindly help me to achieve the expected result..
If you add disabled:true property to formControl it will exclude fromControl from formGroup then you can enable formControl manually
creds.push(this.fb.group({
action: '',
name: '',
label: {disabled:true, value: ""}
}));
Then enable using enable method
changeAction(e,index) {
if(e === "set" || e === "go") {
this.showLabel = true;
this.form.get('credentials').at(index).get('label').enable();
} else {
this.showLabel = false;
this.form.get('credentials').at(index).get('label').disable();
}
}
Example:https://stackblitz.com/edit/angular-form-array-example-5buwyr
Please, NOT use (ngModelChange) or changeAction($event) to get the value of a control in an array -well, ngModelChange is for Template driven form, not for Reactive Form.
First change your form, create the form using a div with formArrayName="credentials", and a inner div *ngFor="let creds of form.get('credentials').controls
<!--a div with formArrayName--->
<div formArrayName="credentials" >
<!--a div *ngFor over "controls", in this div don't forget formGroupName-->
<div *ngFor="let creds of form.get('credentials').controls; let i = index"
[formGroupName]="i" style="display: flex">
<select formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}">
{{option.value}}
</option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="??????????????">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
Well, now the condition. To get the value of "action" you can use
form.get('credentials').at(i).value.action
//or, better
creds.value.action
So, your div becomes like
<div *ngIf="creds.value.action=='set' ||
creds.value.action=='go'">
<input placeholder="Label" formControlName="label">
</div>
This aproach avoid unnecesary code in your .ts.
this.showLabel
The scope of this variable is your whole component. Therefore, turning it on or off will show and hide all inputs.
You need a per-row value (creds.showLabel in your interface), or use this in your template :
*ngIf="['set','go'].includes(creds.action)"
Updated Stackblitz
By the way, this :
changeAction(e) {
if(e === "set" || e === "go") {
this.showLabel = true;
} else {
this.showLabel = false;
}
}
is more elegant written this way :
changeAction(e) {
this.showLabel = ['set','go'].includes(e)
}
or this way :
changeAction(e) {
this.showLabel = e in ['set','go']
}
Well, Jeremy's answer is pretty nice and enforces to use most of the native apis given by platform/language, however, here is a traditional approach to understand what is the actual flow and scope of objects, etc,etc...
Root Cause: The show hide field is made global to the scope of component, not at per formgroup level. So changing a single value, and using for all, will affect to all.
Solution:
Use Jeremy's answer for clean coding and less error prone code.
Manage an array of variable that will take care of sho/hide detail for each form group.
In the below answer, added some comments for easy understanding, and added console.log to see what exactly happening. Also, played with index i created in *ngFor and showed how you can make use of these things in future.
import { Component } from '#angular/core';
import { FormControl, FormGroup, FormArray, FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: 'app.component.html',
})
export class AppComponent {
form: FormGroup ;
showLabel = [];
creds : FormArray;
options : any = [
{ "key" : "set", "value": "Set" },
{ "key" : "wait", "value": "Wait" },
{ "key" : "go", "value": "Go" }
]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
credentials: this.fb.array([]),
});
this.creds = this.form.controls.credentials as FormArray;
}
addCreds() {
this.creds.push(this.fb.group({
action: '',
name: '',
label: ''
}));
// every time adding new foem grp, adding lable show/hide data also.
this.showLabel.push(false);
}
changeAction(e, i) {
//every time input clikced, managing show/hide for that particular data also
console.log("iii", i, e.target.value);
if( e.target.value == "set" || e.target.value == "go") {
this.showLabel[i] = true;
} else {
this.showLabel[i] = false;
}
}
}
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" *ngFor="let creds of form.controls.credentials?.value; let i = index">
<div [formGroupName]="i" >
<select (change)="changeAction($event, i)" formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}"> {{option.value}} </option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="showLabel[i]">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
<pre>
{{ form ?.value | json }}
</pre>
See live stackblitz working code
Note: Traditional is meant here.. as we every time do i.e. handle our problem on our own and create new problem on us to solve. It's not a tradition. :P