I am having trouble enabling and disabling a form controls in my application. It is probably because the form is being enabled/disabled in an asynchronous context.
Here is how the code is.
user.component.html
<form [formGroup]="form">
<input type="text" id="name" formControlName="name" />
<input type="text" id="age" formControlName="age" />
</form>
user.component.ts
ngOnInit() {
this._route.queryParams.subscribe(params => {
getUser(params.userId).subscribe(user => {
this._buildForm(user);
})
});
}
private _buildForm(user){
this.form = _fb.group({
name: [{value: user.name, disabled: user.someCondition}, Validators.required],
age: [{value: user.age, disabled: user.anotherCondition}, Validators.required]
})
}
When the first time the user is loaded upon param change, the controls do get enabled/disabled on the basis of their relevant conditions. When the params change subsequently, the state of the controls remain the same although the values are set appropriately.
I have tried different ways to fix this but no help. For example, I have tried the following at the end of the _buildForm method.
this.form.disable() //Doesn't do anything
this.form.controls.name.disable(); //Doesn't help
One thing that does work as expected is the following (but this isn't what is required).
<button (click)="form.disable()" value="Disable Form" />
<button (click)="form.enable()" value="Enable Form" />
What I feel is that the problem is due the fact the _buildForm() is being called from the asynchronous context (subscribe method).
How can I fix this problem?
UPDATE
Please note that the Observable subscription is triggered on the basis of the navigation as below.
this._router.navigate([], {
relativeTo: this._route,
queryParams: {userId: 10}
})
UPDATE 2
https://angular-cvf1fp.stackblitz.io
This is an easier way to understand my problem
You just need enable/disable methods of form control.
Here is stackblitz example. It works perfectly.
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
group;
isDisabled = true;
constructor(fb: FormBuilder) {
this.group = fb.group({
stuff: fb.control({value: 'stuff', disabled: this.isDisabled})
});
}
toggle() {
this.isDisabled = !this.isDisabled;
this.group.controls.stuff[this.isDisabled ? 'enable' : 'disable']();
}
}
You're right. The subscription is only fired once, so after submitting the form the user entity does not change anymore. To do it, add for instance an event handler to your form submit.
user.component.html
<form [formGroup]="form" (submit)="_updateForm()">
<input type="text" id="name" fromControlName="name" />
<input type="text" id="age" fromControlName="age" />
</form>
user.component.ts
private userId: any;
ngOnInit() {
this._route.queryParams.subscribe(params => {
this.userId = params.userId;
this._updateForm();
});
}
private _buildForm(user){
this.form = _fb.group({
name: [{value: user.name, disabled: user.someCondition}, Validators.required],
age: [{value: user.age, disabled: user.anotherCondition}, Validators.required]
});
}
// The update function has to be public to be called inside your template
public _updateForm(){
getUser(this.userId).subscribe(user => {
this._buildForm(user);
});
}
Create the formGroup directly in ngOnit w/controls disabled by default so you're sure it exists w/o worrying about async issues.
In this._route.queryParams.subscribe(...) you can use this.form.setValue({//set all control values}) or this.form.patchValue({//set individual controls}) to update the individual controls as needed/once available.
Use component instance variables for condition and someOtherCondition (or a user object as a whole if you want to separate your data model from the form's model - they don't have to be the same). Initialize these to "false", and refer to them instead for enabling/disabling the controls in the formGroup creation. You can also update those variables in the same .subscribe() block once the user data is in place, instead of waiting to access user data model directly.
This way your controls and formGroup exist, and are disabled, until the async data is available.
<form [formGroup]="form">
<input type="text" id="name" fromControlName="name" />
<input type="text" id="age" fromControlName="age" />
</form>
do you see a typo here?fromControlName should be formControlName
After that change toggling will work ;)
https://angular-rivxs5.stackblitz.io
Related
Let's say I have this:
<input formControlName="someName" (click)="onClick()">
I want my onClick function to be generic and set the value of the corresponding FormControl (the one I clicked).
How can I pass the concerned FormControl as a parameter of onClick?
I thought I could retrieve it from the control property of FormControlDirective or FormControlName but none of them have an exportAs attribute.
I think this is what you want to achieve.
import { Directive, HostListener, Optional, Output, EventEmitter } from '#angular/core';
import { NgControl, FormControl } from '#angular/forms';
#Directive({
selector: '[appOnClickControl]' // if you want to target specific form control then use custom selector else you use can use input:
// selector: 'input' to target all input elements
})
export class TestDirective {
#Output() emitFormControl = new EventEmitter<FormControl>();
constructor(#Optional() private formControl: NgControl) {
}
/**
* If your only goal is to set value to the form control then use this
*/
#HostListener('click')
onClick() {
if (this.formControl) {
this.formControl.control.setValue('test');
}
}
/**
* If you wanna pass the form control through the function you might use this
*/
#HostListener('click')
getFormControl(): void {
this.emitFormControl.emit(this.formControl.control as FormControl);
}
}
<input
appOnClickControl // use this to initialize directive
(emitFormControl)="yourMethod($event)" // $event is the clicked form control
formControlName="test"
></input>
In your html:
<input formControlName="someName" (click)="onClick($event)">
And then define your onClick function as:
onClick(event): void {
alert(event.target.value)
}
Edit
To get FormControl:
<input formControlName="someName" (click)="onClick(Form.get('someName'))">
and
onClick(formControl): void {
formControl.setValue(someValue);
}
kinda repetitive but pretty sure you can only do this:
<input formControlName="someName" (click)="onClick('someName')">
then use someName on your form group to find the control
onClick(name: string) {
const fc = this.form.get(name);
// do whatever
}
Not exactly sure what you're after, but try if the following works for you. It uses setValue() method to set values for the form. You can also use patchvalue if you want to set only partial values of the form.
Template
<form [formGroup]='groupedform' >
<label>Name: <br>
<input type="text" formControlName='Name' required (mousedown)="onMouseDown(groupedform)"/>
</label>
<br>
<br>
<label>Email: <br>
<input type="email" formControlName='Email' required (mousedown)="setEmail(groupedform)"/>
</label>
<p>
<button type="submit" [disabled]="!groupedform.valid" (click)="updateName()">Update Name</button>
</p>
</form>
Component
export class AppComponent {
name = 'Angular';
firstname = new FormControl('');
groupedform = this.fb.group({
Name : ['', Validators.required],
Email: [],
});
constructor(private fb:FormBuilder) { }
updateName() {
console.log(this.groupedform.value);
}
onMouseDown(formControl: any) {
this.groupedform.setValue({
'Name': 'sample',
'Email': 'sample#example.com'
});
}
setEmail(formControl: any) {
this.groupedform.patchValue({
'Email': 'sample#example.com'
});
}
}
Working example: Stackblitz
You can use template variable for this
<input formControlName="someName" #temp (click)="onClick(temp)">
onClick(val) {
console.log(val);}
I am not sure what type of value you want to set, but you can use an attribute directive for this type of common operation (if it is simple).
You can create an attribute directive and set on all the form controls, the attribute directive logic will automatically handle it for you. You can definetely configure the values passed to directives.
import { Directive, HostListener, ElementRef, Input } from "#angular/core";
#Directive({
selector: '[clickMe]'
})
export class ClickDirective {
#Input('clickMe') clickValue:string;
constructor(private elementRef: ElementRef){}
#HostListener('click') onClick() {
this.elementRef.nativeElement.value = this.clickValue;
}
}
Now just use these directives with the the form controls and pass your values, you can use data binding as well.
<form [formControl]="myForm">
Firstname:<input type="text" fromControlName="firstname" [clickMe]="'first value'" />
<br>
Lastname:<input type="text" fromControlName="lastname" [clickMe]="'last value'" />
</form>
Please find the working stackblitz here:https://stackblitz.com/edit/angular-j1kwch
You could use the event passed and get the attribute name out of it:
<input type="text" class="form-control" formControlName="yourName" (blur)="yourMethod($event)" />
Then in your method get the name of the formControlName from the target:
yourMethod(event: any) {
const controlName = e.target.getAttribute('formcontrolname');
this.formGroup.get(controlName); // this is your FormControl
....
}
I have a form that updates an object directly like so
component.html
<input type="text" [(ngModel)]="user.name" />
<input type="text" [(ngModel)]="user.surname" />
<input type="text" [(ngModel)]="user.age" />
component.ts
user = {
name: null,
surname: null,
age: null
}
now in my component.html I have a save button that is in a child component
<app-save-btn [user]="user"></app-save-btn>
and in the app-save-btn component I am listening for changes using OnChanges
#Input() user: User;
userForCompare: User;
ngOnChanges() {
if (!this.userForCompare) {
this.userForCompare = {...this.user};
}
console.log(this.userForCompare, this.user);
}
now my issue is, I get the first console log of the two user objects, but as I update further fields I no longer get change updates..
now I know that I could create seperate variables to hold each value, but I have like 30 field items so I dont want to change my implementation to much...
any help would be appreciated
#Input property will trigger ngOnChanges function only when the Input object is mutated.
In your example, input fields are mutating each single property in the same user object, the user object for the #Input always refers to the same object (even though its child property name, surename and age are mutated), so ngOnChanges will not be triggered.
Therefore, you have to make this.user in component.ts equals to a brand new user object whenever any input field is changed.
Hope it helps!
I would suggest creating a FormGroup with Reactive Forms to manipulate your fields. Using FormBuilder.group() you can create a form containing each for the property of the User object... that way you would only need to pass form.value to your child component.
Reactive form keeps the data model pure by providing it as an immutable data structure. Each time a change occurs to the form, the FormControl instance will not update the existing data model, it will return a new state (a new data model).
https://www.ibrahima-ndaw.com/blog/5-reasons-to-use-reactive-form/
Here is a live StackBlitz example using the code below:
https://stackblitz.com/edit/angular-stackoverflow-60424523
example.component.html
<form [formGroup]="form">
<input [formControlName]="'name'" />
<input [formControlName]="'surname'" />
<input [formControlName]="'age'" />
</form>
<app-save-btn [user]="form.value"></app-save-btn>
example.component.ts
import { Component, OnInit} from '#angular/core';
import { FormBuilder, FormGroup} from '#angular/forms';
#Component({ ... })
export class AppComponent implements OnInit {
form: FormGroup;
user: User = {
name: 'Initial name',
surname: 'Initial surname',
age: 99
};
constructor(
private formBuilder: FormBuilder,
) { }
ngOnInit() {
this.form = this.formBuilder.group(this.user);
}
}
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, FormArray, Validators, ValidatorFn, AbstractControl } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
myForm: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.buildForm();
}
get vehicleGroup(): FormArray {
return <FormArray>this.myForm.get('vehicleGroup');
}
buildForm(): void {
this.myForm = this.fb.group({
name: ['', Validators.required],
vehicleGroup: this.fb.array([
this.fb.group({
vehicle: ['', Validators.required]
})
], [Validators.required]),
});
}
addVehicles(): void{
const itemToAdd = this.fb.group({
vehicle: ['', Validators.required]
});
this.vehicleGroup.push(itemToAdd);
}
deleteVehicle(i: number){
this.vehicleGroup.removeAt(i);
}
save(): void{
console.log('save');
}
}
<form novalidate (ngSubmit)="save()" [formGroup]="myForm">
<div class="form-group">
<label for="name">Name</label>
<input id="name" type="text" formControlName="name">
</div>
<div class="form-group">
<label for="vehicle">Vehicle</label>
<div formArrayName="vehicleGroup" *ngFor="let vehicle of vehicleGroup.controls; let i=index">
<div class="form-group" [formGroupName]="i">
<div>
<input id="{{'vehicle'+i}}" type="text" formControlName="vehicle">
<button type="button" (click)="deleteVehicle(i)"
*ngIf="vehicleGroup.length >= 2">remove
</button>
</div>
</div>
</div>
<div class="form-group">
<button type="button" class="link-button" [disabled]="!vehicleGroup.valid" (click)="addVehicles()">
+ Add more vehicles
</button>
</div>
</div>
</form>
I have this (stackBlitz) simple form created with angular formBuilder
I simple need to how to validate each elements of the dynamic formArray and display a unique message for each of them if that particular field is not valid.
I tried several solutions and also tried custom validator function with return an ValidatorFn. with that I could simply validate the formArray, but It's not good enough for my scenario and still I can not display messages according to the validate functions behavior. How ever to narrow it down, I simply need to know if there is a better way to validate each dynamic elements of this formArray. these are the validate rules.
each filed value should be unique.
need to validate real time
after adding few elements, someone edit previously added field, it also should be validate in real time with every other field values(this is where I got struck, I could validate upwards from the editing field, but not the fields below the editing field is validated accordingly)
If some one can show me some way to achieve this in a right way, It would be really great, since I'm struck with this for almost 3 days now, and still can't get an better solution.
I have used unique validator of #rxweb/reactive-form-validators in my project. It can be used directly in the component without creating any custom validation function.
You can edit your addVehicles method as follows:
addVehicles(): void{
const itemToAdd = this.fb.group({
vehicle: ['', RxwebValidators.unique()]
});
this.vehicleGroup.push(itemToAdd);
}
and adding
ReactiveFormConfig.set({"validationMessage":{"unique":"Enter unique value in the input"}});
to ngOnInit.
Here is the forked stackblitz
I have multiple components and each having some input, select and other form elements. This all elements developed using template driven form.
But previously it was developed not in component wise, so we were able to check form status. Now as we have divided everything by component but validation we are not able to get it.
I have two questions.
For getting child component form elements to parent component, I am using ControlValueAccessor, is this correct way to do it ? In my child component i have multiple form controls like input, select and other.
Simple validations like required on input element, its not working inside child component. What should i do ?
This is Plunkr which mostly demonstrates what i wanted to achieve.
#Component({
selector: 'my-child',
template: `
<h1>Child</h1>
<input [ngModel]="firstName" (ngModelChange)="firstSet($event)" required>
<input [ngModel]="lastName" (ngModelChange)="lastSet($event)" required> <!-- required validation is not working -->
`,
providers: [
{provide: NG_VALUE_ACCESSOR, useExisting: Child, multi: true}
]
})
export class Child implements ControlValueAccessor, OnChanges {
private firstName: string,
private lastName: string;
fn: (value: any) => void;
constructor(){
}
writeValue(value: any) {
console.log("write value");
console.log(value);
if(value){
this.firstName = value.firstName;
this.lastName = value.lastName;
}
}
registerOnChange(fn: (value: any) => void) {
this.fn = fn;
}
registerOnTouched() {}
firstSet(v: string){
this.firstName = v;
this.fn({
firstName: this.firstName,
lastName: this.lastName
});
}
lastSet(v: string){
this.lastName = v;
this.fn({
firstName: this.firstName,
lastName: this.lastName
});
}
}
If you find wrong way please mention which is correct way to do it ?
Note: Please suggest solution which works on Template Driven Form. and i forgot to mention i am using Angular 2.4.8 version.
Not sure why you wouldn't use Reactive Forms - they are pretty much designed for validation.
You can also mix and match , you aren't locked into using Template Driven Forms just because you are using ngModel.
You can utilise Reactive Forms just for validation.
component.ts
form: FormGroup;
constructor(fb : FormBuilder) {
this.form = fb.group({
'firstName' : ['', Validators.required],
'lastName' : ['', Validators.required]
})
}
hasError(n: string){
return this.form.get(n).hasError('required') && this.form.get(n).touched;
}
template.html
<h1>Child</h1>
<input [ngModel]="firstName" name="firstName" [formControl]="form.get('firstName')" required>
<div *ngIf="hasError('firstName')">This field is required</div>
<input [ngModel]="lastName" name="lastName" [formControl]="form.get('lastName')" required> <!-- required validation is not working -->
<div *ngIf="hasError('lastName')">This field is required</div>
Plunkr found here
I am trying to disable a form control when one of my form group value changes but the problem is that the method disable() and enable() also updates the form status, so I have an infinite loop.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input type="text" formControlName="other"/>
</form>
`,
})
export class App {
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if(v.question === 'test') {
this.questionForm.get('other').disable();
} else {
...enable()
}
})
}
}
How can I solve this problem?
Well, according to the official docs you could use an directive for custom validation, this could of course in your case could be applied with logic of checking what the user has input and then disable and enable the other field. There's quite a lot of code there, sooooo...
...You could also do a smaller hack if you do not want all that bunch of code. Let's call a function that checks what user has typed. We also need to bind this, so we can refer to the questionForm:
this.questionForm = this.fb.group({
question: ['', [this.checkString.bind(this)]],
other: ['']
});
Then the function:
checkString(control: FormControl) {
if(control.value == 'test') {
this.questionForm.get('other').disable()
}
// we need to check that questionForm is not undefined (it will be on first check when component is initialized)
else if (this.questionForm) {
this.questionForm.get('other').enable()
}
}
This seems to serve it's purpose.
Demo
Hope this helps! :)
I would first add a property isDisabled to your component, then I would use the [disabled] directive. This would solve your looping issue.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input type="text" formControlName="other" [disabled]="isDisabled" />
</form>
`,
})
export class App {
isDisabled = false;
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if (v.question === 'test') {
this.isDisabled = true;
} else {
this.isDisabled = false;
}
})
}
}
With reactive forms, you do it something like this:
question: {value: '', disabled: true }
Adding another answer to work around the reactive forms issue using two controls and an *ngIf directive. See https://github.com/angular/angular/issues/11324.
This solution may not be ideal as it requires that you add a second form control that will appear when the conditions for changing the isDisabled property are met. This means there are two controls to manage when submitting the form.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input *ngIf="isDisabled" type="text" formControlName="other" disabled />
<input *ngIf="!isDisabled" type="text" formControlName="other2" />
</form>
`
providers: [FormBuilder]
})
export class App {
isDisabled = true;
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: [''],
other2: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if(v.question === 'test') {
this.isDisabled = false;
} else {
this.isDisabled = true;
}
});
}
}
Check out or play with the plunker if you wish:
https://plnkr.co/edit/0NMDBCifFjI2SDX9rwcA