KnockoutJS not picking up jQuery .change() event - javascript

I've read every related post on this and spent last two days trying to figure out what I am doing wrong here without much success.
Working with this JS fiddle here as an example: http://jsfiddle.net/rniemeyer/dtpfv/ I am trying to implement a dirty flag. The only difference is that I am changing a data inside a regular span vs an input filed and using mapping plugin vs manually assigning observables.
$(document).ready(function(){
ko.dirtyFlag = function(root, isInitiallyDirty) {
var result = function() {},
_initialState = ko.observable(ko.toJSON(root)),
_isInitiallyDirty = ko.observable(isInitiallyDirty);
result.isDirty = ko.computed(function() {
return _isInitiallyDirty() || _initialState() !== ko.toJSON(root);
});
result.reset = function() {
_initialState(ko.toJSON(root));
_isInitiallyDirty(false);
};
return result;
};
$.getJSON('/environments/data.json', function(jsondata) {
var mapping = {
create: function (options) {
var innerModel = ko.mapping.fromJS(options.data);
for (var i=0; i < innerModel.deployments().length; i++) {
innerModel.deployments()[i].dirtyFlag = new ko.dirtyFlag(innerModel.deployments()[i]);
}
return innerModel;
}
}
var viewModel = ko.mapping.fromJS(jsondata, mapping);
self.save = function() {
console.log("Sending changes to server: " + ko.toJSON(this.dirtyItems));
};
self.dirtyItems = ko.computed(function() {
for (var i = 0; i < viewModel().length; i++ ) {
return ko.utils.arrayFilter(viewModel()[i].deployments(), function(deployment) {
return deployment.dirtyFlag.isDirty();
});
}
}, viewModel);
self.isDirty = ko.computed(function() {
return self.dirtyItems().length > 0;
}, viewModel);
self.changeTag = function (data, event) {
// Neither .change() nor .trigger('change') work for me
$(event.target).parents().eq(4).find('span.uneditable-input').text(data.tag).change()
// This value never changes.
console.log('Dirty on Change: '+self.dirtyItems().length)
}
ko.applyBindings(viewModel);
});
})
Here is stripped down piece of the HTML that triggers the changeTag() function, which replaces current_tag() with a selection from the drop down menu. This, however, does not trigger KnockoutJS update.
<div>
<span data-bind="text: current_tag() }"></span>
<div>
<button data-toggle="dropdown">Select Tag</button>
<ul>
<!-- ko foreach: $.parseJSON(component.available_tags() || "null") -->
<li></li>
<!-- /ko -->
</ul>
</div>
</div>
I am on day two of trying to figure this out. Any idea what I am doing wrong here? Am i supposed to use an input field and not a regular span element? Do i need to be changing values my viewModel directly instead of using jQuery to manipulate DOM? (I have actually tried that, but changing viewModel and then re-binding to it seems to slow things down, unless I am doing it wrong)
Thank you.

As said in the comments, it is considered 'bad Knockout practise' to update a value through jQuery, when you can also update the observable directly. Knockout promotes a data-driven approach.
In response to your last comment (not sure yet how to answer comments on stack overflow): the reason the UI isn't picking up the change is because you assigned the value wrong:
var x = ko.observable(1); // x is now observable
x = 3; // x is no longer observable. After all, you've assigned the value 3 to it. It is now just a number
x(3); // this is what you're after. x is still an observable, and you assigned a new value to it by using Knockout's parentheses syntax. If x is bound to the ui somewhere, you'll see the value 3 appear
So you want to do
jsondata[environment()].deployments[deployment()].current_tag(ko.dataFor(event.target).tag);

Related

How can I delay the ng-init method until my scope Variable finished loading?

