I have a backbone application that gives the user the option to display a map by clicking a button. In the CharactersView, the 'showMap' event is set up (triggered by clicking the #map_button) with the corresponding showMap function to display a map once the event's triggered. The CharactersView also renders the character_template, and the character_template contains the #map_button and the actual map code with the class 'map.'Everything works fine if the #map_button is located in the same template (the chracters_template) as the map, which gets rendered from CharactersView. However, if I move the #map_button outside of the character_template (for example, into another template or directly into the body of the page), then the showMap event in CharactersView is no longer triggered, and the map no longer gets displayed in the character_template. Note, I am waiting until the characters template gets rendered to the page before I try to click the #map_button and it's still not working.
From the above description and the code below, can you tell me what the issue here might be and how I might solve it.
Code
Set up event binding in CharactersView so that a map is displayed when a button is clicked
events: {
'click #map_button' : 'showMap'
},
showMap: function(){
$(".map").toggle();
}
The character_template rendered by the characters view. The actual map with the class 'map' that gets displayed is in the same template that gets rendered by the characters view.
<script id="character_template" type="text/x-handlebars">
<input class='action_button' id='map_button' type='button' value='Show Map' />
<%= image_tag "http://maps.google.com/maps/api/staticmap?size=300x200&sensor=false&zoom=12&markers={{ latitude }}%2C{{ longitude }}", :class => "map" %>
</script>
If I removed the button from the character_template and put it in another tempalte, the click #map_button event in CharactersView no longer works. It's also the same result if I just put the button directly in the body of the html.
... other code ommitted...
When you set events in a view using the events hash, Backbone eventually uses jQuery to listen to the event. In Backbone.delegateEvents, you can see this code:
this.$el.on(eventName, selector, method);
Using this.$el.on() (where 'this' is your view) is only listening for the event within that view's DOM element ($el is a cached jQuery object).
So, if you move the button to another view, you need to add your event listener to that view as well:
events: {
'click #map_button' : 'showMap'
},
Basically, using the views event hash is saying, 'listen to this event only within this view's DOM element.'
Related
I am using a javascript library that will add/remove elements to the DOM when some data is updated.
To add a new element to the DOM it calls a template function returning an element. In my case, the template function is defined inside an angular directive and returns something like return $compile(html)(scope)[0]; because I need to use the UI Bootstrap Popover directive inside the added element.
For the Popover, I need to use popover-append-to-body="true".
My problem is, if the triggering element is removed from the DOM, the popover is never removed. So if we add a new triggering element, a second popover will be appended to body, etc.
Here is a simplified example : http://plnkr.co/edit/AFsXBcaLBAs0F2garbAu?p=preview
Clicking on the "Click" button opens the popover, clicking "Remove" removes the "Click" button, clicking "Add" re-adds the "Click" button and clicking "Click" again adds a second popover to the DOM.
How can I remove the Popover directive when the triggering element is removed from the DOM ?
I need to totally deletes it, not only hide it/remove it from the DOM (I can hide it with popover-is-open but when this is set back to true, I see the popover still exists).
Is there a way to call destroy on the Popover directive of the element that will be deleted ?
You shouldn't do DOM manipulation in you a controller, both in JS and HTML, that's why directives are for, and for your case there were a couple of built in directives you could have used.
you should have kept an array to represent your buttons and popover states
you should place all you JS code in your controller, and used ng-click to bind click events to functions in your controller
don't use onclick when you have ng-click
The angular API works completely different then vanilla JS and even jquery, so don't mix them, use what Angular provides you, refer to the docs for help.
Here is your "revamped" code
So I'm trying to build a custom autocomplete dropdown for a text input. To do it, I am listening for the keydown event and if it's an up or down arrow press, I'm setting a $scope.arrowSelectedItem variable to the proper one in the list. (As a side note, all the functionality works as far as selecting an item from the list that pops up. All I'm trying to do is highlight the current one that they've marked with the up/down arrows).
On the markup side, the items in the autocomplete list are output with ng-repeat, with ng-repeat="item in itemList". The ng-class expression I'm using is ng-class="{highlighted: item === arrowSelectedItem}". I know that the $scope.arrowSelectedItem is being updated on each arrow press by using console.log, but for some reason the class isn't being updated to the list item properly.
What I've found is that after the first time of hitting an arrow key, if I make the text input box lose focus, then the class is added. Then if I click back in the box, move the arrow to select a different item, click out of the input box, then click back in, the class is added to the new one. I know that sounds weird, but that's what I've found.
What I'm not sure about is why the ng-class expression isn't being evaluated on every arrow key press. Does anyone have any ideas why?
The answer here is that "raw" DOM events which fire outside of one of angular's built in directives (such as click events via ng-click etc) will not trigger a $digest cycle update. Until this happens the $scope properties will not be updated.
If you are in a position where you are listening for DOM events by using another framework, or simply using addEventListener(), you will need to let angular know about any changes by using $scope.$apply(), or by wrapping the code in a $timeout().
If you do this in your event handler, angular will trigger a new $digest cycle update for every keypress and your new scope values will propagate to the view.
Adding the view to dom id main-wrapper one router calls the view.Page loads one by one because of the sub view added in within main-wrapper. Need to know something similar to smooth loading the dom with fade in effect or shows the page once the dom is loaded.
Another approach you can take is hold off on adding the view to the main-wrapper until all subviews are created. Create a document fragment (default behavior if "el" is not defined). Once your view and its descendent views are all created in document fragment, then add it to the main-wrapper div. This way all of your content are shown at the same time instead of one view at a time.
Either use jQuery's onReady event or the native window.onload event. In both cases you register a callback function that then kicks of the initialization of the respective Backbone views.
Weird problem with event propagation in Backbone. Most people ask how to stop event propagation, but I'm struggling with getting my events to propagate!!
Here I have two View objects. The MainView which contains Item views and listens to click events to call run():
var MainView = Backbone.View.extend({
...
events: {
"click .item": "run" // works only if no click event in Item
},
render: {
// Item View object children
},
run: function() {
//run :)
}
});
Item view objects also listen to click events on themselves to toggle on/off behaviour:
var Item = Backbone.View.extend({
...
events: {
"click" : "toggle" // MainView click event works when this is removed
},
toggle: function() {
this.model.toggle();
}
});
The problem being that MainView.run() is not fired when the Item is clicked, while it has a click event for Item.toggle().
However, MainView.run() DOES fire if I remove the Item.toggle() click event. Leading me to the conclusion that the event is somehow forced to stop propagating, outside of my control.
How can I solve this problem? Am I missing something obvious, or is this unavoidable?
Thank you for any and all suggestions and answers :).
It appears that the click event in your item view isn't bound to a specific DOM object. It's possible that listening for a generic click event is overriding Backbone from listening for your specific .item click event. Try adding an ID or class name to your item view click event to remove any ambiguity.
var Item = Backbone.View.extend({
...
events: {
"click .some-class" : "toggle" // This should fix your problem
},
...
Jay B. Martin answered the question.
The problem is that the View calls this.model.toggle();
The toggle() function sets some variables which the MainView is listening for, causing a render() event to fire.
When MainView calls render(), the Item views are in turn removed, rendered and added to the DOM. This loses the bound event to the DOM element using events: {}.
Instead _.bind() or _.bindAll() should have been used to permanently bind the events, regardless of the context / state of the element bound to in the DOM.
Original comment answer:
#Dan0, sorry I'm a little confused about how toggle could be the root of your issue. I think it's a symptom of the context ambiguity created by binding to an implicit DOM element in a nested view. Once toggle is called, the click event loses the context to which it was initially bound (i.e., this.el). The idiomatic way of solving this is to either a) pass an explicit element so that it can rebind on subsequent events, or b) use _.bind or _.bindAll, so that the click event is permanently bound to the itemview as the context changes. – Jay B. Martin Aug 10 at 23:46
I am using Backbone and Backbone.ModelBinder.
I have a bunch of text fields that are bound via BackboneModelBinder. Everything works as expected however when I make a change to a text field and I don't unfocus the field first (click off the input field) before hitting the SAVE button, I have to hit the Save button twice -- once to unfocus the fields, and then a second time to actually fire the save button handler (which should have fired the first time)
(Save is a standard html button with a backbone event bound to it).
Does anyone have any knowledge of why this might be?
I hope this made sense :|
Thanks for any help or direction
-Kirk
That's because ModelBinder by default set the new value to the model's attributes on "blur" or "change" events (it dependes on the input's type). You can modify this behavior by changing the source code, adding keyup as binding event in those two methods:
_bindViewToModel:function () {
$(this._rootEl).delegate('', 'change keyup', this._onElChanged);
// The change event doesn't work properly for contenteditable elements - but blur does
$(this._rootEl).delegate('[contenteditable]', 'blur keyup', this._onElChanged);
},
_unbindViewToModel: function(){
if(this._rootEl){
$(this._rootEl).undelegate('', 'change keyup', this._onElChanged);
$(this._rootEl).undelegate('[contenteditable]', 'blur keyup', this._onElChanged);
}
},