Angular: $scope.$watch a nested collection - javascript

In my Angular app, I have a checkbox list which is generated via a nested ng-repeat, like so:
<div ng-repeat="type in boundaryPartners">
<div class="row">
<div class="col-xs-12 highlight top-pad">
<div ng-repeat="partner in type.partners" class="highlight">
<label class="checkbox-inline">
<input type="checkbox" value="partner"
ng-model="ids[$parent.$index][$index]"
ng-true-value="{{partner}}"
ng-false-value="{{undefined}}">
<p><span ></span>{{partner.name}}<p>
</label>
</div>
</div>
</div>
</div>
and in my controller:
$scope.ids = [];
$scope.$watchCollection('ids', function(newVal) {
for (var i = 0, j = newVal.length; i < j; i++) {
// Create new participatingPatners tier if it doesn't exist
if(!$scope.report.participatingPartners[i]) $scope.report.participatingPartners[i] = {};
// Give it an id
$scope.report.participatingPartners[i].id = i + 1;
// Map our values to it
$scope.report.participatingPartners[i].entities = $.map(newVal[i], function(value, index) {
return [value];
});
}
});
The problem is, this $scope.$watchCollection stops watching once I've added one of each top-level ids, so if I add a given number of inputs from the first nested list, then another from the second list, My $scope.report.participatingPartners object never gets updated.
How can I $watch for changes within ids[$parent.$index][$index], making sure updated my object whenever a checkbox gets ticket or unticked?

You are creating an array of arrays:
$scope.ids = [
[],
[],
//...
]
But use $watchCollection to watch for changes in the outer array, i.e. of $scope.ids. This will only identify changes when nested arrays become different objects (or created the first time).
You could use $scope.$watch("ids", function(){}, true) - with true standing for "deep-watch", but that would be very wasteful, since it's an expensive check that would be performed on every digest cycle, whether a checkbox was clicked or not.
Instead, use ng-change to trigger the handler:
<input type="checkbox" value="partner"
ng-model="ids[$parent.$index][$index]"
ng-change="handleCheckboxChanged()">
$scope.handleCheckboxChanged = function(){
// whatever you wanted to do before in the handler of $watchCollection
}

$watchCollection is similar to $watch in that it checks the physical object reference, but goes one step further; it also goes one level deep and does a reference check on those properties.
You'll need to use $watch, but set the objectEquality flag to true. This will tell $watch to perform deep reference checking. Depending on the depth of the item being watched this can hurt performance significantly.
$watch(watchExpression, listener, [objectEquality]);

Can you try to watch for object equality :
$scope.$watchCollection('ids', function(newVal) {
}, true);

Related

angular checkboxes with 2 dimensional array

I have a template:
<mat-card *ngFor="let cargo of cargos" class="cont-mat">
/*...*/
<form [formGroup]="cargoSignupForm" (submit)="onSignupForCargo(cargo.id)" *ngIf="!isLoading">
<p>Odaberite kamione s kojima želite izvršiti prijevoz - Težina robe: {{cargo.weight}} kg</p>
<div *ngFor="let truck of (trucksByCargoId.get(cargo.id))">
<input type="checkbox" (change)="onChange(truck._id, $event.target.checked, cargo.id)">{{truck.regNumber}}
</div>
<input type="submit" value="GO" class="button-basic">
</form>
and 2 component functions:
truckFormArray = [[]];
ngOnInit() {
/*...*/
this.cargoSignupForm = new FormGroup({
trucksId: this.fb.array([])
});
/*...*/
}
onChange(truckId: string, isChecked: boolean, cargoId: string) {
this.truckFormArray[cargoId] = <FormArray>this.cargoSignupForm.controls.trucksId;
if (isChecked) {
this.truckFormArray[cargoId].push(new FormControl(truckId));
} else {
let index = this.truckFormArray[cargoId].controls.findIndex(x => x.value == truckId)
this.truckFormArray[cargoId].removeAt(index);
}
}
onSignupForCargo(cargoId: string) {
console.log(this.truckFormArray[cargoId]);
}
I just want to console_log(this.truckFormArray[cargoId]). There must be different truckFormArray for every cargoId. With that solution I'm getting trucksFormArray from previus cargoId checkboxes also. I hope you understand what I mean. Somewhere is a small mistake, but also if you think there is a better solution to do that you are welcome. Thanks in advance
truckFormArray should be an object
It is safe to assume that cargoId is not a sequentially increasing number starting from 0, hence there is no sense in declaring it as an array, declare it as an object instead:
truckFormArray = {};
Reason: Arrays in Javascript are always indexed by numbers starting from 0 and increasing sequentially until the last index.
truckFormArray is not a data member of the instance of your object
Since it is not initialized as this.truckFormArray, you do not have the this in front of it. So change all occurrences of this.truckFormArray to truckFormArray.
Reason: You will always need to be consistent when you refer to your resources.
Initializing truckFormArray
You have
this.truckFormArray[cargoId] = <FormArray>this.cargoSignupForm.controls.trucksId;
and it seems to be incorrect. Your trucksId seems to be a number and you try to assign it to a resource which is treated as an array, so there is a type discrepancy. If you want to store each trucksId into an array identified by the cargo that is present on the trucks, then you need to do a push, but only if the checkbox is checked. So, instead of the above, you will need something like this:
if (!truckFormArray[cargoId]) {
//Here I created a JS array for the sake of simplicity, but you can
//create a FormArray instance instead if that's more helpful for you
this.truckFormArray[cargoId] = [];
}
Reason: if the array you need to refer to does not exist yet, then you need to create it.
Summary
You will need to
fix the initialization of truckFormArray
ensure that you refer it consistently with the way it is defined
initialize each of its cargo array when needed

