Javascript module using jQueryUI.autocomplete and Backbone JS - javascript

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);
}
}
});
});

Related

Updating collection and view in Backbonejs

I've created a search bar, but when the data is gathered from the user, it displays the default data over again rather then the users new search criteria.
I'm resetting the collection and giving it a new URL when the user searches, but it doesn't seem to update correctly, and I'm having trouble figuring out where my problem(s) are.
(function(){
'use strict';
var red = red || {};
//model////////////////////////////////////////////////
red.RedditModel = Backbone.Model.extend({
defaults: {
urlTarget: $('#textBox').val(),
urlStart: 'https://www.reddit.com/r/',
urlEnd: '.json'
},
initialize: function() {
this.on('change:urlTarget', function() {
console.log('The Url Target has changed to ' + this.get("urlTarget"));
});
this.on('change:concatURL', function() {
console.log('The model Url has changed to ' + this.get("concatURL"));
});
this.on('change:url', function() {
console.log('The collection url has changed to: ' + this.get('url'));
});
}
});
var redditModel = new red.RedditModel();
var fullURL = new red.RedditModel({
concatURL: redditModel.attributes.urlStart + redditModel.attributes.urlTarget + redditModel.attributes.urlEnd
});
var listElmement,
$list = $('.list');
//collections//////////////////////////////////////////
red.redditCollection = Backbone.Collection.extend({
model: red.RedditModel,
url: fullURL.attributes.concatURL,
parse: function(response) {
var redditData = response.data.children;
return redditData;
}
});
//view////////////////////////////////////
red.RedditView = Backbone.View.extend({
model: fullURL,
collection: redditCollection,
el: '.searchBar',
events: {
'click .searchButton': function(e) {
this.updateModel(e);
this.updateCollection(e);
},
'change #textBox': 'initialize'
},
updateModel: function() {
this.$urlTarget = $('#textBox').val()
this.model.set('urlTarget', this.$urlTarget);
this.model.set('concatURL', redditModel.attributes.urlStart + this.$urlTarget + redditModel.attributes.urlEnd);
},
updateCollection: function() {
this.collection.reset();
this.$urlTarget = $('#textBox').val();
var newUrl = redditModel.attributes.urlStart + this.$urlTarget + redditModel.attributes.urlEnd;
this.collection.add({ urlTarget: this.$urlTarget });
this.collection.add({ url: newUrl });
console.log(newUrl);
},
tagName: 'li',
className: 'listItems',
initialize: function() {
$list.html('');
this.collection.fetch({
success: function(redditData) {
redditData.each(function(redditData) {
redditData = redditData.attributes.data.title
listElmement = $('<li></li>').text(redditData);
$list.append(listElmement);
})
}
});
},
render: function() {
}
});
var redditCollection = new red.redditCollection({
redditModel,
fullURL
});
var myRedditView = new red.RedditView({
model: redditModel,
collection: redditCollection
});
$('.page').html(myRedditView.render());;
})();
Parse within the model, and use it for its intended purpose. No need to store the reddit url and other search related info in a model.
red.RedditModel = Backbone.Model.extend({
parse: function(data) {
return data.data;
},
})
Since you already take care of the reddit url here. Don't be afraid to make yourself some utility functions and getters/setters in your Backbone extended objects (views, model, collection, etc).
red.RedditCollection = Backbone.Collection.extend({
url: function() {
return 'https://www.reddit.com/r/' + this.target + this.extension;
},
initialize: function(models, options) {
this.extension = '.json'; // default extension
},
setExtension: function(ext) {
this.extension = ext;
},
setTarget: function(target) {
this.target = target;
},
parse: function(response) {
return response.data.children;
}
});
Don't be afraid to have a lot of views, Backbone views should be used to wrap small component logic.
So here's the item:
red.RedditItem = Backbone.View.extend({
tagName: 'li',
className: 'listItems',
render: function() {
this.$el.text(this.model.get('title'));
return this;
}
});
Which is used by the list:
red.RedditList = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
},
render: function() {
this.$el.empty();
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(model) {
var view = new red.RedditItem({ model: model });
this.$el.append(view.render().el);
}
});
And the list is just a sub-component (sub-view) of our root view.
red.RedditView = Backbone.View.extend({
el: '.searchBar',
events: {
'click .searchButton': 'onSearchClick',
},
initialize: function() {
// cache the jQuery element for the textbox
this.$target = $('#textBox');
this.collection = new red.RedditCollection();
this.list = new red.RedditList({
collection: this.collection,
// assuming '.list' is within '.searchBar', and it should
el: this.$('.list'),
});
},
render: function() {
this.list.render();
return this;
},
onSearchClick: function(e) {
this.collection.setTarget(this.$target.val());
console.log(this.collection.url());
this.collection.fetch({ reset: true });
},
});
Then, you only need the following to use it:
var myRedditView = new red.RedditView();
myRedditView.render();
Notice the almost non-existent use of the global jQuery selector. If you're using Backbone and everywhere you're using $('#my-element'), you're defeating the purpose of Backbone which is, in part, to apply MVC concepts on top of jQuery.
Some notes on the code posted
Take time to understand what's going on. There are several lines of code in your question that doesn't do anything, or just don't work at all.
Though it's been removed in your answer, the following doesn't make sense because the collection constructor is Backbone.Collection([models], [options]) and what you have here translates to passing an options object (using ES6 shorthand property names { a, b, c}) to the models parameter.
var redditCollection = new red.redditCollection({
redditModel,
fullURL
});
This line does nothing, because .render() doesn't do anything and doesn't return anything.
$('.page').html(myRedditView.render());
Here, you're creating a new element manually using jQuery while you have Backbone which does this for you.
$('<li></li>').text(redditData);
Don't use the attributes directly, always use .get('attributeKey') unless you have a good reason not to.
redditModel.attributes.urlStart
Favor local variables whenever you can. The listElement var here is defined at the "app" level without a need for it.
listElmement = $('<li></li>').text(redditData);
$list.append(listElmement);
A Backbone collection is automatically filled with the new instances of models on success. You do not need to re-parse that in the success callback (in addition to the ambiguity with redditData).
this.collection.fetch({
success: function(redditData) {
redditData.each(function(redditData) {
redditData = redditData.attributes.data.title;
I don't mean to be rude and I took the time to write that long answer to try to help, you, and any future reader that comes by.

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
});

How to call a backbone view function from another view in separate files

I have two backbone views defined in two separate files namely:
LandingView.js
define([
'jquery',
'underscore',
'backbone',
'marionette',
'text!templates/landing/landingTemplate.html',
'text!templates/invitations/invitationsTemplate.html',
'views/invitations/InvitationsView',
], function ($, _, Backbone, Marionette, landingTemplate, invitationsTemplate, InvitationsView) {
var LandingView = Backbone.View.extend({
el : $("#landing"),
id : 'landing',
transition : 'slide',
initialize : function () {
this.GetNotificationsCounts();
},
events : {
'click #invitations' : 'onInvitations',
},
render : function () {
var that = this;
$('.menu li').removeClass('active');
$('.menu li a[href="#"]').parent().addClass('active');
this.$el.html(landingTemplate);
},
cleanup: function() {
this.undelegateEvents();
$(this.el).empty();
},
onInvitations : function () {
//do something
},
GetProfile: function (userLogin) {
// do something
},
GetNotificationsCounts: function () {
// do something
},
formatAccountName: function () {
//do something
}
});
return LandingView; });
Then there is another file InvitationsView.js
define([
'jquery',
'underscore',
'backbone',
'marionette',
'views/landing/LandingView',
'text!templates/invitations/invitationsTemplate.html',
], function ($, _, Backbone, Marionette, LandingView, invitationsTemplate ) {
var InvitationsView = Backbone.View.extend({
el : $("#invitations"),
id : 'invitations',
transition : 'slide',
initialize : function () { debugger;
this.$el.attr('data-transition', this.transition);
this.currentUserLogin = currentUserLogin;
var that = this;
},
events : {
},
render : function () { debugger;
$('.menu li').removeClass('active');
$('.menu li a[href="#"]').parent().addClass('active');
this.GetUserInvitationDetails();
this.$el.html(invitationsTemplate);
},
cleanup: function() {
this.undelegateEvents();
$(this.el).empty();
},
GetUserInvitationDetails: function () {
var landingView = new LandingView();
currentUserName= landingView.formatAccountName();
curUser = currentUserName.replace("\\", "^").replace("^", "%5E");
var profilemsg = landingView.GetProfile(currentUserName);
},
});
return InvitationsView;});
Now I need to call the formatAccountName and GetProfile functions defined in the first JS to the second JS. I am unable to do that. I get errors.
When I try
var landingView = new LandingView();
currentUserName= landingView.formatAccountName();
This also fails. Can somebody help me in this regard and tell me how can I achieve this
Your current approach of calling the formatAccountName method works. The following jsfiddle shows this:
http://jsfiddle.net/v4h11qac/
The problem is likely caused by another error that has not been handled correctly, resulting in the code not being run. You should fix the existing errors and the method call should work as expected.
Orignal Answer:
You could call the method directly on the prototype object:
LandingView.prototype.formatAccountName();
If you need to pass through a new context you can use the call or apply method as below:
LandingView.prototype.formatAccountName.call(context);
A better approach might involve creating a helper module that can be shared by both views.
var viewHelpers = {
formatAccountName: function(account) {
// ...
}
};
var ViewOne = Backbone.View.extend({
render: function() {
var formattedName = viewHelpers.formatAccountName(this.model);
// ...
}
};
var ViewTwo = Backbone.View.extend({
render: function() {
var formattedName = viewHelpers.formatAccountName(this.model);
// ...
}
};
You could also use a system bus, however that may be a little too heavy for such a use case. If you want to take a look at that path, then Backbone.Radio provides a request/response pattern which could be used to fulfill this requirement.
You could use a global event dispatcher that you declare in some kind of main-js-file like this:
var vent = _.extend({}, Backbone.Events);
Then in your view, listen for an event and run function.
vent.on('your:event',this.function_to_run_from_other_view,this);
Dispatch the event like this from the other view.
vent.trigger('your:event',args)

Create new Backbone Views in click event of Backbone View

in the click event of the PodcastView I would like to create multiple new PodcastItemView objects. The jquery $.get works flawlessly, btw.
If I do console.debug(this.pinfo) in the start-function, I receive the JSON array of my podcast items (title, desc, url,...), so this is not the problem. Also it iterates x times through this array, so this works, too.
PodcastView = Backbone.View.extend({
tagName: "li",
itemTpl: _.template($("#podcast-item").html()),
events: {
"click .p-click" : "start"
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
render: function() {
this.$el.html(this.itemTpl(this.model.toJSON()));
return this;
},
start: function() {
$.get(restUri + "podcast/" + this.model.get("title") + "/items", _.bind(function(data) {
this.pinfo = data;
_.each(this.pinfo, function(o) {
var v = new PodcastItemView({model: o});
$("#podcast-items").append(v.render().el);
}, this);
}));
}
});
What does not work, however, is the the creation of new PodcastItemView and to append them to #podcast-items. I get the following error:
TypeError: obj[implementation] is not a function (Backbone.js:225)
This is my PodcastItemView.
PodcastItemView = Backbone.View.extend({
tagName: "div",
itemTpl: _.template($("#podcast-item-list").html()),
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
render: function() {
this.$el.html(this.itemTpl(this.model.toJSON()));
return this;
}
});
I am thankful for every tip or response.
Rewrite the start function to:
start: function() {
$.get(restUri + "podcast/" + this.model.get("title") + "/items", _.bind(function(data) {
this.pinfo = data;
_.each(this.pinfo, function(o) {
var model = new Backbone.Model(o)
var v = new PodcastItemView({model: model});
$("#podcast-items").append(v.render().el);
}, this);
}));
As mentioned in comments your code fails because you are trying to bind native object to the view instead of Backbone.Model.
Hope this helps.

Marionette CompositeView called on every element add from collection, which belongs to the CompositeView... Why?

The problem is that every single time I add a new object through the fetch function of a collection (MovieCollection) on each element add some sort of event triggers and it initialises a new MovieList object to which the collection was passed after it has fetched all the items.
Here is my app.js:
// Global App skeleton for backbone
var App = new Backbone.Marionette.Application();
_.extend(App, {
Controller: {},
View: {},
Model: {},
Page: {},
Scrapers: {},
Providers: {},
Localization: {}
});
// set database
App.addRegions({ Window: ".main-window-region" });
App.addInitializer(function(options){
var mainWindow = new App.View.MainWindow();
try{
App.Window.show(mainWindow);
} catch(e) {
console.error("Couldn't start app: ", e, e.stack);
}
});
So basically I create the MainWindow View which looks like:
(function(App) {
"use strict";
var MainWindow = Backbone.Marionette.LayoutView.extend({
template: "#main-window-tpl",
id: 'main-window',
regions: {
Content: '#content',
},
initialize: function() {
//Application events
//App.vent.on('movies:list', _.bind(this.showMovies, this));
},
onShow: function() {
this.showMovies();
//App.vent.trigger('main:ready');
},
showMovies: function(e) {
var browser = new App.View.MovieBrowser();
this.Content.show(browser);
}
});
App.View.MainWindow = MainWindow;
})(window.App);
Here is the MovieBrowser which should make the collection, fetch it and then on the show would pass it to the MovieList.
(function(App) {
'use strict';
var MovieBrowser = Backbone.Marionette.LayoutView.extend({
template: '#movie-browser-tpl',
className: 'movie-browser',
regions: {
MovieList: '.movie-list-region'
},
initialize: function() {
console.log('Init MovieBrowser');
this.movieCollection = new App.Model.MovieCollection([], {});
this.movieCollection.fetch();
},
onShow: function() {
var ml = new App.View.MovieList({
collection: this.movieCollection
});
this.MovieList.show(ml);
}
});
App.View.MovieBrowser = MovieBrowser;
})(window.App);
So in turns the MovieCollection is (with I guess a major problem being something with it):
(function(App) {
"use strict";
var MovieCollection = Backbone.Collection.extend({
model: App.Model.Movie,
initialize: function(models, options) {
console.log('Init MovieCollection');
},
fetch: function() {
this.add([{"imdb":"1598172","title":"Once Upon a Time in Brooklyn","year":2013,"MovieRating":"4.1","image":"http://zapp.trakt.us/images/posters_movies/217035-300.jpg","bigImage":"http://zapp.trakt.us/images/posters_movies/217035-300.jpg","torrents":{"1080p":{"url":"https://yts.re/download/start/739F9B2F114DB4B48D34DEE2787725FF0747F6F3.torrent","size":"1768399511","seed":"2712","peer":"1709"},"720p":{"url":"https://yts.re/download/start/E482DC66BA1117F3706FACD0292BD32F1CDFE5F5.torrent","size":"854590014","seed":"1553","peer":"828"}},"backdrop":"http://zapp.trakt.us/images/fanart_movies/217035-940.jpg","synopsis":"After being released from prison, Bobby goes back to the mob connected streets. When forced to make a life altering decision the truth is revealed that he was too blind to see.","genres":[],"certification":"R","runtime":116,"tagline":"","trailer":""}]);
},
});
App.Model.MovieCollection = MovieCollection;
})(window.App);
And MovieList is:
(function(App) {
"use strict";
var SCROLL_MORE = 200;
var ErrorView = Backbone.Marionette.ItemView.extend({
template: '#movie-error-tpl',
onBeforeRender: function() {
this.model.set('error', this.error);
}
});
var MovieList = Backbone.Marionette.CompositeView.extend({
template: '#movie-list-tpl',
tagName: 'ul',
className: 'movie-list',
itemView: App.View.MovieItem,
itemViewContainer: '.movies',
initialize: function() {
console.log('Init MovieList')
if (typeof this.collection !== 'undefined') {
//this.listenTo(this.collection, 'loading', this.onLoading);
//this.listenTo(this.collection, 'loaded', this.onLoaded);
//this.collection.fetch();
} else {
console.trace()
}
},
});
App.View.MovieList = MovieList;
})(window.App);
This is the MovieItem:
(function(App) {
"use strict";
var MovieItem = Backbone.Marionette.ItemView.extend({
template: '#movie-item-tpl',
tagName: 'li',
className: 'movie-item',
ui: {
coverImage: '.cover-image',
cover: '.cover'
}
});
App.View.MovieItem = MovieItem;
})(window.App);
With finally the Movie model being:
(function(App) {
"use strict";
var Movie = Backbone.Model.extend({
idAttribute: 'imdb',
initialize: function() {
},
});
App.Model.Movie = Movie;
})(window.App);
So in order to run it I basically have to check if collection is undefined. To see if it would continue and it does but the application itself is not working properly as it creates several MovieList items adding several ul elements with the same id.
here is the html templates:
http://jsfiddle.net/tarazo8e/
and this is the order of inclusion of the JS files:
<!-- App Initialization -->
{{ HTML::script('js/App/app.js') }}
<!-- Backbone Views and Controllers -->
{{ HTML::script('js/App/views/main_window.js') }}
{{ HTML::script('js/App/views/movie_browser/movie_browser.js') }}
{{ HTML::script('js/App/views/movie_browser/movie_item.js') }}
<!-- Backbone Models -->
{{ HTML::script('js/App/models/movie.js') }}
{{ HTML::script('js/App/models/movie_collection.js') }}
{{ HTML::script('js/App/views/movie_browser/movie_list.js') }}
I tried moving movie_list.js at almost any position with no luck.
Any help would be so so so much appreciated as this is driving me crazy for the past 5 days.
Credits: The code has been taken from an old version of Popcorn-Time for only learning purposes.
Edit:
Live versions that shows the exact error:
http://googledrive.com/host/0Bxf4SpRPErmGQmdKNmoyaEVSbmc
Try to swap moview_browser.js and moview_item.js. ItemView should be defined first.
The default rendering mode for a CompositeView assumes a hierarchical, recursive structure. If you configure a composite view without specifying an childView, you'll get the same composite view class rendered for each child in the collection.

Categories