Checked event not firing sometimes on checkboxes rendered by knockout in chrome - javascript

I have create a simple jsfiddle example: https://jsfiddle.net/c2wus7qg/3/
Here you will see four list items with checkboxes. If you uncheck any of them the change event will not fire. Then if you uncheck the remaining item it will fire.
Also if you check any unchecked item and uncheck again the change event will not fire in this case as well.
This seem to happen in chrome only. It works as expected in firefox and safari.
Here are code examples (simplified):
html:
<ul>
<!-- ko foreach: categoriesComputed -->
<li>
<input data-bind="attr:{value: id}, checked: state,
event: {change: $root.categoryChanged}" type="checkbox">
<span data-bind="text: name"></span>
</li>
<!-- /ko -->
</ul>
javascript:
function viewModel() {
var self = this;
self.categories = ko.observableArray();
self.Category = function(id, name, state) {
this.id = id;
this.name = name;
this.state = ko.observable(state);
};
self.categoriesComputed = ko.computed(function() {
return self.categories()
.sort(function(a, b) {
return a.state() < b.state() ? 1 : -1;
})
.slice(0, 4);
});
self.categoryChanged = function() {
console.log('Yep, definitely changed!');
}
}
var KO = new viewModel();
ko.applyBindings(KO);
setTimeout(function() {
var categories = [];
for ( var i = 1; i <= 20; i++ ) {
//set state to true only for some elements
var state = i >= 5 && i <= 6;
categories.push(new KO.Category(i, 'category #' + i, state));
}
KO.categories(categories);
}, 1000);

I think what is happening here is that you are unbinding the checkbox before its "change" event can fire. Your computed is only showing the top 4, but as soon as you uncheck one of the options it gets sorted out of those top 4 and removed from the DOM.
If you remove the .slice(0, 4); from your computed function you should see the event fire properly each time.
Edit:
One possible solution depending on your use-case is to use an array on the root model to store the selected options rather than a state observable on each individual option.
self.selectedCategories = ko.observableArray([5,6]);
The checked binding will use the array to store the selected models, or, when paired with the checkedValue binding will use the array to store just that property value of the selected option. For example if you use:
<input data-bind="attr:{value: id}, checked: $root.selectedCategories, checkedValue: id" type="checkbox">
the selectedCategories array will contain the IDs of the option objects (5 and 6 to begin with).
This way you can create a single subscription on that observable array to watch for changes to any of the checkboxes, and bypass the event binding which relies on the model being rendered in the DOM.
self.categoryChanged = function() {
console.log('Yep, definitely changed!');
}
self.selectedCategories.subscribe(self.categoryChanged);
Of course your sort function will have to be updated as well to check if an element exists in the selected array rather than checking the "state" of the object. that could look something like:
function(a, b) {
var indexA = self.selectedCategories().indexOf(a.id);
var indexB = self.selectedCategories().indexOf(b.id);
if(indexB == indexA){
return Number(a.id) - Number(b.id);
} else {
return indexB - indexA;
}
}
Example fiddle

