Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checkbox Group handling and Validation in Angular2

I don't get, how checkbox groups should be handled in a model driven form in angular2.

The model has a property languages which I instantiate like this:

this.model = {
  languages: []
};

Using FormBuilder to create the form:

this.modelForm = this.formBuilder.group({
  'languages': [model.languages, Validators.required],
});

And the template:

<div *ngFor="let language of translateService.get('languages') | async | key_value">
  <input type="checkbox" name="languages[]" formControlName="languages" value="{{ language.key }}" id="language_{{ language.key }}">
  <label attr.for="language_{{ language.key }}">{{ language.value }}</label>
</div>

The div is needed for styling purposes (custom checkbox), key_value makes keys and values of the language available in the loop, obviously.

The first problem comes with validation. If one checks and unchecks a checkbox (and no other checkbox is checked), the input is still valid - somehow strange.

The second problem comes with the value, which is true if two ore more languages are checked and false otherwise.

Then there is a third problem with the initial value of languages which is just an empty array, but causes all checkboxes to be checked initially (doesn't happen if the initial value is set to a string), although I can not spot any checked attribute in the DOM.

I'm working with the latest ionic beta (2.0.0-beta.10) which uses angular/core version 2.0.0-rc.4 and angular/forms version 0.2.0.

So, is there any guide on how to work with checkbox groups? Any advice or ideas?

like image 649
Rico Leuthold Avatar asked Sep 24 '16 09:09

Rico Leuthold


2 Answers

The first problem comes with validation. If one checks and unchecks a checkbox (and no other checkbox is checked), the input is still valid - somehow strange.

I noticed that if the value of languages is an empty array, it passes the Validations.required check.

The second problem comes with the value, which is true if two ore more languages are checked and false otherwise.

Then there is a third problem with the initial value of languages which is just an empty array, but causes all checkboxes to be checked initially (doesn't happen if the initial value is set to a string), although I can not spot any checked attribute in the DOM.

I think the problem is the way you are binding multiple controls to a single FormControl, I believe a FormArray needs to be involved, potentially with a different FormControl storing the result of your checkbox array.

So, is there any guide on how to work with checkbox groups? Any advice or ideas?

Sure, I took a stab at implementing it, I will post the implementation first followed by some notes. You can view it in action at https://plnkr.co/edit/hFU904?p=preview

@Component({
  template: `
    <template [ngIf]="loading">
      Loading languages...
    </template>
    <template [ngIf]="!loading">
      <form [formGroup]="modelForm">
        <div [formArrayName]="'languages'" [class.invalid]="!modelForm.controls.selectedLanguages.valid">
          <div *ngFor="let language of modelForm.controls.languages.controls; let i = index;" [formGroup]="language">
            <input type="checkbox" formControlName="checked" id="language_{{ language.controls.key.value }}">
            <label attr.for="language_{{ language.controls.key.value }}">{{ language.controls.value.value }}</label>
          </div>
        </div>
        <hr>
        <pre>{{modelForm.controls.selectedLanguages.value | json}}</pre>
      </form>
    </template>
  `
})
export class AppComponent {  
    loading:boolean = true;
    modelForm:FormGroup;
    languages:LanguageKeyValues[];
  
    constructor(public formBuilder:FormBuilder){
    }
    
    ngOnInit() {
      
      this.translateService.get('languages').subscribe((languages:LanguageKeyValues[]) => {
        
        let languagesControlArray = new FormArray(languages.map((l) => {
          return new FormGroup({
            key: new FormControl(l.key),
            value: new FormControl(l.value),
            checked: new FormControl(false),
          });
        }));
        
        this.modelForm = new FormGroup({
          languages: languagesControlArray,
          selectedLanguages: new FormControl(this.mapLanguages(languagesControlArray.value), Validators.required)
        });
        
        languagesControlArray.valueChanges.subscribe((v) => {
          this.modelForm.controls.selectedLanguages.setValue(this.mapLanguages(v));
        });
        
        this.loading = false;
      });
    }
    
    mapLanguages(languages) {
      let selectedLanguages = languages.filter((l) => l.checked).map((l) => l.key);
      return selectedLanguages.length ? selectedLanguages : null;
    }
}

The main difference here is that I merged your model.languages into your modelForm, and am now repeating on the modelForm.languages FormArray in the template.

modelForm.languages has become modelForm.selectedLanguages, and is now a computed value based on the checked values in modelForm.languages. If nothing is selected, modelForm.selectedLanguages is set to null, to fail validation.

modelForm is not instantiated until languages are available, this is mostly personal preference, I'm sure you could asynchronously attach languages and selectedLanguages to your modelForm, but it simplifies things to construct it synchronously.

I took out translateService.get('languages') | async, I noticed some strange behavior with this function being called in the template, and I prefer to unwrap my observables in the component anyway, to capture loading/error states.

It's not as elegant as some native checkbox array form control could be, but it's clean and very flexible. Check out the plunker and let me know if you have any questions!

like image 124
Skyler Avatar answered Nov 20 '22 08:11

Skyler


If you check a checkbox and then uncheck it, the FormControl will still show it as valid, even though it is unchecked. It may happen that it takes only the true and false values.You can try this sample of code as it worked for me -

                             mustBeChecked(control: FormControl): {[key: string]: string} {
                              if (!control.value) {
                              return {mustBeCheckedError: 'Must be checked'};
                               } else {
                                       return null;
                                      }
                                }

You can also refer to this plunker

There is currently an open issue here where click event triggers before changing the value. Refer to the github link here.Hope It may help you.

There are different workarounds for multiple checkboxes like using

event.target.checked

instead of the value from the model.

YOu can use it as in this sample -

           <input type="checkbox"  
                (change)="expression && expression.Option1=$event.target.checked ? true : undefiend"
                 [ngModel]="expression?.Option1">
           <input type="checkbox"  
                (change)="expression && expression.Option2=$event.target.checked ? true : undefiend"
                 [ngModel]="expression?.Option2">
like image 2
Pritish Vaidya Avatar answered Nov 20 '22 10:11

Pritish Vaidya