Bind comma separated list to checkboxes in Angular 9 - arrays

In my reactive form I have an array that I bind the checkbox list to using following syntax:
structure of array is {id:number, name:string}
TS
ngOnInit(): void {
this.initFCForm();
this.addCheckboxes();
}
initFCForm(): void {
this.fcForm = this.formBuilder.group({
frequency : new FormControl('', [Validators.required]),
rules: new FormArray([])
});
}
get rulesFormArray() {
return this.fcForm.controls.rules as FormArray;
}
private addCheckboxes() {
this.businessrules.forEach(() => this.rulesFormArray.push(new FormControl(false)));
}
HTML
<label class="col-md-7" formArrayName="rules"
*ngFor="let rule of rulesFormArray.controls; let i = index">
<input type="checkbox" [formControlName]="i">
{{businessrules[i].name}}
</label>
Now I need to populate it when the page loads to have the first and the third option selected. I have tried the following code but it only select first two options.
TS
this.fc.rules.patchValue([1,3])

When you do this.fc.rules.patchValue([1,3]), it will set value 1 to first control of the rules FormArray and 3 to the second control of the rules FormArray but nothing to third.
Checkbox formcontrol expects a boolean value (true or false) and here, when setting 1 and 3 using patch, values are automatically 'converted' to true because these are truthy values.
If you want to have the first and third value populated, try this :
this.fc.rules.patchValue([true,null, true])

Related

Angular 12 FormGroup dynamically Array checkboxes custom validator does not work

I am creating an Angular 12 app, with Material.
I have a form with an checkbox array loaded dynamically from database.
I need to validate that at least one checkbox is selected
I defined like this in my OnInit():
ngOnInit(): void {
this.form = this.fb.group({
Id: new FormControl(null),
Name: new FormControl('',Validators.required),
Recipents: new FormControl('',[Validators.required, matchingEmailValidator()]),
IsActive: new FormControl(true),
ProcessorName: new FormControl('',Validators.required),
Channel: new FormArray([]),
}, { validators: [customValidateArrayGroup()] }
);
}
I need a custom validation for channel form array. If I added it in the definition of the channel, it does not fire when I check it. So, I decided to do it at the form level..
I added:
{ validators: [customValidateArrayGroup()] }
Every time an object changes, it fires this validator.
This is my custom validator:
export function customValidateArrayGroup(): 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 < 1) {
return {
requireCheckboxToBeChecked: true,
}
}
return null
}
}
Here is my Html where I defined the Checkbox Array
<mat-label><strong>Channel</strong></mat-label>
<li *ngFor="let chanel of notification.NotificationChannelLogLevels">
<mat-checkbox id= {{chanel.NotificationLogLevel.Id}} formArrayName="Channel"
[checked]="chanel.IsActive"
(change)="changeEventFunc($event)">
{{chanel.NotificationLogLevel.Name}}
</mat-checkbox>
</li>
The problem I have is that the custom validator does not fire when a checkbox is clicked. Maybe is becouse they are loaded dinamically and are not recognized by formGroup.controls
How can I validate this?
You have an odd mix of using formarray and your js array in the template. Currently your formarray is completely empty, so it would be expected that it does not run when checkboxes are checked. You can choose to iterate your JS array and push / remove to formarray, or then you push the values to the formarray when you receive the data and then just iterate that one in the template. The below solution does the latter:
Shortened code....
Build form:
this.form = this.fb.group({
Channel: this.fb.array([], [customValidateArrayGroup()]),
});
I attached the custom validator to the formarray itself. When you have the dynamic data ready, then iterate it and push form controls to your formarray. I like to use a getter as well. Push whatever properties you need, here I choose IsActive and Name only:
get channels() {
return this.form.get('Channel') as FormArray;
}
// when you have data accessible:
this.notificationChannelLogLevels.forEach(value => {
this.channels.push(this.fb.group({
isActive: value.IsActive,
name: value.Name
}))
})
Now iterate this formarray in the template:
<div formArrayName="Channel">
<li *ngFor="let chanel of channels.controls; let i = index">
<ng-container [formGroupName]="i">
<mat-checkbox formControlName="isActive">
{{ chanel.get('name').value}}
</mat-checkbox>
</ng-container>
</li>
<small *ngIf="channels.hasError('hasError') && channels.touched">
Choose at least one
</small>
</div>
The custom validator checks that at least one checkbox field has isActive as true:
export function customValidateArrayGroup() {
return function validate(formArr: AbstractControl): ValidationErrors | null {
const filtered = (formArr as FormArray).value.filter(chk => chk.isActive);
return filtered.length ? null : { hasError: true }
};
}
A STACKBLITZ for your reference.
I think you have your FormArray setup incorrectly in your template.
You are applying the formArrayName attribute the each checkbox when it needs to be applied to a parent container,
<div formArrayName="myFormArray">
<div *ngFor="*ngFor="let chanel of notification.NotificationChannelLogLevels; let i = index">
//Use the index here to dynamically tie each mat-checkbox to a FormControl
<mat-checkbox [FormControl]="myCheckboxes[i]"></mat-checkbox>
</div>
</div>
And then in your .ts file you'll have to define myCheckboxes as a FormArray with instances of form control inside it. Otherwise myCheckboxes[i] will be either null or an index out of bounds. You can use the form array you added to your form group, but the indexes you reference in the template have to be defined.
Here is a good blog post going over how to handle adding/removing instances from the form array,
https://netbasal.com/angular-reactive-forms-the-ultimate-guide-to-formarray-3adbe6b0b61a
And another,
https://blog.angular-university.io/angular-form-array/
As a side note, if your logging levels are static, it may just be easier or more intuitive to define the list of checkbox controls as a FormGroup and apply your validator to the form group.