In my project I have a table with multiple cells. In each cell I have a dropdown menu with an initial value. When the page load, I call a function in ng-init which returns the correct value for this dropdown menu.
My Problem is, that this function depends on a value I get through an asynchronus call to a server. That means sometimes (if the table is large), the necessary data is loaded after the ng-init. This way I can't initialize my dropdowns correctly.
So basically I just need a way to be sure that when ng-init is called, all data has already been received from the server.
Normally I would use a $scope variable to simply update the view after I got all the data from the server, but I can't do it here, because I would need a $scope variable for each cell in the table and the table has dynamic width and heigth.
Any ideas how to solve this?
Here is a brief code example of what i mean:
HTML:
<div>
<md-select ng-model="connectionType"
ng-init="connectionType = ctrl.getConnectionType(person1.id, person2.id)"
placeholder=""
class="md-no-underline">
<!-- Options -->
</md-select>
Javascript to load the data from the server:
dataService.getPersons(curView, function(result) {
var persons = JSON.parse(result);
if (persons.length) {
for (var i = 0;i < persons.length; i++) {
$scope.persons.push(persons[i]);
$scope.$apply();
}
}
});
Javascript to init the dropdown:
self.getConnectionType = function(id1, id2) {
for (var i = 0; i < $scope.persons.length; i++) {
if ($scope.persons[i].sourceId === id1&& $scope.persons[i].id2=== activityId) {
return $scope.persons[i].connectionType;
}
}
};
Based on the good comment above, don't use ng-init if you can help it (which you can, since you can initialize everything you need within your CTRL as soon as it's instantiated). Simply combine both the init of the $scope.persons and the init of the dropdown connectionType.
.controller("MyCtrl", function() {
var init = {
connectionType: function(id1, id2) {
angular.forEach($scope.people, function(person) {
if (person.sourceId == id1 && person.id2 == activityId) {
$scope.connectionType = person.connectionType;
}
});
},
people: function() {
dataService.getPersons(curView).
then(function(response) {
// depending on what response brings back, my APIs return a 'data' property for each response
$scope.people = angular.copy(response.data);
if ($scope.people.length > 1) {
var person1 = $scope.people[0],
person2 = $scope.people[1];
// set up the connectionType between person1 and person2
init.connectionType(person1.id, person2.id);
}
});
}
}
// gets called when CTRL is initialized;
init.people();
})
U could use a $scope variable with ng-if="" on the table and only display it once you have all your data?
You can use this code :
angular.element(document).ready(function () {
$timeout(function () {
//$scope.init(); Your loading method
}, 500)
});
Here you can call your init method

Using Knockout bindings to return string

I'm trying to use Knockout to make the usage for an infinite scroll plugin I am using a bit nicer, but struggling with how to bind it. Not sure if it's even possible in the current form.
The scroller calls a data function which loads the next block of data via AJAX. It then calls a factory function that converts that data into HTML, and it then loads the HTML into the container, and updates its internal state for the current content size.
I'm stuck on the fact that it expects an HTML string.
What I want to do is this:
<div class="scroller" data-bind="infiniteScroll: { get: loadItems }">
<div class="item">
<p>
<span data-bind="text:page"></span>
<span class="info" data-bind="text"></span>
</p>
</div>
</div>
And my binding, which I'm completely stuck on, is this - which is currently just hardcoding the response, obviously - that's the bit I need to replace with the template binding:
ko.bindingHandlers.infiniteScroll = {
init:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if($.fn.infiniteScroll)
{
// Get current value of supplied value
var field = f_valueaccessor();
var val = ko.unwrap(field);
var options = {};
if(typeof(val.get) == 'function')
options = val;
else
options.get = val;
options.elementFactory = options.elementFactory ||
function(contentdata, obj, config)
{
var s = '';
for(var i = 0; i < contentdata.length; i++)
{
var o = contentdata[i];
// NEED TO REPLACE THIS
s += '<div class="item"><p>Item ' + o.page + '.' + i + ' <span class="info">' + o.text + '</span></p></div>';
}
return s;
};
$(el).infiniteScroll(options);
return { controlsDescendantBindings: true };
}
}
};
contentdata is an array of objects e.g. [ { page:1, text:'Item1' }, { page:1, text:'Item2' } ... ]
Page sizes may differ between calls; I have no way of knowing what the service will return; it is not a traditional page, think of it more as the next block of data.
So in the element factory I want to somehow bind the contentdata array using the markup in .scroller as a template, similar to foreach, then return that markup to the scroller plugin.
Note that I can modify the infinite scroller source, so if if can't be done with strings, returning DOM elements would also be fine.
I just can't get how to a) use the content as a template, and b) return the binding results to the plugin so it can update its state.
NOTE: The page I eventually intend to use this is currently using a foreach over a non-trivial object model; thus the need to use the same markup; it needs to be pretty much a drop in replacement.
I have actually found out how to do it using the existing scroller following this question: Jquery knockout: Render template in-memory
Basically, you use applyBindingsToNode(domelement, bindings), which will apply KO bindings to a nodeset, which importantly does not have to be connected to the DOM.
So I can store the markup from my bound element as the template, then empty it, then for the element factory, create a temporary node set using jQuery, bind it using the above function, then return the HTML.
Admittedly, this would probably be better off refactored to use a pure KO scroller, but this means I can continue to use the tested and familiar plugin, and the code might help people as this seems to be quite a common question theme.
Here is the new code for the binding (markup is as above).
ko.bindingHandlers.infiniteScroll = {
init:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if($.fn.infiniteScroll)
{
// Get current value of supplied value
var field = f_valueaccessor();
var val = ko.unwrap(field);
var options = {};
if(typeof(val.get) == 'function')
options = val;
else
options.get = val;
var template = $(el).html();
options.elementFactory = options.elementFactory ||
function(contentdata, obj, config)
{
// Need a root element for foreach to use as a container, as it removes the root element on binding.
var newnodes = $('<div>' + template + '</div>');
ko.applyBindingsToNode(newnodes[0], { foreach: contentdata });
return newnodes.html();
};
$(el)
.empty()
.infiniteScroll(options);
return { controlsDescendantBindings: true };
}
}
};

