Angular 4 : Check if radio button selected in a reactive form - javascript

I have a reactive form with 4 inputs type radio and 4 inputs type text.
I want to display dynamically inputs text following the radio buttons
selected. For example, if button radio 1 selected, i want to display
its corresponding input text 1.
I encounter 2 problems :
The first radio button is not selected by default (even with attribute checked) and all the radio buttons can be selected at the same time.
All the inputs text are displayed, i don't know with a *ngIf how to
check if the radio button corresponding to the input is selected.
component.html :
<section class="CreateItem" *ngIf="formGroupItemSelection">
<form (ngSubmit)="addItem()" [formGroup]="formGroupItemSelection">
<input formControlName="refNumber" type="radio" value="refNumber" checked> ref number
<br>
<input formControlName="partNumber" type="radio" value="partNumber"> part number
<br>
<input formControlName="itemName" type="radio" value="itemName"> item name
<br>
<input formControlName="manufacturerName" type="radio" value="manufacturerName">manufacturer name
<br>
<div *ngIf="formGroupItemSelection.controls.refNumber.valid">
<input list="refNumbers" formControlName="refNumberSelected" type="text" name="refNumberSelected">
<datalist id="refNumbers">
<option *ngFor="let ref of listOfItems">{{ref.refNumber.input}}</option>
</datalist>
</div>
<div *ngIf="formGroupItemSelection.controls.partNumber.valid">
<input list="partNumbers" formControlName="partNumberSelected" type="text" name="partNumberSelected">
<datalist id="partNumbers">
<option *ngFor="let ref of listOfItems">{{ref.partNumber.input}}</option>
</datalist>
</div>
<div *ngIf="formGroupItemSelection.controls.itemName.valid">
<input list="itemsName" formControlName="itemNameSelected" type="text" name="itemNameSelected">
<datalist id="itemsName">
<option *ngFor="let ref of listOfItems">{{ref.itemDesignation.input}}</option>
</datalist>
</div>
<div *ngIf="formGroupItemSelection.controls.manufacturerName.valid">
<input list="manufacturersName" formControlName="manufacturerNameSelected" type="text" name="manufacturerNameSelected">
<datalist id="manufacturersName">
<option *ngFor="let ref of listOfItems">{{ref.manufacturerName.input}}</option>
</datalist>
</div>
<button type="submit [disabled]="!formGroupItemSelection.valid">Valider</button>
</form>
</section>
component.ts :
import { Component, OnInit } from '#angular/core';
import {FormControl, FormGroup, FormBuilder, Validators} from '#angular/forms'
import { ManagementArbologistiqueService } from '../../management-arbologistique.service';
import { ActivatedRoute, Params } from '#angular/router';
import { matchValuesRefNumber, matchValuesPartNumber, matchValuesItemName, matchValuesManufacturerName } from '../Validators/validators';
#Component({
selector: 'app-item-selection',
templateUrl: './item-selection.component.html',
styleUrls: ['./item-selection.component.css']
})
export class ItemSelectionComponent implements OnInit {
formGroupItemSelection:FormGroup;
listOfItems = [];
constructor(public fb:FormBuilder,private managementArbo: ManagementArbologistiqueService, private route: ActivatedRoute) { }
ngOnInit() {
this.getListBdd();
}
initializeForm() {
this.formGroupItemSelection = this.fb.group({
refNumber : '',
partNumber: '',
itemName: '',
manufacturerName: '',
refNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesRefNumber(this.listOfItems)])
],
partNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesPartNumber(this.listOfItems)])
],
itemNameSelected: [
null,
Validators.compose([Validators.required, matchValuesItemName(this.listOfItems)])
],
manufacturerNameSelected:[
null,
Validators.compose([Validators.required, matchValuesManufacturerName(this.listOfItems)])
]
})
}
getListBdd() {
this.route.params.subscribe((params: Params) => {
let subroute = "getRefNumber";
this.managementArbo.getProducts(subroute)
.subscribe(
res => {
this.listOfItems = res; console.log('bdd:' + res);
this.initializeForm();
},
err => console.log(err),
() => console.log('getProducts done'));
});
}
addItem() {
}

1.-NOT use "checked". just when you create the form give the correct value. And the correct value is not "true"
this.formGroupItemSelection = this.fb.group({
refNumber : ['refNumber'], //NOT is true or false
partNumber: [],
itemName: []
....
})
2.-When we make a *ngIf into a reactive form use myform.get('mycontrol'), e.g.
<div *ngIf="formGroupItemSelection.get('partNumber').valid">...</div>