Blazor, Checkbox keep checked, event after StateHasChanged() method called

I have this code :
#foreach (var item in list)
{
<input type="checkbox" #onchange="#(e => HandleCheckChanged(e, item.Name))" checked="#item.IsChecked">
}
private async Task HandleCheckChanged(ChangeEventArgs e, string itemName)
{
// do something ...
StateHasChanged();
}
if I check a checkbox it calls the HandleCheckChanged and the checkbox is checked
But if I change the list items the previews checked checkbox is still checked and is not updated based on the new list items.
for example suppose that I have a list of ordered numbers {1-20} I follow these steps :
1 : list = GetAll().Where(c => c.Id < 10);
2 : I check the first checkbox (number 1)
3 : list = GetAll().Where(c => c.Id >= 10); (list updated and state has changed)
4 : the problem raises here , checkbox 11 is checked ??? but its value is false
This happens because the renderer just sees the 'same' list of checkboxes and sees no reason to send an update.
A simple solution using #key to express that they are different items:
#foreach (var item in list)
{
<input #key="item" type="checkbox" #onchange="#(e => HandleCheckChanged(e, item.Name))" checked="#item.IsChecked">
}
You do not have to call StatehasChanged() but it won't do any harm either.
I had the same problem, what #Henk said is correct and there is also other solution: You can wrap your HTML input with <EditForm Model='YourModel'>. because whenever EdtitForm.Model changes, EditForm.OnParametersSet is excuted and OnParametersSet will call StateHasChanged to rerender the existing component.
Blazor-university has good explanation.

Multiple Select not working, only giving last clicked value

