CanJS future element event binding - javascript

The CanJS documentation has an example like this:
var Todos = can.Control.extend({
init: function( element , options ) { ... },
'li click': function( li ) { ... },
'li .destroy {destroyEvent}': function( el, ev ) {
// previous destroy code here
}
});
// create Todos with this.options.destroyEvent
new Todos( '#todos', { destroyEvent: 'mouseenter' } );
However, if #todos is created after new Todos is called, no event is bound the future element, or if a method within Todos removes a pre-created #todos dummy as deemed necessary. How can I rebind custom events within a Control? After a Control instantiation call?

Just use Control.on();
http://canjs.com/docs/can.Control.prototype.on.html
You can specifiy which event to be listen to or just call the function without parameters like this the control listen to all events.

Related

Disable anonymous event listener or pass params & events to non-anonymous listener

I'm using the following listener to listen for swipe and touch events on mobile. It has the following signature:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
my_element.addEventListener('touchmove', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
}
I like this because it allows me to:
$("foo").onSwipe({
left: (event) => { ... }, // I can define this right here.
right: (event) => { ... },
up: (event) => { ... },
down: (event) => { ... },
})
For left, right, and so on, I can define functions in the scope of assigning the listener while also being able see the event in the listener.
I've tried doing event.preventDefault in my direction handlers, but this still prevents scroll (which I'd like to enable by removing the event listener).
Problem:
I can't remove the event since it's anonymous.
I don't know how I would create a named function while being able to pass it in the same way such that the addEventListner will pass the event and direction handlers (like handlers.left()) to my handleSwipe event.
Note: I am not interested in using other third-party libraries.
Since you're already using jQuery, one option is to attach the listeners with .on instead, allowing you to remove them all with .off, without having to save a reference to them:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
$(this).on('touchmove', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
};
$.fn.offSwipe = function() {
$(this).off('touchmove');
};
If you might have other touchmove listeners attached to the same element, then you'll need to save a reference to the created function when called. Without using jQuery (except for the $.fn part):
const handlersByElement = new Map();
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
const handler = function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
};
for (const elm of this) {
handlersByElement.set(elm, handler);
elm.addEventListener('touchmove', handler);
}
};
$.fn.offSwipe = function() {
for (const elm of this) {
handlersByElement.set(elm, handler);
elm.removeEventListener('touchmove', handlersByElement.get(elm));
}
};
You can also use event namespaces with jQuery to simplify adding and removing of events without having to save a reference to them and without removing all events of that type, thanks #VLAZ:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
$(this).on('touchmove.myswiper', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
};
$.fn.offSwipe = function() {
$(this).off('touchmove.myswiper');
};

Implementation knockout custom binding with afterRender functionality

I'm working with a list of images. The images are loaded dynamically; the list of references is stored in observableArray.
After a full load of the image list I want to connect handlers of DOM-elements. My implementation at the moment:
in View:
<div class="carousel_container" data-bind="template: { 'name': 'photoTemplate', 'foreach': ImageInfos, 'afterRender': renderCarousel }">
<script type="text/html" id="photoTemplate">
//...content of template
</script>
in ViewModel:
self.counterCarousel = 0;
self.renderCarousel = function (elements) {
var allImagesCount = self.ImageInfos().length;
self.counterCarousel++;
if (self.counterCarousel >= allImagesCount) {
self.counterCarousel = 0;
// ... add handlers here
}
}
This is a very ugly approach. In addition, user can add / delete images, so after each addition or removal is required remove all handlers and connect it again. How can I organize a custom binding to handle this scenario?
I don't see why this approach would not work -
ko.utils.arrayForEach(ImageInfos(), function (image) {
// ... add handlers here
});
Or better yet, bind an event to each item with a class of 'image-info' so that you don't have to redo the bindings when items are added or changed -
var afterRender = function (view) {
bindEventToImages(view, '.image-info', doSomething);
};
var bindEventToImages= function (rootSelector, selector, callback, eventName) {
var eName = eventName || 'click';
$(rootSelector).on(eName, selector, function () {
var selectedImage = ko.dataFor(this);
callback(selectedImage);
return false;
});
};
function doSomething(sender) {
alert(sender);
// handlers go here
}
This binds an event to every class 'image-info' and on-click handles the calling element, executing doSomething.

jQuery callbacks cumulation

I'm building a simple jQuery plugin called magicForm (How ridiculous is this?). Now face to a problem that I think I'm not figuring out properly.
My plugin is supposed to be applied on a container element, that will show each of its inputs one by one as user fills them. That's not the exact purpose of my problem.
Each time I initialize the container, I declare an event click callback. Let me show an example.
(function($){
var methods = {
init: function(options){
return this.each(function(){
var form, inputs;
var settings = {
debug: false
};
settings = $.extend(settings, options);
form = $(this);
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
});
},
reset: function() {
}
}
$.fn.magicForm = function(method) {
if ( methods[method] ) {
return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist.' );
}
};
})($);
I'm focusing on a specific part of this code :
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
Because each time the init method is called, that poor callback is registered.
I was experiencing this painfully, when I invoked my plugin on an element nested in a twitter bootstrap 'tab', nested itself in a bootstrap modal :
I was calling init each time the event 'shown' of my bootstrap modal was triggered.
So, this is how I fixed it in my init method :
// Prevent callback cumulation
if (!$(this).data('form_initialized')) {
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
$(this).data('form_initialized', true);
}
And I'm far from feeling sure about this.
Thank your for your time !
Many jquery plugins use data to know if their plugins were initialized. Most often, they use the name of their own plugin as a part (or in whole) as the data. For example:
$(this).data('magicForm')
So your approach of using that to signal is not a bad one.
However, you have two other options:
1) Pull the event handler out so the handler is a single instance. Above your methods, do var fnOnSubmit = function() { ... } Then you can simply ensure proper binding by calling $('a.submit', form).unbind('click', fnOnSubmit) before rebinding it the way you are already doing it.
2) Another option is to use event namespaces.
$('a.submit', form).unbind('click.magicForm'); then rebinding it with .on('click.magicForm') This namespace approach ensures that when you unbind it only unbinds in the context of your namespace magicForm, thus leaving all other click events (e.g. from other plugins) intact.
I hope this helps.
You could first explicitely remove the click-handler:
$('a.submit', form).off('click').on('click', function(event){ ... })
However, I would suggest you use event namespacing to prevent all click handlers (even those perhaps set by code not your own) from being removed:
$('a.submit', form).off('click.magicForm').on('click.magicForm', function(event){ ... })

