Strategy for converting a JS singleton to an instance/ctor - javascript

Below is a sample from the Durandal starter kit which is returning a singleton. I’m curious what the simplest way possible is to convert it to an instance, preferably without completely changing the syntax.
define(['plugins/http', 'durandal/app', 'knockout', 'lodash'], function (http, app, ko, _) {
var displayName = 'Flickr',
images = ko.observableArray([]),
activate = function () {
//the router's activator calls this function and waits for it to complete before proceeding
if (this.images().length > 0) {
return;
}
var that = this;
return http.jsonp('http://api.flickr.com/services/feeds/photos_public.gne', { tags: 'mount ranier', tagmode: 'any', format: 'json' }, 'jsoncallback').then(function(response) {
that.images(response.items);
});
},
select = function(item) {
//the app model allows easy display of modal dialogs by passing a view model
//views are usually located by convention, but you an specify it as well with viewUrl
item.viewUrl = 'views/detail';
app.showDialog(item);
},
somePrivate = function() { return ‘blah’; },
canDeactivate = function () {
//the router's activator calls this function to see if it can leave the screen
return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
};
return {
displayName: displayName,
images: images,
activate: activate,
select: select,
canDeactivate: canDeactivate
};
});
This works but is a pain to add “this.” Everywhere and I also lose the public/private distinction from above:
define(['plugins/http', 'durandal/app', 'knockout', 'lodash'], function (http, app, ko, _) {
var ctor = function() {
this.displayName = 'Flickr';
this.images = ko.observableArray([]);
this.activate = function () {
//the router's activator calls this function and waits for it to complete before proceeding
if (this.images().length > 0) {
return;
}
var that = this;
return http.jsonp('http://api.flickr.com/services/feeds/photos_public.gne', { tags: 'mount ranier', tagmode: 'any', format: 'json' }, 'jsoncallback').then(function(response) {
that.images(response.items);
});
};
this.select = function(item) {
//the app model allows easy display of modal dialogs by passing a view model
//views are usually located by convention, but you an specify it as well with viewUrl
item.viewUrl = 'views/detail';
app.showDialog(item);
};
this.canDeactivate = function () {
//the router's activator calls this function to see if it can leave the screen
return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
};
};
return ctor;
});
I would like something like the below to work– any tips?
define(['plugins/http', 'durandal/app', 'knockout', 'lodash'], function (http, app, ko, _) {
var ctor = function() {
var displayName = 'Flickr',
images = ko.observableArray([]),
activate = function () {
//the router's activator calls this function and waits for it to complete before proceeding
if (this.images().length > 0) {
return;
}
var that = this;
return http.jsonp('http://api.flickr.com/services/feeds/photos_public.gne', { tags: 'mount ranier', tagmode: 'any', format: 'json' }, 'jsoncallback').then(function(response) {
that.images(response.items);
});
},
select = function(item) {
//the app model allows easy display of modal dialogs by passing a view model
//views are usually located by convention, but you an specify it as well with viewUrl
item.viewUrl = 'views/detail';
app.showDialog(item);
},
canDeactivate = function () {
//the router's activator calls this function to see if it can leave the screen
return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
};
return {
displayName: displayName,
images: images,
activate: activate,
select: select,
canDeactivate: canDeactivate
};
};
return _.bind(ctor, this);
});
I also tried _.bindAll inside the ctor both before and after the vars.

Have you tried making ctor an IIFE? Like so:
define(['plugins/http', 'durandal/app', 'knockout', 'lodash'], function (http, app, ko, _) {
var ctor = (function() {
var displayName = 'Flickr',
images = ko.observableArray([]),
activate = function () {
//the router's activator calls this function and waits for it to complete before proceeding
if (this.images().length > 0) {
return;
}
var that = this;
return http.jsonp('http://api.flickr.com/services/feeds/photos_public.gne', { tags: 'mount ranier', tagmode: 'any', format: 'json' }, 'jsoncallback').then(function(response) {
that.images(response.items);
});
},
select = function(item) {
//the app model allows easy display of modal dialogs by passing a view model
//views are usually located by convention, but you an specify it as well with viewUrl
item.viewUrl = 'views/detail';
app.showDialog(item);
},
canDeactivate = function () {
//the router's activator calls this function to see if it can leave the screen
return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
};
return {
displayName: displayName,
images: images,
activate: activate,
select: select,
canDeactivate: canDeactivate
};
}());
return ctor;
});