Knockout computed vs. subscription, timing issues

Just found out, that in KnockoutJS subscription functions are evaluated before dependent computables and need someone who can commit that, because I can't find anything about Knockouts timing in the docs or discussion forums.
That means: If I have a model like this...
var itemModel = function (i) {
var self = this;
self.Id = ko.observable(i.Id);
self.Title = ko.observable(i.Title);
self.State = ko.observable(i.State);
};
var appModel = function () {
var self = this;
self.Items = ko.observableArray() // <-- some code initializes an Array of itemModels here
self.indexOfSelectedItem = ko.observable();
self.selectedItem = ko.computed(function () {
if (self.indexOfSelectedItem() === undefined) {
return null;
}
return self.Items()[self.indexOfSelectedItem()];
});
};
where I want to keep track of the selected array item with an observable index field, and I subscribe to this index field like this...
appModel.indexOfSelectedItem.subscribe(function () {
// Do something with appModel.selectedItem()
alert(ko.toJSON(appModel.selectedItem()));
}
...the subscription function is evaluated before the computed is reevaluated with the new index value, so I will get the selectedItem() that corresponds to the last selected Index and not the actual selected Index.
Two questions:
Is that right?
Then why should I make use of ko.computed() if a simple function gets me the current selected Item every time I call it, while ko.computed gets evaluated at anytime where everything is done already and I dont need it anymore?
By default all computeds in Knockout are evaluated in an eager fashion, not lazily (i.e., not when you first access them).
As soon as one dependency changes, all all subscriptions are notified and all connected computeds are re-evaluated. You can change that behavior to "lazy" by specifying the deferEvaluation option in a computed observable, but you cannot do this for a subscription.
Hoewever, I think there is no need to depend on the index of the selected item. In fact, that's even bad design because you are not really intested in the numerical value of the index, but rather in the item it represents.
You could reverse the dependencies by creating a writeable computed observable that gives you the index of the currently selected item (for diplay purposes) and allows to change it as well (for convenience).
function AppModel() {
var self = this;
self.Items = ko.observableArray();
self.selectedItem = ko.observable();
self.indexOfSelectedItem = ko.computed({
read: function () {
var i,
allItems = self.Items(),
selectedItem = self.selectedItem();
for (i = 0; i < allItems.length; i++) {
if (allItems[i] === selectedItem) {
return i;
}
}
return -1;
},
write: function (i) {
var allItems = self.Items();
self.selectedItem(allItems[i]);
}
});
}
Knockout favors storing/handling the actual values instead of just indexes to values, so it would probably not be difficult to make the necessary changes to your view. Just make everything that previously wrote to indexOfSelectedItem now write to selectedItem directly. Dependencies on selectedItem will continue to work normally.
In a well-designed Knockout application you will rarely ever have the need to handle the index of an array item. I'd recommend removing the write part of the computed once everything works.
See: http://jsfiddle.net/4hLLn/

KnockoutJS custom binding calling click event during bind, not on click

I`m attempting to bind an observable array of people two a two column responsive layout with click events using knockoutjs.
I have created a custom binding called TwoCol that loops through the array, and appends nodes to the DOM to create my suggested layout, but the click events are giving me trouble when I try to apply them in a custom binding nested in a loop.
I have played with it quite a bit, and encountered all types of results, but where I`m at now is calling my ~click~ event during binding, rather than on click.
http://jsfiddle.net/5SPVm/6/
HTML:
<div data-bind="TwoCol: Friends" id="" style="padding: 20px">
JAVASCRIPT:
function FriendsModel() {
var self = this;
this.Friends = ko.observableArray();
this.SelectedFriend = "";
this.SetSelected = function (person) {
alert(person);
self.SelectedFriend = person;
}
}
function isOdd(num) {
return num % 2;
}
ko.bindingHandlers.TwoCol = {
update: function (elem, valueAccessor) {
var i = 0;
var rowDiv;
var vFriends = ko.utils.unwrapObservable(valueAccessor());
$(elem).html('');
while (i < vFriends.length) {
//create row container every other iteration
if (!isOdd(i)) {
rowDiv = document.createElement("div");
$(rowDiv).addClass("row-fluid");
elem.appendChild(rowDiv);
}
//add column for every iteration
var colDiv = document.createElement("div");
$(colDiv).addClass("span6");
rowDiv.appendChild(colDiv);
//actual code has fairly complex button html here
var htmlDiv = document.createElement("div");
var htmlButton = vFriends[i]
htmlDiv.innerHTML = htmlButton;
colDiv.appendChild(htmlDiv);
//i think i need to add the event to the template too?
//$(htmlDiv).attr("data-bind", "click: { alert: $data }")
//it seems that the SetSelected Method is called while looping
ko.applyBindingsToDescendants(htmlDiv, { click: friends.SetSelected(vFriends[i]) });
i++;
}
return { controlsDescendantBindings: true };
}
}
var friends = new FriendsModel();
friends.Friends.push('bob');
friends.Friends.push('rob');
friends.Friends.push('mob');
friends.Friends.push('lob');
ko.applyBindings(friends);
I don't think you're using ko.applyBindingsToDescendants correctly. I admit I'm a little confused as to the meaning of some of the values in your code, so I may have interpreted something incorrectly.
Here's a fiddle where I think it's working the way you intended:
http://jsfiddle.net/5SPVm/7/
http://jsfiddle.net/5SPVm/8/
Notice if manually control descendant bindings (return { controlsDescendantBindings: true };), you need to set that up in the init callback, instead of update. The update callback is too late for that.
Quick rundown of the changes (edited):
Moved the controlsDescendantBindings into the init binding callback
Added the necessary parameter names to the binding param list to access additional values.
I re-enabled the html.attr call. Notice that now, because the binding context is set to the actual item, the SetSelected method doesn't exist at that level anymore, so it is necessary to use $parent.SetSelected.
$(htmlDiv).attr("data-bind", "click: $parent.SetSelected")
Fixed the ko.applyBindingsToDescendants call. This method takes a binding context, which is created from the current binding context, and also takes the element to apply the binding to. You don't want to reapply the binding, which is why this whole thing needs to be in the init handler.
var childBindingContext = bindingContext.createChildContext(vFriends[i]);
ko.applyBindingsToDescendants(childBindingContext, colDiv);

Binding a property to modelview function in knockout

I have:
userAccess object:
var userAccess = new (
function() {
this.userLogedIn = false;
}
);
I have modelview, binded to UI
var modelview = new (
function(){
this.itemVisible =
function(data) {
if(data.id === "ID2")
return userAccess.userLogedIn;
return true;
};
this.items = [{id:"ID1", text:"text1"}, {id:"ID2", text:"text2"}];
}
);
on UI, inside foreach binding I have:
<span data-bind="text: text, visible:$parent.itemVisible($data)"> </span>
so the visibility of the span element is binded to modelview's function.
The function determines a visibility of the current item based on its ID and value of userAccess.
Problem:
The two way binding doesn't work in this scenario. For example if I make userAccess.userLogedIn = true the element "ID2" doesn't become visible.
This is because of lack of observable, but I can not, seems to me, fit an observable in this pattern.
I know also that I can update binding manually, but would like to avoid this, if this is possible.
I have feeling that I'm missing something obvious here.
Complete source on CodePen
You should probably refactor your whole setup to use observables. Otherwise, the usage of knockout does not make much sense due to the lack of automated view updates (as you noticed).
var userAccess = new (
function() {
// It is likely that this value will change, so make it an observable!
this.userLogedIn = ko.observable(false);
}
);
// Create a "class" for the items in the list be able to encapsulate behavior /
// properties such as "is this item visible"?
var Item = function(id, text) {
var self = this;
self.id = id; // <-- will most likely never change (?) => not an observable
self.text = ko.observable(text);
// Use a "computed observable" for things that require more sophisticated logic
self.visible = ko.computed(function() {
if (self.id === "ID2") {
return userAccess.userLogedIn(); // <-- observable = () required!
} else {
return true;
}
});
};
var modelview = new (
function() {
this.items = ko.observableArray([
new Item("ID1", "text1"), new Item("ID2", "text2")
]);
}
);
and in the HTML
<span data-bind="text: text, visible: visible"> </span>
Example: http://jsfiddle.net/a89VL/

Categories

Resources