CheckAll/UncheckAll issue with Subscribe ? Knockout - checkbox

I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .

Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.

I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/

The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/

Related

Enable/disable validation for angular form with nested subforms using `ng-form`

I need to enable/disable all validation rules in Angular form or subform under ng-form="myForm" based on a scope variable $scope.isValidationRequired. So, if isValidationRequired is false, none of the validations set for the designated group of fields will run, and the result will always be myForm.$valid==true, otherwise, the validation rules will run as usual.
I did a lot of research, and realized that this feature is not available out of the box with Angular. However, I found some add-ons or with some customization, it is possible.
For example, I can use the add-on angular-conditional-validation (github and demo) with custom directive enable-validation="isValidationRequired". This will be perfect, except that I cannot apply this feature for a group of fields under ng-form. I have to add this directive for each and every field where applicable.
The other solution is to use custom validation using Angular $validators pipeline. This requires some extra effort and I don't have time since the sprint is almost over and I have to give some results in a few days.
If you have any other suggestions please post an answer.
Use Case:
To clarify the need for this, I will mention the use-case. The end user can fill the form with invalid data and he can click Save button and in this case, the validation rules shouldn't be triggered. Only when the user clicks Validate and Save then the validation rules should be fired.
Solution:
See the final plunker code here.
UPDATE: as per comments below, the solution will cause the browser to hang if inner subforms are used under ng-form. More effort is needed to debug and resolver this issuer. If only one level is used, then it works fine.
UPDATE: The plunker here was updated with a more general solution. Now the code will work with a form that has sub-forms under ng-form. The function setAllInputsDirty() checks if the object is a $$parentForm to stop recursion. Also, the changeValidity() will check if the object is a form using $addControl then it will call itself to validate its child objects. So far, this function works fine, but it needs a bit of additional optimization.
One idea is to reset the errors in the digest loop if the validation flag is disabled. You can iterate through the form errors on change and set them to valid, one by one.
$scope.$watch(function() {
$scope.changeValidity();
}, true);
$scope.changeValidity = function() {
if ($scope.isValidationRequired === "false") {
for (var error in $scope.form.$error) {
while ($scope.form.$error[error]) {
$scope.form.$error[error][0].$setValidity(error, true);
}
}
}
}
Here is a plunkr: https://plnkr.co/edit/fH4vGVPa1MwljPFknYHZ
This is the updated answer that will prevent infinite loop and infinite recursion. Also, the code depends on a known root form which can be tweaked a bit to make it more general.
References: Pixelastic blog and Larry's answer
Plunker: https://plnkr.co/edit/ycPmYDSg6da10KdoNCiM?p=preview
UPDATE: code improvements to make it work for multiple errors for each field in each subform, and loop to ensure the errors are cleared on the subform level
var app = angular.module('plunker', []);
app.controller('MainCtrl', ["$scope", function($scope) {
$scope.isValidationRequired = true;
var rootForm = "form";
function setAllInputsDirty(scope) {
angular.forEach(scope, function(value, key) {
// We skip non-form and non-inputs
if (!value || value.$dirty === undefined) {
return;
}
// Recursively applying same method on all forms included in the form except the parent form
if (value.$addControl && key !== "$$parentForm") {
return setAllInputsDirty(value);
}
if (value.$validate){
value.$validate();
}
// Setting inputs to $dirty, but re-applying its content in itself
if (value.$setViewValue) {
//debugger;
return value.$setViewValue(value.$viewValue);
}
});
}
$scope.$watch(function() {
$scope.changeValidity();
}, true);
$scope.changeValidity = function(theForm) {
debugger;
//This will check if validation is truned off, it will
// clear all validation errors
if (!theForm) {
theForm = $scope[rootForm];
}
if ($scope.isValidationRequired === "false") {
for (var error in theForm.$error) {
errTypeArr = theForm.$error[error];
angular.forEach (errTypeArr, function(value, idx) {
var theObjName = value.$name;
var theObj = value;
if (theObj.$addControl) {
//This is a subform, so call the function recursively for each of the children
var isValid=false;
while (!isValid) {
$scope.changeValidity(theObj);
isValid = theObj.$valid;
}
} else {
while (theObj.$error[error]) {
theObj.$setValidity(error, true);
}
}
})
}
} else {
setAllInputsDirty($scope);
}
}
}]);

UI-grid saveState service circular logic