Change entire object in ng-repeat function AngularJs

Hello I have a questions on ng-repeat on Angularand function for change value.
I have this ng-repeat that cycling a ObjectArray and have a button for reset value.
<div ng-repeat="element in data.elements">
<button ng-click="reset(element)" >reset</button>
</div>
Where data.elements is array of objects example:
[{id:1, name:"element1"},{id:2, name : "element2"}];
In my Controller I set function Reset in $scope that should do a copy of object passed to an default object:
$scope.reset = function(el){
$scope.defaultObject = {id:500, name:"default"};
el = angular.copy($scope.defaultObject);
}
But doesn't work, but if I do:
$scope.reset = function(el){
$scope.defaultObject = {id:500, name:"default"};
el.name = $scope.defaultObject.name;
}
It work.
So I would like that when I do (in this example):
el = angular.copy($scope.defaultObject);
have the object el equals to object $scope.defaultObject my question is, Can i copy entire object without cycling all properties?
You're passing an object to the reset function, then you're overwriting this object. That's it, nothing happens because it won't affect the original object, which is in the data.elements array.
You need to use a different approach. Track the element by its index :
<div ng-repeat="element in data.elements track by index">
<button ng-click="reset(index)" >reset</button>
</div>
...then amend data.elements[index]:
$scope.reset = function(index){
$scope.data.elements[index] = {id:500, name:"default"};
}
are you saying, your updated object does not reflect on the UI? you can try forcing the scope update by running $scope.$apply() after you have assigned the object.

AngularJS custom filter called twice and delete input data on second call

Here are the codes.
var app = angular.module("nameApp", ["ngRoute"]);
app.controller("ctrlname", function ($scope, $http, $filter, apiKey, apiUrl) {
$scope.data = {};
$scope.currentPage = 1;
$scope.pageSize = 5;
});
The $scope.data will contain an array of data from an HTTP GET request.
The following is a code for a custom filter for the purpose of pagination of results. Basically, this will limit the results to only 5. Buttons for pagination will update $scope.currentPage's value.
app.filter("limitResults", function ($filter, $log) {
return function (data, page, size) {
if (angular.isArray(data) & angular.isNumber(page) && angular.isNumber(size)) {
var startPage = (page - 1) * size;
if (data.length < startPage) {
return [];
} else {
$log.info(data);
$log.info(page);
$log.info(size);
$log.info(startPage);
return $filter("limitTo")(data.splice(startPage), size);
}
} else {
return data;
}
}
});
This is the HTML page that will render the data.
<div class="row resultItems" ng-repeat="video in data.videos | limitResults:currentPage:pageSize">
<div class="col-sm-3 testing">
<img ng-src="{{video.snippet.thumbnails.default.url}}">
</div>
<div class="col-sm-9 testing">
<h5>
{{video.snippet.title}}
</h5>
<p>
{{video.snippet.channelTitle}}
</p>
<p>
{{video.snippet.description}}
</p>
</div>
</div>
I put a few lines of $log.info code in the custom filter in order to see what really happens when the filter is applied. The filter runs twice, which is a normal behaviour.
What I find confusing is that when the custom filter runs for the first time, $log.info(data) logs the original data received from a HTTP GET call to the console. However, when the custom filter runs for the second time, $log.info(data) logs an empty array to the console.
Given the fact that "$log.info(data); $log.info(page); $log.info(size);" get logged to the console, it is obvious that the second IF statement (if (data.length < startPage)) is evaluated to TRUE and the filter (return $filter("limitTo")(data.splice(startPage), size);) is applied.
I just don't understand why the array, which is the data passed to the custom filter, gets emptied when the filter runs the second time.
The reason you are seeing empty array is because of the splice method.
$filter("limitTo")(data.splice(startPage), size);
Splice method syntax
array.splice(start, deleteCount[, item1[, item2[, ...]]])
If splice method is called without second parameter, that means if deleteCount is not passed, deleteCount will be treated as [arr.length - start]. In your case, when the first time filter executes, the entire array becomes empty.
See this doc for splice method