define(['plugins/http', 'durandal/app', 'knockout', 'lodash'], function (http, app, ko, _) {
var ctor = function() {
var displayName = 'Flickr',
images = ko.observableArray([]),
activate = function () {
//the router's activator calls this function and waits for it to complete before proceeding
if (images().length > 0) {
return;
}
var that = this;
return http.jsonp('http://api.flickr.com/services/feeds/photos_public.gne', { tags: 'mount ranier', tagmode: 'any', format: 'json' }, 'jsoncallback').then(function(response) {
that.images(response.items);
});
},
select = function(item) {
//the app model allows easy display of modal dialogs by passing a view model
//views are usually located by convention, but you an specify it as well with viewUrl
item.viewUrl = 'views/detail';
app.showDialog(item);
},
canDeactivate = function () {
//the router's activator calls this function to see if it can leave the screen
return app.showMessage('Are you sure you want to leave this page?', 'Navigate', ['Yes', 'No']);
};
_.extend(this, {
displayName: displayName,
images: images,
activate: activate,
select: select,
canDeactivate: canDeactivate
});
};
return ctor;
});
This seems to be pretty close to what I'm looking for and is an almost direct translation.

Related

RequireJS modules are undefined in Backbone view

I have been pretty much beginner at this part of javascript and I would appreciate any ideas how could be solved this problem.
I use requirejs to define my own modules where I also use backbone.js.
Let say I have the main module where I initialize my Backbone view which is rendered without any problem. Also, the click event where is calling method createSchemeForm creates the form correctly. The problem raises up in a situation when I call cancel method by click and the modules which are defined for Backbone view (e.g. "unicorn/sla/dom/helper"...) are undefined but when I called method createSchemeForm at the beginning the modules were executed without any problem.
Thank you in advance for any suggestions.
Backbone view
define("unicorn/sla/view/scheme", [
"unicorn/sla/dom/helper",
"unicorn/soy/utils",
"unicorn/sla/utils"
], function (DOMHelper, soyUtils, jsUtils) {
return Backbone.View.extend({
el: 'body',
inputData: {},
btnSaveScheme: 'btn-save-sla-scheme',
btnCancel: 'btn-cancel-sla-scheme',
btnCreate: 'btn-create-sla-scheme',
btnContainer: '#sla-scheme-buttons-container',
schemeContent: '#sla-scheme-content-section',
btnSpinner: '.button-spinner',
events: {
'click #btn-create-sla-scheme' : "createSchemeForm",
'click #btn-cancel-sla-scheme' : "cancel"
},
initialize: function(){
console.log("The scheme view is initialized...");
this.render();
},
createSchemeForm: function () {
this.spin();
DOMHelper.clearSchemeContent();
DOMHelper.clearButtonsContainer();
//Get button
$btnSave = soyUtils.getButton({isPrimary: 'true', id: this.btnSaveScheme, label: 'Save'});
$btnCancel = soyUtils.getButton({isPrimary: 'false', id: this.btnCancel, label: 'Cancel'});
//Append new created buttons
DOMHelper.addContent(this.btnContainer, AJS.format("{0}{1}", $btnSave, $btnCancel));
//Call service to get entry data for scheme creation form
AJS.$.ajax({
url: AJS.format('{0}={1}',AJS.I18n.getText('rest-url-project-scheme-input-data'), jsUtils.getProjectKey()) ,
type: "post",
async: false,
context: this,
global: false,
}).done(function (data) {
this.inputData = data;
$slaSchemeForm = soyUtils.getSchemeCreateForm({slaScheme : data, helpText: AJS.I18n.getText("sla-time-target-tooltip-text")});
DOMHelper.addContent(this.schemeContent, $slaSchemeForm);
jsUtils.scroll(this.schemeContent, 'slow');
}).fail(function () {
jsUtils.callFlag('error', AJS.I18n.getText("message-title-error"), AJS.I18n.getText("sla-error-load-scheme-input-data"));
}).always(function () {
this.stopSpin();
});
},
spin: function () {
AJS.$('.button-spinner').spin();
},
stopSpin: function () {
AJS.$('.button-spinner').spinStop();
},
cancel: function () {
jsUtils.clearButtonsContainer();
jsUtils.clearSchemeContent();
$btnCreateScheme = soyUtils.getButton({isPrimary: 'false', id: this.btnCreate, label: 'Create SLA Scheme'});
DOMHelper.addContent(this.btnContainer, $btnCreateScheme);
DOMHelper.addContent(this.schemeContent, soyUtils.getSchemesTable(new Array())); // TODO - get current data from server instead of empty array
}
});
});
Main module where is Backbone view initialize
define("unicorn/sla/project/batch", [
"unicorn/sla/utils",
"unicorn/sla/data/operations",
"unicorn/sla/data/validator",
"unicorn/sla/dom/helper",
"unicorn/sla/model/confirm/message",
"unicorn/sla/view/scheme",
"exports"
], function (jsUtils, operations, validator, DOMHelper, ConfirmMessage, SchemeView, exports) {
//Load project batch
exports.onReady = function () {
$schemeView = new SchemeView();
$schemeView.render();
}
});
AJS.$(function () {
AJS.$(document).ready(function () {
require("unicorn/sla/project/batch").onReady();
});
});

