Spine.js Unknown Record (possible scope error?) - javascript

If I have a list of "albums," and I click on one, I navigate to another view (/#/album/:id) which is controlled by a controller called SingleAlbum. It fetches the data correctly, but I can't get it to render. I've looked over other 'Unknown Record' issues on SO and on the Spine Google Group, but no dice. Here's my code:
var SingleAlbum = Spine.Controller.sub({
apiObj: {
url: '/api/album',
processData: true,
data: {
id: ''
}
},
model: Album,
panel: $('.album_single'),
tmpl: $('#albumTpl'),
init: function() {
this.apiObj.url = this.model.url;
if(this.panel.children.length > 0) {
this.panel.html('');
}
},
render: function(id) {
console.log('render');
var template = singleAlbum.tmpl.html(),
data = Album.find(id), // <-- this doesn't want to work
html = Mustache.to_html(template, data);
singleAlbum.panel.html(html);
},
getData: function(id) {
//var me = this;
console.log('get data');
this.apiObj.data.id = id;
this.apiObj.url = this.model.url;
this.model.bind('refresh change', function(id) {
//me.render(id);
singleAlbum.render(id);
console.log('should be rendered');
});
this.model.fetch(this.apiObj);
console.log('record: ',this.model.find(id));
if(Object.keys(this.model.find(id)).length > 0) {
//this.render(id);
}
}
});
The problem happens when I call .render() on the event handler. I can manually see that Album.all() has records, and can do Album.find(id) anywhere else in the app, but when I do it on var data = Album.find(id) it fails. Is this a scope issue? Am I missing something obvious?
By the by, please excuse the verboseness of my code. I'm actually making a SingleItem controller, of which SingleArtist and SingleAlbum will be subclasses. I thought that might be an issue, so I ripped out the code to test it on it's own.
EDIT: Specifically, my route looks like this:
'/album/:id': function(params) {
console.log('navigated to /album/', params.id);
singleAlbum.getData(params.id);
}

Aha! I was passing id in my event handler, which was overwriting the other id variable. Here's what it should look like:
render: function(id) {
var template = this.tmpl.html(),
data = this.model.find(id),
html = Mustache.to_html(template, data);
this.panel.html(html);
},
getData: function(id) {
var me = this;
this.apiObj.data.id = id;
this.apiObj.url = this.model.url;
this.model.bind('refresh change', function() { // <-- don't need to pass anything
me.render(id);
});
this.model.fetch(this.apiObj);
}

Related

Prevent multiple ajax requests onclick

I've an issue with multiple ajax requests. For example I've a form with a button, and onclick it runs a service which essentially load list of items in a table; for now it should load a single item into a table when I hit the button.
However, when I hit the button multiple times, the same item is duplicated when its loaded.
How can I prevent while there is still no callback from the first one?
current ng service
var getItems = function () {
var def = $q.defer();
Items.get().then(function (items) {
def.resolve(items);
}, function (err) {
...
});
};
Not sure if this is a solution, but when I write above code like this:
var def = false;
var getItems = function () {
def = $q.defer();
Items.get().then(function (items) {
def.resolve(items);
}, function (err) {
...
});
};
This stops the duplication when I initialize the def = false, not sure if this is the correct approach by resetting the previous/old request to false?
You can put a lock on the function to prevent the code from running multiple times at once or at all:
// your service
$scope.isRunning = false;
var getItems = function () {
if(!$scope.isRunning){
$scope.isRunning = true;
var def = $q.defer();
Items.get().then(function (items) {
def.resolve(items);
}, function (err) {
...
}).finally(function(){
//$scope.isRunning = false; // once done, reset isRunning to allow to run again. If you want it to run just once then exclude this line
});
}
};
Unsure how you want to handle the button in terms of being clicked multiple times
You can hide it on click:
<button ng-hide="isRunning">Stuff</button>
You can disable it on click:
<button ng-disabled="isRunning">Stuff</button>
if disabling, you should probably give feedback like changing opacity:
<button ng-disabled="isRunning" ng-class='{"opacity-half": isRunning}'>Stuff</button>
.opacity-half { opacity: 0.5 }
the below code should do the trick I am avoiding some angular specific syntax hope that helps;
function yourContoller(/*all injectables*/) {
var requesting = false;
$scope.buttonClick = function() {
if (!requesting) {
requesting = true;
yourService.getItems().then(function(response) {
/*your code to handle response*/
requesting = false;
});
}
};
}
if you want to disable a button in the view you can expose this variable by simply using scope ($scope.requesting = false;) with ng-disabled.
you can create a reusable directive so that on any button which is clickable, it doesnt get pressed twice
app.directive('clickAndDisable', function() {
return {
scope: {
clickAndDisable: '&'
},
link: function(scope, iElement, iAttrs) {
iElement.bind('click', function() {
iElement.prop('disabled',true);
scope.clickAndDisable().finally(function() {
iElement.prop('disabled',false);
})
});
}
};
});
This can be used on a button as follows:
<button click-and-disable="functionThatReturnsPromise()">Click me</button>

How to access a backbone view in another file from backbone router

I have a backbone application which works fine but was getting a bit heavy to be in one file so I have started to separate it into different files I now have:
backbone-view.js
backbone-router.js
...
I am using my backbone router to instantiate views when the URL changes like so:
var Router = Backbone.Router.extend({
routes: {
'our-approach.php': 'instantiateOurApproach',
'our-work.php': 'instantiateOurWork',
'who-we-are.php': 'instantiateWhoWeAre',
'social-stream.php': 'instantiateSocialStream',
'contact.php': 'instantiateContact'
},
instantiateOurApproach: function() {
if(window.our_approach_view == null) {
window.our_approach_view = new OurApproachView();
}
},
instantiateOurWork: function() {
if(window.our_work_view == null) {
window.our_work_view = new OurWorkView();
}
},
instantiateWhoWeAre: function() {
if(window.who_we_are_view == null) {
window.who_we_are_view = new WhoWeAreView();
}
},
instantiateSocialStream: function() {
if(window.social_stream_view == null) {
window.social_stream_view = new SocialStreamView();
}
},
instantiateContact: function() {
if(window.contact_view == null) {
window.contact_view = new ContactView();
}
}
});
The problem I am now having is that I cannot access the views as they are declared in a separate file so the following are all undefined:
OurApproachView()
OurWorkView()
WhoWeAreView()
SocialStreamView()
ContactView()
I have tried doing:
window.OurApproachView()
But this doesn't work.
I am not sure what my next move is so if anyone can help that would be awesome.
Thanks!
EDIT
OK so it seems doing:
window.OurApproachView()
does actually work, my apologies there, but does anyone have a better suggestion?
You can take this approach:
// sample-view.js
var app = app || {};
$(function() {
app.SampleView = Backbone.View.extend({
el: '#sample-element',
template : // your template
events: {
// your events
},
initialize: function() {
// do stuff on initialize
},
render: function() {
// do stuff on render
}
});
});
Similarly, all your js files (models, collections, routers) can be setup like this. You would then be able to access any view from the router by doing:
var view = new app.SampleView();
This works:
window.our_work_view = new window.OurApproachView();
But I don't like it as a solution.
Anyone suggest anything better?

Reusing a modal template

On my current project, there are starting to be a few views that are modal views that are being used to delete items on the site. They are currently generic in that it's just a text description of the item they are deleting. Maybe in the future there will be an icon or a short description as well. There are now tasks to have that functionality to delete other stuff on our site. I'm new to the web, MVC, asp.net, etc, and what I want to know is if it's better to reuse our current modal view somehow, and pass in the objects we need to show in the view. Because the view needs to send the url back to the server on which items to delete, that part of code would need to be different for the view as well. Here is some of the stuff in our view along with a .cshtml template that's pretty generic that I didn't include.
Views.DeleteGiftModal = (function () {
return Backbone.View.extend({
template: Templates["template-gift-delete-modal"],
tagName: 'div',
initialize: function (options) {
$(window).bind("disposeModal", _.bind(this.disposeModal, this));
_.bindAll(this, "showDialog", "disposeModal", "displayResults");
this.eventAggregator = options.eventAggregator;
this.itemsToDelete = options.model;
this.errors = {};
this.render();
return this;
},
events: {
"click #delete-btn": "deleteItems",
"click #ok-btn": "disposeModal",
"click #cancel-btn": "disposeModal"
},
disposeModal: function (event, refresh) {
this.$el.modal("hide");
if (event != null && event.currentTarget != null && event.currentTarget.id == 'ok-btn')
refresh = true;
this.trigger("modalClosed", refresh);
this.remove();
this.unbind();
},
showDialog: function () {
this.$el.modal("show");
},
deleteItems: function () {
var self = this;
var element = this.$el;
var numberGifts = this.getKeys(this.itemsToDelete).length;
this.results = [];
var hasError = false;
element.find("#actions").hide();
element.find("#ok-actions").show();
$.each(this.itemsToDelete, function(i, v) {
// tell model to go away
var gift = new Gift({ id: i });
gift.destroy({
success: function (model, response) {
self.results.push({ id: model.id, response: response });
numberGifts--;
if (numberGifts <= 0) {
if (!hasError) {
self.disposeModal(null, true);
} else {
self.displayResults();
}
}
}
});
});
},
displayResults: function () {
var element = this.$el;
$.each(this.results, function(i, v) {
// to do check response for error message
var list = element.find("#delete-item-" + v.id);
if (v.response.message == "Deleted") {
list.append(" - <span align='right' style='color: green'>Deleted</span>");
} else {
hasError = true;
list.append(" - <span align='right' style='color: red'>" + v.response.message + "</span>");
}
});
},
render: function () {
this.$el.append(this.template);
this.$el.find("#ok-actions").hide();
// show list of item names
var list = this.$el.find("#items-to-delete-list");
$.each(this.itemsToDelete, function (i, v) {
$("<li id='delete-item-" + i + "'>" + v.name + "</li>").appendTo(list);
});
this.$el.attr('id', 'delete-gift-dialog');
return this;
}
});
})();
As I am looking through the code, and this being my first real project, it seems like a lot of things that could be quite similar, like deleting a Gift, deleting a Toy, etc have different Controllers for each (GiftController, ToyController), and hit different URLs. So currently things are all in their own class like that. I was wondering if that's the more standard way to approach these types of problems as well with views. Thanks in advance!
The app we're developing at work had a similar issue. We're using Backbone too so I can completely relate to this. What I ended up doing is have a sort of ModalBuilder that builds a form in a modal for you and binds events on the form elements for submit. The initialization of it could look like this:
new ModalBuilder({
form: [
{
tag: 'select[name="id"]',
options: [
{ name: 'Item 1', id: 12 },
{ name: 'Item 2', id: 32 }
]
},
{
tag: 'input[type="submit"]',
value: 'Delete'
}
],
events: function(){
$('input[type="submit"]').on('click', function(){
// Delete via ajax
})
}
})
What we do is we have different templates for every form element, inputfields and textareas and so on and we reuse it all over the place. ModalBuilder takes these arguments and builds a form
Also for certain cases it might be better to render the form server-side and deliver it to your modal via ajax. You have to weigh what makes your app more performant I suppose.

Zombie views and events in Backbone marionette layout... Have I got them?

I think I might have a problem with zombie views in my Backbone Marionette app.
How can I check for unclosed views and memory leaks? I'm using the illuminations-for-developers.com add-on for Firefox and as I move around my application I see over 1000 views piling up in the 'widgets' illuminations tab - and when I inspect the HTML for them the majority are not in the DOM. Are these zombied views?
Have added the code I'm using below to get peoples opinion on if I'm attacking the problem the right way.
I'm trying to build a UI similar to the Facebook multiple friend selector dialog (see pic).
I have a layout with two collection views, one populated with a list of users, and an empty one in which the selected users are added to.
I want to use this layout in multiple areas of my app. So I have built a controller object that handles initializing it and loading the data for the collections, and then I initialize it and show it in another region whenever it is required.
Would appreciate tips on how to go about this, thanks.
Codez:
MyApp.UserFilterController
MyApp.UserFilterController = (function(){
var UserFilterController = {};
var selectedUsersCol;
var userFilterColView;
var selectedUsersColView;
var usersCol;
UserFilterController.initialize = function ( callback, excludeUsers ) {
// make a query...
// exclude the users...
var usersQ = new Parse.Query(Parse.User);
// just users with email addresses
usersQ.exists('email');
usersQ.exists('name');
usersQ.limit(1000);
usersQ.ascending('name');
usersQ.notContainedIn('objectId',excludeUsers);
usersCol = usersQ.collection();
// tell it where to render... append to the body give it an element?
userFilterColView = new MyApp.UserFilterUserCollectionView({
collection:usersCol
});
usersCol.fetch({
success:function (col) {
console.log("users collection fetched", col.length);
},
error:function () {
console.log("error fetching users collection");
}
});
$('#subpage-header').text("Users Selection");
// empty collection to hold the selected users
selectedUsersCol = new MyApp.Users();
// view to show the selected users
selectedUsersColView = new MyApp.SelectedUserCollectionView({
collection:selectedUsersCol
});
_.extend(selectedUsersCol, newBackboneAddMethod());
MyApp.userFilterLayout = new MyApp.UserFilterLayout();
MyApp.slideUp.content.show(MyApp.userFilterLayout);
MyApp.userFilterLayout.selectedusers.show(selectedUsersColView);
MyApp.userFilterLayout.allusers.show(userFilterColView);
//When user clicks on user in all users then its added to selected users
userFilterColView.on("itemview:clicked", function(childView, model){
console.log(model);
selectedUsersCol.add(model);
});
userFilterColView.on("collection:rendered", function(childView, model){
console.log('its rendered');
});
//When user clicks on selected user then it is removed from the collection
selectedUsersColView.on("itemview:clicked", function(childView, model){
console.log(model);
console.log(model.id);
selectedUsersCol.remove(model);
});
MyApp.App.vent.bind("slideUp:send",function(){
console.log("send button has been clicked. attempting call back")
callback(selectedUsersCol);
});
//unbinds the trigger above when view is being closed
userFilterColView.on('collection:before:close', function (){
MyApp.App.vent.unbind("slideUp:send");
console.log('colView before:close')
});
};
UserFilterController.removeUser = function ( user ) {
//console.log("you asked to remove", usersArray.length, 'users');
selectedUsersCol.remove(user);
usersCol.remove(user);
};
UserFilterController.generateListview = function ( user ) {
userFilterColView.$el.listview();
};
UserFilterController.resetSelected = function (user) {
selectedUsersCol.reset();
};
UserFilterController.cleanup = function () {
console.log("its closing");
//selectedUsersColView.unbindAll();
// selectedUsersColView.close();
userFilterColView.close();
// userFilterLayout.unbindAll();
// MyApp.userFilterLayout.close();
// MyApp.slideUp.content.close();
// MyApp.slideUp.close();
};
return UserFilterController;
}());
MyApp.EventDisplayLayout
MyApp.EventDisplayLayout = Backbone.Marionette.Layout.extend({
template: '#event-display-layout',
id: "EventDisplayLayout",
events: {
'click #invite': 'showUserFilter'
},
// User clicked on 'invite' button
showUserFilter: function () {
$.mobile.changePage($('#subpage'), {changeHash: false,transition: 'slideup'});
MyApp.UserFilterController.generateListview();
}
}
MyApp.showEventDisplay
MyApp.showEventDisplay = function (event) {
var eventDisplayLayout = new MyApp.EventDisplayLayout({});
MyApp.App.mainRegion.show(eventDisplayLayout);
var Invitees = event.get("invitees");
var excludeIds = [];
_.each(Invitees,function(invitee){
excludeIds.push(invitee.id);
});
MyApp.UserFilterController.initialize(function (selectUsersCol){
console.log("In call back: ",selectUsersCol);
},excludeIds);
};
MyApp.SlideUpPageLayout
// The generic layout used for modal panel sliding up from bottom of page.
MyApp.SlideUpPageLayout = Backbone.Marionette.Layout.extend({
el: '#subpage',
//template: '#homepage-temp',
regions: {
header: '.header',
content: '.content'
},
events:{
'click .send':'slideUpSend',
'click .cancel':'slideUpCancel'
},
onShow: function () {
console.log("SlideUpPage onShow");
this.$el.trigger('create');
},
initialize: function () {
// make user collection...
},
slideUpSend: function () {
console.log("send button has been pressed");
MyApp.App.vent.trigger('slideUp:send');
$.mobile.changePage($('.type-home'),{transition: 'slideup',reverse:true});
},
slideUpCancel: function () {
// MyApp.App.vent.trigger('slideUp:cancel');
$.mobile.changePage($('.type-home'),{transition: 'slideup',reverse:true});
}
});
MyApp.UserFilterLayout
// The layout used for the user filter panel sliding up from bottom of page.
MyApp.UserFilterLayout = Backbone.Marionette.Layout.extend({
template: '#userfilterlayout',
//template: '#homepage-temp',
regions: {
selectedusers: '.selectedusers',
allusers: '.allusers'
},
onShow: function () {
console.log("userfilterlayout onShow");
this.$el.trigger('create');
}
});

Jquery confirmation box

I'm looking to create a generic confirmation box that can be used by multiple widgets easily, but I'm running into problems with scope and was hoping for a clearer way of doing what I'm trying to do.
Currently I have the following -
(function() {
var global = this;
global.confirmationBox = function() {
config = {
container: '<div>',
message:''
}
return {
config: config,
render: function(caller) {
var jqContainer = $(config.container);
jqContainer.append(config.message);
jqContainer.dialog({
buttons: {
'Confirm': caller.confirm_action,
Cancel: caller.cancel_action
}
});
}
}
} //end confirmationBox
global.testWidget = function() {
return {
create_message: function(msg) {
var msg = confirmationBox();
msg.message = msg;
msg.render(this);
},
confirm_action: function() {
//Do approved actions here and close the confirmation box
//Currently not sure how to get the confirmation box at
//this point
},
cancel_action: function() {
//Close the confirmation box and register that action was
//cancelled with the widget. Like above, not sure how to get
//the confirmation box back to close it
}
}
}//end testWidget
})();
//Create the widget and pop up a test message
var widget = testWidget();
widget.create_message('You need to confirm this action to continue');
Currently I'm just looking to do something as simple as close the box from the within the widget, but I think I've wrapped my own brain in circles in terms of what knows what.
Anyone want to help clear my befuddled brain?
Cheers,
Sam
The resulting code:
I thought it might be useful for people who find this thread in later days looking for a solution to a similar problem to see the code that resulted from the helpful answers I got here.
As it turns out it was pretty simple in the end (as most of the frustrating mind-tangles are).
/**
* Confirmation boxes are used to confirm a request by a user such as
* wanting to delete an item
*/
global.confirmationBox = function() {
self = this;
config = {
container: '<div>',
message: '',
}
return {
set_config:config,
render_message: function(caller) {
var jqContainer = $(config.container);
jqContainer.attr('id', 'confirmation-dialog');
jqContainer.append(config.message);
jqContainer.dialog({
buttons: {
'Confirm': function() {
caller.confirm_action(this);
},
Cancel: function() {
caller.cancel_action(this);
}
}
});
}
}
} // end confirmationBox
global.testWidget = function() {
return {
create_message: function(msg) {
var msg = confirmationBox();
msg.message = msg;
msg.render(this);
},
confirm_action: function(box) {
alert('Success');
$(box).dialog('close');
},
cancel_action: function(box) {
alert('Cancelled');
$(box).dialog('close');
}
}
}//end testWidget
You could pass jqContainer to the confirm/cancel functions.
Alternately, assign jqContainer as a property of caller. Since the confirm/cancel functions are called as methods of caller, they will have access to it via this. But that limits you to tracking one dialog per widget.
Try something like this:
(function() {
var global = this;
/*****************This is new****************/
var jqContainer;
global.confirmationBox = function() {
config = {
container: '<div>',
message:''
}
return {
config: config,
render: function(caller) {
// store the container in the outer objects scope instead!!!!
jqContainer = $(config.container);
jqContainer.append(config.message);
jqContainer.dialog({
buttons: {
'Confirm': caller.confirm_action,
Cancel: caller.cancel_action
}
});
}
}
} //end confirmationBox
global.testWidget = function() {
return {
create_message: function(msg) {
var msg = confirmationBox();
msg.message = msg;
msg.render(this);
},
confirm_action: function() {
//Do approved actions here and close the confirmation box
//Currently not sure how to get the confirmation box at this point
/*******Hopefully, you would have access to jqContainer here now *****/
},
cancel_action: function() {
//Close the confirmation box and register that action was
//cancelled with the widget. Like above, not sure how to get
//the confirmation box back to close it
}
}
}//end testWidget
})();
//Create the widget and pop up a test message
var widget = testWidget();
widget.create_message('You need to confirm this action to continue');
If that doesn't work, try defining your callbacks (confirm_action, cancel_action) as private members of your object. But they should be able to access the outer scope of your main object.

Categories