How to assign and validate arrays to forms in Angular2
Asked Answered
D

2

6

My model (this.profile) in javascript has a property called emails, which is an array of {email, isDefault, status}

Then I defined it as below

  this.profileForm = this.formBuilder.group({
    .... other properties here
    emails: [this.profile.emails]
  });

  console.log(this.profile.emails); //is an array
  console.log(this.profileForm.emails); // undefined

in html file I used it as

    <div *ngFor="let emailInfo of profileForm.emails">
        {{emailInfo.email}}
        <button (click)="removeEmail(emailInfo)">
           Remove 
        </button>
    </div>

If I don't add it in formGroup and use it as an array - like below - it's working fine, but then I have a business rule that this array shouldn't be empty and I am unable to set form validation based on length of this

  emails : [];
  this.profileForm = this.formBuilder.group({
    .... other properties here
  });

  this.emails = this.profile.emails;
  console.log(this.profile.emails); //is an array
  console.log(this.emails); // is an array

also I tried using formBuilder.array but that one is for having array of controls not array of data.

   emails: this.formBuilder.array([this.profile.emails])

then my question is how should I bind an array from model to UI and how should I validate the length of array?

Darky answered 20/2, 2017 at 19:29 Comment(2)
What's the content of this.emails? You can use minLength to validate the formArray length with the latest versions of Angular.Ramayana
this.emails is an array, [{email:xx,isDefault:false},{email:yy,isDefault:true}], how can I define it in formArray and access it in html, because right now the value become undefined?Darky
R
8

How should I bind an array from model to UI?

Well, I'd prefer to push all the emails from profile.emails to the formArray, otherwise you'll have the values, but no validation.

How should I validate the length of array?

You can use the Validators.minLength(Number) as any other control.

Demo code:

Component:

export class AnyComponent implements OnInit {

  profileForm: FormGroup;
  emailsCtrl: FormArray;

  constructor(private formBuilder: FormBuilder) { }

  ngOnInit(): void {

    this.emailsCtrl = this.formBuilder.array([], Validators.minLength(ANY_NUMBER));
    this.profile.emails.forEach((email: any) => this.emailsCtrl.push(this.initEmail(email)));

    this.profileForm = this.formBuilder.group({
      // ... other controls
      emails: this.emailsCtrl
    });
  }

  private initEmail = (obj: any): FormGroup => {
    return this.formBuilder.group({
      'email': [obj.email], //, any validation],
      'isDefault': [obj.isDefault] //, any validation]
    });
  }
}

Template:

<div *ngFor="let emailInfo of emailsCtrl.value">
  {{emailInfo.email}}
  <button (click)="removeEmail(emailInfo)">
    Remove
  </button>
</div>
<div *ngIf="emailsCtrl.hasError('minlength')">
  It should have at least {{emailsCtrl.getError('minlength').requiredLength}} emails
</div>

PS1: Note that the param of Validators.minLength(param) method must be greater than 1, otherwise it won't validate.

As you can see in source when the control is empty it automatically returns null.

Then, to make it work as you expected you could add the required Validator:

this.emailsCtrl = this.formBuilder.array([], Validators.compose([Validators.required, Validators.minLength(ANY_NUMBER > 1)]);

And in template:

<div *ngIf="emailsCtrl.invalid">
  <span *ngIf="emailsCtrl.hasError('required')">
    It's required
  </span>
  <span *ngIf="emailsCtrl.hasError('minlength')">
    It should have at least {{emailsCtrl.getError('minlength').requiredLength}} emails
  </span>
</div>

PS2:

I think makes more sense to pass the index of the email that you want to delete in your removeEmail function, so you willn't have to call indexOf to get the index of the specific email. You can do something like this:

<div *ngFor="let emailInfo of emailsCtrl.value; let i = index">
  {{emailInfo.email}}
  <button (click)="removeEmail(i)">
    Remove
  </button>
</div>

Component:

removeEmail(i: number): void {
  this.emailsCtrl.removeAt(i);
}

Take a look at this simple DEMO

Ramayana answered 20/2, 2017 at 20:50 Comment(6)
Thanks a lot, it's working as I was expected, except for validation While emailsCtrl.value.length is 1 and email address are shown in html, but it still has validation error, to clarify I added this to html, {{emailsCtrl.value.length}} {{emailsCtrl.errors | json}} and result in html is 1 { "minlength": { "requiredLength": 1, "actualLength": 0 } }Darky
as a workaround I developed a custom validator for emailsCtrl, then the other issue is when I remove an email,customValidator is not called, removeEmail(emailInfo: ContactEmail) { this.zone.run(() => { this.emailsCtrl.value.splice(this.emailsCtrl.value.indexOf(emailInfo), 1); }); } , no matter using or not using ngZoneDarky
I ended up with having a customValidation to check the length, and re initializing emailsCtrl on removeDarky
Hello @RezaRahmati, please take a look at the edit version. I provide some more explanation and examples. Regards :)Ramayana
I start using same code as you did, but still not valid, after hours of investigation I found out you are using ng2.4.8 and I am using 2.2.1, so I changed your plunker to 2.2.1 and the issue exists there too, so it seems it was a bug which is fixed in 2.4.8 (plnkr.co/edit/ZbokVq1qbzfZI2A1uJop?p=preview) even when you have 2 or 3 emails it keep saying It should have at least 2 emails ;)Darky
@Ramayana , thanks for the direction. I have a follow-on question to your example if you're able to help. #47685064Peridotite
L
6

This works for me (angular 2.1.2), this approach gives you the flexibility to define a custom validation for your emails:

 this.profileForm = this.formBuilder.group({
    emails: [this.profile.emails, FormValidatorUtils.nonEmpty]
    // ......
  });

export class FormValidatorUtils {

  static nonEmpty(control: any) {
    if (!control.value || control.value.length === 0) {
      return { 'noElements': true };
    }
    return null;
  }
}
Lattimore answered 20/2, 2017 at 20:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.