I solved my problem !
Firstly, I found important information in the angular doc to answer my first question :
"To use radio buttons in a template-driven form, you'll want to ensure that radio buttons in the same group have the same name attribute. Radio buttons with different name attributes do not affect each other.".
Then, in the case of a reactive form :
"When using radio buttons in a reactive form, radio buttons in the same group should have the same formControlName. You can also add a name attribute, but it's optional."
So, i gave the same formControlName at each radio button and the mention "checked" works now.
Otherwise, to answer my second question, I identified each radio buttons following the "value" property of input, and checked if radio buttons selected :
<div *ngIf="formGroupItemSelection.controls.radioBoutton.value==='refNumber'">
Hope it can help !

initializeForm() {
this.formGroupItemSelection = this.fb.group({
refNumber : new FormControl(true),
partNumber: new FormControl(false),
itemName: new FormControl(false),
manufacturerName: '',
refNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesRefNumber(this.listOfItems)])
],
partNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesPartNumber(this.listOfItems)])
],
itemNameSelected: [
null,
Validators.compose([Validators.required, matchValuesItemName(this.listOfItems)])
],
manufacturerNameSelected:[
null,
Validators.compose([Validators.required, matchValuesManufacturerName(this.listOfItems)])
]
})
//This will set the radio button checked no need to add checked attribute you can set cheked dynamically
this.formGroupItemSelection.get('refNumber ').setValue(true);
this.formGroupItemSelection.get('partNumber').setValue(true);
this.formGroupItemSelection.get('itemName').setValue(true)
}

Related

How to use subscribe in Angular with an array of inputs and how to sum their values?

I have a reactive angular form. The form is a simple scoreboard. I would like to be able to add the rounds up together to make up a score. So when a round is finished and the score for that round is entered, it will sum up all the total rounds into that players score.
This is what I have so far:
form.component.html
<section [formGroup]="board">
<table class="table table-bordered" formArrayName="scoreboard">
<tr>
<th colspan="2">Scoreboard:</th>
<th width="150px">
<button type="button" (click)="addPlayer()" class="btn btn-primary">
Add Additional Players
</button>
</th>
</tr>
<tr
*ngFor="let quantity of scoreboard().controls; let i = index"
[formGroupName]="i"
>
<td>
Name :
<input type="text" formControlName="name" class="form-control" />
</td>
<td>
Round1
<input type="text" formControlName="round1" />
Round2
<input type="text" formControlName="round2" />
</td>
<td>
Score:
<input type="text" formControlName="score" class="form-control" />
</td>
</tr>
</table>
{{ this.board.value | json }}
</section>
form.component.ts
import { Component } from '#angular/core';
import { FormBuilder, FormGroup, FormArray } from '#angular/forms';
#Component({
selector: 'app-basic-form',
templateUrl: './basic-form.component.html',
styleUrls: ['./basic-form.component.css']
})
export class BasicFormComponent {
board: any;
constructor (private fb: FormBuilder) {
this.board = this.fb.group({
scoreboard: this.fb.array([this.game(), this.game()])
});
}
scoreboard() : FormArray {
return this.board.get("scoreboard") as FormArray
}
game(): FormGroup {
return this.fb.group({
name: '',
score: '',
round1: [''],
round2: ['']
})
}
addPlayer() {
this.scoreboard().push(this.game());
}
onSubmitForm () {
console.log(this.board.value);
}
}
Really just starting to learn Angular and wanted to try something on my own. If you could be detailed or show me somewhere I can learn additional information about the solution that would be great!
You can listen to your group's controls value changes, and update the score accordingly. Your form array is made of list of form groups (the return value of game function), these groups have controls which holds the values for their respective index, so when you create a form group, you can listen to round1 and round2 changes, and update score accordingly. For example, what I did below is using combineLatest function to merge both value changes observables (round1 + round2) and then update the score accordingly. So now every new form group pushed to your form array will have its own value change listener and will update the score.
game(): FormGroup {
const gameGroup = this.fb.group({
name: '',
score: '',
round1: [''],
round2: ['']
});
combineLatest(gameGroup.get('round1').valueChanges,gameGroup.get('round2').valueChanges)
.subscribe(([r1,r2]) => gameGroup.get('score').setValue(Number(r1) + Number(r2)));
return gameGroup;
}