It is a timing issue (as Jason noted), with the DOM being updated before the change event can fire. I modified your code to add an echoState member that gets the value of state after a brief delay. I based categoriesComputed on this, with the result being that the DOM isn't updated before the change event can fire.
I doubt this is useful for any real-world situations. :)
function viewModel() {
var self = this;
self.categories = ko.observableArray();
self.Category = function(id, name, state) {
this.id = id;
this.name = name;
this.state = ko.observable(state);
this.echoState = ko.observable(state);
this.state.subscribe((newValue) => {
setTimeout(() => {
this.echoState(newValue)
}, 0);
});
};
self.categoriesComputed = ko.computed(function() {
return self.categories
.sort(function(a, b) {
return a.echoState() < b.echoState() ? 1 : -1;
})
.slice(0, 4);
});
self.categoryChanged = function(data) {
console.log('Yep, definitely changed!', data);
}
}
var KO = new viewModel();
ko.applyBindings(KO);
setTimeout(function() {
var categories = [];
for (var i = 1; i <= 20; i++) {
//set state to true only for some elements
var state = i >= 5 && i <= 6;
categories.push(new KO.Category(i, 'category #' + i, state));
}
KO.categories(categories);
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
Uncheck any of first two checkboxes and the change event won't fire!
<ul>
<!-- ko foreach: categoriesComputed -->
<li>
<input data-bind="attr:{value: id}, checked: state,
event: {change: $root.categoryChanged}" type="checkbox">
<span data-bind="text: name"></span>
</li>
<!-- /ko -->
</ul>

Related

Observable is changed only for one viewModel

I have collection of Tags (array of some string)
For each of Tags I create knockout viewModel TagsViewModel
var TagsViewModel = function() {
var vm = this;
vm.showTags = ko.observable(false);
window.shouter.subscribe(function(newValue) {
vm.showTags(newValue);
}, vm, 'toggleReviewTags');
}
And I have another "toggler" to show/hide tags in another partial view. For it I've created separate viewModel TagFiltersViewModel and use knockout pubSub to communicate with TagsViewModel
var TagFiltersViewModel = function() {
var vm = this;
vm.isTagsVisible = ko.observable(false);
vm.isTagsVisible.subscribe(function(newValue) {
window.shouter.notifySubscribers(newValue, 'toggleReviewTags');
});
vm.toggleTags = function() {
vm.isTagsVisible(!vm.isTagsVisible());
}
}
Each TagsViewModel I bind to container with calculated id "tag-container-"+ {tagId}
and for each do next thing
var element = document.getElementById(tagModel.tagsContainer);
ko.cleanNode(element);
ko.applyBindings(new TagsViewModel(tagModel), element);
Problem - locally only one tag from collection is shown after click on toggle button. I have created jsFiddle, but there I can't reproduce my problem.
Any thoughts what is the problem in my case?
I would suggest doing something like the following, it should make it much easier to manage.
There may be a specific reason you are binding each tag seperately using theapplyBindings method but you'll have to elaborate on that.
var arrayOfTags = ['tag1', 'tag2', 'tag3'];
var ViewModel = function() {
var self = this;
// Return an array TagsViewModels using the map method
this.Tags = ko.observableArray(arrayOfTags.map(function(tag) {
return new TagsViewModel(tag);
}));
// Observable to track if all tags are hidden/shown
this.TagsVisible = ko.observable(true);
// Loop through tags, show and set flag to true
this.ShowTags = function() {
self.Tags().forEach(function(tag) {
tag.Visible(true);
})
self.TagsVisible(true);
};
// Loop through tags, hide and set flag to false
this.HideTags = function() {
self.Tags().forEach(function(tag) {
tag.Visible(false);
})
self.TagsVisible(false);
};
};
var TagsViewModel = function(name) {
this.Name = ko.observable(name)
this.Visible = ko.observable(true);
};
var model = new ViewModel();
ko.applyBindings(model);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="click: ShowTags, visible: !TagsVisible()">Show Tags</button>
<button data-bind="click: HideTags, visible: TagsVisible">Hide Tags</button>
<hr>
<!-- ko if: Tags() -->
<!-- ko foreach: Tags -->
<span data-bind="text: Name, visible: Visible"></span>
<!-- /ko -->
<!-- /ko -->

AngularJS: How to untick a checkbox automatically when the element related is expired from a date set

I have a table with several items inside. Those items have an active column with a checkbox which indicates if that item is active.
The user can manually untick to disable the items but the items have also an expiration date. What I want to achieve is that when the expiration of an item arrive the box has to untick automatically and the status has to be changed from enable to disable. So the user see is not anymore enable.
I'm sharing with you what I have as an example:
This is the controller:
for (var i = 0; i < $scope.item.length; i++) {
var a = new Date($scope.item[i].to)
var b = new Date()
if ($scope.item[i].to != null && (a < b)) {
$scope.item[i].expired = true
}
}
$scope.changeStatus = function(item) {
item.isDisabled = !item.isDisabled;
$scope.save(item);
};
$scope.save = function(item) {
ItemResource.update(item, function success() {
$scope.loadingAction = false
toaster.pop({ type: 'success', body: $translate.instant('notifications', { name: item.name }) })
}, function error() {})
}
View:
<td class="td-with-button center valign-top">
<span class="cursor-pointer" ng-click="changeStatus(item)">
<i class="icon--big fa fa-check-circle-o" ng-class="{'fa-check-circle-o' : !item.isDisabled, 'fa-circle-o' : item.isDisabled}"></i>
</span>
</td>
The best way to continually check if something is expired would probably be creating a background factory. You could check over and over, or you could create a timeout when the page loads that runs a function to do whatever you'd like (in this case, send a broadcast back to the controller to uncheck that box).
angular.module('expirationCheck', []).factory('expirationCheck', ['$rootScope', function ($rootScope) {
var service = {}
var expDate = null;
service.setexpDate = function(date) {
expDate = date;
var expiration = new Date(expDate);
var timeout = expiration.getTime() - (new Date()).getTime();
setTimeout (function () {
$rootScope.$broadcast("Uncheck box");
}, timeout);
}
return service;
}]);
In your controller, you would inject the facotry and call the "setExpDate" to start the timer. Then you set a watch for "Uncheck box" and do the unchecking there.

Expand one element and collapse all other element in knockout js

In knockout is it possible to collapse all other opened row and expand only clicked row.
I am referring this Fiddle example for it.
view -
<ul data-bind="foreach: items">
<button data-bind="text:name"></button>
<div data-bind="visible:expanded">
<input data-bind="value:name"></input>
</div>
</ul>
viewModel -
function Sample(item) {
var self = this;
self.name = ko.observable(item.name);
self.id = ko.observable(item.id);
self.expanded = ko.observable(false);
self.toggle = function (item) {
self.expanded(!self.expanded());
};
self.linkLabel = ko.computed(function () {
return self.expanded() ? "collapse" : "expand";
}, self);
}
var viewModel = function () {
var self = this;
var json = [{
"name": "bruce",
"id": 1
}, {
"name": "greg",
"id": 2
}]
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item); // making things independent here
});
self.items = ko.observableArray(data);
};
ko.applyBindings(new viewModel());
Here its not collapsing already opened row. I tried to fetch complete items in toggle function but it did not work.
I am new to knock out. please suggest.
Update -
I tried this code to make first one extended by default -
var index=0;
var data = ko.utils.arrayMap(json, function(item) {
if(index++===0){
return new Sample(item,true);
}else{
return new Sample(item,false);
}
});
But above given code is not working as expected.
This is very common "problem" when you're working with knockout. You want to keep your Sample instances independent, while their behavior might still influence the behavior of any siblings... I usually pick one of three options:
Move the functionality that influences siblings to the parent viewmodel. For example:
var viewModel = function() {
/* ... */
self.toggle = function(sample) {
self.items().forEach(function(candidateSample) {
candidateSample.expanded(sample === candidateSample);
});
}
};
With data-bind:
<a data-bind="click: $parent.toggle"></a>
Personally, I'd go with this option. Here's it implemented in your fiddle: http://jsfiddle.net/cxzLsz56/
Pass siblings to each item:
self.items = ko.observableArray();
var data = ko.utils.arrayMap(json, function (item) {
return new Sample(item, self.items);
});
self.items(data);
And in Sample:
function Sample(item, siblings) {
self.toggle = function() {
siblings().forEach(/* collapse */);
self.expanded(true); // Expand
};
};
Create some sort of postbox/eventhub/mediator mechanism and make a Sample trigger an event. Each Sample listens to this event and collapses when another Sample triggers it.

