Is there a way to create a reactive form basing on an existing data model with all the validation magic. In the example below author passes whole new object into a formbuilder but what I want to achieve is elegant way to tell formbuilder what field is required or needs some other validation.
https://malcoded.com/posts/angular-fundamentals-reactive-forms
export class PersonalData {
email: string = '';
mobile: string = '';
country: string = '';
}
...
createFormGroupWithBuilderAndModel(formBuilder: FormBuilder) {
return formBuilder.group({
personalData: formBuilder.group(new PersonalData()),
requestType: '',
text: ''
});
}
I just want to skip the process of assigning a FormControl for each field in the model.
#EDIT
After some research and little hint from #xrobert35 I wanted to try and used https://www.npmjs.com/package/#rxweb/reactive-form-validators
They could be "many" way to do what you want to do, but by just extending your actual solution : Your personnal data could look like :
export class PersonalData {
email: string = ['', Validators.required];
mobile: string = ['', Validators.required, Validators.maxLength(10)];
country: string = '';
}
If you need domain base validation (for reusable purpose) you can use rxweb validations.
export class PersonalData {
#required()
email: string;
#required()
mobile: string;
#greaterThan({ fieldName: 'number2' })
number1: number;
#prop()
number2: number;
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
formGroup: FormGroup;
constructor( private validation: RxFormBuilder) {
}
ngOnInit() {
let person: PersonalData = new PersonalData();
this.formGroup = this.validation.formGroup(person);
}
}
If I understand you just want to add validators to your field.
https://angular.io/guide/form-validation
I can't be more precise to giving you the official documentation.
ngOnInit(): void {
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
'alterEgo': new FormControl(this.hero.alterEgo),
'power': new FormControl(this.hero.power, Validators.required)
});
}
In this exemple the fields name and power are required and of course the syntaxe could differ but the flow is the same.
Does it helps you ?
#EDIT
There is the same post for your use case:
https://stackoverflow.com/a/47576916/7133262
Related
Hi I am trying to set/ take away custom validators for different elements in a form array that can change around, so far what I have tried to do is create a switch statement and loop through all of the input types that are set so I could set the validation rule as well as send a message to the user if the rule isn't met. The problem I am having is the form is initialized before the form data is set.
So my question is how can I loop through the array and set the validation rules. If someone could let me know if im along the right tracks with using a switch statement but have code in the wrong place or if there is a different and better approach it would be most helpful thank you
export class ReactiveComponent implements OnInit {
public form: FormGroup;
public fieldList: any;
types: Array<any>;
formData: any;
Param: string;
setData: any;
formLabelNames: any;
get contactFormGroup() {
return this.form.get('inputs') as FormArray;
}
constructor(
private route: ActivatedRoute,
private fb: FormBuilder,
private api: FormService,
private notifiy: NotificationService,
private auth: AuthService,
private router: Router) { }
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.compose([Validators.required])],
organization: ['', Validators.compose([Validators.required])],
inputs: this.fb.array([this.createForm()])
});
this.route.paramMap.subscribe(params => {
this.Param = params.get('id');
this.getForm(this.Param);
});
// set fieldslist to this field
this.fieldList = this.form.get('inputs') as FormArray;
}
// formgroup
createForm(): FormGroup {
return this.fb.group({
type: ['', Validators.compose([Validators.required])],
name: ['', Validators.compose([Validators.required])],
value: ['', this.validators()]
});
}
getForm(id) {
this.api.getForm(id).subscribe(
(data: any) => this.setForm(data)
);
}
getFieldsFormGroup(index): FormGroup {
const formGroup = this.fieldList.controls[index] as FormGroup;
return formGroup;
}
getContactsFormGroup(index): FormGroup {
const formGroup = this.fieldList.controls[index] as FormGroup;
return formGroup;
}
setForm(data) {
const d = data.results;
this.setData = d;
this.formLabelNames = d[0].fields;
this.form.patchValue({
name: [d[0].form_name],
organization: [d[0].org],
});
this.form.setControl('inputs', this.setExistingFields(d[0].fields));
}
setExistingFields(fields: any): FormArray {
const formArray = new FormArray([]);
this.fieldList = formArray;
fields.forEach(f => {
formArray.push(this.fb.group({
name: f.name,
type: f.type,
value: f.value
}));
});
return formArray;
}
/* This is where I have tried to create a switch statement but I get a undefined error because the setform function is being called after this one */
validators() {
this.formLabelNames.type.forEach((field: any) => {
switch (field.type) {
case 'email':
}
});
}
submit() {
if (this.form.valid) {
const formId = this.Param;
const local = this.auth.decodePayload();
const userId = local.sub;
this.router.navigateByUrl('/dashboard');
this.api.sendForm(this.form.value, formId, userId).subscribe();
this.form.reset();
} else {
this.notifiy.showFailure('Form is not valid', 'Error');
}
}
}
You have several problems, as far as I can judge:
You don't initialize neither the formControls nor the formArray correctly inside of you formGroup. It should rather look like this:
this.form = this.fb.group({
name: new FormControl( "", {validators: [Validators.required]}),
organization: new FormControl( "", {validators: [Validators.required]}),
inputs: this.fb.array([new FormControl(), new FormControl()]),
});
Besides: What is the point of using a formArray when it consists of only one formGroup?
Off course you can set the validators for any abstractControl be it a formControl or a formGroup. For the array it would look like something like this:
this.formArray.controls.map((ctrl) => {
ctrl.setValidators([Validators.required, Validators.email]);
});
It doesn't matter where in the component class you put your method. Though it surely matters when that method is invoked!
I am trying to implement a custom validator for a form in Angular. It would be perfect if I could access the this of the controller, but it's undefined in the validator function.
This is my validator function:
validateSuccessShortName(control: AbstractControl) {
//can't read this.randomProp, because this is undefined
if (control.value.length > this.randomProp.length) {
return {value: control.value};
}
return null;
}
Here is a STACKBLITZ demonstrating the issue.
Am I doing something wrong or is this simply impossible in the Angular framework?
https://stackblitz.com/edit/ionic-tqp9o1?embed=1&file=pages/home/home.ts
And for future improvements way better move all validation in another file, and first params for each function must be config. You validators must not dependents from this just from config object
Just change on this.validateSuccessShortName.bind(this), because you function missing context
shortName: new FormControl('', [
this.validateSuccessShortName.bind(this)
])
Your angular validator doesn't reference your component property. To fix this you need to bind this to the validator.
export class HomePage {
private arr: Array<any>;
private thevalue: string;
public thisForm: FormGroup;
public randomProp: string;
constructor(
private modal: ModalController,
public navCtrl: NavController) {
this.thevalue = "Initial value";
this.randomProp = "This is a random property";
this.thisForm = new FormGroup({
shortName: new FormControl('', [
this.validateSuccessShortName.bind(this)
])
});
}
validateSuccessShortName(control: AbstractControl) {
if (control.value.length > this.randomProp.length) {
return {value: control.value};
}
return null;
}
}
I am trying to make an interactive form that on each row lists an item together with a remove button (called verwijderen in my example). These items are retrieved from the database and each instantiated as a custom object called LaborPeriod.
These objects are then transformed into FormGroup object and then added to a FormArray. The definition of this formgroup is the following:
let formGroup = this.fb.group({
id: this.account.laborperiods[i].id,
beginDate: this.account.laborperiods[i].beginDate,
endDate: this.account.laborperiods[i].endDate,
hours: this.account.laborperiods[i].hours,
account: this.account.laborperiods[i].account
})
if(!this.laborPeriodArray){
this.laborPeriodArray = new FormArray([])
}
this.laborPeriodArray.push(formGroup)
}
The definition of the laborPeriodArray in the main FormGroup is also laborPeriodArray. The main FormGroup looks like the following:
constructor(private fb: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService) {
this.title = "Account aanpassen";
//console.log(this.account.enabled + " : " + this.account.role)
this.accountUpdateForm = this.fb.group({
name: [''],
username: ['', [Validators.required, Validators.email]],
status: '',
permission:'',
laborPeriodArray:this.fb.array([])
});
}
The entire component looks like this: Here you can see that laborPeriodArray gets initialized in the onInit method.
constructor(private fb: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService) {
this.title = "Account aanpassen";
//console.log(this.account.enabled + " : " + this.account.role)
this.accountUpdateForm = this.fb.group({
name: [''],
username: ['', [Validators.required, Validators.email]],
status: '',
permission:'',
laborPeriodArray:this.fb.array([])
});
}
ngOnInit(): void {
let formGroupArray: FormGroup[] = [];
this.route.paramMap
.switchMap((params: ParamMap) =>
this.accountService.getAccount(params.get("id")))
.subscribe(account => {
this.account = account;
for(let i=0; i < account.laborperiods.length; i++){
let formGroup = this.fb.group({
id: this.account.laborperiods[i].id,
beginDate: this.account.laborperiods[i].beginDate,
endDate: this.account.laborperiods[i].endDate,
hours: this.account.laborperiods[i].hours,
account: this.account.laborperiods[i].account
})
if(!this.laborPeriodArray){
this.laborPeriodArray = new FormArray([])
}
this.laborPeriodArray.push(formGroup)
}
console.log("laborPeriod" + JSON.stringify(this.laborPeriodArray.length))
this.ngOnChanges();
});
}
ngOnChanges(): void {
if (this.account !== undefined) {
this.accountUpdateForm.reset({
name: this.account.name,
username: this.account.username,
status: this.account.enabled,
permission: this.account.admin,
laborPeriodArray: this.laborPeriodArray
});
}
}
All the FormGroup items are added but not displayed. The rows are blank. This is the relevant snippet from my HTML page
<table class="table table-hover">
<thead>
<tr>
<th>Begin datum</th>
<th>Eind datum</th>
<th>Aantal uren</th>
<th>Actie</th>
</tr>
</thead>
<tbody>
<tr formArrayName="laborPeriodArray" *ngFor = "let laborperiod of laborPeriodArray.controls; let i = index" [formGroupName]="i">
<td formControlName="beginDate">{{laborperiod.value.get('beginDate') | date:'yyyy-MM-dd'}}</td>
<td formControlName="endDate">{{laborperiod.value.get('endDate') | date:'yyyy-MM-dd'}}</td>
<td formControlName="hours">{{laborperiod.value.get('hours')}}</td>
<button type="button" class="btn btn-default">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
Verwijderen
</button>
</tr>
This is giving me the browser error ERROR Error: Cannot find control with unspecified name attribute but I am specifying the formControlName for each td row. So I don't understand what is causing the error. Furthermore I would also like to know how I can link the Remove button to the data corresponding to each row. I take it I have to use the index I for this but i'm not sure.
EDIT:
After applying AJT_82's solution I still don't have it working. It appears to have something to do with the database retrieved data itself. When I add the example account array of AJT_82 to my ngOnInit() like so:
account1 = { laborperiods: [{ id: 1, hours:11 }, { id: 2, hours:22 }, { id: 3, hours:33 }] }
ngOnInit(): void {
this.route.paramMap
.switchMap((params: ParamMap) =>
this.accountService.getAccount(params.get("id")))
.subscribe(account => {
this.account = account;
for(let i=0; i < account.laborperiods.length; i++){
let formGroup = this.fb.group({
id: this.account1.laborperiods[i].id,
beginDate: this.account.laborperiods[i].beginDate,
endDate: this.account.laborperiods[i].endDate,
hours: this.account1.laborperiods[i].hours,
account: this.account.laborperiods[i].account
})
this.laborPeriodArray.push(formGroup)
}
console.log(JSON.stringify(account.laborperiods[0].beginDate))
this.ngOnChanges();
});
}
it works but it shows only 3 rows. That's the total length of the example account array.
This is the account class:
export class Account {
id: number;
username: string;
name: string;
enabled: boolean
password: string;
person: Person;
admin: boolean;
laborperiods: LaborPeriod[]
remainingLeaveHours:number;
}
and this is the LaborPeriod class:
export class LaborPeriod{
id: number
beginDate: Date
endDate: Date
hours: number
account: Account
}
Is there anything wrong with its field declarations?
You cannot have formArrayName on the same element as your iteration and formGroupName, you need to move the formArrayName to an upper level. Also I see no use of the formControlName, as these are not editable fields, and Angular will throw error for this. Also the use of for example...
{{laborperiod.value.get('beginDate') | date:'yyyy-MM-dd'}}
is incorrect, it should be just
{{laborperiod.value.beginDate | date:'yyyy-MM-dd'}}
Assumingly in your code, the variable laberPeriodArray is declared...
this.laborPeriodArray =
this.accountUpdateForm.controls.laborPeriodArray as FormArray
since you are referring to this.laborPeriodArray in your code, therefore the following:
if(!this.laborPeriodArray){
this.laborPeriodArray = new FormArray([])
}
is redundant, you have already declared it in the build of the form as an empty FormArray. But that is just a sidenote :)
All in all, your template should look something like this:
<table formArrayName="laborPeriodArray" >
<tr *ngFor="let laborperiod of laborPeriodArray.controls;
let i = index" [formGroupName]="i">
<td>{{laborperiod.value.hours}}</td>
</tr>
</table>
DEMO
Try replacing
laborperiod.value.get('beginDate')
By
laborperiod.value['beginDate']
I also recommande that you FormGroup variable to be a Class attribute and not a OnInit() one.
I also recommande using ReactiveForms with it's API FormControls.
Let me know what happened
I have a form,and when i submit the form the form data is submitting to the angular firebase database.so if we sigin again i want to show the data into the form which has been submitted before.
my .ts file is below
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormBuilder,Validators } from "#angular/forms";
import { AF } from "app/providers/af";
import { FirebseService } from "app/firebse.service";
import { Router } from "#angular/router";
import { AngularFireDatabase, FirebaseListObservable, FirebaseObjectObservable } from 'angularfire2/database';
#Component({
selector: 'app-candidate-registration',
templateUrl: './candidate-registration.component.html',
styleUrls: ['./candidate-registration.component.css']
})
export class CandidateRegistrationComponent implements OnInit {
itemsAsObjects = [{value: 0, display: 'Angular'}, {value: 1, display: 'React'}];
complexForm : FormGroup;
contents:any;
constructor(fb: FormBuilder,
private firebaseService:FirebseService,
private router: Router,
private db: AngularFireDatabase) {
var Userid=localStorage.getItem('user');
console.log(Userid);
let content= this.db.object('/candidates_list/'+Userid)
content.subscribe(data => {
console.log(data);
this.contents=data;
console.log(this.contents);
})
if(this.contents){
this.complexForm = fb.group({
// To add a validator, we must first convert the string value into an array. The first item in the array is the default value if any, then the next item in the array is the validator. Here we are adding a required validator meaning that the firstName attribute must have a value in it.
'firstName' : ["pranav", Validators.required],
// We can use more than one validator per field. If we want to use more than one validator we have to wrap our array of validators with a Validators.compose function. Here we are using a required, minimum length and maximum length validator.
'lastName': ["kk", Validators.compose([Validators.required, Validators.minLength(1), Validators.maxLength(10)])],
'gender' : [null, Validators.required],
'email' : [null, Validators.required],
'contact_number':[null, Validators.compose([Validators.required, Validators.minLength(10), Validators.maxLength(10)])],
'experience':[null, Validators.required],
'skills':[null, Validators.required],
'notice_period':[null, Validators.required],
})
}else
{
this.complexForm = fb.group({
// To add a validator, we must first convert the string value into an array. The first item in the array is the default value if any, then the next item in the array is the validator. Here we are adding a required validator meaning that the firstName attribute must have a value in it.
'firstName' : [null, Validators.required],
// We can use more than one validator per field. If we want to use more than one validator we have to wrap our array of validators with a Validators.compose function. Here we are using a required, minimum length and maximum length validator.
'lastName': [null, Validators.compose([Validators.required, Validators.minLength(1), Validators.maxLength(10)])],
'gender' : [null, Validators.required],
'email' : [null, Validators.required],
'contact_number':[null, Validators.compose([Validators.required, Validators.minLength(10), Validators.maxLength(10)])],
'experience':[null, Validators.required],
'skills':[null, Validators.required],
'notice_period':[null, Validators.required],
})
}
}
ngOnInit() {
}
submitForm(user){
console.log(user);
this.firebaseService.addtolist(user);
this.complexForm .reset();
this.router.navigate(['/reg-complete']);
}
}
this below code(part of .ts file) is working fine.i am getting the signed in users data into console.But i dont know how to use it into if condition, to set the data of the already signed in users into the registration form.
anyone please help me?Thanks in advance.
var Userid=localStorage.getItem('user');
console.log(Userid);
let content= this.db.object('/candidates_list/'+Userid)
content.subscribe(data => {
console.log(data);
this.contents=data;
console.log(this.contents);
})
Well, the main problem here is that when you check this statement:
if (this.contents) { ... }
It will always go to else.
Why? Because the async operation where you define this.contents isn't resolved yet, and as you didn't defined an initial value for this.contents, it's undefined.
For a deep explanation I'd suggest you to check this question.
That said, I want to suggest another approach to solve your problem:
Instead of having the if/else with full duplicate code, let's break it into a method, like this:
initForm(data: Object = {}) {
this.complexForm = fb.group({
firstName: [data.firstName, Validators.required],
lastName: [data.lastName, [Validators.required, Validators.minLength(1), Validators.maxLength(10)],
gender: [data.gender, Validators.required],
email: [data.email, Validators.required],
contact_number: [data.contact_number, [Validators.required, Validators.minLength(10), Validators.maxLength(10)],
experience: [data.experience, Validators.required],
skills: [data.skills, Validators.required],
notice_period: [data.notice_period, Validators.required]
});
}
Explanation:
In the signature method I'm initializing data as a clean object, so if nothing or undefined | null is passed to the function it'll automatically become a clean object like this {}.
It's useful because it prevents the undefined.<property> errors.
Full code:
constructor(private fb: FormBuilder,
private router: Router,
private db: AngularFireDatabase,
private firebaseService:FirebseService) {
const userId = localStorage.getItem('user');
if (userId) {
this.db.object(`/candidates_list/${userId}`)
.subscribe(data => {
this.contents = data; // This variable isn't used anymore.
this.initForm(data);
});
} else {
this.initForm();
}
}
initForm(data: Object = {}) {
this.complexForm = fb.group({
firstName: [data.firstName, Validators.required],
lastName: [data.lastName, [Validators.required, Validators.minLength(1), Validators.maxLength(10)],
gender: [data.gender, Validators.required],
email: [data.email, Validators.required],
contact_number: [data.contact_number, [Validators.required, Validators.minLength(10), Validators.maxLength(10)],
experience: [data.experience, Validators.required],
skills: [data.skills, Validators.required],
notice_period: [data.notice_period, Validators.required]
});
}
Notes:
1 - The Validators.compose isn't needed, you can just pass an array, or if it's a single validator, the validator itself, in both parameters (2nd and 3rd).
2 - I'd suggest you to move this code from constructor to ngOnInit.
3 - You can face some error in template since the complexForm won't be filled until the async operation is resolved (when you have a user stored, of couse).
To be more specific it'll throw this error:
ORIGINAL EXCEPTION: formGroup expects a FormGroup instance. Please
pass one in.
So, you'll have to use some flag to handle the form in template.
Supposing that you've something like this in your template:
<form [formGroup]="complexForm">
...
</form>
You can solve solve this doing the following:
Component:
import 'rxjs/add/operator/finally';
...
isLoading: boolean = true;
...
this.db.object(`/candidates_list/${userId}`)
.finally(() => this.isLoading = false)
.subscribe(data => {
this.contents = data;
this.initForm(data);
});
Template:
<ng-container *ngIf="!isLoading">
<form [formGroup]="complexForm">
...
</form>
</ng-container>
There is already a similar question here (Setting initial value Angular 2 reactive formarray) but I am not satisfied with the answer or maybe looking for some other solution.
I think whole point of having FormArray is to pass the array of objects and it should create equal number of components. But in this above example if you look at the provided plunker , even after providing two Addresses object one Address was created because its blank version was already created in ngOnInit() .
So my question is if in ngOnInit() I have it like this addresses: this._fb.array([]) // blank list,
then how should I set its value that it dynamically creates N number of addresses from N number of addresses in my TypeScript array ?
To set and remove the values from the form array refer to the below code. The given code is just for your reference, please adjust to your code accordingly.
import { FormArray, FormBuilder, FormGroup} from '#angular/forms';
export class SomeComponent implements OnInit {
consutructor(public fb:FormBuilder) { }
ngOnInit() {
public settingsForm: FormGroup = this.fb.group({
collaborators:this.fb.array([this.buildCollaboratorsGroup(this.fb)])
});
this.setFormArrayValue();
}
public buildCollaboratorsGroup(fb:FormBuilder): FormGroup {
return fb.group({
email:'',
role:''
});
}
// Here I'm setting only one value if it's multiple use foreach
public setFormArrayValue() {
const controlArray = <FormArray> this.settingsForm.get('collaborators');
controlArray.controls[0].get('email').setValue('yourEmailId#gmail.com');
controlArray.controls[0].get('role').setValue(2);
}
// Here removing only one value if it's multiple use foreach
public removeFormArrayValue() {
const controlArray = <FormArray> this.settingsForm.get('collaborators');
controlArray.removeAt(0);
}
}
I am having the same issue. Here is how I do it:
As you mentioned, you initialize your form Array like this:
addresses: this._fb.array([])
Then inside ngOnInit() (or in my case ionViewDidLoad() - using Ionic 2), you do your async operation to hit your remote database and get back the value either via promise or observable (and subscribe to the observable). Then you patchValue to all the other form control that is NOT formArray (don't use setValue if you have form group and form array!!).
For the formArray, do this:
this.yourForm.setControl('addresses', this.fb.array(data.addresses || []));
Where data.addresses is an array of addresses (you create them from the same form on previous operation.)
Hope this solve your question as well as mine :) FormArray is powerful, wish there is more resources to teach us how to use it correctly.
This is a working code. You can insert it into the project and test it.
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators, FormArray, FormBuilder } from '#angular/forms';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
private addresses: string[] = ['Address 1', 'Address 2', 'Address 3'];
private form: FormGroup;
constructor(private formBuilder: FormBuilder){}
ngOnInit(){
// Init Form
this.form = new FormGroup({
'userData': new FormGroup({
'username': new FormControl(null, [Validators.required]),
'email': new FormControl(null, [Validators.required, Validators.email])
}),
'addresses': new FormArray([])
});
// If you want to insert static data in the form. You can use this block.
this.form.setValue({
'userData': {
'username': 'Vic',
'email': 'email#email.com'
},
'addresses': [] // But the address array must be empty.
});
// And if you need to insert into the form a lot of addresses.
// For example, which belong to one user and so on.
// You must use this block.
// Go through the array with addresses and insert them into the form.
this.addresses.forEach((value) => {
const control = new FormControl(value, Validators.required);
(<FormArray>this.form.get('addresses')).push(control);
});
// Or you can use more better approach. But don't forget to initialize FormBuilder.
this.form.setControl('addresses', this.formBuilder.array(this.addresses || []));
}
}