How to create a multiple radio group in angular

How to create a multiple radio where there's a question and you will just choose Passed or Failed. it will not affect the other item when you choosing the passed or failed
for example:
Item 1 - What do you think? you will pass or fail? : *Passed * Failed
Item 2 - Your classmate anna will be pass or fail? : *Passed * Failed
Item 3 - Your best friend grade will be pass or fail? : *Passed * Failed
Item.ts file
class Item{
id : number;
question : string;
isPassed : boolean;
}
app.component.ts
export class AppComponent {
items : Item[]=[];
constructor(){
console.log("onitnit")
this.items.push({
'id':1,
'question': 'question1?',
'isPassed':false,
})
this.items.push({
'id':2,
'question': 'question2?',
'isPassed':false,
})
this.items.push({
'id':3,
'question': 'question3?',
'isPassed':false,
})
}
resultHandler(){
console.log(this.items);
}
}
app.component.html file
<div *ngFor = "let item of items" style="display : flex;">
{{item.question}}
<form style="margin-left:20px">
passed: <input type="radio" name="orders" [value]="item.id"/>
failed: <input type="radio" name="orders" [value]="item.id"/>
</form>
</div>
working demo
See console for final result generated.
Multiple radio group with reactive form.
HTML:
<form [formGroup]="form">
<div *ngFor="let que of questions">
<span>{{que.question}}</span>
<mat-radio-group [formControlName]="que.id" aria-label="Select an option">
<mat-radio-button value="true">Passed</mat-radio-button>
<mat-radio-button value="false">Failed</mat-radio-button>
</mat-radio-group>
</div>
<pre>{{form.value | json}}</pre>
</form>
TS:
form: FormGroup;
questions = [{
id: "1",
question: "What do you think? you will pass or fail?",
result: false
},
{
id: "2",
question: "Your classmate anna will be pass or fail?",
result: false
},
{
id: '3',
question: "Your best friend grade will be pass or fail?",
result: false
}]
constructor(private fb: FormBuilder) {
this.form = this.fb.group({ });
this.questions.forEach(question => {
this.form.addControl(question.id, this.fb.control(null, Validators.required));
})
}
stackblitz link : https://stackblitz.com/edit/angular-pyfprq
Created a example for your functionality
Stackblitz example
You can set name attribute unique for each group. Let say -
<input type="radio" name="{{index}}"/>

Angular : set reactive form with dynamic looping inputs

I've to set reactive form validation for a form where the inputs are made with data looping :
my form builder would look like this :
constructor(private formBuilder: FormBuilder) {
this.userForm = this.formBuilder.group({
'inputOne': ['', [Validators.required]],
'inputOne': ['', [Validators.required]],
...
'inputN': ['', [Validators.required]]
});
}
and my template would look like this :
<form [formGroup]="userForm">
<div class="form-group" *ngFor="let item of items; let i=index;">
<label for="lastName">{{item.name}}</label>
<input class="form-control" name="lastName" id="lastName" type="text" [formControlName]="item.name">
</div>
</form>
where items are loaded dynamically from my my backend
How to populate controls dynamically in Angular reactive forms?
sounds like you want a form array here, not a group...
constructor(private formBuilder: FormBuilder) {
// build an empty form array
this.userForm = this.formBuilder.array([]);
}
// call this whenever you need to add an item to your array
private addItem(item) {
// build your control with whatever validators / value
const fc = this.formBuilder.control(i.lastName, [Validators.required]);
this.userForm.push(fc); // push the control to the form
}
// call this function whenever you need to reset your array
private resetFormArray(items) {
this.userForm.clear(); // clear the array
items.forEach(i => this.addItem(i)); // add the items
}
<form [formGroup]="userForm">
<div class="form-group" *ngFor="let item of items; let i=index;">
<label for="lastName">{{item.name}}</label>
<input class="form-control" name="lastName" id="lastName" type="text" [formControlName]="i">
</div>
</form>
notice you're using the index for the form control name here

Disable a field in angular reactive form