AngularJS watch array of objects with index

I have a question about Angular watch within an array of objects.
I have an array $scope.chartSeries with objects as following:
[{"location": {values}, "id":"serie-1", "meter":{values}, "name": "seriename", "data":[{1,2,4,5,7,4,6}]}]
This is used to draw a linechart with highcharts.
I want to watch this array, and if a value changes I want to know the index and the value that is being changed.
I found several options for watch but none of them seem to fit my situation. Can you help me out?
If you render and change your array in ng-repeat, you can use ng-change directive and pass in it a $index parameter.
For example:
<div ng-repeat="item in array">
<input type="text" ng-model="item.location" ng-change="changeValue($index)"/>
</div>
Or you can use $watch and work with newValue, oldValue parameters
$scope.$watch('array', function (newValue, oldValue) {
for(var i = 0; i < newValue.length; i++) {
if(newValue[i].location != oldValue[i].location)
var indexOfChangedItem = i;
//bla-bla-bla
}
}, true);
You can use $watchGroup(watchExpressions, listener).
For e.g.:
$scope.chartSeries = [{"location": {values}, "id":"serie-1", "meter":{values}, "name": "seriename", "data":[{1,2,4,5,7,4,6}]}];
$scope.$watchCollection('chartSeries ', randomFunction(var) {
//code
});
Or you can use watch for individual values in array to know which ones got changed.

Update Position Of Observable Array With New Item In Knockout.js

I have a situation where I need to replace a certain item in an observable array at a certain position of it. Right now I am doing it below with the slice method. Is there a better way that is built in to knockout.js to do this at a certain position? I was even thinking about doing a push, and then do a sort on that row with a order property but I have lots of rows and thought that was to much.
var position = ko.utils.arrayIndexOf(self.list(), game);
if (position != -1) {
self.list.remove(self.game);
self.list.splice(position, 0, newGame);
}
Code With Replace, Trying To Update Property Matchup That Has A New Property Called Name
var game = self.game;
if (game) {
var position = ko.utils.arrayIndexOf(self.list(), game);
if (position != -1) {
if (game.Matchup) {
game.Matchup = new Matchup(response.Data);
game.Modified(true);
}
else if (self.game) {
game = new Matchup(response.Data);
}
self.list.replace(self.list()[position], game);
}
}
HTML
<!-- ko foreach: Games -->
<td class="item-container draggable-item-container clearfix">
<div class="item clearfix draggable-active draggable-item" data-bind="draggableCss: { disabled: $data.Disabled(), matchup: $data.Matchup }, draggableGameHandler : { disabled: !$data.Matchup, disabledDrop: $data.Disabled() }, delegatedClick: $root.members.eventSchedule.editGame.open.bind($data, true, ($data.Matchup && $data.Matchup.Type == '#((int)ScheduleType.Pool)'), $parent.Games)">
<span data-bind="if: $data.Matchup">
<span data-bind="attr: { title: Matchup.Title }"><span data-bind="html: Matchup.Name"></span></span>
</span>
</div>
</td>
<!-- /ko -->
data-bind="html: Matchup.Name" doesn't update with replace.
Replacing an item in an observable array
The replace method is one option for replacing an item in an observable array. In your case, you could call it like this:
list.replace(game, newGame);
Bindings update when an observable dependency changes
But your question isn't only about replacing an item in an array. You've stated that the binding html: Matchup.Name isn't updated, so let's look at what could cause it to update:
If Name is an observable, modifying Name will cause an update.
If Matchup is an observable, modifying it will cause an update, but then you'd have to bind it like Matchup().Name and update it like game.Matchup(Matchup(response.Data));.
Replacing the entry in the observable array (is it Games or list?) with a new object will cause the whole inner template to re-render, obviously replacing each binding.
Looking through your code, I can see that in one case (if (game.Matchup)), none of these three things happen, and thus there's no way Knockout can know to update the binding. The first two obviously aren't occurring and although you do call replace on the array, it's the equivalent of this:
list.replace(game, game); // replace the item with itself
The foreach binding doesn't see the above as a change and doesn't update anything. So, to update the binding, you need to make a real update to an observable.
Further comments
To get the index of an item in an observable array, use the indexOf method:
var position = self.list.indexOf(game);
To replace an item at a specific index, use the splice method with a second parameter of 1:
self.list.splice(position, 1 /*how many to remove*/, newGame);
Observable arrays have a built in .replace method you can use to do this:
var position = ko.utils.arrayIndexOf(self.list(), game);
self.list.replace(self.list()[position], game);

Categories

Resources