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();
Related
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).
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 am maintaining a javascript application and I would like there to be a jquery function invoked on pretty much every view. It would go something like this:
SomeView = Backbone.Marionette.ItemView.extend
initialize: ->
#on( 'render', #after_render )
after_render: ->
this.$el.fadeOut().fadeIn()
Clearly there is a better way to do this than have an after_render() in each view? What is the better way to do it? If you can give an answer that includes jasmine tests, I'll <3 you ;)
The event you are looking for is onDomRefresh. See here for the documentation:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#view-domrefresh--ondomrefresh-event
Create your own base view class and put your afterRender code in it. When you create a view, inherit from this class.
var MyApp.ItemView = Backbone.Marionette.ItemView.extend({
afterRender: function() {
// This will be called after rendering every inheriting view.
}
});
var SpecificItemView = MyApp.ItemView.extend({
// this view will automatically inherit the afterRender code.
});
In general, it seems to be considered good practice to define your own base views for all 3 view types. It will enable you to easily add global functionality later.
There is a common pattern used across all Backbone frameworks, normally they have a render method which in turn calls beforeRender, renderTemplate and afterRender methods.
render:function(){
this.beforeRender();
this.renderTemplate();// method names are just indicative
this.afterRender();
return this;
}
In your Base view you can have these methods to be empty functions, and implement them wherever you want it. Not sure this answer applies to Marionette
Combining thibaut's and Robert Levy's answer, the correct solution would be:
var baseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function() {
// This will be triggered after the view has been rendered, has been shown in the DOM via a Marionette.Region, and has been re-rendered
// if you want to manipulate the dom element of the view, access it via this.$el or this.$('#some-child-selector')
}
});
var SpecificItemView = baseView.extend({
// this view will automatically inherit the onDomRefresh code.
});
I have a simple Backbone view which uses jQuery UI's buttons.
For example I have a delete button, which should delete the model (and view). I managed to add the button to the view in the render method using the following:
var self = this;
$(this.el).find("#deleteButton").button({text : false});
$(this.el).find("#deleteButton").bind( "click", self.deleteView);
and then I have the corresponding method in the view:
deleteView: function(){
console.log(this.model);
}
The "deleteView" method gets called, but it will print "undefined" to the console, as "this" is referring to the button and not the view. Replacing "this" with "self" doesn't work either. Also, passing the model or the view as an argument to the method doesn't seem to work as the argument will be the click event.
What is the correct way to handle such callbacks with backbone?
You would want to use the backbone.viewevents property instead. This is the suggested method rather than manually using jQuery to bind events to child elements during render. http://documentcloud.github.com/backbone/#View-delegateEvents
Would be as simple as adding this to your view:
events: {
"click #deleteButton": "deleteView",
},
The context of a function changes when it's bound as a jQuery event.
Use another function, in which you use self, or jQuery.proxy.
An example of maintaining the context through $.proxy: http://jsfiddle.net/bAeQZ/
var self = {
deleteView: function(){console.log(this.model)},
model: '...some model...'
};
$(document).click($.proxy(self.deleteView, self));
// Alternative without $.proxy:
$(document).click(function(){self.deleteView();});
I need to run a layout script as soon as my views are inserted into the DOM. So...
$(".widgets").append(widgets.render().el)
$(".widgets .dashboard").isotope # <-- This needs to be called whenever new widgets are inserted
The problem is I have to insert new widgets a few different views and re-call this script a few different places, which is not DRY. I am wondering how I can define the isotope in the View class.
Would it be a good idea to define an event listener to watch for append into the ".widgets" and to run the script? Is there a built in way of building views that are smart about when they are added to the DOM?
(For that matter, it would be also useful to define a callback for when a View is removed from the DOM.)
How about calling the isotope each time the view is rendered? You'll need to be careful to call render() only after the widget is injected, but this ought to take care of your problem:
//in Backbone.view.extend({
initialize: function() {
// fix context for `this`
_.bindAll(this);
},
render: function() {
// .. do rendering..
this.isotope();
return this;
}
// }) // end .extend
use:
var self = this;
this.$el.on('DOMNodeInserted', function(evt){
self.isotope();
$(evt.target ).stopPropagation();
})