Backbone.js routing depending on model value - javascript

I have a collection that looks something like this
[
{
"year": 1868,
....
]
},
{
"year": 1872,
....
},
{
...
}
]
Is there a way to set a route either with '/year/:year': 'year' or '/(:year)': 'year' ?
I have tried making a lookup table in the main App view, which passes the year index to the model views. I have tried using _.map, _.each, _.pluck and _.where but I guess I must be doing something wrong.
Here is a non Backbone view of what it looks like. So navigating to /(:year) would go straight to that year, which corresponds to an model index
Edit: to clarify, basically I want the user to be able to go to /year/:year, but :year value corresponds to a certain model (see above). In this case going to /year/1868, would render the first model from the above collection.
EDIT #2: Here is how my app looks like.
this is the router
var router = Backbone.Router.extend({
routes: {
'': 'root',
'year(/:year)': 'year'
},
root: function() {
new App();
},
year: function(year) {
new App({
year: year
});
}
});
which calls this file
define(['backbone', 'assets/js/collections/elections.js', 'assets/js/views/election.js', 'jqueryui'], function(Backbone, Elections, ElectionView, simpleSlider) {
var AppView = Backbone.View.extend({
current_election_index: 0,
active_btn_class: 'dark_blue_bg',
wiki_base: 'http://en.wikipedia.org/wiki/United_States_presidential_election,_',
started: 0,
el: 'body',
playback: {
id: '',
duration: 1750,
status: false
},
initialize: function() {
elections = new Elections();
_.bindAll(this, 'render');
this.listenTo(elections, 'reset', this.render);
elections.fetch();
this.remove_loader();
},
render: function () {
if (this.started === 0) {
this.election_year();
}
var view = new ElectionView({
model: elections.at(this.current_election_index),
election_index: this.current_election_index
});
this._make_slider();
this.update_ui();
return this;
},
I took out some of the methods, since they are non essential.

A typical solution would looks something like this:
var AppRouter = Backbone.Router.extend({
routes: {
"year(/:year)" : "electionByYear"
},
electionByYear: function(year) {
//all of your data, wherever you get it from
var results = this.allElectionResults;
//if a year parameter was given in the url
if(year) {
//filter the data to records where the year matches the parameter
results = _.findWhere(results, { year: parseInt(year, 10)});
}
doSomething(results);
}
});
Edit based on comments: If your view is responsible for creating the collection, you should move the above logic to your view, and simply pass the year parameter to your view as an argument:
var View = Backbone.View.extend({
initialize: function(options) {
this.year = options.year;
}
});
var view = new View({year:year});
view.render();
Or if you're using an existing view:
view.year = year;
view.render();

Related

Uncaught Type Error: View is not a constructor

I have Uncaught Type Error : UserRegisterView is not a constructor.I dont understand this error.I looked all code but i dont find it.
Sorry of my bad english.Please help me
Thanks for answer
UPDATED
UserRegisterView is here
var UserRegisterView = Backbone.View.extend({
model: User,
el: '#form',
events: {
'click input[id="infoWeek"]': 'infoWeek',
'click input[id="infoMonth"]': 'infoMonth'
},
infoWeek: function() {
this.$el.find("#dayOfMonth").hide();
this.render();
},
infoMonth: function() {
this.$el.find("#dayOfWeek").hide();
this.render();
}
});
var AddUserView = Backbone.View.extend({
el: $(".page"),
events: {
'click #saveUser': 'saveUser'
},
saveUser: function() {
var user = new User();
user.set({
username: $("#username").val(),
lastName: $("#lastName").val(),
regNumber: $("#regNumber").val(),
password: $("#password").val(),
departmentName: $("#departmentName").val(),
email: $("#email").val(),
role: $("#role").val()
});
user.save();
if (document.getElementById('isOpen').checked) {
user.set("isOpen", $("#isOpen").val("1"));
user.save();
} else {
user.set("isOpen", $("#isOpen").val("0"));
user.save();
}
if (document.getElementById('dayOfWeek').checked) {
user.set("dayOfWeek", $("#dayOfWeek").val());
user.save();
} else if (document.getElementById('dayOfMonth').checked) {
user.set("dayOfMonth", $("#dayOfMonth").val());
user.save();
}
$("#username").val("");
$("#firstName").val("");
$("#lastName").val("");
$("#regNumber").val("");
$("#password").val("");
$("#deparmentName").val("");
$("#email").val("");
$("#isOpen").val("");
$("#dayOfWeek").val("");
$("#dayOfMonth").val("");
},
render: function() {
var that = this;
var template = Handlebars.compile(UserRegister);
var myHtml = template(that.model.toJSON());
that.$el.html(myHtml);
return this;
}
});
return {
AddUserView: AddUserView,
UserRegisterView: UserRegisterView
};
});
router user func.
define([
'jquery',
'underscore',
'backbone',
'handlebars',
'spin',
'app/models/LoginModel',
'app/views/LoginView',
'app/views/UserRegisterView'
], function($,
_,
Backbone,
Handlebars,
Spinner,
Login,
LoginView,
UserRegisterView
) {
var Router = Backbone.Router.extend({
routes: {
'search': 'search',
'login': 'login',
'travels': 'travels',
'user': 'user',
'menu': 'menu',
'': 'home'
},
user: function() {
disposeView(new UserRegisterView().render());
}
dispose.view on util.js
function disposeView(view) {
Backbone.View.prototype.close = function() {
this.unbind();
this.undelegateEvents();
};
/* Şu anki viewi yok et */
if (this.currentView !== undefined) {
this.currentView.close();
}
/* Yeni view oluştur. */
this.currentView = view;
this.currentView.delegateEvents();
return this.currentView;
}
What's happening
Your UserRegisterView module returns an object which contains two constructors.
return {
AddUserView: AddUserView,
UserRegisterView: UserRegisterView
};
When using this module, what you're getting is the object above.
define([
// ...
'app/views/UserRegisterView'
], function(
// ...
UserRegisterView // value of the return in the module
) {
So you're kind of misleading yourself by calling it UserRegisterView as it's not the constructor, but the object containing the constructor.
To get a new UserRegisterView view instance with the current way your module is setup, you'd need to call it like so:
var userView = new UserRegisterView.UserRegisterView();
Or to create a AddUserView instance:
var addView = new UserRegisterView.AddUserView();
Solutions
Split up the module, one for each view constructor.
Change the name so at least it's not misleading (like UserViewsModule)
Other improvements
That being said, there are other improvements that could be made to your Backbone code.
var UserRegisterView = Backbone.View.extend({
// that's useless (if not used) and not a view property.
// model: User,
// don't use `el` like that, especially when using the view as a shared Constructor
el: '#form',
events: {
'click input[id="infoWeek"]': 'onInfoWeekClick',
'click input[id="infoMonth"]': 'onInfoMonthClick'
},
initialize: function() {
// Cache jQuery object of the view's element
this.$dayOfMonth = this.$("#dayOfMonth");
this.$dayOfMonth = this.$("#dayOfMonth");
// also use the shortcut function instead of `this.$el.find()`
}
onInfoWeekClick: function(e) {
this.$dayOfMonth.hide();
// calling render here is useless unless your using it as a parent
// view, where the child view overrides the render function.
},
onInfoMonthClick: function(e) {
this.$dayOfMonth.hide();
}
});
The disposeView function could be simplified:
function disposeView(view) {
var current = this.currentView;
if (current) current.close();
current = this.currentView = view;
current.delegateEvents();
return current;
}
Don't change the default Backbone view prototype each time the function is called. Instead, add the function once.
_.extend(Backbone.View.prototype, {
close: function() {
this.unbind();
this.undelegateEvents();
},
// any other function you want to add can go here.
});
In another answer, I go into details on how to extend Backbone's core classes with requirejs transparently.
You're already using jQuery, so don't use JavaScript DOM API document.getElementById('isOpen') interspersed with jQuery selectors $('#isOpen').
I made some improvements to the following view. Take the time to create yourself some utility functions (like reset and getValues) to simplify the flow of the code and encapsulate the complexity.
var AddUserView = Backbone.View.extend({
el: $(".page"),
events: {
'click #saveUser': 'saveUser'
},
// compile the template once while creating the view class
template: Handlebars.compile(UserRegister),
// get the selector string out of the code and place them in one place
// easy to change and maintain.
fields: {
username: "#username",
firstName: "#firstName",
lastName: "#lastName",
regNumber: "#regNumber",
password: "#password",
deparmentName: "#deparmentName",
email: "#email",
isOpen: "#isOpen",
dayOfWeek: "#dayOfWeek",
dayOfMonth: "#dayOfMonth",
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
// cache jQuery object of every field once after a render
this.field = _.reduce(this.fields, function(fields, selector, key) {
fields['$' + key] = this.$(selector);
return fields;
}, {}, this);
return this;
},
reset: function() {
// reset all the fields once without repeating code.
_.each(this.field, function($field) {
$field.val("");
});
return this;
},
getValues: function(keys) {
// get the value of multiple fields returned in a nice object
// ready to be sent to a Backbone model.
return _.reduce(keys, function(data, key) {
data[key] = this.field[key].val();
return data;
}, {}, this);
},
saveUser: function() {
var field = this.field,
user = new User(this.getValues([
'username',
'lastName',
'regNumber',
'password',
'departmentName',
'email',
'role',
]));
user.set({ isOpen: field.$isOpen.is(':checked') });
if (field.$dayOfWeek.is(':checked')) {
user.set("dayOfWeek", field.$dayOfWeek.val());
} else if (field.$dayOfMonth.is(':checked')) {
user.set("dayOfMonth", field.$dayOfMonth.val());
}
user.save();
this.reset();
},
});
In the following snippet, you're putting the context (this) into a local variable. I see that a lot and I could say that 90% of the times I see it on Stack Overflow questions, it makes no sense. It clearly screams copy-pasted.
render: function() {
var that = this;
// ...
that.$el.html(myHtml);
return this;
}
Please tell me you see that you're putting this into that, then using that throughout the function, then you still return this?!
Putting the context into a local variable is useful when the object is needed in a dynamically created callback.
render: function() {
var that = this; // this is available here
setTimeout(function() {
// here this is not available.
that.handleCallback();
}, 10);
// here we are in the same context as the first line.
return this;
}

Backbone.Paginator infinite mode, with Marionette

In my Marionette app, I have a Collection view, with a childView for it's models.
The collection assigned to the CollectionView is a PageableCollection from Backbone.paginator. The mode is set to infinite.
When requesting the next page like so getNextPage(), the collection is fetching data and assigning the response to the collection, overwriting the old entries, though the full version is store in collection.fullCollection. This is where I can find all entries that the CollectionView needs to render.
Marionette is being smart about collection events and will render a new childView with it's new model when a model is being added to the collection. It will also remove a childView when it's model was removed.
However, that's not quite what I want to do in this case since the collection doesn't represent my desired rendered list, collection.fullCollection is what I want to show on page.
Is there a way for my Marionette view to consider collection.fullCollection instead of collection, or is there a more appropriate pagination framework for Marionette?
Here's a fiddle with the code
For those who don't like fiddle:
App = Mn.Application.extend({});
// APP
App = new App({
start: function() {
App.routr = new App.Routr();
Backbone.history.start();
}
});
// REGION
App.Rm = new Mn.RegionManager({
regions: {
main: 'main',
buttonRegion: '.button-region'
}
});
// MODEL
App.Model = {};
App.Model.GeneralModel = Backbone.Model.extend({});
// COLLECTION
App.Collection = {};
App.Collection.All = Backbone.PageableCollection.extend({
model: App.Model.GeneralModel,
getOpts: function() {
return {
type: 'POST',
contentType: 'appplication/json',
dataType: 'json',
data: {skip: 12},
add: true
}
},
initialize: function() {
this.listenTo(Backbone.Events, 'load', (function() {
console.log('Load more entries');
// {remove: false} doesn't seem to affect the collection with Marionette
this.getNextPage();
})).bind(this)
},
mode: "infinite",
url: "https://api.github.com/repos/jashkenas/backbone/issues?state=closed",
state: {
pageSize: 5,
firstPage: 1
},
queryParams: {
page: null,
per_page: null,
totalPages: null,
totalRecords: null,
sortKey: null,
order: null
},
/*
// Enabling this will mean parseLinks don't run.
sync: function(method, model, options) {
console.log('sync');
options.contentType = 'application/json'
options.dataType = 'json'
Backbone.sync(method, model, options);
}
*/
});
// VIEWS
App.View = {};
App.View.MyItemView = Mn.ItemView.extend({
template: '#item-view'
});
App.View.Button = Mn.ItemView.extend({
template: '#button',
events: {
'click .btn': 'loadMore'
},
loadMore: function() {
Backbone.Events.trigger('load');
}
});
App.View.MyColView = Mn.CollectionView.extend({
initialize: function() {
this.listenTo(this.collection.fullCollection, "add", this.newContent);
this.collection.getFirstPage();
},
newContent: function(model, col, req) {
console.log('FullCollection length:', this.collection.fullCollection.length, 'Collection length', this.collection.length)
},
childView: App.View.MyItemView
});
// CONTROLLER
App.Ctrl = {
index: function() {
var col = new App.Collection.All();
var btn = new App.View.Button();
var colView = new App.View.MyColView({
collection: col
});
App.Rm.get('main').show(colView);
App.Rm.get('buttonRegion').show(btn);
}
};
// ROUTER
App.Routr = Mn.AppRouter.extend({
controller: App.Ctrl,
appRoutes: {
'*path': 'index'
}
});
App.start();
You could base the CollectionView off the full collection, and pass in the paged collection as a separate option:
App.View.MyColView = Mn.CollectionView.extend({
initialize: function(options) {
this.pagedCollection = options.pagedCollection;
this.pagedCollection.getFirstPage();
this.listenTo(this.collection, "add", this.newContent);
},
// ...
}
// Create the view
var colView = new App.View.MyColView({
collection: col.fullCollection,
pagedCollection: col
});
Forked fiddle

BackboneJS Hide Model when date has expired

I have a Backbone App where I display a Collection of Models based on JSON data. Inside the JSON data, I have endDate-which gives me the realtime date. It is based on a competition module. What I want to achieve is that if the given date has expired I want to hide (or even maybe remove) the Model from the collection, so that the competition is no longer available.
So far my competition.js, with the Model in the bottom looks like this:
Competition.View = Backbone.View.extend({
tagName: 'ul',
template: 'competition',
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
},
serialize: function() {
return this.model.toJSON();
}
});
Competition.CompetitionModel = Backbone.Model.extend({
url: function() {
return App.APIO + '/i/contests';
},
comparator: function(item) {
return item.get('endDate');
},
defaults: {
"data": []
}
});
Then in my main module, I import the competition.js, and here I fetch the model and render it in specific HTML element (dont know if its necessary to copy/paste it here for my original question):
function (App, Backbone, Competition) {
var CompetitionOverview = App.module();
CompetitionOverview.View = Backbone.View.extend({
template: 'competitionOverview',
initialize: function(){
this.render();
},
beforeRender: function() {
var competitionModel = new Competition.CompetitionModel();
this.insertView('.singleComp', new Competition.View({model: competitionModel}));
competitionModel.fetch();
},
});
return CompetitionOverview;
}
So, how can I achieve to hide/remove the Models which dates have expired?
thanks in advance...
You state that you have Collection of Models, but your Competition.CompetitionModel extends Backbone.Model instead of Backbone.Collection. In my answer I assume that CompetitionModel is a Backbone.Collection and not a Backbone.Model.
That said, I think you have two options:
Check in your render function of Competition.View whether you should actually show something based on the end-date:
Competition.View = Backbone.View.extend({
tagName: 'ul',
template: 'competition',
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
},
serialize: function() {
return this.model.toJSON();
},
render: function(){
//dependending on in which format your date is you might have to convert first.
if(this.model.get('endDate') < new Date().getTime()){
//render the view
}
});
Or, and I think this is more clean, you check the date as soon as the data comes in from the server. I think backbone triggers an "add" event on the collection when the collection is fetched from the server. So, again, make your Competition Model a Competition Collection and listen to the add event. Change
Competition.CompetitionModel = Backbone.Collection.extend({
initialize: function () {
this.on("add", checkDate)
},
checkDate: function(model, collection, options){
//again, get the right conversion
//but here you should compare your end date with the expire date
if(model.get('endDate') < new Date().getTime()){
this.remove(model.id);
}
},
url: function () {
return App.APIO + '/i/contests';
},
comparator: function (item) {
return item.get('endDate');
},
defaults: {
"data": []
}
});

Backbone: how to get and display just one model data in a view via router

I have created a very basic backbone app, to understand how it works.
In the router, I just wanna display just 1 model, i.e. a user, not the whole collection, by passing an id in the url, how to do that?
For example, I'd like to do someapp.com/app/#user/2, and this would display just user no2 details.
Please see my work in jsfiddle
// router
var ViewsRouter = Backbone.Router.extend({
routes: {
'': 'viewOne',
'one': 'viewOne',
'two': 'viewTwo',
'user/:id': 'user'
},
viewOne: function() {
var view = new TheViewOne({ model: new TheModel });
},
viewTwo: function() {
var view = new UserView({ model: new TheModel });
},
user: function(id) {
// how to get just 1 user with the corresponding id passed as argument
// and display it???
}
});
Many thanks.
https://github.com/coding-idiot/BackboneCRUD
I've written a complete Backbone CRUD with no backend stuff for beginners. Below is the part where we get the user from the collection and show/render it.
View
var UserEditView = Backbone.View.extend({
render: function(options) {
if (options && options.id) {
var template = _.template($("#user-edit-template").html(), {
user: userCollection.get(options.id)
});
this.$el.html(template);
} else {
var template = _.template($("#user-edit-template").html(), {
user: null
});
// console.log(template);
this.$el.html(template);
}
return this;
},
Router
router.on('route:editUser', function(id) {
console.log("Show edit user view : " + id);
var userEditView = new UserEditView({
el: '.content'
});
userEditView.render({
id: id
});
});
Update
Particularly, sticking to your code, the router will look something like this :
user: function(id) {
var view = new UserView({ model: userCollection.get(id) });
}

Backbone: trying to figure out the basic control flow, could someone tell me if this is sane?

I'm building a simple Backbone application with a search form and some results. I'd like to:
allow the user to change the value of an 'order by' select field, and update the URL and results appropriately
ensure that when the user arrives via a preset URL, the correct results are shown, and the selected form elements match the URL and results.
I think I've figured out the right control flow to use, but I'd really like to have someone sanity-check it. Basically, I'm letting my Views update the URLs directly, and then the Router does the work of updating the Model. Other Views then listen to the Model.
It seems to work OK, but I have to construct the URL by hand in the View, and parse it again in the Router, which feels repetitive. So it would just be great to know if this is the right way to do things in Backbone, or if I can do things better.
Here's the pseudocode:
User changes value of select to "price-asc"
|
|
v
Form view hears change event, and
updates the URL to #order-by-price-asc
|
|
v
Router looks at new route, <----- User loads page, with
sets model value #order-by-price-asc route
|
|
v
Results view hears change
to model value, loads in
new results via Ajax
Here is the code:
HTML:
<select id="order">
<option value=''></option>
<option value='price-asc'>price (low to high)</option>
<option value='price-desc'>price (high to low)</option>
</select>
<div id="results"></div>
JS:
var SearchModel = Backbone.Model.extend({
initialize: function() {
this.bind("change:query", function() {
this.performSearch();
});
},
performSearch: function() {
// Will eventually do Ajax search here,
// but for the moment, just bind to query.
this.set("results", this.get("query"));
},
});
var SearchFormView = Backbone.View.extend({
el: $('#order'),
events: {
"change": "updateURL"
},
initialize: function() {
this.model.on("change:results", this.updateFormClasses, this);
},
updateFormClasses: function(model, query) {
this.$el.val(query);
},
updateURL: function(e) {
var url = '';
($('#order').val() !== '') ? url += "/by-" + $('#order').val() : null;
SearchApp.navigate(url, {trigger: true});
}
});
var SearchResultsView = Backbone.View.extend({
el: "#results",
initialize: function() {
this.model.on("change:results", this.displayResults, this);
},
displayResults: function(model, query) {
this.$el.text(query)
}
});
var AppRouter = Backbone.Router.extend({
routes: {
"*str": "index",
},
initialize: function() {
this.searchModel = new SearchModel();
this.searchFormView = new SearchFormView({
model: this.searchModel
});
this.searchResultsView = new SearchResultsView({
model: this.searchModel
});
},
index: function(str) {
var ordering = str.match(/by-.*/gi);
if (ordering !== null) {
this.searchModel.set({"query": ordering[0].replace('by-','')});
}
}
});
var SearchApp = new AppRouter();
Backbone.history.start({});
There are few optimizations you can do:
Use an optional route parameter instead of parsing the url manually:
var AppRouter = Backbone.Router.extend({
routes: {
"*str(/by-:sort)": "index",
},
index: function(str, sort) {
if(sort) {
//...
}
}
});
Consider allowing view to handle the model change, instead of going via the router. In order to avoid code duplication, encapsulate this logic in a model method.
var SearchModel = Backbone.Model.extend({
sort: function(field) {
this.set({query:field});
}
});
var SearchFormView = Backbone.View.extend({
el:"#order",
events: {
"change": "updateSort"
},
updateSort: function(e) {
var sortOrder = this.$el.val();
//update model
this.model.sort(sortOrder);
//navigate without trigger:true, will update the URL but not trigger route
SearchApp.navigate(sortOrder ? "/by-" + sortOrder : "");
}
}
var AppRouter = Backbone.Router.extend({
index: function(str, sort) {
if(sort) {
this.searchModel.sort(sort);
}
}
});
Notice also that I changed the view.el declaration just to #order, and refer to the element with this.$el instead of a jQuery selector. These are unrelated, but still best practices.

Categories

Resources