BackboneJS not creating the Wrapper div - javascript

I am creating a simple backbone template popup. Backbone is not creating the Wrapping <div> element that it was suppose to create. When the template is being generated there is no <div class="theme-overlay"> is generated. Backbone dumps the html from template without any wrapper.
I have searched around but I haven't found any similar issue. I am very new with Backbone so I think I am missing something.
NOTE: I am working with WordPress environment that is why there is an wp global variable wp.Backbone is an internal adaptation of Backbone for avoiding conflict with PHP. Using Backbone.View.extend() instead of wp.Backbone.View.extend() gives me the same result.
Find Code Below
window.wp = window.wp || {};
(function($){
var importer = {};
importer.data = _kallzuDemoSettings;
_.extend( importer, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
importer.View = wp.Backbone.View.extend({
template: wp.template('demo'),
el: '#theme-overlay',
className: 'theme-overlay',
events: {
'click .close' : 'collapse'
},
render: function(demo_title){
var data = _.find(importer.data.demos, function(item){
return item.name == demo_title;
});
if( data == undefined ){
alert( 'No data found!');
return;
}
this.$el.html( this.template( data ) ); // insert into dom
},
collapse: function( event ) {
var self = this;
event = event || window.event;
if ( $( event.target ).is( '.close' ) ) {
// Add a temporary closing class while overlay fades out
$( 'body' ).addClass( 'closing-overlay' );
// With a quick fade out animation
this.$el.fadeOut( 130, function() {
// Clicking outside the modal box closes the overlay
$( 'body' ).removeClass( 'closing-overlay' );
// Handle event cleanup
self.closeOverlay();
});
}
},
closeOverlay: function() {
$( 'body' ).removeClass( 'modal-open' );
this.remove();
this.unbind();
this.trigger( 'importer:collapse' );
},
});
window.installDemo = function( demo_title ){
var view = new importer.View();
view.render( demo_title );
}
})(jQuery);
I don't think other part of the script like the template isn't necessary to show. But if you need them let me know in the comment.

You should remove the el: '#theme-overlay' and instead append the view to it after init.
window.installDemo = function( demo_title ){
var view = new importer.View();
view.$el.appendTo('#theme-overlay')
view.render( demo_title );
}
This way, your wrapper div will be created and then appended to the div.
The DOM should end up looking like:
<div id="theme-overlay">
<div class="theme-overlay"><!-- your rendered content --></div>
</div>

Related

Automatically initialise a custom jQuery plugin

I'm in the process of creating a new accordion plugin using the jQuery Boilerplate as a starting point.
I'm a jQuery novice when it comes to building plugins but it currently seems to be working great.
The user can initialize the plugin the traditional way ( $('elem').plugin({"option":"value"}) ) or inline, on the element itself ( <div data-plugin='{"option":"value"}'> ).
The issue is the plugin needs to be called outside of the plugin code, inside a document ready for each method ( For example - $('elem').plugin() ).
What I want to do with this plugin is have the plugin automatically initialize on set element(s) within the markup, such as <div data-accordion>. I've tried wrapping the element in a jQuery wrapper,inside a document ready but it doesn't seem to work.
The code for the boilerplate is:
;( function( $, window, document, undefined ) {
"use strict";
var pluginName = "defaultPluginName",
defaults = {
propertyName: "value"
};
function Plugin ( element, options ) {
this.element = element;
this.settings = $.extend( {}, defaults, options );
this._defaults = defaults;
this._name = pluginName;
this.init();
}
$.extend( Plugin.prototype, {
init: function() {
this.yourOtherFunction( "jQuery Boilerplate" );
},
yourOtherFunction: function( text ) {
$( this.element ).text( text );
}
} );
$.fn[ pluginName ] = function( options ) {
return this.each( function() {
if ( !$.data( this, "plugin_" + pluginName ) ) {
$.data( this, "plugin_" +
pluginName, new Plugin( this, options ) );
}
} );
};
} )( jQuery, window, document );
Hope someone out there can help.
Thanks :)

Location.hash returns empty string

