First off, I have an Angular reactive form that has a button that can add another FormArray to the form. All validation good and working as expected. Things have gotten a little tricky when introducing another dynamic control to the already dynamic form group. This control is shown/hidden based on a selection made in another form control.
When the control is shown I introduce validation, when the control is hidden the validation is cleared. This ensures that my form remains valid/invalid correctly.
Its acting a little buggy e.g. when I complete a group of inputs and add another dynamic group of inputs, both triggering the hidden control... then to amend the previous hidden input - the form remains true. e.g.
Selecting 123 triggers the "Other Code" control, removing the value should make the form invalid, but it stays true at this point.
I have a change function assigned to the select to determine whats been selected.
selectClick(x) {
const f = this.form;
let items = this.form.get('justificationItems') as FormArray;
if (x === '123') {
items.controls.forEach(i => {
console.log(i)
i['controls'].other.setValidators([Validators.required]);
// i['controls'].other.updateValueAndValidity();
});
} else {
items.controls.forEach(i => {
i['controls'].other.clearValidators();
// i['controls'].other.updateValueAndValidity();
});
}
f.updateValueAndValidity();
}
I suspect when changing the select property to trigger the above it does not do it to the correct index item, and it does it for all?
StackBlitz - https://stackblitz.com/edit/angular-prepopulate-dynamic-reactive-form-array-ufxsf9?file=src/app/app.component.ts
the best way to "clear/add Validators" really is enabled or disabled the formControls. Remember a formControl has as status one of this:
type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
So we can simple enabled/disabled the FormControl. Futhermore, when we create the formGroup we can created disabled, so at first will be not INVALID
Well, the code is a bit confussed. first the use of i['controls'].other (really you can use i.controls.other an a estrange mix using FormBuilder and new Form.
As always we has a FormArray we create a getter
get justificationItems()
{
return this.form.get('justificationItems') as FormArray;
}
In stead use two differents functions to create the form, we can use and unique
createJustificationField(x: any=null): FormGroup {
x=x || {name:null,description:null,code:null,other:null}
return new FormGroup({
name: new FormControl(x.name, [Validators.required]),
description: new FormControl(x.description, [Validators.required]),
code: new FormControl(x.code, [Validators.required]),
other: new FormControl({value:x.other,
disabled:x.code!=='123'},[Validators.required]),
});
}
See that we can use as
this.createJustificationField(..an object..)
this.createJustificationField()
Our functions: createForm, addItem and selectClick (I like more another name like codeChange but is a minor change) becomes like
createForm() {
this.service.getmodel().subscribe((response:any) => {
this.form = new FormGroup({
justificationItems: new FormArray(
response.justificationItems.map(x=>
this.createJustificationField(x))
),
});
});
}
addItem(): void {
this.justificationItems.push(this.createJustificationField());
this.form.updateValueAndValidity();
}
selectClick(x,i) {
if (x === '123')
this.justificationItems.at(i).get('other').enable()
else
this.justificationItems.at(i).get('other').disable()
this.form.updateValueAndValidity();
}
And the .html becomes more clear in the way
<form *ngIf="form" [formGroup]="form">
<div formArrayName="justificationItems">
<div
*ngFor="
let orgs of justificationItems.controls;
let i = index;
let last = last
"
[formGroupName]="i"
>
<label>Name </label>
<input formControlName="name" placeholder="Item name" /><br />
<label>Description </label>
<input
formControlName="description"
placeholder="Item description"
/><br />
<label>Code </label>
<select
(change)="selectClick($event.target.value, i)"
formControlName="code"
>
<option value="123">123</option>
<option value="456">456</option></select
><br />
<ng-container *ngIf="justificationItems.at(i).value.code === '123'">
<label>Other Code </label>
<input formControlName="other" placeholder="other" /><br /><br />
</ng-container>
<button
*ngIf="last"
[disabled]="justificationItems.at(i).invalid"
type="button"
(click)="addItem()"
>
Add Item
</button>
</div>
</div>
<button [disabled]="!form.valid" type="button">Submit</button>
</form>
<p>Is form valid: {{ form?.valid | json }}</p>
see the stackblitz
Root cause: When selectClick trigger, you clear or set validation for all controls other in array form. You should set only for one form in formArray.
I rewrite your function:
selectClick(x, index) {
const f = this.form;
let items = this.form.get('justificationItems') as FormArray;
if (x === '123') {
items.controls[index]['controls'].other.setValidators([Validators.required]);
} else {
items.controls.forEach(i => {
items.controls[index]['controls'].other.clearValidators();
i['controls'].other.updateValueAndValidity();
});
}
items.controls[index]['controls'].other.updateValueAndValidity();
}
change code in template:
<select
(change)="selectClick($event.target.value, i)"
formControlName="code"
>
Related
I have an input field which is being generated using *ngFor and I want to capture all those values inside the input field on submit button click. I have tried all possible ways but not able to get the entered data, please help me. Below is my code I have tried.
html Code:
arr= [{name:"test", percentage: "29"},{name:"abc", percentage: "45"}, {name:"def", percentage: "63"}]
<div *ngFor= "let obj of arr">
<input type="text" [value]="obj.percentage">
</div>
<button (click)="submit()"></button>
Your current solution does not have any sort of form manager. There are two solutions:
Reactive Forms
// component.ts
form = new FormGroup( {
test: new FormControl(null)
// Add your other fields here
} );
// Patch your values into the form
arr.forEach(value => {
this.form.get(value.name).patchValue(value.percentage);
})
// html
<div *ngFor= "let obj of arr">
<input type="text" [formControl]="form.get(obj.name)" >
</div>
Your form will store all the values. To get them once, you could use form.value
NgModel
// component.ts
test: boolean;
// Add your other fields here
// Patch your values into the form
arr.forEach(value => {
this.[value.name] = value.percentage;
})
// html
<div *ngFor= "let obj of arr">
<input type="text" [(ngModel)]="obj.name" >
</div>
Both of these examples are cutting corners but they should give you enough of an idea of where to head next. I'd suggest the Reactive Forms path.
I am doing an add form for that I need the formcontrol,formgroup setup for all these values entered in the ts. Its normal stuff maybe. But here I have it very different. I have a add button which duplicates the set of forms in the html. I am finding it difficult to write formgroup logic and validators in the ts for it.
Kindly help if u know(You can atleast tell me how to set up the formcontrol names ang bring it on to ts)
Note: Pls comment below if my question was unclear.
My stackblitz link : https://stackblitz.com/edit/angular-ivy-g8cty1?file=src%2Fapp%2Fapp.component.html
You can declare your FormGroup like below.
myForm = new FormArray([]);
addRow() {
const group = new FormGroup({
firstName: new FormControl("", [Validators.required]),
value: new FormControl("", [Validators.required])
});
this.myForm.push(group);
}
and invoke this function whenever user click on Add button.
In Template to show the form. Loop through FormControl and bind it like below.
<div *ngFor="let form of myForm.controls;">
<ng-container [formGroup]="form">
<div>
<label>Name</label
><input type="text" required formControlName="firstName" />
</div>
<span class="text-danger" *ngIf="isInValidFormControl(form,'firstName')">
Name is required
</span>
<div>
<label>Value</label><input type="text" formControlName="value" />
</div>
</ng-container>
</div>
Your validation with respect to total will be like below.
isValidTotal() {
var values = this.myForm.controls.map((x) =>
x ? Number(x.value.value) : 0
);
let sumTotal = values.reduce((a, b) => a + b);
return sumTotal <= this.total;
}
And you can call this from template like this.
<button (click)="addRow()" [disabled]="!isValidTotal()">Add</button>
<button (click)="submit()" [disabled]="!isValidTotal()">Submit</button>
<div *ngIf="!isValidTotal()">
<p>{{errorMessage}}</p>
</div>
Working sandbox
https://codesandbox.io/s/formarraydynamic-rqdhc?file=/src/app/app.component.html
I'm trying to create a script that keeps our main button disabled until specific field requriments are met.
jQuery(document).ready(function() {//check if all are filled else disable submit
var inputFields = jQuery('#list-item-cc input, #field_28_50 input,#field_28_18 input');
inputFields.keyup(function() {
var empty = false;
inputFields.each(function() {
if (jQuery(this).val().length == 0) {
empty = true;
}
});
if (empty) {
jQuery('#gform_submit_button_28').attr('disabled', 'disabled');
} else {
jQuery('#gform_submit_button_28').removeAttr('disabled');
}
I'm having trouble thinking of a way to ensure my inputFields variable can be passed to my inputFields.each(function() in a way that would allow the loop.
We're not worried about all input fields. Just the specific inputs in our inputFields variable.
Is this an effective way to ensure a button is disabled if certain fields are not filled out and can I create the selector in the way that i did and use that in an each statement?
Looks like you are using gravity forms? In that case I would add a css class to each field that you want to validate. That way you don't have to go searching for ID's and change the code for multiple forms.
https://docs.gravityforms.com/css-ready-classes/
Here is a fiddle in which I pretend that I added "ensure-filled" to each item in the gravity forms builder
https://jsfiddle.net/dokLz4hm/3/
Also note that I added a .trim() to the value so that blank spaces aren't counted as input and made the submit button generic so it would work with any field in a form that contains the ensure-filled class
Html
<div>
<div id="arbitraty_id_1">
<input type="text" class="ensure-filled" />
</div>
<div id="arbitraty_id_2">
<input type="text" class="ensure-filled" />
</div>
<div id="arbitraty_id_3">
<input type="text" class="ensure-filled" />
</div>
<input type="submit" value="submit" disabled>
</div>
JS
$(document).ready(function() {
var inputFields = $('.ensure-filled');
inputFields.keyup(function() {
var empty = false;
inputFields.each(function() {
if ($(this).val().trim().length == 0) {
empty = true;
}
});
$('input[type="submit"]').attr('disabled', empty);
})
})
First of all, I don't see how the modal could have anything to do with this issue since its actually in this component's code, not a child. Still, this is in a modal, just in case.
I'm opting to not use FormArray since I need to keep track of my selections that may be added in a separate object, so I'm just creating unique IDs for the controls and adding them to the FormGroup. I can access the controls in the ts code, set values, get values, etc, but the form isn't binding and I can't figure out why not.
I can have an unknown number of items in this modal form, which will each have a selector (dropdown to select a property) and then the input to be able to modify some data. The input could be of different types, so it needs to be added and binded upon the choice from the selector.
<form [formGroup]="multiEditFormGroup" novalidate>
<div *ngFor="let item of multiEditSelections; let i = index">
<div>
<mdb-select [options]="availablePropsSelect"
placeholder="Choose property to edit"
class="colorful-select dropdown-main"
(selected)="multiEditSelectProp(item.selectorId, $event)"></mdb-select>
<label>Property to edit</label>
</div>
<div>
<div>
<input mdbActive
type="text"
class="form-control"
[formControlName]="item.controlId" />
</div>
</div>
</div>
</form>
Excepts of ts code:
public multiEditFormGroup = new FormGroup({});
onModalOpen():void {
const selectorId = this.utils.uuid();
this.multiEditFormGroup.addControl(selectorId, this.propSelector);
this.multiEditSelections.push({
selectorId: selectorId,
prop: '',
label: '',
type: '',
controlId: '' // not yet known since type hasn't been chosen
});
}
onSelect(selectorId: string, selectEvent: any): void {
let selection = this.multiEditSelections.find(selection => {
return selection.selectorId === selectorId;
});
const controlId = this.utils.uuid();
const prop = selectEvent.value;
this.multiEditFormGroup.get(selection.selectorId).setValue(prop);
this.multiEditFormGroup.markAsDirty();
this.multiEditFormGroup.markAsTouched();
const model = this.multiEditModel.find(model => {
return model.prop === prop;
});
this.multiEditFormGroup.addControl(controlId, this.testCtrl);
selection.controlId = controlId;
selection.prop = prop;
selection.label = model.label;
selection.type = model.type;
}
Logging to console shows that items are being added to the FormGroup, but the binding isn't happening to the DOM. For example, I can add a (keyup) event handler to my input and set the value in the form control which has already been created, and the FormGroup is updated. However, any input added in the front-end doesn't update the FG since it obviously isn't binding. Is this a timing issue or because the controlId is being updated later? I'm creating the FormControl before updating my array that is being iterated.
Oh and I get no errors in console on this.
I think you need to make this change:
[formControlName]="item.controlId"
needs to be:
formControlName="{{item.controlId}}"
Let's say, we have an object like:
$scope.company = { name: { de: '', en: '' } };
and an input field saying:
<input type="text" ng-model="company.name[currentLanguage]" />
<button ng-click="currentLanguage='de'">Deutsch</button>
<button ng-click="currentLanguage='en'">English</button>
If the user fills in this field, the field receives the ng-valid class. If the user then changes the language ($scope.currentLanguage in fact), the input field is correctly updated (gets empty), but it has still the ng-valid class, which is wrong. The expected behavior would be rather ng-pristine. How to update this in real time?
Would be great to know that.
Cheers
PS. There isn't any more code. That's just it.
PS2. It is another Problem as you suggest in the duplicate thread. I do not use ng-repeat.
Once an input's value is changed in any way, it doesn't reset to ng-pristine unless you force it to.
You could manage the classes in your controller like so:
$scope.currentLanguage = 'de';
$scope.company = { name: { de: '', en: '' } };
$scope.setCurrentLanguage = function(str) {
$scope.currentLanguage = str;
var input = angular.element(document).find('input')[0];
if ($scope.company.name[str] == '') {
angular.element(input).removeClass('ng-dirty');
angular.element(input).removeClass('ng-invalid');
angular.element(input).addClass('ng-pristine');
} else {
angular.element(input).removeClass('ng-pristine');
angular.element(input).addClass('ng-dirty');
}
}
and in the html:
<input type="text" ng-model="company.name[currentLanguage]" />
<button ng-click="setCurrentLanguage('de')">Deutsch</button>
<button ng-click="setCurrentLanguage('en')">English</button>