$(this.el).find() works in event handler, not in initialize function (backbone.js)

I am using backbone.js to create a View which contains a Like button. The model of this View contains the attribute is_liked, and if its value is 1, then the function setStateLike called will change the style of the Like button.
Problem: I am not able to select the button using this.setStateLike() in the initialize function. Doing so just returns a []. However, when I define this.setStateLike as a click event handler, selecting the button works! The stranger thing is that this.setStateLike() called within initialize is able to select $(this.el), but not $(this.el).find()!
Any idea what has happened here and how can it be fixed? Thanks!
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
events: {
'click': 'setStateLike'
},
initialize: function() {
this.setStateLike();
},
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
return this;
},
setStateLike: function() {
console.log( $(this.el).find('#like') ); // returns []
if(this.model.get('is_liked')) {
console.log( $(this.el) ); // returns correctly
console.log( $(this.el).find('#like') ); // returns []
// Change icon to Active state
$(this.el).find('#like.photo_btn').addClass('photo_btn_active').attr('id', 'unlike');
}
}
});
If your script comes before the bulk of the body HTML and if the initialize function is being called immediately, that's your issue: the DOM isn't actually built yet, so no elements can be selected. Either run the script at the end of the </body>, or use jQuery's DOM-ready handler.

Javascript MVC + listening and dispatching events with jQuery

I'm learning javascript and have a question about listening and dispatching events with jQuery.
In my Model, I have a function that triggers a change event:
Model.prototype.setCurrentID = function(currentID) {
this.currentID = currentID;
$('body').trigger('change');
}
The trigger event requires an element, so I bound it to the 'body'. Is this good practice or bad practice?
In AS3, which I'm more familiar, I would simply dispatch a global event from the model, passing in a const value, listening for this event with an instance of the Model:
var model:Model = new Model();
model.addEventListener(CONST_VALUE, handlerFunction);
In jQuery, within my View object, I need to attach an element to the listener as well, so I bound it to the 'body' once again:
var View = function(model, controller) {
var model;
var controller;
this.model = model;
this.controller = controller;
$('body').change(function(evt) { updateSomething(evt); });
function updateSomething(evt){console.log('updating...')};
}
It's working, but I'm interested in your take on the subject.
I recommend using a private dispatcher, something that isn't exposed to the public.
For instance, your logic may fail if the user or a plugin unbinds all the events on the body(your dispatcher) :
$('body').unbind();
This can be avoided by creating a dom node and not expose it to the end user (do not append it to the dom) :
var dispatcher = $('<div />');
Model.prototype.setCurrentID = function(currentID) {
this.currentID = currentID;
dispatcher.trigger('change');
}
var View = function(model, controller) {
this.model = model;
this.controller = controller;
dispatcher.bind('change',function(evt) { updateSomething(evt); });
function updateSomething(evt){console.log('updating...')}
}
Another good thing to have in mind when developing event-programming app with jQuery is that jQuery allows you to bind/trigger custom events and also allows you to namespace your events. This way you can control more efficiently the event binding and triggering :
Model.prototype.setCurrentID = function(currentID) {
this.currentID = currentID;
dispatcher.trigger('modelIdChange.' + this.currentID);
}
Model.prototype.destroy = function() {
// unbind all the event handlers for this particular model
dispatcher.unbind('.'+this.currentID);
}
var View = function(model, controller) {
/*...*/
// this will be triggered for all the changes
dispatcher.bind('modelIdChange',function(evt) { updateSomething(evt); });
// this will be triggered only for the model with the id "id1"
dispatcher.bind('modelIdChange.id1',function(evt) { updateSomething(evt); });
/*...*/
}
I'd go a step further and create custom global events. With jQuery you can trigger a global custom event like so:
$.event.trigger('change');
Any element can subscribe to that event:
$('#myDiv').bind('change', function() {
console.log($(this));
});
The this keyword in the event handler is the DOM element which subscribed to the triggered event.
My objections are:
I wouldn't bind events that have the same name as broswer events, there might be interferences.
Your code works if you have one model, but if you have 2 or more, you'd want to separate them, and not bind/trigger both on the same element.
How about:
Model.prototype.bind = function(event, func) {
if (!this._element) this._element = $('<div>');
this._element.bind(this.name+'_'+event, $.proxy(func, this));
return this;
};
Model.prototype.trigger = function(event) {
if (!this._element) this._element = $('<div>');
this._element.trigger(this.name+'_'+event);
return this;
};
This way you solve both. Note I'm appending this.name+'_' to event names (which assume each model has some sort of name, and makes sure events won't match with browser events), but you can also drop the the prefix.
I'm also using $.proxy in bind so that the this in the event handler refers to the model.
var View = function(model, controller) {
....
model.bind('change', function() {...});
}

Categories

Resources