Model method error while trying to navigate

I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.

How to test this with Jasmine test (Behaviour Driven Development)?

I've just developed this JavaScript/Backbone module as a part of a web page I am developing. I would like to create a Jasmine test for it, but I am brand new to Jasmine, therefore I am not sure what should I be testing in this class. What should be the "skeleton" of the test? In order to avoid redundancy in tests, what parts will you test?
editdestinationview.js:
define([
'common/jqueryex',
'backbone',
'marionette',
'handlebars',
'text!education/eet/templates/editdestination.hb',
'text!common/templates/validationerror.hb',
'lang/languageinclude',
'common/i18nhelper'
], function ($, Backbone, Marionette, Handlebars, templateSource, errorTemplateSource, i18n) {
'use strict';
var errorTemplate = Handlebars.compile(errorTemplateSource),
EditDestinationView = Marionette.ItemView.extend({
initialize: function (options) {
this._destinationTypes = options.destinationTypes;
},
onRender: function () {
this.stickit();
this._bindValidation();
},
_bindValidation: function () {
Backbone.Validation.bind(this, {
valid: this._validAttributeCallback,
invalid: this._invalidAttributeCallback,
forceUpdate: true
});
},
_validAttributeCallback: function (view, attr) {
view.$('#error-message-' + attr).remove();
},
_invalidAttributeCallback: function (view, attr, error) {
view.$('#error-message-' + attr).remove();
view.$('#destinationTypes').parent('div').append(errorTemplate({
attr: attr,
error: error
}));
},
template: Handlebars.compile(templateSource),
ui: {
saveAnchor: '#ed_eetSaveDestinationAnchor',
deleteAnchor: '#ed_eetDeleteDestinationIcon'
},
triggers: {
'click #ui.saveAnchor': 'click:saveDestination',
'click #ui.deleteAnchor': 'click:deleteDestination'
},
bindings: {
'select#destinationTypes': {
observe: 'destinationTypeId',
selectOptions: {
collection: function () {
return this._destinationTypes;
},
labelPath: 'description',
valuePath: 'destinationTypeId',
defaultOption: {label: i18n.EDUCATION_EET_SELECT_INTENDED_DESTINATION, value: null}
}
}
}
});
return EditDestinationView;
});
Thanks everyone!
UPDATE:
After thinking a lot about it, I think that I should try these aspects:
-Triggers: Check if they can be clicked.
-"_validAttributeCallback" and "_invalidAttributeCallback": Check if they behave accordingly to the code.
-Template: Spy on it to check if it is performing it's mission. (Optional test)
So, the test skeleton will be:
define([
'education/eet/views/editdestinationview'
], function (EditDestinationView) {
describe('description...', function () {
beforeEach(function () {
//EditDestinationView.triggers
});
describe('blablabla', function () {
beforeEach(function () {
// ...
});
it('blablabla', function () {
// blablabla
});
});
});
});
Any help on how to test this please?
One common pattern is to use two describe statements, one for the class and one for the method being tested, and then an it statement for each thing you want to test about that method. The rspec people have a convention (which I use in my JS tests) of using a '#' on the method describe for an instance method, and a "." for a describe of a static method.
Now, if you adopt all of the above, and you want to test (for instance) that your View's click-handling method triggers a certain event on the View's Model, it would look something like this:
define([
'education/eet/views/editdestinationview'
], function (EditDestinationView) {
describe('EditDestinationView', function () {
var view;
beforeEach(function () {
// do setup work that applies to all EditDestinationView tests
view = new EditDestinationView({model: new Backbone.Model()});
});
describe('#handleClick', function () {
beforeEach(function () {
// do setup work that applies only to handleClick tests
});
it('triggers a foo event', function () {
var wasTriggered;
view.model.on('foo', function() {
wasTriggered = true;
});
view.handleClick();
expect(wasTriggered).toBe(true);
});
});
});
});
P.S. Instead of creating a fake "foo" handler like I did, most people use a mocking library like Sinon. Using that library our "it" statement could instead be:
it('triggers a foo event', function () {
var triggerStub = sinon.stub(view.model, 'trigger');
view.handleClick();
expect(triggerStub.calledOnce).toBe(true);
expect(triggerStub.args[0][0]).toBe('foo');
//NOTE: args[0][0] == first arg of first call
});

Backbone.js model.save() sending multiple PUT requests