In my angular 6 application, i am making a reactive form which has,
Html
<form [formGroup]="form">
<h2>Click the add button below</h2>
<button (click)="addCreds()">Add</button>
<div formArrayName="credentials" >
<div *ngFor="let creds of form.get('credentials').controls; let i = index"
[formGroupName]="i" style="display: flex">
<select formControlName="action">
<option *ngFor="let option of options" value="{{option.key}}">
{{option.value}}
</option>
</select>
<input placeholder="Name" formControlName="name">
<div *ngIf="creds.value.action=='set' ||
creds.value.action=='go'">
<input placeholder="Label" formControlName="label">
</div>
</div>
</div>
</form>
Here i have used
<div *ngIf="creds.value.action=='set' || creds.value.action=='go'">
<input placeholder="Label" formControlName="label">
</div>
which will display the field label if the condition is true or else it will not be displayed.
But i need to only disable that field and should not remove it completely..
For which i have tried,
<input [disabled]="creds.value.action != 'set' ||
creds.value.action != 'go' " placeholder="Label" formControlName="label">
But it doesn't works.
Stackblitz with *ngIf https://stackblitz.com/edit/angular-form-array-example-25y1ix
Stackblitz with disabled https://stackblitz.com/edit/angular-form-array-example-laftgu
Kindly help me how to disable the label field if the selected action (first dropdown) value is wait..
Just create a CSS class:
.disabled {
pointer-events:none;
background-color: #D3D3D3;
}
Add a method to your Component:
getValueByIndex(index) {
const actionValue = ( < FormArray > this.form.get('credentials')).at(index).value.action;
return actionValue !== 'set' && actionValue !== 'go';
}
Use this method in your template:
<input
[class.disabled]="getValueByIndex(i)"
placeholder="Label"
formControlName="label">
Here's a Working Sample StackBlitz for your ref.
UPDATE:
Alternatively, you can do the following:
Make the label field disabled by default:
addCreds() {
const creds = this.form.controls.credentials as FormArray;
creds.push(this.fb.group({
action: '',
name: '',
label: {
value: '',
disabled: true
},
state: 'na'
}));
}
And then listen to the (ngModelChange) event on the form to update the state of the label field:
<select
formControlName="action"
(ngModelChange)="($event === 'set' || $event === 'go') ? creds.controls.label.enable() : creds.controls.label.disable()">
<option
*ngFor="let option of data"
[value]="option.key">
{{option.value}}
</option>
</select>
Here's a Working Sample StackBlitz for this approach.
Thanks to A.Winnen for his comments on the performance implications of using the previous approach.
In Reactive Form you cannot use the disabled directive. Instead, you'll need to disable the formControl the 'reactive way' by calling enable() or disable() function.
addCreds() {
const creds = this.form.controls.credentials as FormArray;
const newGroup = this.fb.group({
action: '',
name: '',
label: ''
});
newGroup.controls.action.valueChanges.subscribe((currentAction) => {
if (typeof currentAction === "string" && currentAction.toLowerCase() === 'wait') {
newGroup.controls.label.disable();
} else {
newGroup.controls.label.enable();
}
});
creds.push(newGroup);
}
I modified your stackblitz example: https://stackblitz.com/edit/angular-form-array-example-ymysw4