I am currently using jQuery, Twitter Bootstrap and CanJS for my web application. I'm trying to implement routing with CanJS. I'm using Bootstrap's tab, and when I click on a tab, it was supposed to bring #tabSearch, #tabUser or #tabPermissions, but the hash is returned with an empty string.
What am I doing wrong?
I´m using this to change the tabs:
TabControl = can.Control.extend({}, {
init: function (element, options) {
element.find('a[href="' + options.defaultTab + '"]').tab('show');
this.userSearch = null;
this.userForm = null;
}
, 'a[data-toggle="tab"] show': function (e) {
this.options.tabChangeCallback(e);
}
});
UserTab = new TabControl('#tabctrlUser', {
defaultTab: '#tabSearch',
tabChangeCallback: function (e) {
if (e.context.hash == '#tabSearch') {
if (!this.userSearch) {
this.userSearch = new FormUserQuery('#tabSearch', {});
}
} else if (e.context.hash == '#tabUser') {
if (!this.userForm) {
this.userForm = new FormUser('#tabUser', {});
}
this.userForm.renderView(currentUser);
} else if (e.context.hash == '#tabPermissions') {
new FormPermissions('#tabPermissions', {});
}
}
});
Here is the HTML part:
<ul id="tabctrlUser" class="nav nav-tabs">
<li>Pesquisar</li>
<li>Cadastrar/Alterar</li>
<li>Acessos</li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="tabSearch"></div>
<div class="tab-pane" id="tabUser"></div>
<div class="tab-pane" id="tabPermissions"></div>
</div>
You need to use can.route or can.Control.Route, the way you do it is complicated take a look at this question
Also there's 2 good articles about routing
look at this gist
http://jsfiddle.net/Z9Cv5/2/light/
var HistoryTabs = can.Control({
init: function( el ) {
// hide all tabs
var tab = this.tab;
this.element.children( 'li' ).each(function() {
tab( $( this ) ).hide();
});
// activate the first tab
var active = can.route.attr(this.options.attr);
this.activate(active);
},
"{can.route} {attr}" : function(route, ev, newVal, oldVal){
this.activate(newVal, oldVal)
},
// helper function finds the tab for a given li
tab: function( li ) {
return $( li.find( 'a' ).attr( 'href' ) );
},
// helper function finds li for a given id
button : function(id){
// if nothing is active, activate the first
return id ? this.element.find("a[href=#"+id+"]").parent() :
this.element.children( 'li:first' );
},
// activates
activate: function( active, oldActive ){
// deactivate the old active
var oldButton = this.button(oldActive).removeClass('active');
this.tab(oldButton).hide();
// activate new
var newButton = this.button(active).addClass('active');
this.tab(newButton).show();
},
"li click" : function(el, ev){
// prevent the default setting
ev.preventDefault();
// update the route data
can.route.attr(this.options.attr, this.tab(el)[0].id)
}
});
// configure routes
can.route(":component",{
component: "model",
person: "mihael"
});
can.route(":component/:person",{
component: "model",
person: "mihael"
});
// adds the controller to the element
new HistoryTabs( '#components',{attr: 'component'});
new HistoryTabs( '#people',{attr: 'person'});
I am not familiar with CanJS, but this could have something to do that anchor click event happens before navigation, and location.hash is set after. E.g. if (as a test) you define your link as
Pesquisar
And in plain vanilla JS try
function tabChangeCallback() {
alert(location.hash)
}
It will display blank.
But if you try something like
function tabChangeCallback() {
setTimeout(function() {
alert(location.hash)
}, 100)
}
It will display the hash. So in your case either set your code to be executed on timeout after main event or (and I think it's a better option) instead of hash you should read href attribute of the anchor element that caused the event.
Update The plain JS example below shows how to get the hash value without timeout:
function tabChangeCallback(e) {
var elem = e ? e.target : event.srcElement
alert(elem.getAttribute("href"))
}

Backbone collection is not updating when api is polled

I'm having some trouble getting change events to fire when a model is updated via polling of an endpoint. I'm pretty sure this is because the collection is not actually updated. I'm using the new option (update: true) in Backbone 0.9.9 that tries to intelligently update a collection rather than resetting it completely.
When I insert a console.log(this) at the end of the updateClientCollection function, it appears that this.clientCollection is not updating when updateClientCollection is called via setInterval. However, I do see that the endpoint is being polled and the endpoint is returning new and different values for clients.
managementApp.ClientListView = Backbone.View.extend({
className: 'management-client-list',
template: _.template( $('#client-list-template').text() ),
initialize: function() {
_.bindAll( this );
this.jobId = this.options.jobId
//the view owns the client collection because there are many client lists on a page
this.clientCollection = new GH.Models.ClientStatusCollection();
this.clientCollection.on( 'reset', this.addAllClients );
//using the initial reset event to trigger this view's rendering
this.clientCollection.fetch({
data: {'job': this.jobId}
});
//start polling client status endpoint every 60s
this.intervalId = setInterval( this.updateClientCollection.bind(this), 60000 );
},
updateClientCollection: function() {
//don't want to fire a reset as we don't need new view, just to rerender
//with updated info
this.clientCollection.fetch({
data: {'job': this.jobId},
update: true,
reset: false
});
},
render: function() {
this.$el.html( this.template() );
return this;
},
addOneClient: function( client ) {
var view = new managementApp.ClientView({model: client});
this.$el.find( 'ul.client-list' ).append( view.render().el );
},
addAllClients: function() {
if (this.clientCollection.length === 0) {
this.$el.find( 'ul.client-list' ).append( 'No clients registered' );
return;
}
this.$el.find( 'ul.client-list' ).empty();
this.clientCollection.each( this.addOneClient, this );
}
});
managementApp.ClientView = Backbone.View.extend({
tagName: 'li',
className: 'management-client-item',
template: _.template( $('#client-item-template').text() ),
initialize: function() {
_.bindAll( this );
this.model.on( 'change', this.render );
},
render: function() {
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
From what I can gather from your code, you're only binding on the reset event of the collection.
According to the docs, Backbone.Collection uses the .update() method after fetching when you pass { update: true } as part of your fetch options.
Backbone.Collection.update() fires relevant add, change and remove events for each model. You'll need to bind to these as well and perform the relevant functions to update your UI.
In your case, you could bind to your existing addOneClient method to the add event on your collection.
In your ClientView class, you can bind to the change and remove events to re-render and remove the view respectively. Remember to use listenTo() so the ClientView object can easily clean-up the events when it remove()'s itself.

Backbone: event lost in re-render

I have super-View who is in charge of rendering sub-Views. When I re-render the super-View all the events in the sub-Views are lost.
This is an example:
var SubView = Backbone.View.extend({
events: {
"click": "click"
},
click: function(){
console.log( "click!" );
},
render: function(){
this.$el.html( "click me" );
return this;
}
});
var Composer = Backbone.View.extend({
initialize: function(){
this.subView = new SubView();
},
render: function(){
this.$el.html( this.subView.render().el );
}
});
var composer = new Composer({el: $('#composer')});
composer.render();
When I click in the click me div the event is triggered. If I execute composer.render() again everything looks pretty the same but the click event is not triggered any more.
Check the working jsFiddle.
When you do this:
this.$el.html( this.subView.render().el );
You're effectively saying this:
this.$el.empty();
this.$el.append( this.subView.render().el );
and empty kills the events on everything inside this.$el:
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So you lose the delegate call that binds events on this.subView and the SubView#render won't rebind them.
You need to slip a this.subView.delegateEvents() call into this.$el.html() but you need it to happen after the empty(). You could do it like this:
render: function(){
console.log( "Composer.render" );
this.$el.empty();
this.subView.delegateEvents();
this.$el.append( this.subView.render().el );
return this;
}
Demo: http://jsfiddle.net/ambiguous/57maA/1/
Or like this:
render: function(){
console.log( "Composer.render" );
this.$el.html( this.subView.render().el );
this.subView.delegateEvents();
return this;
}
Demo: http://jsfiddle.net/ambiguous/4qrRa/
Or you could remove and re-create the this.subView when rendering and sidestep the problem that way (but this might cause other problems...).
There's a simpler solution here that doesn't blow away the event registrations in the first place: jQuery.detach().
http://jsfiddle.net/ypG8U/1/
this.subView.render().$el.detach().appendTo( this.$el );
This variation is probably preferable for performance reasons though:
http://jsfiddle.net/ypG8U/2/
this.subView.$el.detach();
this.subView.render().$el.appendTo( this.$el );
// or
this.$el.append( this.subView.render().el );
Obviously this is a simplification that matches the example, where the sub view is the only content of the parent. If that was really the case, you could just re-render the sub view. If there were other content you could do something like:
var children = array[];
this.$el.children().detach();
children.push( subView.render().el );
// ...
this.$el.append( children );
or
_( this.subViews ).each( function ( subView ) {
subView.$el.detach();
} );
// ...
Also, in your original code, and repeated in #mu's answer, a DOM object is passed to jQuery.html(), but that method is only documented as accepting strings of HTML:
this.$el.html( this.subView.render().el );
Documented signature for jQuery.html():
.html( htmlString )
http://api.jquery.com/html/#html2
When using $(el).empty() it removes all the child elements in the selected element AND removes ALL the events (and data) that are bound to any (child) elements inside of the selected element (el).
To keep the events bound to the child elements, but still remove the child elements, use:
$(el).children().detach(); instead of $(.el).empty();
This will allow your view to rerender successfully with the events still bound and working.

I can't access array/obj outside jQuery event function

So I've got an object, or array, declared at the beginning of anything, outside everything:
var Thing = {title:'horse'};
Then I've got:-
$('.clickedIt').fadeOut(200, function() { console.log(Thing.title); }
That will fail. However, if I place above that same console log out of fadeOut, it'll be fine.
If you want something to be global, just define it on the window Object.
window.Thing = { title: 'horse '};
Then use it like so:
$( '.clickedIt' ).fadeOut(200, function() {
console.log( window.Thing.title );
});
Just a note, putting a number of variables on the window Object is not recommended, I would recommend looking into name-spacing: http://addyosmani.com/blog/essential-js-namespacing/
Here is an example:
//simple JavaScript module
( function( window ) {
//define your applications root namespace
window.myApp = {
Thing: { title: 'horse '}
};
})( window );
//jQuery ready function
$( function() {
$( '.clickedIt' ).fadeOut( 200, function() {
console.log( myApp.Thing.title );
});
});
The value you assigned to title horse is undefined wrap it within quotes to make it string litral,
Live Demo
var Thing = {title:'horse'};
$('.clickedIt').fadeOut(200, function() { console.log(Thing.title); })​

Categories