knockout.js 3.3 - rerendering component in foreach binding

My viewModel consist of observable array with observable elements.
// viewmodel
var viewModel = function () {
this.o = ko.observableArray();
for (var i = 0; i < 3; i++)
this.o.push(ko.observable(0));
};
I need to change the values of these elements. And for this purpose I create the component. Simple example of it is below:
//custom element <component>
ko.components.register("component", {
viewModel: function (params) {
var self = this;
this.value = params.value;
console.log("init component");
this.i = 1;
this.change = function () {
self.value(self.i++);
console.log("change to " + self.value());
}
},
template: "<span data-bind='text: value'></span> <button data-bind='click:change'>Change</button>"
});
This component can change value of observable element which come in params.value.
My view is very simple:
<!--ko foreach:o-->
<component params="value: $rawData"></component>
<!--/ko-->
Full example: http://jsfiddle.net/tselofan/xg16u5cg/7/
Problem is when value of observable element in observable array is changed, component is rendered again, because it is located inside foreach binding. You can see this in logs. What the best practice can I use in this situation? Thank you
The component is being recreated each time the number changes because the context of the component is the number.
http://jsfiddle.net/Crimson/xg16u5cg/8/
<!-- ko foreach: o -->
<component params="value: $data.myNumber"></component>
<!-- /ko -->
//Test how components work in foreach binding
//custom element <component>
ko.components.register("component", {
viewModel: function (params) {
var self = this;
this.value = params.value;
console.log("init component");
this.i = 1;
this.change = function () {
self.value(self.i++);
console.log("change to " + self.value());
}
},
template: "<span data-bind='text: value'></span> <button data-bind='click:change'>Change</button>"
});
// viewmodel
var viewModel = function () {
this.o = ko.observableArray();
for (var i = 0; i < 3; i++)
this.o.push({myNumber: ko.observable(0)});
};
ko.applyBindings(new viewModel());
Knockout-Repeat (https://github.com/mbest/knockout-repeat) is an iterative binding that does not create a new binding context, and has a foreach mode, so it should work as you expect with your component.

Add multiple items to text-area with duplicate items

Add multiple items to text-area with duplicate items.
I have one text-area which store data after clicked add data link.
How can i prevent add duplicate items to text-area?
JavaScript call DOM event:
var Dom = {
get: function(el) {
if (typeof el === 'string') {
return document.getElementById(el);
} else {
return el;
}
},
add: function(el, dest) {
var el = this.get(el);
var dest = this.get(dest);
dest.appendChild(el);
},
remove: function(el) {
var el = this.get(el);
el.parentNode.removeChild(el);
}
};
var Event = {
add: function() {
if (window.addEventListener) {
return function(el, type, fn) {
Dom.get(el).addEventListener(type, fn, false);
};
} else if (window.attachEvent) {
return function(el, type, fn) {
var f = function() {
fn.call(Dom.get(el), window.event);
};
Dom.get(el).attachEvent('on' + type, f);
};
}
}()
};
JQuery add data to textarea:
$("#lkaddlanguage").click(function(){
var totalstring;
var checkconstring = $("#contentlng").text();
var strLen = checkconstring.length;
myStr = checkconstring.slice(0,strLen-1);
//alert(myStr);
var checkedItemsArray = myStr.split(";");
var j = 0;
var checkdup=0;
totalstring=escape($("#textval").val()) ;
var i = 0;
var el = document.createElement('b');
el.innerHTML = totalstring +";";
Dom.add(el, 'txtdisplayval');
Event.add(el, 'click', function(e) {
Dom.remove(this);
});
});
HTML Display data
<input type="textbox" id="textval">
<a href="#lnk" id="lkaddlanguage" >Add Data</a>
<textarea readonly id="txtdisplayval" ></textarea>
This seems a very straightforward requirement to me, so I'm not quite clear where you're getting stuck. I have not tried too hard to figure out your existing code given that you are referencing elements not shown in your html ("contentlng"). Also, mixing your own DOM code with jQuery seems a bit pointless. You don't need jQuery at all, but having chosen to include it why then deliberate not use it?
Anyway, the following short function will keep a list of current items (using a JS object) and check each new item against that list. Double-clicking an item will remove it. I've put this in a document ready, but you can manage that as you see fit:
<script>
$(document).ready(function() {
var items = {};
$("#lkaddlanguage").click(function(){
var currentItem = $("#textval").val();
if (currentItem === "") {
alert("Please enter a value.");
} else if (items[currentItem]) {
alert("Value already exists.");
} else {
items[currentItem] = true;
$("#txtdisplayval").append("<span>" + currentItem + "; </span>");
}
// optionally set up for entry of next value:
$("#textval").val("").focus();
return false;
});
$("#txtdisplayval").on("dblclick", "span", function() {
delete items[this.innerHTML.split(";")[0]];
$(this).remove();
});
});
</script>
<input type="textbox" id="textval">
<a href="#lnk" id="lkaddlanguage" >Add Data</a><br>
<div id="txtdisplayval" ></div>
<style>
#txtdisplayval {
margin-top: 5px;
width : 200px;
height : 100px;
overflow-y : auto;
border : 1px solid black;
}
</style>
Note I'm using a div (styled to have a border and allow vertical scrolling) instead of a textarea.
As you can see I've coded it to display an alert for duplicate or empty items, but obviously you could remove that and just ignore duplicates (or substitute your own error handling). Also I thought it might be handy to clear the entry field and set focus back to it ready for entry of the next value, but of course you can remove that too.
Working demo: http://jsfiddle.net/LTsBR/1/
I'm confused.
The only variable that might have duplicates comes from:
var checkedItemsArray = myStr.split(";");
However, checkedItemsArray is not used for anything.
Incidentally, the escape method is deprecated in favour of encodeURIComopnent.
When setting the value of the textarea, do just that: assign to its value property, not to its innerHTML (it can't have markup inside it or any elements, only text nodes).
If you want to check that the members of checkedItemsArray are unique, and you don't mind if they are sorted, you can use a simple function like:
function unique(arr) {
arr.sort();
var i = arr.length;
while (i--) {
if (arr[i] == arr[i - 1]) {
arr.splice(i, 1);
}
}
return arr;
}
Orignal order can be maintained, but it's a bit more code.

Categories

Resources