Delete function gets called before button exists - javascript

I've had this issue before, essentially I'd like to keep everything my app separate i.e caching the dom, binding events, no html in javascript etc.
I have an issue where in my bindevents method I have a click on the delete button, however the delete button only exists once a to do has been added.
I'm there getting an error:
Uncaught TypeError: Cannot read property 'addEventListener' of null
Because I guess I'm searching the dom for an element that doesn't exist, how do I keep the structure as it is but only search the DOM for the delete button once an item has been added?
JS
(function() {
var toDo = {
data: [],
cacheDom: function() {
this.toDoApp = document.getElementById('to-do-app');
this.toDoTemplate = document.getElementById('to-do-template');
this.addToDo = document.getElementById('add-to-do');
this.addToDoValue = document.getElementById('add-to-do-value');
this.deleteToDo = document.querySelector('.to-do-delete');
},
load: function() {
this.toDoTemplate = Handlebars.compile(this.toDoTemplate.innerHTML);
},
render: function() {
this.toDoApp.innerHTML = this.toDoTemplate(this.data);
},
bindEvents: function() {
this.addToDo.addEventListener("click", this.add.bind(this));
this.deleteToDo.addEventListener("click", this.delete.bind(this));
},
add: function(e) {
var toDoValue = this.addToDoValue.value;
if(toDoValue) {
var toDoObj = {
value: toDoValue,
id: Date.now()
}
this.data.push(toDoObj);
}
this.render();
},
delete: function() {
console.log("delete!");
},
init: function() {
this.cacheDom();
this.bindEvents();
this.load();
this.render();
}
}
toDo.init();
})();

You should be bind()ing your event handler functions to their scope when they're defined, not when they're setup as listeners.
bindEvents: function() {
this.addToDo.addEventListener("click", this.add);
this.deleteToDo.addEventListener("click", this.delete);
},
add: function() {
// ...
}.bind(this),
delete: function() {
// ...
}.bind(this),

At first, you may just try to get Elements after onload:
window.addEventListener("DOMContentLoaded",toDo.init.bind(toDo));
And you could listen at window for all clicks, then filter them:
window.addEventListener("click",function(evt){
var el = evt.target;
do {
if(el.classList.contains("someclass")){
somefunc.call(el);
}
} while ( el = el.parentElement);
});

I would remove the deletetodo caching and event assignment from the init procedure (which is always going to be null anyway) and listen for the event when the button is added instead.

Related

How bind a event and use apply/call to change the scope

+function ($) {
'use strict';
var popup = {
init: function(element) {
this._active = 'products__popup--active';
this._product = $('.products__popup');
this._element = $('[data-popup-to]');
this._TIME = 500;
popup.attachEvt();
},
attachEvt: function() {
var that = this;
that._element.bind('click', popup.handlerEvt.call(that));
},
handlerEvt: function() {
console.log(this);
console.log('test');
}
};
$(window).on('load', function() {
popup.init();
});
}(jQuery);
I have this script, and is not working yet, I cant show you a working example because it is not ready, I'm organizing the code first.
And there is a problem with the attachEvt function, inside it I want to call another function of my object, this function will bind a click in the that._element, but I want pass to the handlerEvt the scope of this (the clicked element) and the that (the object), but this is not working:
that._element.bind('click', popup.handlerEvt.call(that));
I'm just passing the that scope and when the script loads, the element will be clicked without click, I want avoid this.. this is possible?
UPDATE:
Resuming:
I want be able to use the scope of the object (that) and the scope of the clicked element (this) inside the handlerEvt function, but without make the event click when the script loads.. :B
Try utilizing .bind() , with this set to that._element , that passed as parameter to handlerEvent . Note order of parameters at handlerEvent: obj: that first , evt event object second
+function ($) {
'use strict';
var popup = {
init: function(element) {
this._active = 'products__popup--active';
this._product = $('.products__popup');
this._element = $('[data-popup-to]');
this._TIME = 500;
popup.attachEvt();
},
attachEvt: function() {
var that = this;
that._element.bind('click', popup.handlerEvt.bind(that._element, that));
},
handlerEvt: function(obj, evt) {
console.log(evt, obj, this);
console.log('test');
}
};
$(window).on('load', function() {
popup.init();
});
}(jQuery);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div data-popup-to="true">click</div>

Old Backbone View Causing extra Event Triggers

OK, I've done some reading on this and I'm pretty sure I know what the problem relates to I Just don't know the best way to fix it. I've got the standard backbone router that sets me up with an item details view, then when I click on a button called "start" it creates a new view which takes me to a sort of a game that people can play with some buttons on the bottom that have "click" events attached. This second view is not called through the router but directly from the first view.
The problem is the second time someones goes back to the homescreen and does it again, this time there are two events attached to each button. The third time there are three events. Obviously the original views are still listening to these buttons. I've read about this and calling the Remove() method but is this what I need to do? If so where do I call remove? Relevant Code below:
1ST VIEW
window.GameDrillView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .start" : "startGameDrill",
},
startGameDrill: function () {
var start = $('#start').val();.
var stop = $('#stop').val();.
var StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
}
});
START VIEW
window.GameDrillStartView = Backbone.View.extend({
// declare variables
initialize: function () {
this.render();
},
events: {
"click .nextstage" : "nextstage", // 2ND TIME THROUGH GETS CALLED TWICE
},
nextstage: function () {
// Do some stuff //
this.render(); //Re-render
},
render: function () {
// Do some variables stuff
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)));..
return this;
}
});
When changing view you need to call undelegateEvents() method from the Backbone.View. It disable listening all the elements events mentioned in events { } block. Also if you need to destroy old view you can call remove() method of the view which will call undelegateEvents() internally.
update (example from official site)
var Workspace = Backbone.Router.extend({
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
help: function() {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new HelpView();
},
search: function(query, page) {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new SearchView();
}
});
An option is to create only one instance of the view:
if(_.isUndefined(this.StartView))
this.StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
else
this.StartView.render();
In the render method of GameDrillStartView add the empty method
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)))
In this way you won't add more event listeners but you'll update the page everytime the user presses the button.
You can manage the life cycle of StartView in GameDrillView since it seems like a better place to do so.
Got same trouble. Messy solution:
var current_view = false;
var prev_view = false;
var AppRouter = Backbone.Router.extend({
routes: {
"events/:id": "viewEvent",
}
});
var app_router = new AppRouter;
app_router.on('route:viewEvent', function (event_id) {
var _event = new Event({id:event_id});
current_view = new EventView({
model: _event,
});
});
//Will be called after route:viewEvent
app_router.on('route', function () {
if(prev_view) {
prev_view.undelegateEvents();
}
prev_view = current_view;
});
Not sure, how to make it without having current_view and prev_view out of router scope.

