I am struggling a bit with implementing a custom tooltip binding that is invoked with a mouseover, so I need an event handler to display the tooltip. Now, the content of the tooltip can be either a static string, or an observable with content that can change - but the tooltip module itself is a singleton. Plus, there can be more than one tooltip elements on the page, so the tooltip show function just receives the event and the content to display. For me, the easiest way would be if I could access the actual bound value from the event target in the listener alone.
Now I know there is the ko.dataFrom function, but for the static variant, I cannot see the actual bound value anywhere (i.e. the same that is returned with valueAccessor() in the init function), just the whole viewmodel.
Here is some simplified code to illustrate it:
<span class="icon help" data-bind="tooltip: 'some static string'"></span>
(But as I said there also might be an observable behind the tooltip binding)
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor) {
element.addEventListener("mouseover", ko.bindingHandlers.tooltip.showTooltip);
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
element.removeEventListener("mouseover", ko.bindingHandlers.tooltip.showTooltip);
});
},
showTooltip: function(event) {
var data = ko.dataFor(this);
tooltip.show(event, data);
}
}
The problem is now that "data" only contains the whole viewmodel the view was bound to. But where is 'some static string' to be found?
I also tried with "ko.contextFor", but then $root, $data and $rawData were all the same. I am sure I overlooked something here, or is that not possible without adding the string to a data attribute or something similar, which I would like to avoid.
The fundamental issue here is that you shouldn't be trying to reuse the event handler, or at least, if you're going to, bind it.
I'd just move showTooltip into init:
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor) {
var showTooltip = function(event) {
tooltip.show(event, ko.unwrap(valueAccessor()));
};
element.addEventListener("mouseover", showTooltip);
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
element.removeEventListener("mouseover", showTooltip);
});
}
};
Small functions are very cheap.
Live Example (using click instead of mouseover and just appending to the document):
var tooltip = {
show: function(event, value) {
var div = document.createElement('div');
div.innerHTML = "tooltip: " + value;
document.body.appendChild(div);
}
};
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor) {
var showTooltip = function(event) {
tooltip.show(event, ko.unwrap(valueAccessor()));
};
element.addEventListener("click", showTooltip);
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
element.removeEventListener("click", showTooltip);
});
}
};
var vm = {
items: [
{text: "one", tooltip: "uno"},
{text: "two", tooltip: "due"},
{text: "three", tooltip: "tre"},
{text: "four", tooltip: "quattro"}
]
};
ko.applyBindings(vm, document.body);
// To prove the observables work, update one after a delay
setTimeout(function() {
vm.items[2].tooltip = "TRE";
}, 100);
<div data-bind="foreach: items">
<div data-bind="text: text, tooltip: tooltip"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Alternately, use bind:
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor) {
var boundHandler = ko.bindingHandlers.tooltip.showTooltip.bind(
null,
valueAccessor
);
element.addEventListener("click", boundHandler);
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
element.removeEventListener("click", boundHandler);
});
},
showTooltip: function(valueAccessor, event) {
tooltip.show(event, ko.unwrap(valueAccessor()));
}
};
var tooltip = {
show: function(event, value) {
var div = document.createElement('div');
div.innerHTML = "tooltip: " + value;
document.body.appendChild(div);
}
};
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor) {
var boundHandler = ko.bindingHandlers.tooltip.showTooltip.bind(
null,
valueAccessor
);
element.addEventListener("click", boundHandler);
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
element.removeEventListener("click", boundHandler);
});
},
showTooltip: function(valueAccessor, event) {
tooltip.show(event, ko.unwrap(valueAccessor()));
}
};
var vm = {
items: [
{text: "one", tooltip: "uno"},
{text: "two", tooltip: "due"},
{text: "three", tooltip: "tre"},
{text: "four", tooltip: "quattro"}
]
};
ko.applyBindings(vm, document.body);
// To prove the observables work, update one after a delay
setTimeout(function() {
vm.items[2].tooltip = "TRE";
}, 100);
<div data-bind="foreach: items">
<div data-bind="text: text, tooltip: tooltip"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Note how the bound value will be in front of the event argument.
Related
I need some help to check a checkbox on page load using knockout & icheck plug in.
I have created a custom binding in order to listen to 'ifChecked' method of check but it's not working.
<input type="checkbox" id="access-user-information" name="edit_existing_user" data-bind = "iCheck: { checked: selectedUser() && selectedUser().edit_existing_user==1}">
Knockout Code:
ko.bindingHandlers.iCheck = {
init: function (element, valueAccessor) {
$(element).iCheck({
checkboxClass: 'icheckbox_square-red'
});
$(element).on('ifChecked', function (event) {
var observable = valueAccessor();
observable.checked(true);
});
},
update: function (element, valueAccessor) {
var observable = valueAccessor();
}
};
Some changes:
Listen for a change at the checkbox ('ifToggled' event)
The observable of a checkbox should be a boolean, so set the value according to the checkbox state (true/false). I used the jQuery .is(':checked') for that.
Set the initial state in the "update" function.
The code for the binding:
ko.bindingHandlers.iCheck = {
init: function(element, valueAccessor) {
$(element).iCheck({
checkboxClass: 'icheckbox_flat-red'
});
$(element).on('ifToggled', function(event) {
var observable = valueAccessor();
observable($(element).is(':checked'));
});
},
update: function(element, valueAccessor) {
var observable = valueAccessor();
observable($(element).is(':checked'));
}
};
Here a little jsbin to test it.
Hope that helps.
If your checkbox is enabled, and you can give it a new value by tapping it, it has to be a ko.computed with a read and write method.
When you select the current user, you want it to automatically be checked. This is the read part:
this.isEditingCurrentUser = ko.computed(function() {
return this.selectedUser() &&
this.selectedUser().edit_existing_user === 1;
}, this);
You should recognise this expression: it's what you used as your data-bind. Having a ko.computed in your viewmodel is very similar to using expressions in data-binds.
Now, there's a problem when you want to override this value with true or false: knockout can't decide for you how to reverse the logic. This, you have to specify. It'll look like this:
this.isEditingCurrentUser = ko.computed({
read: function() { /* the expression above */ },
write: function(newValue) { /* How to handle a user input override */ }
});
I show how this works in the example below. I've left out the custom binding since the real logic problem needs to be solved first:
var VM = function() {
var currentUserId = 1;
this.users = [
{ id: 1, name: "User 1" },
{ id: 2, name: "User 2" },
{ id: 3, name: "User 3" },
];
this.selectedUser = ko.observable(2);
this.editCurrent = ko.computed({
read: function() {
return this.selectedUser() &&
this.selectedUser().id === currentUserId;
}.bind(this),
write: function(val) {
var newUser = val
? this.users.find(function(user) {
return user.id === currentUserId;
})
: null;
this.selectedUser(newUser);
}.bind(this)
}, this);
};
ko.applyBindings(new VM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: users, value: selectedUser, optionsCaption: 'Select a user', optionsText: 'name'"></select>
<div>
<input type="checkbox" data-bind="checked: editCurrent">
Edit current user
</div>
I'm generating a drop down list from Backbone.View.
After attaching it to the DOM, change event is not fired. The delegateEvents doesn't fixes it. Can somebody show me where the blind spot is?
Model and collection:
App.Models.DictionaryItem = Backbone.Model.extend({
default: {
key: '',
value: '', id: 0
}
});
App.Collections.Dictionary = Backbone.Collection.extend({
model: App.Models.DictionaryItem,
initialize: function (models, options) {
},
parse: function (data) {
_.each(data, function (item) {
// if(item){
var m = new App.Models.DictionaryItem({ key: item.code, value: item.name });
this.add(m);
// }
}, this);
}
});
Views:
App.Views.ItemView = Backbone.View.extend({
tagName: 'option',
attributes: function () {
return {
value: this.model.get('key')
}
},
initialize: function () {
this.template = _.template(this.model.get('value'));
},
render: function () {
this.$el.html(this.template());
return this;
}
});
App.Views.CollectionView = Backbone.View.extend({
tagName: 'select',
attributes: {
'class': 'rangesList'
},
events: {
'change .rangesList': 'onRangeChanged'
},
initialize: function (coll) {
this.collection = coll;
},
render: function () {
_.each(this.collection.models, function (item) {
this.$el.append(new App.Views.ItemView({ model: item }).render().el);
}, this);
// this.delegateEvents(this.events);
return this;
},
selected: function () {
return this.$el.val();
},
onRangeChanged: function () {
alert('changed');
}
});
Rendering:
var coll = new App.Collections.Dictionary(someData, { parse: true });
var v= new App.Views.CollectionView(coll);
var vv=v.render().el;
// new App.Views.CollectionView(coll).render().el;
$('body').append(vv)
The tagName and attributes on CollectionView:
tagName: 'select',
attributes: {
'class': 'rangesList'
},
say that the el will be <select class="rangesList">. But your events:
events: {
'change .rangesList': 'onRangeChanged'
},
are listening to 'change' events from a .rangesList inside the view's el. From the fine manual:
Events are written in the format {"event selector": "callback"}. [...] Omitting the selector causes the event to be bound to the view's root element (this.el).
So you're trying to listen for events from something that doesn't exist. If you want to listen for events directly from the view's el then leave out the selector:
events: {
'change': 'onRangeChanged'
}
Is there a way to have transitional animations between "page" changes while using knockout for changing templates? I'm looking for something similar to Knockback-Navigators. I cant figure out a way to do this? Is there a package I can use to make this easier? Here is a JSFiddle with the same type of binding my project uses. And a sample of my javascript here:
var View = function (title, templateName, data) {
var self = this;
this.title = title;
this.templateName = templateName;
this.data = data;
this.url = ko.observable('#' + templateName);
};
var test1View = {
test: ko.observable("TEST1")
};
var test2View = {
test: ko.observable("TEST2")
};
var viewModel = {
views: ko.observableArray([
new View("Test 1", "test1", test1View),
new View("Test 2", "test2", test2View)]),
selectedView: ko.observable(),
}
//Apply knockout bindings
ko.applyBindings(viewModel);
//Set up sammy url routes
Sammy(function () {
//Handles only groups basically
this.get('#:view', function () {
var viewName = this.params.view;
var tempViewObj = ko.utils.arrayFirst(viewModel.views(), function (item) {
return item.templateName === viewName;
});
//set selectedView
viewModel.selectedView(tempViewObj);
});
}).run('#test1');
There are plenty of ways of doing this, here is one
ko.bindingHandlers.withFade = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var $element = $(element);
var observable = valueAccessor();
var wrapper = ko.observable(observable());
observable.subscribe(function(value) {
var current = wrapper();
fadeIn = function() {
wrapper(value);
$element.fadeIn();
};
if(current) {
$element.fadeOut(fadeIn);
} else {
$element.hide();
fadeIn();
}
});
ko.applyBindingsToNode(element, { with: wrapper }, bindingContext);
return { controlsDescendantBindings: true };
}
};
http://jsfiddle.net/7E84t/19/
You can abstract the effect like this
ko.transitions = {
fade: {
out: function(element, callback) {
element.fadeOut(callback);
},
in: function(element) {
element.fadeIn();
}
},
slide: {
out: function(element, callback) {
element.slideUp(callback);
},
in: function(element) {
element.slideDown();
}
}
};
html
<div data-bind="withFade: { data: selectedView, transition: ko.transitions.slide }">
http://jsfiddle.net/7E84t/23/
I'm working on a project where im using .net web api, knockout and in this example, the jquery plugin select2.
What im trying to do is to set some field values after the change of the selection.
The select2 control list is loaded after ajax call and the objects contain more data than just id and text.
How can i get the rest of the data, so i can fill the other inputs with it?
Shortly, im trying to update a viewmodel after the change of the selection (but i get the data when this plugin makes the ajax call).
Here is a sample data that the selected object should contain:
{
"Customer":{
"ID":13,
"No":"0000012",
"Name":"SomeName",
"Address":"SomeAddress",
"ZipCode":"324231",
"City":"SimCity",
"Region":"SS",
"Phone":"458447478",
"CustomerLocations":[]
}
}
Here is where i am for now:
Sample html:
<input type="hidden" data-bind="select2: { optionsText: 'Name', optionsValue: 'ID', sourceUrl: apiUrls.customer, model: $root.customer() }, value: CustomerID" id="CustomerName" name="CustomerName" />
<input type="text" data-bind="........" />
<input type="text" data-bind="........" />
etc...
and this is the custom binding:
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var obj = valueAccessor(),
allBindings = allBindingsAccessor();
var optionsText = ko.utils.unwrapObservable(obj.optionsText);
var optionsValue = ko.utils.unwrapObservable(obj.optionsValue);
var sourceUrl = ko.utils.unwrapObservable(obj.sourceUrl);
var selectedID = ko.utils.unwrapObservable(allBindings.value);
var model = ko.utils.unwrapObservable(obj.model);//the object that i need to get/set
$(element).select2({
placeholder: "Choose...",
minimumInputLength: 3,
initSelection: function (element, callback) {
if (model && selectedID !== "") {
callback({ id: model[optionsValue](), text: model[optionsText]() });
}
},
ajax: {
quietMillis: 500,
url: sourceUrl,
dataType: 'json',
data: function (search, page) {
return {
page: page,
search: search
};
},
results: function (data) {
var result = [];
$.each( data.list, function( key, value ) {
result.push({ id: value[optionsValue], text: value[optionsText] });
});
var more = data.paging.currentPage < data.paging.pageCount;
return { results: result, more: more };
}
}
});
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).select2('destroy');
});
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var obj = valueAccessor(),
allBindings = allBindingsAccessor();
var model = ko.utils.unwrapObservable(obj.model);//the object that i need to get/set
var selectedID = ko.utils.unwrapObservable(allBindings.value);
$(element).select2('val', selectedID);
$(element).on("change", function (e) {
//...
});
}
};
Getting the selected id or text is not a problem, but how not to loose the rest of the information after the ajax call?
Where can i set/get this object so i can have all the data that it contains?
Thank you
When you build a object literal for your results, added the full object as a "data" property.
result.push({ id: value[optionsValue], text: value[optionsText], data: value });
Then handle the select2-selected event thrown by select2. The event object this should contain your object literal as the choice property.
$element.on('select2-selected', function(eventData) {
if ( eventData.choice ) {
// item selected
var dataObj = eventData.choice.data;
var selectedId = eventData.choice.id;
} else {
// item cleared
}
});
For select2 v4, you can use $(elem).select2('data') to get the selected objects.
$('selected2-enabled-elements').on('change', function(e) {
console.log($(this).select2('data'));
});
Example: https://jsfiddle.net/calvin/p1nzrxuy/
For select2 versions before v4.0.0 you can do:
.on("select2-selecting", function (e) {
console.log(e.object);
})
From v4.0.0 on and upwards the following should work:
.on("select2-selecting", function (e) {
$(this).select2('data')
})
I have an observable array with items bound to an ul. I need to calculate the width of the entire set of li items once the items have been added so I can set the width of the ul element to the total width of the child li elements.
How can I go about doing this with a foreach binding?
<div>
<h2 data-bind="visible: items().length">Test</h2>
<ul data-bind="foreach: { data: items, afterAdd: $root.myAfterAdd }">
<li data-bind="text: name"></li>
</ul>
</div>
And the JavaScript:
var viewModel = {
items: ko.observableArray([
{ name: "one" },
{ name: "two" },
{ name: "three" }
]),
myAfterAdd: function(element) {
if (element.nodeType === 1) {
alert("myAfterAdd");
}
},
addItem: function() {
this.items.push({ name: 'new' });
}
};
ko.applyBindings(viewModel);
// once foreach has finished rendering I have to set the width
// of the ul to be the total width of the children!
I tried using afterAdd so I can 'update' the width of the ul after each element is added, unfortunately I have discovered that afterAdd does not fire for the initial items! It will only fire when pushing additional items...
Note that the content inside the li items will be of unknown width :)
See this fiddle for a sample scenario.
I had a similar problem. You can use this to make that initial calculation:
ko.bindingHandlers.mybinding {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var foreach = allBindings().foreach,
subscription = foreach.subscribe(function (newValue) {
// do something here (e.g. calculate width/height, use element and its children)
subscription.dispose(); // dispose it if you want this to happen exactly once and never again.
});
}
}
After much 'googling' I have found the solution. The way to achieve this with a foreach is too register a custom event handler:
ko.bindingHandlers.runafter = {
update: function (element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor();
setTimeout(function (element, value) {
if (typeof value != 'function' || ko.isObservable(value))
throw 'run must be used with a function';
value(element);
}, 0, element, value);
}
};
This handler will fire whenever an update occurs (or just once if value is not an observable).
More importantly it will fire once the foreach loop has completed!
See here.
You could do something like this :
var viewModel = {
items: ko.observableArray([
{ name: "one" },
{ name: "two" },
{ name: "three" }
]),
myAfterAdd: function(event) {
if (event.originalEvent.srcElement.nodeType === 1) {
alert("myAfterAdd");
}
},
addItem: function() {
this.items.push({ name: 'new' });
}
};
$('#bucket').bind('DOMNodeInserted DOMNodeRemoved', viewModel.myAfterAdd);
ko.applyBindings(viewModel);
Which bucket is the ul element
See fiddle
I hope it helps