I implemented a multiple select dropdown from react-bootstrap documentation.
It does not let me do multiple select and only gets the last clicked option. I have state variable set to array. What else am I missing? App is created with create-react-app.
I have state set to array inside the class constructor. Binding of event handler also done in the constructor.
Next, I'm showing my event handler followed by form group with onChange and value set to state. (note I have a drop-down above this which is working fine.)
I then pass this value to a few classes before it's parsed to JSON. The last pastes are those classes. I have removed other parameters so easier to read, any ideas, feel free to ask for more info.
this.state = {
codeCoverage: [],
}
this.handleCodeCoverageChange = this.handleCodeCoverageChange.bind(this);
//Event handlers below
handleCodeCoverageChange(event){
this.setState({
codeCoverage: event.target.value
})
}
<Form.Group>
<Form.Label>Please choose your desired code coverage software(s)</Form.Label>
<Form.Control as="select" value={this.state.codeCoverage} onChange={this.handleCodeCoverageChange} multiple>
<option value="">--Please choose an option--</option>
<option value="cobertura">Cobertura</option>
<option value="sonarcube">Sonarcube</option>
</Form.Control>
</Form.Group>
var configurator = new Configurator(this.state.codeCoverage)
class Configurator
{
constructor(
code_coverage)
{
this.pipeline = new Pipeline(code_coverage)
}
}
class Pipeline
{
constructor(code_coverage)
{
this.analysisAndSecurity = new AnalysisAndSecurity(code_coverage)
}
class AnalysisAndSecurity{
parameter
constructor(code_coverage)
{
this.code_coverage = code_coverage
}
}
In your handleChange function you assign state.codeCoverage the value of the selected element instead of adding it to the array of selected element. This is why when you select another element it deletes the old value. I would recommend logging e.target.value and this.state.codeCoverage to better understand. As for the solution:
Since you are using multiple select it expects an array as value instead of a single value. So you need to change two things in your handleChange method.
First you need to add your element to existing values and not replace them.
You need to handle when a selected element is clicked again and needs to become unselected.
You can do both these tasks as shown below:
handleChange = e => {
const { codeCoverage } = this.state;
// Find the value selected the codeCoverage array
const index = codeCoverage.indexOf(e.target.value);
// If the value is not found then add it to the array
if (index === -1) codeCoverage.push(e.target.value);
// If value found then remove the value to unselect
else codeCoverage.splice(index, 1);
// Set the state so that the component reloads with the new value
this.setState({ codeCoverage: [...codeCoverage] });
};

How to call a function on uncheck and on check with Aurelia

I have a list of items coming in from an API and they won't always be the same, so the number of items in the array is always changing. I'm creating a checkbox for each item.
The user has the ability to check/uncheck each item. Here's what I want to do:
When an item is checked, it will push the ID of that item into an array
When an item is unchecked, it will remove the ID of that item from the array
I just need to know how I call something based on whether it was checked or unchecked. I've tried a "checked.delegate" and a "checked.trigger" and I can't seem to get that to work.
Just a regular click.delegate won't work because I can't keep state on whether it's true or false and I can't set variables for all of them because I don't always know which items are going to be coming in from the API. Any suggestions?
Try change.delegate or change.trigger like this:
VM method:
logchange(value) {
console.log(value);
}
View:
<input type="checkbox" change.delegate="logchange($event.target.checked)" />
There is (since when?) official documentation of how to solve exactly this specific problem cleanly: https://aurelia.io/docs/binding/checkboxes#array-of-numbers
No need to handle events!
Aurelia can bind directly to your array and handle everything for you - all you need to do is tell Aurelia what property of the elements you are repeating over to store in the array (the id).
The gist of it:
app.js
export class App {
products = [
{ id: 0, name: 'Motherboard' },
{ id: 1, name: 'CPU' },
{ id: 2, name: 'Memory' },
];
selectedProductIds = [];
}
app.html
<template>
<form>
<h4>Products</h4>
<label repeat.for="product of products">
<input type="checkbox" model.bind="product.id" checked.bind="selectedProductIds">
${product.id} - ${product.name}
</label>
<br>
Selected product IDs: ${selectedProductIds}
</form>
</template>
No other code is needed.
One way you can do this is with the help of a setter. Let's say you have a checkbox like this:
<input type="checkbox">
Create a private field in your View Model and then wrap it with a getter and a setter:
get isChecked(){
return this._isChecked;
}
set isChecked(value){
this._isChecked = value;
//enter your extra logic here, no need for an event handler
}
private _isChecked: boolean;
Then bind isChecked to the view:
<input type="checkbox" checked.bind="isChecked">
Every time the checkbox is checked or unchecked the setter will be called and you can call any method you want from within the setter.
Another more unconventional way to achieve this is by using the #bindable decorator like this:
#bindable isChecked: boolean;
It's unconventional because you probably don't want isChecked to be bindable but the decorator gives you access to the isCheckedChanged method:
isCheckedChanged(newValue, oldValue){
//Logic here
}
And of course there is the change event which you can catch with change.trigger and change.delegate but that has already been mentioned in another answer

How to read dynamically populated checkbox values in Laravel along with checkbox id and value?

I have been able to dynamically populate checkboxes in my view. But I am having problem in reading the checked checkboxes in my controller. I want id, name and value of the checkboxes. For example, if I have two checkboxes, then I want each checkbox's id, name and value indicating whether checked or not. Right now all I am getting is an array that just gives values 1.
I have the view:
<div class="control-group">
{{ Form::label('permission-lbl', 'Permissions') }}
<div class="controls">
#foreach($permissions as $p)
{{ Form::checkbox('permissions[]',$p->id,false,array('class'=>'permission')) }} {{Form::label('permissions[]',$p->name)}}
#endforeach
</div>
</div>
And the controller is:
public function store() {
$validation = new CreateGroupValidator;
if ($validation->passes()) {
try {
// Create the group
$permissions = Input::get('permissions');
if(is_array($permissions))
{
//i need each checkbox's id, name and value here
}
$group = Sentry::createGroup(array(
'name' => Input::get('name')
));
Notification::success('New group was saved.');
return Redirect::route('admin.groups.index');
}
catch (\Cartalyst\Sentry\Groups\GroupExistsException $e)
{
Notification::error('A group with same name already exists.');
}
}
return Redirect::back()->withInput()->withErrors($validation->errors);
}
Any suggestions please?
The permissions array that you'll receive from the form will be in this form:
// In the form [position] => Input `Value` Attribute
[permissions] => Array (
[0] => 1,
[1] => 2,
)
Where the key is the position of the element in the array (not-useful), and the value is the attribute value of all the inputs that were checked.
In your specific case the Input Value Attribute is the value of $p->id in each iteration of the loop.
Have in mind that you will only receive the checkbox that were checked in the form, if no one is checked, you will receive an empty array.

Resources