backbone.js doesn't fire my events

I am writing a backbone.js app, and I have a problem.
My collections do not fire events, can anyone spot the problem in the code bellow? I get the render-feedback, the initializer feedback.. but the append method is never called. I know that the "../app" returns a list with tro json items. And I can even see that these are being created in the collection.
Why do my event not get called?
window.TablesInspectorView = Backbone.View.extend({
tagName: "div",
initialize: function () {
console.log('Initializing window.TablesInspectorView');
// setup the tables
this.data = new Backbone.Collection();
this.data.url = "../app";
this.data.fetch();
// some event binds..
this.data.on("change", this.render , this);
this.data.on("add" , this.append_item, this);
},
render: function(){
console.log("render");
_.each(this.data.models, this.append_item);
},
append_item: function(item) {
console.log("appended");
}
});
According to my knowledge , the backbone fetch() is an asynchronous event and when it completes the reset event is triggered ,
When the models belonging to the collection (this.data) are modified , the change event is triggered, so im guessing you have not got that part correct.
so i would do something like this :
window.TablesInspectorView = Backbone.View.extend({
tagName: "div",
initialize: function () {
console.log('Initializing window.TablesInspectorView');
// setup the tables
this.data = new Backbone.Collection();
this.data.url = "../app";
this.data.fetch();
// some event binds..
this.data.on("reset", this.render , this); //change here
this.data.on("add" , this.append_item, this); // i dont see a this.data.add() in you code so assuming this was never called ?
},
render: function(){
console.log("render");
_.each(this.data.models, this.append_item);
},
append_item: function(item) {
console.log("appended");
}
});

dojo.hitch() scope for window.setInterval()

I am trying to produce a blinking effect using dojo fadeIn/Out.
The following snippet of code is defined inside the declaration of a widget class:
_startHighlightEffect : function() {
var blinkInterval = 5000; //Scope here is that of the parent widget
window.setInterval ( function() {
dojo.fadeOut(
{
node: this._headerDiv.domNode,
onEnd: function() {
dojo.fadeIn({node: this._headerDiv.domNode},3000).play();
}
},3000).play();
}, blinkInterval);
},
_highlightEffect : function() {
this.func = dojo.hitch(this,this._startHighlightEffect);
this.func();
}
The problem I am facing is that it says,"this._headerDiv is undefined". On checking with firebug, the scope of this._headerDiv is Window instead of the parent widget.
Please help me understand what am I missing here.
What #jbabey describes will work, but in terms of dojo.hitch, you used it on the wrong function. You need to hitch the function that is passed into setInterval.
_startHighlightEffect : function() {
var blinkInterval = 5000; //Scope here is that of the parent widget
// hitch the function that will be executed by the setInterval call *********
window.setInterval (dojo.hitch(this, function() {
dojo.fadeOut(
{
node: this._headerDiv.domNode,
onEnd: dojo.hitch(this, function() {
dojo.fadeIn(
{node: this._headerDiv.domNode},3000).play();
})
},3000).play();
}, blinkInterval));
},
_highlightEffect : function() {
this._startHighlightEffect();
}
you can save the context when it is the context you want, and use it later:
_startHighlightEffect : function() {
var blinkInterval = 5000; //Scope here is that of the parent widget
var that = this; // save the scope
window.setInterval ( function() {
dojo.fadeOut(
{
node: that._headerDiv.domNode, // use the saved scope
onEnd: function() {
dojo.fadeIn({node: that._headerDiv.domNode},3000).play();
}
},3000).play();
}, blinkInterval);
}

Is there a way to pass context to bind in jQuery?

I'm inside a javascript object (vr roxx :) ), but every time I do an event bind with jQuery I have to include the main object instance's context through the data parameter in order to work with it. Isn't there an easy/neat way to do this in jQuery?
var oink =
{
pig: null,
options:
{
showPigMom: 0
},
init: function(pigObj)
{
//Show the pigmom
$(this.doc).bind('keyup click', {o: this}, function(e)
{
var o = e.data.o;
if (o.options.showpath)
o.doWhatever();
});
...
I use the $.proxy() function
init: function(pigObj)
{
//Show the pigmom
$(this.doc).bind('keyup click', $.proxy(function(e) {
if (this.options.showpath)
this.doWhatever();
$(e.currentTarget).text(); // use this to access the clicked element
}, this));
}
init: function() {
var self = this;
$(this.doc).bind('keyup click', function() {
if (self.options.showpath) self.doWhatever();
});
}
init: function() {
$(this.doc).bind('keyup click', function() {
if (this.options.showpath) this.doWhatever();
$(e.currentTarget).text(); // use this to access the clicked element
}.bind(this))
}

Categories