Here is a summary of the problem: I set up a column sortChange() listener, which responds to sort changes by firing off a query to fetch newly sorted data. I save the grid state before the fetch, and restore the grid state after the fetch. The problem is that the restore gridState mechanism triggers the original sort listener, causing the whole process to start over again, and again, and again.
scope.sitesGrid.onRegisterApi = function(gridApi) {
scope.gridApi = gridApi;
scope.gridApi.core.on.sortChanged(scope, function () {
// load new sites on a sort change
scope.initialize();
});
};
scope.initialize = function() {
// save current grid state
scope.gridApi && (scope.gridState = scope.gridApi.saveState.save());
fetchSites().then(function (sites) {
scope.sitesGrid.data = sites
// restore current grid state, but inadvertently retrigger the 'sortChanged' listener
scope.gridApi.saveState.restore(scope,scope.gridState);
})
};
I was thinking that I could set up a click listener on each column header, instead of using a sortChange listener, however this solution seems ugly and requires going into every header cell template and making changes.
How about some kind of scope variable to track the loading of data?
scope.gridApi.core.on.sortChanged(scope, function () {
if (!scope.isLoading) {
scope.initialize();
}
});
and
fetchSites().then(function (sites) {
scope.isLoading = true;
scope.sitesGrid.data = sites;
scope.gridApi.saveState.restore(scope,scope.gridState);
scope.isLoading = false;
})
You might need to add some timeout() calls in places if there are timing issues with this. Creating a Plunker to demonstrate this would help in that case.
I think i find solution. I created restore function in my directive (u can use it where you want). I just block executing next iteration until action is finished.
function restoreState() {
if ($scope.gridState.columns !== undefined && !isRestoring) { //check is any state exists and is restored
isRestoring = true; //set flag
$scope.gridApi.saveState.restore($scope, $scope.gridState)
.then(function () {
isRestoring = false; //after execute release flag
});
}
}
function saveState() {
if (!isRestoring) {
$scope.gridState = $scope.gridApi.saveState.save();
}
}

Lazy repeat calculateItemHeight index is always undefined

I'm using ons-lazy-repeat and I want to have variable item heights depending on the item.
For that I'm using, as described in the documentation, a delegate object with a calculateItemHeight function.
The problem is all item heights are being set to 20 because initially the index variable is always undefined. Afterwards everything seems to work correctly but everything is already rendered on the ui.
Anyone else have the same problem? I don't see what I'm doing wrong here.
This is my delegate object:
$scope.TransactionDelegate = {
configureItemScope: function(index, itemScope) {
itemScope.item = TransactionService.allTransactions[index];
},
calculateItemHeight: function(index) {
if (!index)
return 20;
return 60;
},
countItems: function() {
return TransactionService.allTransactions.length;
},
destroyItemScope: function(index, scope) {
;
}
};
Thanks
This is a know bug that is now fixed and will be released in the next version.

Backbone - trying to make a filter on a collection with nested object

i'm trying to filter a collection which has models with some nested object. Unfortunately, my result are always empty.
So my models returned in the collection are build like this:
My goal is simple:
I have a view with a list of tag and a content view with all the questions. When a user click on tag, for example, "c#", i want to filter my collection to just return questions with tag "c#"
Before i was doing a fetch on my server and it was working fine, but it was not optimize.
I already have a collection with all the questions so why make a new call, a filter is a better solution i think.
But i didn't succeded with my filter and i don't know if it's possible to do. For now i put my filter in my router because it's more easy to test.
i can't make a filter like this because i have an array of object
getQuestionsByTags: function(query) {
var test = this.questionsCollection.filter(function(model) {
return model.attributes.tags.name == query;
})
console.log('result');
console.log(test);
},
So i was thinking to make a loop but my result is always an empty array.
getQuestionsByTags: function(query) {
var test = this.questionsCollection.filter(function(model) {
_.each(model.attributes.tags, function(tag) {
return tag.name == query;
})
})
console.log('result');
console.log(test);
},
It's maybe simple, but i don't know what to do.
Thanks in advance :)
i've just found a solution that work.
getQuestionsByTags: function(query) {
var flag;
var test2 = this.questionsCollection.filter(function(model) {
flag = false;
_.each(model.attributes.tags, function(tag) {
if(tag.name == query) {
flag = true;
}
})
if(flag) {
return model.attributes;
}
})
console.log('result');
console.log(test2);
},
i put a flag. If he turn true inside the loop, the model has this tag so i return it.
I think it's not very conventional, so if someone have another solution, feel free to post it :)

wait for backbone template to be appended to the dom

I am trying to bind events to elements that are placed by appending a backbone template:
appendEditTemplateAndSetEvents: function() {
var associatedCollection = App.Helpers.findAssociatedCollection(this.allCollections, this.associatedCollectionId);
var template = this.setEditTemplateForElement(associatedCollection.type);
var modalBody = this.$el.find('.modal-body');
modalBody.empty();
var firstModel = associatedCollection.at(0);
if(template.mainTemplate !== null) {
modalBody.append($('#edit-form-element-frame').html());
//each mode in collection
associatedCollection.each(function(model){
if(model.get('positionInContainer') === 1) {
firstModel = model;
}
console.log(model.attributes);
modalBody.find('.elements-in-editmodal-wrapper').append(template.mainTemplate(model.toJSON()));
});
}
if( template.templateValidation.length !== 0 ) {
modalBody.append('<hr><h3>Validateregels</h3>');
_.each(template.templateValidation, function(val, index) {
modalBody.append(val(firstModel.toJSON()));
});
}
//set listeners and handlers that apply when a edit modal is open
this.validationEventsForEditModal(firstModel);
this.editErrorMessagesInModal(firstModel);
},
Now the problem is that when the last two functions are called the html of the templates isn't appended yet so the the events are binded to an object with a length of 0.
Does anyone have a decent solution for this async problem? I tried $.Defferred but that did not work, but maybe someone get's it working.
I solved this by using this.$el.find(...) in the functions:
this.validationEventsForEditModal(firstModel);
this.editErrorMessagesInModal(firstModel);
I don't know if it's still an async problem, but this solves it.

Resources