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
Related
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.
I have a situation where i need to have a dynamic array, think like a booking form, where people are adding flights to the list but it could be 1 flight or 10 flights. i did up a dummy example where i have been able to replicate the issue, i'm hoping it's me and not an issue with the Angluar. anyway, here it is.
I start with an empty array with a button to add items, those items are bound with a *ngFor (code below) each of the fields below are in that array, the "Name" values i have populated 1-5 by just typing
I then Decide to delete number 3 which is successful
I then decide to add a new one, here is where everything goes wrong. as you can see below, it successfully adds the 5th item again, but the one that should have #5 in it, is now blank.
I then press "Create Array" which just dumps the array to console, and i see the below, the values are still in there, but not bound to the Input for that 1 item.
Ok, Now for the code:
This is my HTML Template file:
<form name="form" #f="ngForm">
Name: <input class="input" type="text" name="Name" [(ngModel)]="model.Name"
#Name="ngModel" />
Description: <input class="input" type="text" name="Description"
[(ngModel)]="model.Description" #Description="ngModel" />
<br>
<button (click)="addThought()">New Thought</button>
<div class="Thought" *ngFor="let Thought of myObject.Thoughts;let i=index">
Thought Name:<input name="Name-{{i}}" [(ngModel)]=Thought.Name
#Thought.Name="ngModel" type="Text" /><br>
Thought Description:<input name="Description-{{i}}"
[(ngModel)]=Thought.Description #Thought.Description="ngModel" type="Text"
/>
<br>
<br>
<button (click)="removeThought(Thought)">Remove Thought</button>
</div>
<button (click)="CreateThought()">Create Arrays</button>
</form>
and this is my component TS file:
export class CreateThoughtComponent implements OnInit {
model: any = {};
myObject: customObject = new customObject;
constructor(private guid: Guid, private staticData: StaticDataService) {
}
ngOnInit() {
}
CreateThought() {
console.log(this.myObject);
}
addThought() {
let thought: Thought = new Thought;
this.myObject.Thoughts.push(thought);
}
removeThought(t: Thought) {
this.myObject.Thoughts = this.myObject.Thoughts.filter(item => item !==
t);
}
}
And here is the declaration of the array within an object
export class customObject {
Name: string;
Description: string;
Thoughts: Thought[];
constructor() {
this.Thoughts = new Array<Thought>();
}
}
export class Thought {
Name: string;
Description: string;
}
Any help or suggestions would be greatly appreciated.
This is a tricky thing about Angular's change detection mechanism. You can solve your problem easily by creating a clone of your object. e.g.
addThought() {
let thought: Thought = new Thought;
this.myObject.Thoughts.push(thought);
// clone the object in order to force Angular to apply changes
this.myObject = JSON.parse(JSON.stringify(this.myObject));
}
I solved it by removing the name="Name-{{i}}" from the input's, and adding [ngModelOptions]="{standalone: true}" instead. at it seemed to be an issue with the dynamic way i was assigning the Name to the input using the "index"
I was able to solve it by randomly generating a guid for each name but that created a mire of other issues as well.
That being said, i have also tested DiabolicWord's solution above and it works, since it's so simple going to mark his as the answer.
I am trying to create a select component wrapper in Angular 2 using ngModel. The events all fire off correctly once the selection is changed but I cannot set the initial selection when it's rendered.
This is my component:
#Component({
selector: 'my-dropdown',
inputs: ['selectedItem', 'items', 'label'],
outputs: ['selectedItemChange'],
template: `
<div class="field">
<label>{{label}}</label>
<select class="ui search selection dropdown" [ngModel]="selectedItem" (change)="onChange($event.target.value)">
<option value="" selected>Please Select</option>
<option *ngFor="#item of items" [value]="item.value">{{item.label}}</option>
</select>
</div>`
})
export class MyDropdownComponent {
items: DropdownValue[];
selectedItem: DropdownValue;
selectedItemChange: EventEmitter<any> = new EventEmitter();
private onChange(newValue) {
console.log(newValue);
this.selectedItem = this.items.find(item => item.value == newValue);
console.log(this.selectedItem);
this.selectedItemChange.emit(newValue);
}
}
And I'm using it in the view like this:
<my-dropdown [items]="selectItems" [(selectedItem)]="itemToSelect" [label]="'Please Select'"></my-dropdown>
When I set the itemToSelect value in the parent component on init, it does not set the selected option value in the UI.
I have also tried to use the ngModelChange event but it does not fire a change event.
itemToSelect is initially set to an object, so the input property of MyDropdownComponent is initially set to an object. But then in onChange() a string value is emit()ted, which then causes itemToSelect to be set to a string, and hence the input property becomes a string. Not good.
Just consistently use an object and it will work. Also, there is no need to assign this.selectedItem in onChange(), since the selected value will propagate back down from the parent (in general, you should never set input properties in the component – it also looks weird). You can also use ngModelChange now too:
<select class="ui search selection dropdown" [ngModel]="selectedItem.value"
(ngModelChange)="onChange($event)">
private onChange(newValue) {
console.log('nv',newValue);
selectedItem = this.items.find(item => item.value == newValue);
console.log('si',selectedItem);
this.selectedItemChange.emit(selectedItem);
}
}
Plunker
Note that I did not solve the issue of the user selecting "Please select". You'll need to add some logic to onChange() to handle that case.
When I set the itemToSelect value in the parent component on init, it does not set the selected option value in the UI.
Assuming you are using ngOnInit() in the parent, you should set value in one of the lifecycle hooks that are called later (because child doesn't yet exist in ngOnInit()), try ngAfterViewInit()...
I have successfully created a function to toggle the individual rows of my ng-table to open and close using:
TestCase.prototype.toggle = function() {
this.showMe = !this.showMe;
}
and
<tr ng-repeat="row in $data">
<td align="left">
<p ng-click="row.toggle();">{{row.description}}</p>
<div ng-show="row.showMe">
See the plunkr for more code, note the expand/collapse buttons are in the "menu".
However, I can't figure out a way to now toggle ALL of the rows on and off. I want to be able to somehow run a for loop over the rows and then call toggle if needed, however my attempts at doing so have failed. See them below:
TestCase.prototype.expandAllAttemptOne = function() {
for (var row in this) {
if (!row.showMe)
row.showMe = !row.showMe;
}
}
function expandAllAttemptOneTwo(data) {
for (var i in data) {
if (!data[i].showMe)
data[i].showMe = !data[i].showMe;
}
}
Any ideas on how to properly toggle all rows on/off?
Using the ng-show directive in combination with the ng-click and ng-init directives, we can do something like this:
<div ng-controller="TableController">
<button ng-click="setVisible(true)">Show All</button>
<button ng-click="setVisible(false)">Hide All</button>
<ul>
<li ng-repeat="person in persons"
ng-click="person.visible = !person.visible"
ng-show="person.visible">
{{person.name}}
</li>
</ul>
</div>
Our controller might then look like this:
myApp.controller('TableController', function ($scope) {
$scope.persons = [
{ name: "John", visible : true},
{ name: "Jill", visible : true},
{ name: "Sue", visible : true},
{ name: "Jackson", visible : true}
];
$scope.setVisible = function (visible) {
angular.forEach($scope.persons, function (person) {
person.visible = visible;
});
}
});
We are doing a couple things here. First, our controller contains an array of person objects. Each one of these objects has a property named visible. We'll use this to toggle items on and off. Second, we define a function in our controller named setVisible. This takes a boolean value as an argument, and will iterate over the entire persons array and set each person object's visible property to that value.
Now, in our html, we are using three angular directives; ng-click, ng-repeat, and ng-show. It seems like you already kinda know how these work, so I'll just explain what I'm doing with them instead. In our html we use ng-click to set up our click event handler for our "Show All" and "Hide All" buttons. Clicking either of these will cause setVisible to be called with a value of either true or false. This will take care of toggling all of our list items either all on, or all off.
Next, in our ng-repeat directive, we provide an expression for angular to evaluate when a list item is clicked. In this case, we tell angular to toggle person.visible to the opposite value that it is currently. This effectively will hide a list item. And finally, we have our ng-show directive, which is simply used in conjunction with our visible property to determine whether or not to render a particular list item.
Here is a plnkr with a working example: http://plnkr.co/edit/MlxyvfDo0jZVTkK0gman?p=preview
This code is a general example of something you might do, you should be able to expand upon it to fit your particular need. Hope this help!
i'm using knockout 2.2.1.
I have a set of 3 check boxes to concatenate to get the corresponding values all together:
<fieldset data-role="controlgroup" id="top-colours" data-bind="topColoursLabel: { topColoursRed,topColoursBlue,topColoursGreen }">
<legend>Top Colours:</legend>
<input type="checkbox" name="top-colours-red" data-bind="checked: topColoursRed" id="tc-check-1" />
<label for="tc-check-1">Red stripes</label>
<input type="checkbox" name="top-colours-blue" data-bind="checked: topColoursBlue" id="tc-check-2" />
<label for="tc-check-2">Blue stripes</label>
<input type="checkbox" name="top-colours-green" data-bind="checked: topColoursGreen" id="tc-check-3" />
<label for="tc-check-3">Green stripes</label>
</fieldset>
The result shall be for example: "Red stripes, Blue stripes".
My viewmodel is as follows:
function ColoursViewModel() {
var self = this;
self.template = "coloursView";
self.topColoursRed = ko.observable(false);
self.topColoursBlue = ko.observable(false);
self.topColoursGreen = ko.observable(false);
self.topColoursDescription = ko.observable("");
}
How shall be the custom bindings to achieve this?
I try something like that:
ko.bindingHandlers.topColoursLabel = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.utils.unwrapObservable(valueAccessor());
// ...
var checkText = '...';
viewModel.topColoursDescription(checkText);
}
};
I'm not able to find out how to pass the array to my custom bindings to subscribe to the values of the 3 check boxes, because I'm noob to knockout.
It seems to me, that a declaration like:
data-bind="topColoursLabel: { topColoursRed,topColoursBlue,topColoursGreen }"
would be great to achieve this, but i'm searching the right way to do that.
Note: i cannot use a computed observable here, because i need to get some other properties from element - i mean the label-for text - so a custom binding is needed.
Can someone help?
UPDATED jsFiddle: http://jsfiddle.net/Sx87j/
Actually, custom binding handler is not what you really need. You should implement your self.coloursDescription as computed observable which will track checkbox changes and return currently selected stripes:
self.topColoursDescription = ko.computed(function(){
var colors = [];
if (self.topColoursRed()) colors.push('Red stripes');
if (self.topColoursBlue()) colors.push('Blue stripes');
if (self.topColoursGreen()) colors.push('Green stripes');
return colors.join(', ');
});
Also remove all tracks of your custom bindings from markup and you will get something like this: http://jsfiddle.net/h7Bmb/8/
Update
I can make your updated fiddle to work with top colours. Making it work with bottom colors too looks a bit complicated with your current approach.
Enumerate all linked color observables in your binding:
<fieldset data-role="controlgroup" id="top-colours" data-bind="topColoursLabel: [ topColoursRed, topColoursBlue, topColoursGreen ]">
Change your custom binding code (the line where ko.utils.unwrapObservable is called):
ko.utils.arrayForEach(valueAccessor(), ko.utils.unwrapObservable);
Example: http://jsfiddle.net/Sx87j/1/