I have several jquery dom events that are created on DOM load or document ready. These are mostly default behaviors that should be applied to all forms in my application. Example:
$('input:text').focus(function ()
{
$(this).select();
});
Right before applying knockout binding, I can check my dom elements and all events are there:
But when I run the applyBindings method to bind the viewmodel to my DOM, the "with" binding removes all events that are not related to knockout:
I have tried overwriting the cleanExternalData as explained on the documentation and on this answer. But that did not help with this, the function is replaced, but the events are still removed from the DOM when the templating is applied on the binding process.
For the record, this is not an exclusive behavior of the with function, but all anonymous templating functions also do that, foreach, if, ifnot. Using template, as expected, also behaves the same way. The DOM element is completely destroyed, stored as a template, then added again on my document when the condition is satisfied, but now without any jquery event handlers.
How to avoid that knockout removes the events from my DOM elements?
Instead of binding elements to a specific node, you can use a databinding to use the jquery on() functionality to handle events. Here's a binding I use:
define(['knockout'], function (ko) {
ko.bindingHandlers.eventListener = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var params = ko.utils.unwrapObservable(valueAccessor());
if (!(params instanceof Array)) {
params = [params];
}
params.forEach(function (param) {
$(element).on(param.event, param.selector, function (event) {
param.callback(ko.dataFor(this), ko.contextFor(this), event);
});
});
}
}
});
Usage:
<div data-bind="eventListener: [
{ event: 'click', selector: '.copyInclusionRule', callback: copyInclusionRule},
{ event: 'click', selector: '.deleteInclusionRule', callback: deleteInclusionRule}]">
... other knockout template stuff here ...
</div>
The above will listen for click events on either an element with the specified class and perform the callback when the event is received for anything within the div's 'scope'. The value of 'event' param can be anything that on() uses.
I think the reason why you can't leverage the cleanNode overrides is that your dom is being completely destroyed and re-created..at least that's my theory, if there was a way to get some kind of memory ID of the pre-applyBindings() dom elements and then after the applyBindings is called, are those new nodes? If they are new nodes, it's not something you can't fix with cleaning, those nodes are gone.
Alright, here is how I fixed my problem and I hope this can clarify things to others that don't want to destroy their DOM as well. If you don't want that knockout to destroy your DOM, that is possible since version 2.2. And thus, destroying the DOM when that is not necessary is not intended behavior and can be avoided.
I had tried several bidings created by Michael Best before, like his using binding that will come in knockout 3.5, and let or withLight (which became using now). None really worked. These simplified bidings would load the initial object, but not update the dom when this object properties had changed.
But this helped me to figure out what I am doing wrong. When I wanted to update my observable object, I was using myViewModel.observableObject(NewObject), like the documentation told me to do:
To write a new value to the observable, call the observable and pass the new value as a parameter. For example, calling myViewModel.personName('Mary') will change the name value to 'Mary'.
But I wasn't passing a single property's value, I was passing a new object that had the same structure (same properties). And this triggered knockout that the old object was destroyed (and thus, falsy for a second) and a new object took its place, even though all properties are there, they just got different values. Unlike the documentation told me, it didn't simply changed the value, but changed the entire object.
To go around this, instead of doing that, First, I had to initiate my viewModel with this object already created, using dummy data, this makes knockout not destroy the DOM when applyBindings is called. Then, when I want my object to update, I replaced the value of each property of the observable object to have the value of the new object. This didn't destroy the object and knockout updated my binding properly.
myViewModel.setSelectedItem = function setSelectedItem (newObject)
{
for (var prop in myViewModel.myObservableObject())
myViewModel.myObservableObject()[prop](newObject[prop]);
}
The with binding still killed some of my events (my angular ng-change for one of my components, for instance), but it kept all jquery events in there (which is great). And the using binding didn't kill any of my events at all (which is even better).
Related
Sometimes a component/custom element has some UI logic which requires some UI code, it's something which can't be done by binding to the component's view-model.
For example, let's say the component needs to change the way it looks based on available space, and this requires manipulating elements by JavaScript code.
What I need is a controller for the UI.
For example, imagine we have a component called myGadget for which I have myGadget.html, myGadgetViewModel.js and I also want to have myGadgetView.js
Within the myGadgetView.js I want to have something like this:
function myGadgetView(element)
{
// element is the custom element's node
}
What is the best way to do this in Knockout?
Should I combine component with custom binding?
With a custom binding I could get access to the element, so the HTML of the component would look like this:
<script id="myBar-template">
<div data-bind="myGadget : ...">
</div>
</script>
and I need to put somewhere this:
ko.bindingHandlers.myGadget = {
init: function (element, valueAccessor)
{
// I have access to element node
var myGadgetView = new myGadgetView(element);
},
update: function (element, valueAccessor)
{
// I have access to element node
}
}
I'm not sure about using custom binding for this, I wonder if there's a better approach.
For example, I'm looking to the custom component loading, but I don't have a clear idea yet.
When defining a component, you can specify a createViewModel function. This function will be passed the element the component will be bound to. According the Knockout documentation, it's still preferable to use custom bindings to manipulate the view.
Any manipulation of the view should be done in binding handlers, but that doesn't mean you can't make something like a jQuery plug-in, which your myGadgetView.js would be, and use that in the binding handler. You just wouldn't want your plug-in to be aware of your viewmodel, nor your viewmodel to be aware of the plug-in. The binding handler would mediate, mapping viewmodel elements to plug-in parameters.
I'm trying to target an element using jQuery which is embedded in one of my knockout templates:
<script type="text/html" id="video-file-template">
<div class="video" data-bind="attr: { 'data-index': $index }">
</div>
</script>
Yet, when I attempt to select $('.video') using jQuery, wrapped in a document ready function, I get an object with a length of 0 returned:
$(document).ready(function() {
console.log($('.video')); // Returns an object with a length of 0
});
Why is this? Is it because the element is not part of the DOM when my jQuery script is evaluated? If so, how can I target the element when it is loaded into the DOM via Knockout.js?
It's true that the document is ready before ko.applyBindings finishes, so that's why you're not seeing the element. However, you should not be using jQuery to violate the boundary between your view model and the DOM like that. In knockout, the way to accomplish what you need is with custom bindings.
Basically, you define a new knockout binding (like text, value, foreach, etc) and you have access to an init function, which fires when the element is first rendered, and an update function, which fires when the value you pass to the binding is updated. In your case, you would only need to define init:
ko.bindingHandlers.customVideo = {
init: function (element) {
console.log(element, $(element)); // notice you can use jquery here
}
};
And then you use the binding like this:
<div data-bind="customVideo"></div>
Perhaps it's better to add the "video" class and do other initialization right in the init callback:
ko.bindingHandlers.customVideo = {
init: function (element) {
$(element).addClass('video');
}
};
If this feels a little wonky at first, remember there's a very good reason for the indirection. It keeps your view model separate from the DOM it applies to. So you can change the DOM more freely and you can test the view model more independently. If you waited for ko.applyBindings to finish and called some jQuery stuff after that, you'd have a harder time testing that code. Notice that knockout custom bindings are not "special" in any way, and you can see that the built in bindings are defined exactly the same: https://github.com/knockout/knockout/tree/master/src/binding/defaultBindings
As the previous comments have suggested, it's because your $(document).ready fires before your knockout templates have been rendered.
Whenever I need to do this sort of thing I tend to have an 'init' (or whatever) function on my ko view model that I call after applyBindings has completed;
So:
var ViewModel = function(){
var self=this;
//blah
self.init = function(){
//jquery targeting template elements
}
}
var vm = new ViewModel();
ko.applyBindings(vm);
vm.init();
mithril talks plenty about binding and eventing if they are simple variable changes, but what about binding say the + key to functionality? I tried m.withAttr('keyCode') binding to the controller method that I wanted to handle it, but nothing.
Sample Code
Mithril doesn't have a helper for properties that aren't attributes of the DOM element. withAttr only deals with DOM element attributes (as the name implies). For keyCode, you need to define a custom helper
function withKey(key, callback) {
return function(e) {
var ch = String.fromCharCode(e.keyCode)
if (ch == key) callback(key)
else m.redraw.strategy("none") //don't redraw (v0.1.20+ only)
}
}
m("div", {onkeypress: withKey("+", ctrl.doSomething)})
The else statement is just there to prevent a redraw if the pressed key is not the one you're looking for.
Mithril doesn't handle the entire page and it's events for you. You could addEventListener for window.onkeydown and within that callback do what you need, such as update the controller or redraw the page.
http://jsbin.com/hikinoza/1/edit
The m.prop or m.withAttr by themselves are not binding anything anywhere. The real binding happens when you specify some onXXX property for an object such as
m('div', {onClick: myFunc})
This will attach a real onClick event to the real div dom node that will be created.
The Mithril rendering fcn m(tag, attrs, children) allows you to specify the special property config in attrs. It allows you to call methods on the DOM element after it gets created. See the section called Accessing the real DOM element.
You can easily abuse Mithril with this, but its a proper use for attaching non-standard event handlers. Something like the following should work:
m('li', {config: setupKeyHandler}, 'foo');
function setupKeyHandler (el, isInitialized, context) {
el.addEventListener('keyup', function (event) {})
}
when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?
well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event
I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.
I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening
Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.
This seems possible as http://www.knockoutjs.com appears to be doing it. I haven't been able to make enough sense of their code-base to get a similar pattern working though.
Effectively I have a MVVM style application with the UI based on jQuery tabs. Each tab is represented by a view model that I want to be able to validate and fire events based on changes in the model.
I create a representation of my data similar to the following on page load:
$(document).ready(function(){
thisTab = new ThisTab();
});
function ThisTab(){
Load: {Load from my model}
Save: {Save/Persist model to the db (via web service call)}
Validate: {
this.Item1 = function(){Validate item 1, do work, refresh fields, whatever.}
}
}
The model itself is a complex global object and changes to the DOM (inputs, etc.) immediately update the object. Changes to some of those properties should call their associated validate items thisTab.Validate.Item1. I have no issue raising events from the changes. If I bind that event listener to a random DOM element I can call my routines without issue and everything works beautifully. It does seem strange, however, to attach the event to a non-related DOM object.
So the question is: how can I do something like thisTab.addEventListner("someEvent") or $(thisTab).bind("someEvent"), where thisTab is not a DOM element, but instead is a native object. Trying to do it, I always get an error that "this method is not supported".
Attaching an event to a standard object does not use the same methods; basically, you would implement your own eventing like so:
function ThisTab()
{
listeners: [],
addListener: function(callback) { this.listeners.push(callback); },
load: { // Finds DOM elements and such, and attaches to their events. The callback from the DOM event should be a method on your object },
yourDomEventCallback: function()
{
for(var j = 0; j < this.listeners.length; j++)
this.listeners[j]();
}
}
The above code should be used as a starting point, since I just cobbled it together and there are likely syntax errors. Basically, you have taken your object and mapped onto events you want to capture, and then expose methods to attach callback methods that you will call when the hidden DOM events occur. You wont be able to use jQuery's abstractions for DOM events, because such events have no meaning on your custom object.
Bind the event to your regular JS object as you would do for a DOM object.
$(thisTab).bind("someEvent", function() {
// handler's code here
});
See this example. Using this has one side-effect that jQuery will add a housekeeping identifier as a property on the object - it looks something like
jQuery1510587397349299863.
This property named jQuery<timestamp> is added to all DOM objects that have events or data associated with them, and regular objects behave similarly. If you are uncomfortable with your model objects being modified, then use your own callback mechanism which should be fairly easy to add.