Angular ReactiveForms: Producing an array of checkbox values?

前端 未结 12 601
执念已碎
执念已碎 2020-11-29 17:16

Given a list of checkboxes bound to the same formControlName, how can I produce an array of checkbox values bound to the formControl, rather than s

相关标签:
12条回答
  • 2020-11-29 17:50

    Related answer to @nash11, here's how you would produce an array of checkbox values

    AND

    have a checkbox that also selectsAll the checkboxes:

    https://stackblitz.com/edit/angular-checkbox-custom-value-with-selectall

    0 讨论(0)
  • 2020-11-29 17:52

    With the help of silentsod answer, I wrote a solution to get values instead of states in my formBuilder.

    I use a method to add or remove values in the formArray. It may be a bad approch, but it works !

    component.html

    <div *ngFor="let choice of checks; let i=index" class="col-md-2">
      <label>
        <input type="checkbox" [value]="choice.value" (change)="onCheckChange($event)">
        {{choice.description}}
      </label>
    </div>
    

    component.ts

    // For example, an array of choices
    public checks: Array<ChoiceClass> = [
      {description: 'descr1', value: 'value1'},
      {description: "descr2", value: 'value2'},
      {description: "descr3", value: 'value3'}
    ];
    
    initModelForm(): FormGroup{
      return this._fb.group({
        otherControls: [''],
        // The formArray, empty 
        myChoices: new FormArray([]),
      }
    }
    
    onCheckChange(event) {
      const formArray: FormArray = this.myForm.get('myChoices') as FormArray;
    
      /* Selected */
      if(event.target.checked){
        // Add a new control in the arrayForm
        formArray.push(new FormControl(event.target.value));
      }
      /* unselected */
      else{
        // find the unselected element
        let i: number = 0;
    
        formArray.controls.forEach((ctrl: FormControl) => {
          if(ctrl.value == event.target.value) {
            // Remove the unselected element from the arrayForm
            formArray.removeAt(i);
            return;
          }
    
          i++;
        });
      }
    }
    

    When I submit my form, for example my model looks like:

      otherControls : "foo",
      myChoices : ['value1', 'value2']
    

    Only one thing is missing, a function to fill the formArray if your model already has checked values.

    0 讨论(0)
  • 2020-11-29 17:52

    Make an event when it's clicked and then manually change the value of true to the name of what the check box represents, then the name or true will evaluate the same and you can get all the values instead of a list of true/false. Ex:

    component.html

    <form [formGroup]="customForm" (ngSubmit)="onSubmit()">
        <div class="form-group" *ngFor="let parameter of parameters"> <!--I iterate here to list all my checkboxes -->
            <label class="control-label" for="{{parameter.Title}}"> {{parameter.Title}} </label>
                <div class="checkbox">
                  <input
                      type="checkbox"
                      id="{{parameter.Title}}"
                      formControlName="{{parameter.Title}}"
                      (change)="onCheckboxChange($event)"
                      > <!-- ^^THIS^^ is the important part -->
                 </div>
          </div>
     </form>
    

    component.ts

    onCheckboxChange(event) {
        //We want to get back what the name of the checkbox represents, so I'm intercepting the event and
        //manually changing the value from true to the name of what is being checked.
    
        //check if the value is true first, if it is then change it to the name of the value
        //this way when it's set to false it will skip over this and make it false, thus unchecking
        //the box
        if(this.customForm.get(event.target.id).value) {
            this.customForm.patchValue({[event.target.id] : event.target.id}); //make sure to have the square brackets
        }
    }
    

    This catches the event after it was already changed to true or false by Angular Forms, if it's true I change the name to the name of what the checkbox represents, which if needed will also evaluate to true if it's being checked for true/false as well.

    0 讨论(0)
  • 2020-11-29 17:53

    It's significantly easier to do this in Angular 6 than it was in previous versions, even when the checkbox information is populated asynchronously from an API.

    The first thing to realise is that thanks to Angular 6's keyvalue pipe we don't need to have to use FormArray anymore, and can instead nest a FormGroup.

    First, pass FormBuilder into the constructor

    constructor(
        private _formBuilder: FormBuilder,
    ) { }
    

    Then initialise our form.

    ngOnInit() {
    
        this.form = this._formBuilder.group({
            'checkboxes': this._formBuilder.group({}),
        });
    
    }
    

    When our checkbox options data is available, iterate it and we can push it directly into the nested FormGroup as a named FormControl, without having to rely on number indexed lookup arrays.

    const checkboxes = <FormGroup>this.form.get('checkboxes');
    options.forEach((option: any) => {
        checkboxes.addControl(option.title, new FormControl(true));
    });
    

    Finally, in the template we just need to iterate the keyvalue of the checkboxes: no additional let index = i, and the checkboxes will automatically be in alphabetical order: much cleaner.

    <form [formGroup]="form">
    
        <h3>Options</h3>
    
        <div formGroupName="checkboxes">
    
            <ul>
                <li *ngFor="let item of form.get('checkboxes').value | keyvalue">
                    <label>
                        <input type="checkbox" [formControlName]="item.key" [value]="item.value" /> {{ item.key }}
                    </label>
                </li>
            </ul>
    
        </div>
    
    </form>
    
    0 讨论(0)
  • 2020-11-29 17:56

    TL;DR

    1. I prefer to use FormGroup to populate the list of checkbox
    2. Write a custom validator for check at least one checkbox was selected
    3. Working example https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected

    This also struck me for sometimes so I did try both FormArray and FormGroup approaches.

    Most of the time, the list of checkbox was populated on the server and I received it through API. But sometimes you will have a static set of checkbox with your predefined value. With each use case, the corresponding FormArray or FormGroup will be used.

    Basically FormArray is a variant of FormGroup. The key difference is that its data gets serialized as an array (as opposed to being serialized as an object in case of FormGroup). This might be especially useful when you don’t know how many controls will be present within the group, like dynamic forms.

    For the sake of simplicity, imagine you have a simple create product form with

    • One required product name textbox.
    • A list of category to select from, required at least one to be checked. Assume the list will be retrieved from the server.

    First, I set up a form with only product name formControl. It is a required field.

    this.form = this.formBuilder.group({
        name: ["", Validators.required]
    });
    

    Since the category is dynamically rendering, I will have to add these data into the form later after the data was ready.

    this.getCategories().subscribe(categories => {
        this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
        this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
    })
    

    There are two approaches to build up the category list.

    1. Form Array

      buildCategoryFormArr(categories: ProductCategory[], selectedCategoryIds: string[] = []): FormArray {
        const controlArr = categories.map(category => {
          let isSelected = selectedCategoryIds.some(id => id === category.id);
          return this.formBuilder.control(isSelected);
        })
        return this.formBuilder.array(controlArr, atLeastOneCheckboxCheckedValidator())
      }
    
    <div *ngFor="let control of categoriesFormArr?.controls; let i = index" class="checkbox">
      <label><input type="checkbox" [formControl]="control" />
        {{ categories[i]?.title }}
      </label>
    </div>
    

    This buildCategoryFormGroup will return me a FormArray. It also take a list of selected value as an argument so If you want to reuse the form for edit data, it could be helpful. For the purpose of create a new product form, it is not be applicable yet.

    Noted that when you try to access the formArray values. It will looks like [false, true, true]. To get a list of selected id, it required a bit more work to check from the list but based on the array index. Doesn't sound good to me but it works.

    get categoriesFormArraySelectedIds(): string[] {
      return this.categories
      .filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
      .map(cat => cat.id);
    }
    

    That's why I came up using FormGroup for that matter

    2. Form Group

    The different of the formGroup is it will store the form data as the object, which required a key and a form control. So it is the good idea to set the key as the categoryId and then we can retrieve it later.

    buildCategoryFormGroup(categories: ProductCategory[], selectedCategoryIds: string[] = []): FormGroup {
      let group = this.formBuilder.group({}, {
        validators: atLeastOneCheckboxCheckedValidator()
      });
      categories.forEach(category => {
        let isSelected = selectedCategoryIds.some(id => id === category.id);
        group.addControl(category.id, this.formBuilder.control(isSelected));
      })
      return group;
    }
    
    <div *ngFor="let item of categories; let i = index" class="checkbox">
      <label><input type="checkbox" [formControl]="categoriesFormGroup?.controls[item.id]" /> {{ categories[i]?.title }}
      </label>
    </div>
    

    The value of the form group will look like:

    {
        "category1": false,
        "category2": true,
        "category3": true,
    }
    

    But most often we want to get only the list of categoryIds as ["category2", "category3"]. I also have to write a get to take these data. I like this approach better comparing to the formArray, because I could actually take the value from the form itself.

      get categoriesFormGroupSelectedIds(): string[] {
        let ids: string[] = [];
        for (var key in this.categoriesFormGroup.controls) {
          if (this.categoriesFormGroup.controls[key].value) {
            ids.push(key);
          }
          else {
            ids = ids.filter(id => id !== key);
          }
        }
        return ids;
      }
    

    3. Custom validator to check at least one checkbox was selected

    I made the validator to check at least X checkbox was selected, by default it will check against one checkbox only.

    export function atLeastOneCheckboxCheckedValidator(minRequired = 1): ValidatorFn {
      return function validate(formGroup: FormGroup) {
        let checked = 0;
    
        Object.keys(formGroup.controls).forEach(key => {
          const control = formGroup.controls[key];
    
          if (control.value === true) {
            checked++;
          }
        });
    
        if (checked < minRequired) {
          return {
            requireCheckboxToBeChecked: true,
          };
        }
    
        return null;
      };
    }
    
    0 讨论(0)
  • 2020-11-29 18:02

    If you are looking for checkbox values in JSON format

    { "name": "", "countries": [ { "US": true }, { "Germany": true }, { "France": true } ] }
    

    Full example here.

    I apologise for using Country Names as checkbox values instead of those in the question. Further explannation -

    Create a FormGroup for the form

     createForm() {
    
        //Form Group for a Hero Form
        this.heroForm = this.fb.group({
          name: '',
          countries: this.fb.array([])
        });
    
        let countries=['US','Germany','France'];
    
        this.setCountries(countries);}
     }
    

    Let each checkbox be a FormGroup built from an object whose only property is the checkbox's value.

     setCountries(countries:string[]) {
    
        //One Form Group for one country
        const countriesFGs = countries.map(country =>{
                let obj={};obj[country]=true;
                return this.fb.group(obj)
        });
    
        const countryFormArray = this.fb.array(countriesFGs);
        this.heroForm.setControl('countries', countryFormArray);
      }
    

    The array of FormGroups for the checkboxes is used to set the control for the 'countries' in the parent Form.

      get countries(): FormArray {
          return this.heroForm.get('countries') as FormArray;
      };
    

    In the template, use a pipe to get the name for the checkbox control

      <div formArrayName="countries" class="well well-lg">
          <div *ngFor="let country of countries.controls; let i=index" [formGroupName]="i" >
              <div *ngFor="let key of country.controls | mapToKeys" >
                  <input type="checkbox" formControlName="{{key.key}}">{{key.key}}
              </div>
          </div>
      </div>
    
    0 讨论(0)
提交回复
热议问题