I have a backbone view that loads a model and some templates. When I submit the form in the edit template, backbone successfully sends a PUT request, just as it’s supposed to. On success, I navigate the user back to the view template.
However, if I navigate to the edit route again and submit the form, backbone sends two PUT requests. It then GETs the view template. If I navigate to the edit route a third time, backbone sends three PUT requests. The number of PUT requests keep incrementing every time I submit the form. Why might that be?
Here is my view:
// Filename views/users/edit.js
/*global define:false */
define([
'jquery',
'underscore',
'backbone',
'models/user/UserModel',
'text!templates/users/edit.html',
], function($, _, Backbone, UserModel, UserTemplate) {
var UserEdit = Backbone.View.extend({
el: '#page',
render: function (options) {
var that = this;
if (options.id) {
// modify existing user
var user = new UserModel({id: options.id});
user.fetch({
success: function (user) {
var template = _.template(UserTemplate, {user: user});
that.$el.animate({opacity: 0}, 180, function() {
that.$el.html(template).animate({opacity: 1}, 180);
});
}
});
} else {
// create new user
var template = _.template(UserTemplate, {user: null});
that.$el.animate({opacity: 0}, 180, function() {
that.$el.html(template).animate({opacity: 1}, 180);
});
}
},
events: {
'submit #create-user-form': 'createUser'
},
createUser: function (e) {
var postData = $(e.currentTarget).serializeObject();
var user = new UserModel();
user.save(postData, {
success: function (user, response) {
Backbone.history.navigate('#/users/view/' + response, {trigger: true, replace: true});
}
});
return false;
}
});
return UserEdit;
});
In my case, I could fix it by calling undelegateEvents() on the view in the success callback.
createUser: function (e) {
var postData = $(e.currentTarget).serializeObject(),
user = new UserModel(),
that = this;
user.save(postData, {
success: function (user, response) {
that.undelegateEvents();
Backbone.history.navigate('#/users/view/' + response, {trigger: true});
}
});
return false;
}
Thanks, #dbf.

Javascript module using jQueryUI.autocomplete and Backbone JS

Let's suppose I want to use jQueryUi.autocomplete for making a module which take the source from a backboneCollection.
I implement the following code (1) for the autocomplete module and
the following for the Backbone.view (2).
Actually, I don't like it because the fetching of the collection is performed also when the user does not type any letter.
How should I perform the fetching collection or the source function only when the user starts to type something in the input box?
P.S.:
I have already posted a similar question jQuery Autocomplete Plugin using Backbone JS ,
but since the needs of aoutocomplete module could be shared between different view I decided to move the fetch of the collection in autocomplete module.
(1)
/*global define */
define([
'users',
'jquery',
'jqueryUi'
], function (UserCollection, $) {
"use strict";
var autoComplete = function(element) {
var userCollection,
data;
userCollection = new UserCollection();
userCollection.fetch();
data = userCollection.toJSON();
element.autocomplete({
minLength: 3,
source: function(request, response) {
var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i");
response($.grep(data, function(value) {
return matcher.test(value.name);
}));
},
create: function() {
element.val(data.name);
},
focus: function(event, ui) {
element.val(ui.item.name);
return false;
},
select: function(event, ui) {
element.val(ui.item.name);
return false;
}
}).data('autocomplete')._renderItem = function(ul, item) {
return $('<li></li>')
.data('item.autocomplete', item)
.append('<a><img src="' + item.avatar + '" />' + item.name + '<br></a>')
.appendTo(ul);
};
};
return autoComplete;
});
(2)
// View1 (view using the module autocomplete)
define([
'autoCompleteModule'
], function (autoCompleteModule) {
var MyView = Backbone.View.extend({
events: {
'focus #names': 'getAutocomplete'
},
getAutocomplete: function (e) {
autoCompleteModule($('#names'));
}
});
});
//Pseudo code from jQuery Autocomplete Plugin using Backbone JS
var MyView = Backbone.View.extend({
initialize: function () {
this.myCollection = new MyCollection();
},
events: {
'focus #names': 'getAutocomplete',
'keydown #names':'invokefetch'
},
invokefetch : function(){
this.myCollection.fetch();
$("#names").unbind( "keydown", invokefetch);
},
getAutocomplete: function () {
$("#names").autocomplete({
source: JSON.stringify(this.myCollection)
});
}
});
EDIT-20120711---------
how about this:
//Pseudo code ...
// View1 (view using the module autocomplete)
define([
'autoCompleteModule'
], function (autoCompleteModule) {
var MyView = Backbone.View.extend({
events: {
'keydown #names': 'getAutocomplete'
},
getAutocomplete: function (e) {
var el = $('#names');
if(!el.data){
autoCompleteModule(el);
}
}
});
});

Categories