Angular ReactiveForms: Producing an array of checkbox values?

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 simply true/false?
Example:
<form [formGroup]="checkboxGroup">
<input type="checkbox" id="checkbox-1" value="value-1" formControlName="myValues" />
<input type="checkbox" id="checkbox-2" value="value-2" formControlName="myValues" />
<input type="checkbox" id="checkbox-3" value="value-2" formControlName="myValues" />
</form>
checkboxGroup.controls['myValues'].value currently produces:
true or false
What I want it to produce:
['value-1', 'value-2', ...]
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.
Here's a good place to use the FormArray https://angular.io/docs/ts/latest/api/forms/index/FormArray-class.html
To start we'll build up our array of controls either with a FormBuilder or newing up a FormArray
FormBuilder
this.checkboxGroup = _fb.group({
myValues: _fb.array([true, false, true])
});
new FormArray
let checkboxArray = new FormArray([
new FormControl(true),
new FormControl(false),
new FormControl(true)]);
this.checkboxGroup = _fb.group({
myValues: checkboxArray
});
Easy enough to do, but then we're going to change our template and let the templating engine handle how we bind to our controls:
template.html
<form [formGroup]="checkboxGroup">
<input *ngFor="let control of checkboxGroup.controls['myValues'].controls"
type="checkbox" id="checkbox-1" value="value-1" [formControl]="control" />
</form>
Here we're iterating over our set of FormControls in our myValues FormArray and for each control we're binding [formControl] to that control instead of to the FormArray control and <div>{{checkboxGroup.controls['myValues'].value}}</div> produces true,false,true while also making your template syntax a little less manual.
You can use this example: http://plnkr.co/edit/a9OdMAq2YIwQFo7gixbj?p=preview to poke around
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>
I don't see a solution here that completely answers the question using reactive forms to its fullest extent so here's my solution for the same.
Summary
Here's the pith of the detailed explanation along with a StackBlitz example.
Use FormArray for the checkboxes and initialize the form.
The valueChanges observable is perfect for when you want the form to display something but store something else in the component. Map the true/false values to the desired values here.
Filter out the false values at the time of submission.
Unsubscribe from valueChanges observable.
StackBlitz example
Detailed explanation
Use FormArray to define the form
As already mentioned in the answer marked as correct. FormArray is the way to go in such cases where you would prefer to get the data in an array. So the first thing you need to do is create the form.
checkboxGroup: FormGroup;
checkboxes = [{
name: 'Value 1',
value: 'value-1'
}, {
name: 'Value 2',
value: 'value-2'
}];
this.checkboxGroup = this.fb.group({
checkboxes: this.fb.array(this.checkboxes.map(x => false))
});
This will just set the initial value of all the checkboxes to false.
Next, we need to register these form variables in the template and iterate over the checkboxes array (NOT the FormArray but the checkbox data) to display them in the template.
<form [formGroup]="checkboxGroup">
<ng-container *ngFor="let checkbox of checkboxes; let i = index" formArrayName="checkboxes">
<input type="checkbox" [formControlName]="i" />{{checkbox.name}}
</ng-container>
</form>
Make use of the valueChanges observable
Here's the part I don't see mentioned in any answer given here. In situations such as this, where we would like to display said data but store it as something else, the valueChanges observable is very helpful. Using valueChanges, we can observe the changes in the checkboxes and then map the true/false values received from the FormArray to the desired data. Note that this will not change the selection of the checkboxes as any truthy value passed to the checkbox will mark it as checked and vice-versa.
subscription: Subscription;
const checkboxControl = (this.checkboxGroup.controls.checkboxes as FormArray);
this.subscription = checkboxControl.valueChanges.subscribe(checkbox => {
checkboxControl.setValue(
checkboxControl.value.map((value, i) => value ? this.checkboxes[i].value : false),
{ emitEvent: false }
);
});
This basically maps the FormArray values to the original checkboxes array and returns the value in case the checkbox is marked as true, else it returns false. The emitEvent: false is important here since setting the FormArray value without it will cause valueChanges to emit an event creating an endless loop. By setting emitEvent to false, we are making sure the valueChanges observable does not emit when we set the value here.
Filter out the false values
We cannot directly filter the false values in the FormArray because doing so will mess up the template since they are bound to the checkboxes. So the best possible solution is to filter out the false values during submission. Use the spread operator to do this.
submit() {
const checkboxControl = (this.checkboxGroup.controls.checkboxes as FormArray);
const formValue = {
...this.checkboxGroup.value,
checkboxes: checkboxControl.value.filter(value => !!value)
}
// Submit formValue here instead of this.checkboxGroup.value as it contains the filtered data
}
This basically filters out the falsy values from the checkboxes.
Unsubscribe from valueChanges
Lastly, don't forget to unsubscribe from valueChanges
ngOnDestroy() {
this.subscription.unsubscribe();
}
Note: There is a special case where a value cannot be set to the FormArray in valueChanges, i.e if the checkbox value is set to the number 0. This will make it look like the checkbox cannot be selected since selecting the checkbox will set the FormControl as the number 0 (a falsy value) and hence keep it unchecked. It would be preferred not to use the number 0 as a value but if it is required, you have to conditionally set 0 to some truthy value, say string '0' or just plain true and then on submitting, convert it back to the number 0.
StackBlitz example
The StackBlitz also has code for when you want to pass default values to the checkboxes so they get marked as checked in the UI.
TL;DR
I prefer to use FormGroup to populate the list of checkbox
Write a custom validator to check at least one checkbox was selected
Working example https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
This also struck me sometimes so I did try both FormArray and FormGroup approaches.
Most of the time, the list of checkboxes was populated on the server and I received it through API. But sometimes you will have a static set of checkboxes 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 categories 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 the 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 takes a list of selected values as an argument so If you want to reuse the form for editing data, it could be helpful. For the purpose of creating a new product form, it is not 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
formGroup will store the form data as the object, which required a key map to a form control. So it is a 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 this:
{
"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 compared 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 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) {
checked++;
}
});
if (checked < minRequired) {
return {
requireCheckboxToBeChecked: true,
};
}
return null;
};
}
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>
If you want to use an Angular reactive form (https://angular.io/guide/reactive-forms).
You can use one form control to manage the outputted value of the group of checkboxes.
component
import { Component } from '#angular/core';
import { FormGroup, FormControl } from '#angular/forms';
import { flow } from 'lodash';
import { flatMap, filter } from 'lodash/fp';
#Component({
selector: 'multi-checkbox',
templateUrl: './multi-checkbox.layout.html',
})
export class MultiChecboxComponent {
checklistState = [
{
label: 'Frodo Baggins',
value: 'frodo_baggins',
checked: false
},
{
label: 'Samwise Gamgee',
value: 'samwise_gamgee',
checked: true,
},
{
label: 'Merry Brandybuck',
value: 'merry_brandybuck',
checked: false
}
];
form = new FormGroup({
checklist : new FormControl(this.flattenValues(this.checklistState)),
});
checklist = this.form.get('checklist');
onChecklistChange(checked, checkbox) {
checkbox.checked = checked;
this.checklist.setValue(this.flattenValues(this.checklistState));
}
flattenValues(checkboxes) {
const flattenedValues = flow([
filter(checkbox => checkbox.checked),
flatMap(checkbox => checkbox.value )
])(checkboxes)
return flattenedValues.join(',');
}
}
html
<form [formGroup]="form">
<label *ngFor="let checkbox of checklistState" class="checkbox-control">
<input type="checkbox" (change)="onChecklistChange($event.target.checked, checkbox)" [checked]="checkbox.checked" [value]="checkbox.value" /> {{ checkbox.label }}
</label>
</form>
checklistState
Manages the model/state of the checklist inputs. This model allows you to map the current state to whatever value format you need.
Model:
{
label: 'Value 1',
value: 'value_1',
checked: false
},
{
label: 'Samwise Gamgee',
value: 'samwise_gamgee',
checked: true,
},
{
label: 'Merry Brandybuck',
value: 'merry_brandybuck',
checked: false
}
checklist Form Control
This control stores the value would like to save as e.g
value output: "value_1,value_2"
See demo at https://stackblitz.com/edit/angular-multi-checklist
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.
Apparently, this is a very common problem and no one has a "perfect" solution. I believe I was able to come with a pretty elegant solution, using object orientation to extend the capabilities of FormGroup.
Desired API
In a single object I want to be able to have:
The form control for each checkbox
The label and value for each checkbox
The values of all selected checkbox
So the HTML structure can be simple as this:
<div *ngFor="let item of checkboxGroup.items">
<input type="checkbox" [id]="item.value" [formControl]="item.control">
<label [for]="item.value">{{ item.label }}</label>
</div>
And the typescript part can be simple as this:
checkboxGroup.value; // return the list of selected item values
checkboxGroup.control.valid; // return if there's at least one checked value
Solution
As you can see in the HTML part, the checkboxGroup needs to be a class with at least three properties:
items (each item is a checkbox with a value, label and a FormControl)
value (gets all selected items)
control (gets the FormArray control)
So the class will be like:
// # This represents a single checkbox item
class CheckboxItemControl {
label: string; // value to be shown in the UI
value: string; // value to be saved in backend
control: FormControl;
constructor({ label, value, defaultValue = false }: { label: string; value: string; defaultValue?: boolean }) {
this.label = label;
this.value = value;
this.control = new FormControl(defaultValue || false);
}
get selected(): boolean {
return Boolean(this.control.value);
}
}
// # This represents a checkbox group, with several items
class CheckboxGroupControl {
name?: string; // name of the checkbox group
items: CheckboxItemControl[];
control: FormArray;
constructor(name: string, items: CheckboxItemControl[]) {
this.name = name;
this.items = items;
this.control = new FormArray(this.getAllItemsControls(), CheckboxGroupControl.emptyArrayFormValidator);
}
get value(): string[] {
return this.selectedItems.map(item => item.value);
}
private get selectedItems(): CheckboxItemControl[] {
return this.items.filter(item => item.selected);
}
private getAllItemsControls(): FormControl[] {
return this.items.map(item => item.control);
}
private static emptyArrayFormValidator(control: FormControl) {
const valid = (control.value as boolean[]).some(Boolean);
// #todo improve error message
return valid ? null : {
error: 'empty'
};
}
}
You can see how each class exposes a simple API (object.value and object.control) which allows you to easily get all that you need.
Usage
So let's see in practice how it'll work:
HTML
<div *ngFor="let item of checkboxGroup.items">
<input type="checkbox" [id]="item.value" [formControl]="item.control">
<label [for]="item.value">{{ item.label }}</label>
</div>
Typescript
checkboxGroup;
ngOnInit() {
this.createFormInputs();
}
private createFormInputs() {
const checkboxItems = [
new CheckboxItemControl({ value: 'checkbox-1', label: 'Checkbox 1' }),
new CheckboxItemControl({ value: 'checkbox-2', label: 'Checkbox 2' }),
new CheckboxItemControl({ value: 'checkbox-3', label: 'Checkbox 3', defaultValue: true })
];
this.checkboxGroup = new CheckboxGroupControl('name_of_group', checkboxItems);
this.form = new FormGroup({
checkbox: this.checkboxGroup.control
});
// this.checkboxGroup.value returns ['checkbox-1', ...] for the selected checkboxes
// this.checkboxGroup.valid returns if there's any checkbox selected
// this.form.valid returns if the whole form is valid. Which is useful if you include others checkbox groups
}
Other resources
This article sparks the light for me to come up with this solution.
My solution - solved it for Angular 5 with Material View
The connection is through the
formArrayName="notification"
(change)="updateChkbxArray(n.id, $event.checked, 'notification')"
This way it can work for multiple checkboxes arrays in one form.
Just set the name of the controls array to connect each time.
constructor(
private fb: FormBuilder,
private http: Http,
private codeTableService: CodeTablesService) {
this.codeTableService.getnotifications().subscribe(response => {
this.notifications = response;
})
...
}
createForm() {
this.form = this.fb.group({
notification: this.fb.array([])...
});
}
ngOnInit() {
this.createForm();
}
updateChkbxArray(id, isChecked, key) {
const chkArray = < FormArray > this.form.get(key);
if (isChecked) {
chkArray.push(new FormControl(id));
} else {
let idx = chkArray.controls.findIndex(x => x.value == id);
chkArray.removeAt(idx);
}
}
<div class="col-md-12">
<section class="checkbox-section text-center" *ngIf="notifications && notifications.length > 0">
<label class="example-margin">Notifications to send:</label>
<p *ngFor="let n of notifications; let i = index" formArrayName="notification">
<mat-checkbox class="checkbox-margin" (change)="updateChkbxArray(n.id, $event.checked, 'notification')" value="n.id">{{n.description}}</mat-checkbox>
</p>
</section>
</div>
At the end you are getting to save the form with array of original records id's to save/update.
Will be happy to have any remarks for improvement.
Component:
formGroup: FormGroup;
games = [
{ keyword: 'hots', score: 9 },
{ keyword: 'xcom', score: 9 },
{ keyword: 'fallout', score: 8 }
];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.formGroup = this.fb.group(
this.games.reduce((obj, game) => {
obj[game.keyword] = [false];
return obj;
}, {})
);
const enabledGames$ = this.formGroup.valueChanges.pipe(
map(value =>
Object.entries(value)
.filter(([key, enabled]) => enabled)
.map(([key]) =>
this.games.find(({ keyword }) => keyword === key)
)
)
);
}
Template:
<form [formGroup]="formGroup">
<div *ngFor="let control of formGroup.controls | keyvalue">
<input
type="checkbox"
[formControlName]="control.key">
<label>
{{ control.key }}
</label>
</div>
</form>
I was able to accomplish this using a FormArray of FormGroups. The FormGroup consists of two controls. One for the data and one to store the checked boolean.
TS
options: options[] = [{id: 1, text: option1}, {id: 2, text: option2}];
this.fb.group({
options: this.fb.array([])
})
populateFormArray() {
this.options.forEach(option => {
let checked = ***is checked logic here***;
this.checkboxGroup.get('options').push(this.createOptionGroup(option, checked))
});
}
createOptionGroup(option: Option, checked: boolean) {
return this.fb.group({
option: this.fb.control(option),
checked: this.fb.control(checked)
});
}
HTML
This allows you to loop through the options and bind to the corresponding checked control.
<form [formGroup]="checkboxGroup">
<div formArrayName="options" *ngFor="let option of options; index as i">
<div [formGroupName]="i">
<input type="checkbox" formControlName="checked" />
{{ option.text }}
</div>
</div>
</form>
Output
The form returns data in the form {option: Option, checked: boolean}[].
You can get a list of checked options using the below code
this.checkboxGroup.get('options').value.filter(el => el.checked).map(el => el.option);
With two way binding
my.component.html
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<div formGroupName="options">
<mat-checkbox formControlName="myVal1">My Value 1</mat-checkbox>
<mat-checkbox formControlName="myVal2">My Value 2</mat-checkbox>
</div>
<button type="submit">Submit</button>
</form>
my.component.ts
export class ClientStatementReportComponent implements OnInit {
formGroup: FormGroup;
ngOnInit(): void {
this.formGroup = new FormGroup({
options: new FormGroup({
myVal1: new FormControl(false),
myVal2: new FormControl(false)
}),
});
}
onSubmit() {
const options = this.formGroup.value.options;
const result = Object.keys(options).filter(key => options[key])
// is array of checked elements e.g. ["myVal1"]
}
}
With one way binding (to form state)
my.component.html
<form [formGroup]="formGroup">
<mat-checkbox value="val-1" (change)="selectOption($event)">Value 1</mat-checkbox>
<mat-checkbox value="val-2" (change)="selectOption($event)">Value 2</mat-checkbox>
</form>
my.component.ts
export class MyComponent implements OnInit {
formGroup: FormGroup;
ngOnInit(): void {
this.formGroup = new FormGroup({
options: new FormControl([]),
});
}
selectOption($event: MatCheckboxChange) {
const value = $event.source.value;
const optionControl = this.formGroup.controls['options']
const options = optionControl.value as [];
if(checked){
optionControl.setValue([...options, value])
} else {
optionControl.setValue(options.filter(option => option !== value))
}
}
}
TEMPLATE PART:-
<div class="form-group">
<label for="options">Options:</label>
<div *ngFor="let option of options">
<label>
<input type="checkbox"
name="options"
value="{{option.value}}"
[(ngModel)]="option.checked"
/>
{{option.name}}
</label>
</div>
<br/>
<button (click)="getselectedOptions()" >Get Selected Items</button>
</div>
CONTROLLER PART:-
export class Angular2NgFor {
constructor() {
this.options = [
{name:'OptionA', value:'first_opt', checked:true},
{name:'OptionB', value:'second_opt', checked:false},
{name:'OptionC', value:'third_opt', checked:true}
];
this.getselectedOptions = function() {
alert(this.options
.filter(opt => opt.checked)
.map(opt => opt.value));
}
}
}
Add my 5 cents)
My question model
{
name: "what_is_it",
options:[
{
label: 'Option name',
value: '1'
},
{
label: 'Option name 2',
value: '2'
}
]
}
template.html
<div class="question" formGroupName="{{ question.name }}">
<div *ngFor="let opt of question.options; index as i" class="question__answer" >
<input
type="checkbox" id="{{question.name}}_{{i}}"
[name]="question.name" class="hidden question__input"
[value]="opt.value"
[formControlName]="opt.label"
>
<label for="{{question.name}}_{{i}}" class="question__label question__label_checkbox">
{{opt.label}}
</label>
</div>
component.ts
onSubmit() {
let formModel = {};
for (let key in this.form.value) {
if (typeof this.form.value[key] !== 'object') {
formModel[key] = this.form.value[key]
} else { //if formgroup item
formModel[key] = '';
for (let k in this.form.value[key]) {
if (this.form.value[key][k])
formModel[key] = formModel[key] + k + ';'; //create string with ';' separators like 'a;b;c'
}
}
}
console.log(formModel)
}
Template
<div>
<input name="fruits" type="checkbox" value="orange" (change)="change($event)">
<input name="fruits" type="checkbox" value="apple" (change)="change($event)">
<input name="fruits" type="checkbox" value="banana" (change)="change($event)">
</div>
Component
formGroup = this.formBuilder.group(
{
fruits: [[]] //["Orange","Banana",...]
})
change(event: Event) {
let target = (event.target as HTMLInputElement);
let array = (this.formGroup.get(target.name)?.value as Array);
if (target.checked && !array.find(element => {
return (element === target.value);
})) {
array.push(target.value)// element not exists, push (check)
}
else {
array.splice(array.findIndex(element => {
return (element === target.value);//delete element (uncheck)
}), 1)
}
}
This is how I would do it, although I always use Angular Material List
https://material.angular.io/components/list/overview
Everything comes from the factory for these tasks

Categories