Call Knockout JS Extender function after timeout - javascript

I am currently trying to implement an autosave feature in a Knockout JS applcation using extenders. I want to call an autosave function when users stop typing in a field, not just when they tab out of it.
This is the logChange method I want to call when an observable gets updated.
//KO Extender for logging changes and calling the autosave function
ko.extenders.logChange = function (target, precision) {
//create a writable computed observable to intercept writes to our observable
var result = ko.pureComputed({
read: target, //always return the original observables value
write: function (newValue) {
debugger;
var current = target(),
valueToWrite = newValue,
attName = precision;
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
//self.autoSave(attName, target());
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
}).extend({ notify: 'always' });
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
This is how I am setting up the observable in my viewmodel.
self.controlCenter = ko.observable().extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" }, logChange: "ControlCenter" });
And this is my html markup for that observable
<div class="pure-u-1-2 pure-u-md-1-4 pure-u-lg-1-8">
<label for="ddlControlCenter">Jurisdiction</label>
<input type="text" class="pure-input-1 full-text" list="controlCenterList" data-bind="textInput: controlCenter" />
<datalist id="controlCenterList" data-bind="foreach: controlCenters">
<option data-bind="value: $data"></option>
</datalist>
</div>
The logChange method gets called, but it doesn't look like the rateLimit is being applied as the logChange gets called immediately on keypress.

Update:
I've updated the fiddle below. The mistake you were making was that you were calling a function inside the logChange function, right after updating the new value. But the concept of rateLimit is that the notification that the observable value was changed is sent after a delay to all its subscribers.
In other words, only the observable's subscribers are affected by the rateLimit, not anything inside the logChange function. So the correct way to do it would be to call your autoSave function inside the subscriber.
function ViewModel() {
var self = this;
self.autoSave = function(attName, value){
console.log(attName + " is now = " + value);
}
self.precision = ko.observable();
ko.extenders.logChange = function (target, precision) {
//create a writable computed observable to intercept writes to our observable
var result = ko.pureComputed({
read: target, //always return the original observables value
write: function (newValue) {
var current = target(),
valueToWrite = newValue;//,
self.precision(precision);
//attName = precision;
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
//self.autoSave(attName, target());
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
}).extend({ notify: 'always' });
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
self.controlCenter = ko.observable().extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" }, logChange: "ControlCenter" });
self.controlCenters = ko.observableArray([]);
this.controlCenter.subscribe(function (val) {
if (val !== '')
this.controlCenters.push(val);
self.autoSave(self.precision(), val);
}, this);
}
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="pure-u-1-2 pure-u-md-1-4 pure-u-lg-1-8">
<label for="ddlControlCenter">Jurisdiction</label>
<input type="text" class="pure-input-1 full-text" list="controlCenterList" data-bind="textInput: controlCenter" />
<datalist id="controlCenterList" data-bind="foreach: controlCenters">
<option data-bind="value: $data"></option>
</datalist>
</div>

Related

Angular 1: Last checkbox doesn't stay checked after page refresh

I am saving all data into localStorage. When a checkbox is checked function is called to change items state. It works fine. However after page refresh, last checked item gets unchecked (or if it was unchecked, it gets checked) while others are working just fine. Why does that 1 last action gets ignored after page is refreshed?
Here is codepen: http://codepen.io/kunokdev/pen/vGeEoY?editors=1010
(add few items and click on "click me" for all of them and then refresh page, last action will be ignored)
The view:
<div ng-app="TaskApp" ng-controller="ToDoCtrl">
<form>
<input type="text" ng-model="toDoItem">
<input type="submit" ng-click="addToDoItem()">
</form>
<div>
<ul>
<div
ng-repeat="item in toDoItems |
orderBy: 'createdAt'
track by item.createdAt">
<b>Content:</b> {{item.content}} <br>
<b>Completed?</b> {{item.completed}}
<md-checkbox ng-model="item.completed" ng-click="toggleToDoItem(item.completed)" aria-label="todo-checkbox">
CLICK ME
</md-checkbox>
</div>
</ul>
</div>
</div>
And JS:
var ls = {};
ls.get = function(key) {
return JSON.parse(localStorage.getItem(key));
};
// sets or updates a value for a key
ls.set = function(key, val) {
localStorage.setItem(key, JSON.stringify(val));
};
// returns true if value is set, else false
ls.isSet = function(key) {
var val = ls.get(key);
return ( null === val || 'undefined' === typeof val) ? false : true;
};
// removes a set item
ls.remove = function(key) {
localStorage.removeItem(key)
};
var TaskApp = angular.module('TaskApp', [
'ngMaterial',
'taskAppControllers'
]);
var taskAppControllers = angular.module('taskAppControllers',[]);
taskAppControllers.controller('ToDoCtrl', ['$scope',
function($scope){
//
loadToDoItems = function(){
var data = ls.get("toDoData");
if (data == null) data = [];
return data;
};
//
$scope.toDoItems = loadToDoItems();
//
$scope.addToDoItem = function(){
var toDoItems = $scope.toDoItems;
var newToDoItem = {
"content" : $scope.toDoItem,
"createdAt" : Date.now(),
"completed" : false
}
toDoItems.push(newToDoItem);
ls.set("toDoData", toDoItems);
$scope.toDoItem = "";
};
//
$scope.toggleToDoItem = function(item){
console.log('test');
var toDoItems = $scope.toDoItems;
for (var i = 0; i < toDoItems.length; i++)
if (toDoItems[i].createdAt === item){
if (toDoItems[i].completed == true)
toDoItems[i].completed = false;
else
toDoItems[i].completed = true;
}
ls.set('toDoData', toDoItems);
};
//
}]);
md-checkbox is designed to toggle whatever you put in ng-model so with your code, md-checkbox was toggling the completed property and then you were changing it back again in your $scope.toggleToDoItem function. Why this worked for all the items except the last clicked I am unsure.
So I changed the ng-click to only save the items to local storage and still got the same problem which leads to me believe the problem is caused by using ng-click on an md-checkbox.
<md-checkbox ng-model="item.completed" ng-click="saveToLocalStorage()" aria-label="todo-checkbox">
CLICK ME
</md-checkbox>
$scope.saveToLocalStorage = function() {
ls.set('toDoData', $scope.toDoItems);
};
So I removed the ng-click and set up a watch on $scope.toDoItems.
<md-checkbox ng-model="item.completed" aria-label="todo-checkbox">
$scope.$watch("toDoItems", function() {
ls.set("toDoData", $scope.toDoItems);
}, true);
Codepen
-- EDIT --
Just read the documentation and feel like an idiot, you should use ng-change instead of ng-click. From the docs regarding ng-change:
Angular expression to be executed when input changes due to user interaction with the input element.
That being said, the above about not needing to toggle the completed property yourself still stands.
You are passing item.completed (in the HTML) to your toggleToDoItem(item) method. In your array loop, you then compare the item.Created field to the item.completed parameter. This is comparing a Date type to a Bool. How is that supposed to work?

ng-dirty is not working as expected

I am having tough time in understanding why my element shows ng-dirty after updating the model.
I have a collection of bridges which need to be rendered on UI. On each tab click, I am changing the index and rendering the data.
If my first tab data has changed and moved to second tab why are input elements still dirty on second tab. (Function - $scope.changeIndex)
After executing calculate, the model gets updated but still the input elements are still dirty
UI
<td style="padding:10px;text-align:center">
<label>Length:</label>
<input type="text" class="currencyLabel-editable" ng-model="bridgeModel.bridges[currentIndex].length" />
</td>
<td style="padding:10px;text-align:center">
<label>Width:</label>
<input type="text" class="currencyLabel-editable" ng-model="bridgeModel.bridges[currentIndex].width" />
</td>
<td style="padding:10px;text-align:center">
<label> Skew:</label>
<input type="text" class="currencyLabel-editable" ng-model="bridgeModel.bridges[currentIndex].skew" />
</td>
Controller
(function () {
var bridgeCtrl = function ($scope, $bootstrapBridgeData, $crudService,$log) {
$scope.bridgeModel = $bootstrapBridgeData.bridgeModel;
var onCalculateComplete = function (data) {
$scope.bridgeModel.bridges[$scope.currentIndex] = angular.copy(angular.fromJson(data));
}
var onCalculateError = function (reason){
$scope.error = "Unable to perform calculation";
$log.error(reason);
}
var onError = function (reason) {
$scope.error = "Unable to fetch data";
}
//function to null the values which needs to be re-computed
var removeCalculatedValues = function () {
$scope.bridgeModel.bridges[$scope.currentIndex].foundation_PreBoringCalculated = null;
$scope.bridgeModel.bridges[$scope.currentIndex].foundation_DrilledShaftsCalculated = null;
}
//function to compute the bridge values
$scope.calculate = function (url) {
if (!preValidation()) {
return false;
}
removeCalculatedValues();
$crudService.postAndGetData(url, $scope.bridgeModel.bridges[$scope.currentIndex])
.then(onCalculateComplete, onCalculateError)
}
//function to select the bridge and change the index of the bridge
$scope.changeIndex = function (bridgeName,index) {
$scope.selectedBridge = bridgeName;
$scope.currentIndex = index;
}
$scope.save = function (index, url) {
$scope.currentIndex = index;
crudService.postAndGetData(url, $scope.bridges[index])
.then(onUserComplete, onError);
}
//$scope.enableSave = function isFormDirty() {
// if ($(".ng-dirty").length) {
// return false;
// }
// else { return true; }
//}
//Behaviour Changes
//function which changes the css
$scope.isBridgeSelected = function (bridge) {
return $scope.selectedBridge === bridge;
}
var preValidation = function () {
if ($(".ng-invalid").length) {
alert("Please correct the errors.")
return false;
}
else { return true;}
}
}
//Get the module and add a controller to it
var module = angular.module("bridgeModule");
module.controller("bridgeCtrl", bridgeCtrl);
}());
From the documentation
ng-dirty is set if the form is dirty.
This is a check for whether the form itself has been interacted with in any way. It doesn't care what the underlying object binding is. So this is the expected behavior, since you are using the same form but changing the ng-model behind the scenes.
Dunno if this is the problem or not, but the line $scope.$setPristine; is not doing anything. It should be: $scope.$setPristine();

Negate a Property in Knockout

I am actually working with knockout and want to know if there is a way in which i can inverse the knockout property. I have a function IsArchived and want to create a inverse of that, named NotArchived. But I am having issues with it.
The main issue is that I am not seeing any difference in my output. for example there's total of 2000 account in my system out of which its showing 1500 accounts as archived and 2000 account as non archived. Instead of that it should show only 500 non archived accounts.
<li>
<label id="isArchived">
<input type="checkbox" data-bind="checked: isArchived" /><span>Archived</span>
</label>
</li>
<li>
<label id="NotArchived">
<input type="checkbox" data-bind="checked: NotArchived" /><span>Not Archived</span>
</label>
</li
JavaScript:
function WorkersViewModel() {
var self = this;
var initialized = false;
self.isArchived = ko.observable(false);
self.NotArchived = ko.observable(true);
};
self.navigateToSearch = function(uriArray) {
if (self.isArchived()) {
uriArray.push({
isArchived: true
});
}
if (self.NotArchived()) {
uriArray.push({
NotArchived: false
});
}
self.runSearch = function() {
var parameters = {
IsArchived: self.isArchived() ? true : null,
NotArchived: self.isArchived() ? false : null,
};
You can do it by using a computed.
function WorkersViewModel() {
var self = this;
var initialized = false;
self.isArchived = ko.observable(false);
self.NotArchived = ko.computed({
read: function(){ return !self.isArchived() },
write : function(value) { self.isArchived(!value); }
});
};
Depending from the evaluation sequence you need, you may use:
computed observable
subscription
because the solution with a computed observable has been already posted, here is a snippet which uses a subscription:
self.isArchived = ko.observable(false);
self.isNotArchived = ko.observable(true);
self.isArchived.subscribe(function(newValue) {
self.isNotArchived(!newValue);
});
Anozher difference is that the computed observable will be evaluated also for the first time when the view model is instantiated, whereby by using the subscription you should provide to both observables the correct initial value.

Knockout Event Fires twice

in my MVC application im creating multiple Dropdowns by following:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
optionsValue: 'id',
value: selectedItem(1),
event: { change: selectionChange }"></select>
the findgroup(x) and the selectedItem(x) are global functions in my ViewModel while those are for all the dropdowns the same.
the selectedItem(x) should return the currently selected Option of the dropdown. selectedItem(x) is a function to return a computed knockout observable.
Now im facing the Problem that the selectionChange Event is fired twice. See this fiddle for an example: http://jsfiddle.net/LGveR/20/
In this example, if you Change the value of the Dropdown box, you can see that the selectionCahnge Event is fired twice.
When i leave the value: selectedItem(x) out (and thus no computed function in the code) it doesnt: see: http://jsfiddle.net/LGveR/21/
I think that second time the Event is being fired Comes from the fact that in the computed function selectedItem(x) the observable
grp.selectedItem(grp.findItemByValue(value));
is setted.
How to prevent that the Setting of this observable leads to a "Change" Event ?
TIA,
Paul
HTML:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
optionsValue: 'id',
value: selectedItem(1),
event: { change: selectionChange }"></select> <span data-bind="text: 'aantal: ' + findGroup(1).items().length"></span>
<br /> <span data-bind="text: 'Group Selected Country: ' + findGroup(1).selectedItem().country"></span>
<br /> <span data-bind="text: 'Computed Selected Country: ' + selectedItem(1)().country"></span>
<br /> <span data-bind="text: 'after select: ' + counter()"></span>
<br />
Javascript:
var group = function (id) {
this.id = id;
this.items = ko.observableArray() || {};
this.selectedItem = ko.observable();
this.addItem = function (data) {
this.items.push(data);
};
this.findItemByValue = function (id) {
return ko.utils.arrayFirst(this.items(), function (item) {
return item.id === id;
});
}
};
var grpItem = function (id, country) {
this.id = id;
this.country = country;
};
var ViewModel = function () {
this.groups = ko.observableArray() || {};
this.counter = ko.observable(0);
this.selectionChange = function (data, event, selector, item) {
this.counter(this.counter() + 1);
};
this.addGrp = function (data) {
this.groups.push(data);
};
this.findGroup = function (groupId) {
var ret = ko.utils.arrayFirst(this.groups(), function (c) {
return c.id === groupId;
});
return ret;
};
this.selectedItem = function (groupId) {
var grp = this.findGroup(groupId);
return ko.computed({
read: function () {
return this.findGroup(groupId).selectedItem();
},
write: function (value) {
grp.selectedItem(grp.findItemByValue(value));
}
}, this);
};
};
var vm = new ViewModel();
var p = new group(1);
var a = new grpItem(1, 'holland');
var b = new grpItem(2, 'germany');
var c = new grpItem(3, 'brasil');
p.addItem(a);
p.addItem(b);
p.addItem(c);
vm.addGrp(p);
ko.applyBindings(vm);
You're doing a couple odd things in your code which results in the computed being recomputed a bunch of times. Basically, you're setting the computed value by setting an observable with a function that relies on that observable, which recomputes your computed (or something crazy like that, see http://jsfiddle.net/LGveR/25/ to see how many times read and write are being called). There are a couple simple ways you can simplify and remove this issue:
Remove the optionsValue from your select data-bind. This will set
the value to the entire item in the observable array (instead of
just the id). You can then simplify the computed write function.
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
value: selectedItem(1),
event: { change: selectionChange }"></select>
and
this.selectedItem = function (groupId) {
var grp = this.findGroup(groupId);
return ko.computed({
read: function () {
return grp.selectedItem();
},
write: function (value) {
grp.selectedItem(value);
}
}, this);
};
see http://jsfiddle.net/LGveR/23/
Alternatively, you could remove the selectedItem on the viewmodel
entirely, and remove the optionsValue (as in #1). Then, you only need the group observable with the following html:
<select data-bind="options: findGroup(1).items(),
optionsText: 'country',
value: findGroup(1).selectedItem,
event: { change: selectionChange }"></select>
<span data-bind="text: 'aantal: ' + findGroup(1).items().length"></span>
<br />
<span data-bind="text: 'Group Selected Country: ' + findGroup(1).selectedItem().country"></span>
...
See http://jsfiddle.net/LGveR/24/

knockoutjs when checked data binding call function

I need to do following things: When user checks the checkbox, some function is called.
<input type="checkbox" data-bind="what to write here?" />
and in model:
var viewModel = {
this.someFunction = function() {
console.log("1");
}
};
I have not found anything about this is documentation here.
What you need is the click binding:
<input type="checkbox" data-bind="click: someFunction" />
And in your view model:
var ViewModel = function(data, event) {
this.someFunction = function() {
console.log(event.target.checked); // log out the current state
console.log("1");
return true; // to trigger the browser default behavior
}
};
Demo JSFiddle.
Or if you want to you use the checked binding you can subscribe on the change event of your property:
<input type="checkbox" data-bind="checked: isChecked" />
And in your viewmodel:
var ViewModel = function() {
this.isChecked = ko.observable();
this.isChecked.subscribe(function(newValue){
this.someFunction(newValue);
}, this);
this.someFunction = function(value) {
console.log(value); // log out the current state
console.log("1");
}
};
Demo JSFiddle.

Categories