I have a router on my application now it respond as expected when I type in the url. For exapmple if I type in www.example.com#search/groupa, I get the appropriate result back. I have attempted in the search feature to call navigate to set the url so that a user could cut and paste that and send it to another user. The issue is it doesnt work I get the following error when attempting to do so: "Uncaught TypeError: Object function (){return i.apply(this,arguments)} has no method 'navigate'"
IEG = new Backbone.Marionette.Application();
IEG.addRegions({
searchBox: '#searchBox',
resultBox: '#resultBox',
modalBox: '#modalBox',
recipientBox: '#recipientBox',
confirmBox: '#confirmToggleActive'
});
IEG.vent = _.extend({}, Backbone.Events);
IEG.vent.on("default", function () {
var SBV = new SearchBoxView();
IEG.searchBox.show(SBV);
IEG.searchColl = new GroupEntries();
IEG.searchColl.fetch({
data: {
cmd: 0, //search groups
searchStr: null //if null show all groups
},
success: function (data) {
searchResults = new SearchResultsView({ collection: IEG.searchColl });
IEG.resultBox.show(searchResults);
}
});
});
IEG.vent.on("searchGroups", function (searchStr) {
IEG.Router.navigate("search" + searchStr); // CALLING NAVIGATE HERE
IEG.searchColl.fetch({
data: {
cmd: 0, //search groups
searchStr: searchStr
},
success: function (data) {
searchResults = new SearchResultsView({ collection: IEG.searchColl });
IEG.resultBox.show(searchResults);
}
});
});
IEG.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/:str': 'search',
'edit/:grp': 'edit'
},
index: function () {
IEG.vent.trigger("default");
},
search: function (str)
{
IEG.vent.trigger("searchGroups",str);
}
});
$(document).ready(function () {
IEG.start();
new IEG.Router;
Backbone.history.start();
});
You need to call navigate on the instance of the Router class and not on its definition (as you're currently doing). Try updating the code in your document ready handler like this:
$(document).ready(function () {
IEG.start();
IEG.router = new IEG.Router(); // Store an instance of the router on the Application
Backbone.history.start();
});
And your searchGroups handler like this:
IEG.vent.on("searchGroups", function (searchStr) {
IEG.router.navigate("search" + searchStr); // call navigate on the instance
// Fetch code .....
});
Related
I've got a collection view with two filter methods, and a render method which takes a parameter. The problem I'm stuck with is that when rendering the view for the first time it returns me an error. Here's my collection:
var ResumeCollection = Backbone.Collection.extend({
url: 'http://localhost:3000',
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
}
});
And my view:
var ResumeList = Backbone.View.extend({
events { // hash array of filter events },
initialize: function () {
this.collection.fetch();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function (ev) {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function (ev) {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
},
showAll: function (ev) {
this.render(this.collection);
}
});
This view gets rendered for the first time in my router by passing a collection:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function () {
this.layout = new LayoutView();
}
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
});
And this is the layout view within which all the other views are rendered:
var LayoutView = Backbone.View.extend({
el: $('#outlet'),
render: function (view) {
if (this.child && this.child !== view) {
this.child.undelegateEvents();
}
this.child = view;
this.child.setElement(this.$el).render();
return this;
}
});
When I just refresh my page, I get filtered.toArray is not a function error and nothing is rendered respectively. After inspecting everything in the debugger, I found out that when the view gets rendered for the first time, the filtered attribute receives an empty collection, assigns it to data variable, which becomes an empty array and goes to the body of render function, becoming undefined after that. The mysteries go here: whenever I click items, that are bound to my show* events, they act exactly as expected and render either models where interviewed === false, or true or the whole collection. This looks kinda magic to me and I haven't got the faintest idea what can I do with that.
ADDED: GitHub repo with this project
Your home function on the AppRouter has a typo. You have an extra semi-colon.
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection();
}));
}
Should be
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
I needed to remove it to get the JSFiddle working: https://jsfiddle.net/4gyne5ev/1/
I'd recommend adding some kind of linting tool into your IDE or Build process (http://eslint.org/)
You need to add home url content to your db.json file like this
"" : [
{
'somthing': 'somthing'
}
]
After a piece of advice from my mentor I realized that the core of the problem was in asynchronous origin of fetch method -- as I passed this.collection.fetch in my initialize function, it executed after my render method, not before it, so my render method had just nothing to render when the view was called for the first time. So, this fix worked:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection();
// removed .fetch() method from here
},
render: function (filtered) {
var self = this;
var data;
// and added it here:
this.collection.fetch({
success: function (collection) {
if (!filtered) {
data = collection.toArray();
} else {
data = filtered.toArray();
}
self.$el.html(self.template(collection.toJSON()));
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
})
}
});
}
});
And this worked perfectly and exactly as I needed.
I have a Backbone Marionette app with Router and a Controller. In my app you can view a collection of texts (index route with collection fetching from server), can view existing collection of texts (indexPage route without fetching from server) and can create a new text (form route). Views of list texts and create form are different from each other and changes in region.
I want to add a successully saved model to a collection and then redirect to indexPage route, but what is the best way to get a texts collection from _FormView success callback? Or how to restruct an app to do it simple?
I can send event to a controller with Backbone.Radio but want to deal without it.
Routes
router.processAppRoutes(controller, {
'': 'index',
'index': 'indexPage',
'create': 'form'
});
Controller
_Controller = Marionette.Controller.extend({
initialize: function () {
this.list = new _MainTexts();
},
index: function () {
if (!_.size(this.list)) {
var
self = this;
this.list.fetch({
success: function (collection, response, options) {
self.indexPage();
return;
}
});
}
this.indexPage();
},
indexPage: function () {
var
textsView = new _TextsView({
collection: this.list
});
application.getRegion('contentRegion').show(textsView);
},
form: function () {
var
formView = new _FormView({
model: new _MainText()
});
application.getRegion('contentRegion').show(formView);
}
});
Views
_TextView = Marionette.ItemView.extend({
className: 'item text',
template: function (serialized_model) {
return _.template('<p><%= texts[0].text %></p>')(serialized_model);
}
});
_TextsView = Marionette.CollectionView.extend({
className: 'clearfix',
childView: _TextView
});
Form view
_FormView = Marionette.ItemView.extend({
template: '#form-template',
ui: {
text: 'textarea[name="text"]',
submit: 'button[type="submit"]'
},
events: {
'click #ui.submit': 'submitForm'
},
submitForm: function (event) {
event.preventDefault();
this.model.set({
text: this.ui.text.val()
});
this.model.save({}, {
success: function (model, response, options) {
???
}
});
}
});
Ok, my problem solution is here. In controller action "form" I create event listener
var
formView = new _FormView({
model: model
});
formView.on('formSave', function (model) {
if (id == null) {
self.list.add(model);
}
...
});
Then in form view I trigger event
this.model.save({}, {
success: function (model, response, options) {
if (response.state.success) {
self.trigger('formSave', model);
}
}
});
That's all:)
I get data back from a url: /app/api/assetDetail/{id} where the id is a parameter passed to a presenter which sets up the new assetModel and assetView.
I'm struggling with where to build and call the above url with id and then set the model.
asset presenter
define([
'text!html/regions/tplAssetPage.html',
'views/assetView',
'collections/assets',
'models/asset'
],
function (template, AssetView, Assets, Asset) {
return {
load: function (params) {
$(mv.sections.mainContainer).html(template);
var view1 = 'assetView',
id = params || '';
this.model = new Asset({
wid: params, //sets id on model
url: function(){
var url = 'api/assetDetail/' + params;
return url;
}
});
mv.i.views[view1] = new AssetView({
'el': '#asset-container',
model: asset
});
mv.i.views[view1].setup();
},
};
});
asset model
define([], function () {
return Backbone.Model.extend({
defaults: {
id:''
},
initialize: function () {}
});
});
asset view
define([
'text!html/tplAsset.html',
'models/asset'
], function (template, Asset) {
return Backbone.View.extend({
el: '',
template: _.template(template),
initialize: function () {},
render: function () {
//var data = this.model.toJSON();
this.$el.html(this.template(data));
},
setup: function () {
var self = this;
$.when(self.model.fetch())
.done(function () {
//console.log(self.model.toJSON());
self.render();
})
.fail(function () {
console.log('request for data has failed');
});
},
events: {},
});
});
now getting these errors:
ERR: Routing error Error: A "url" property or function must be specified
at Backbone.View.extend.setup (/js/views/assetView.js:36:22)
$.when(self.model.fetch())
at Object.load (/js/presenters/asset.js:34:23)
mv.i.views[view1].setup();
To set the model url dynamically in your model instance:
var asset = new Asset({
wid: params, //sets id on model
url: function(){
var url = '/app/api/assetDetail/' + this.id;
return url;
}
});
Then, after you have set the url, do asset.fetch()
Note that this will now be the URL for any communication with the server (save and fetch) for that model instance. If you need greater flexibiliy, you'll need to do adjust the Bacbkone sync method for your model.
UPDATE:
Once you've fetched the data you want for the model, you then can call the render function:
render: function () {
this.$el.html( this.template(this.model.toJSON() ) );
return this;
}
This will then render model data in your templates. If you're using the underscore templates it will look like:
<p>some html<span> <%= data %> </span><p>
If you want to check what you have fetched, don't forget that fetch accepts success and error callbacks: http://backbonejs.org/#Model-fetch
So, I am able to validate just fine when I am editing an existing item. However, if I want to create, validation for some reason is not getting kicked off. Instead, I am seeing the errors below:
//this is if the field I want to validate is empty
Uncaught TypeError: Object #<Object> has no method 'get'
//this is if everything in the form is filled out
Uncaught TypeError: Cannot call method 'trigger' of undefined
Here is(what I think is) the relative portion of my js. Sorry if its an overload, I wanted to add as much as I can to be as specific as possible:
Comic = Backbone.Model.extend({
initialize: function () {
this.bind("error", this.notifyCollectionError);
this.bind("change", this.notifyCollectionChange);
},
idAttribute: "ComicID",
url: function () {
return this.isNew() ? "/comics/create" : "/comics/edit/" + this.get("ComicID");
},
validate: function (atts) {
if ("Name" in atts & !atts.Name) {
return "Name is required";
}
if ("Publisher" in atts & !atts.Publisher) {
return "Publisher is required";
}
},
notifyCollectionError: function (model, error) {
this.collection.trigger("itemError", error);
},
notifyCollectionChange: function () {
this.collection.trigger("itemChanged", this);
}
});
Comics = Backbone.Collection.extend({
model: Comic,
url: "/comics/comics"
});
comics = new Comics();
FormView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, "render");
this.template = $("#comicsFormTemplate");
},
events: {
"change input": "updateModel",
"submit #comicsForm": "save"
},
save: function () {
this.model.save(
this.model.attributes,
{
success: function (model, response) {
model.collection.trigger("itemSaved", model);
},
error: function (model, response) {
model.trigger("itemError", "There was a problem saving " + model.get("Name"));
}
}
);
return false;
},
updateModel: function (evt) {
var field = $(evt.currentTarget);
var data = {};
var key = field.attr('ID');
var val = field.val();
data[key] = val;
if (!this.model.set(data)) {
//reset the form field
field.val(this.model.get(key));
}
},
render: function () {
var html = this.template.tmpl(this.model.toJSON());
$(this.el).html(html);
$(".datepicker").datepicker();
return this;
}
});
NotifierView = Backbone.View.extend({
initialize: function () {
this.template = $("#notifierTemplate");
this.className = "success";
this.message = "Success";
_.bindAll(this, "render", "notifySave", "notifyError");
comics.bind("itemSaved", this.notifySave);
comics.bind("itemError", this.notifyError);
},
events: {
"click": "goAway"
},
goAway: function () {
$(this.el).delay(0).fadeOut();
},
notifySave: function (model) {
this.message = model.get("Name") + " saved";
this.render();
},
notifyError: function (message) {
this.message = message;
this.className = "error";
this.render();
},
render: function () {
var html = this.template.tmpl({ message: this.message, className: this.className });
$(this.el).html(html);
return this;
}
});
var ComicsAdmin = Backbone.Router.extend({
initialize: function () {
listView = new ListView({ collection: comics, el: "#comic-list" });
formView = new FormView({ el: "#comic-form" });
notifierView = new NotifierView({el: "#notifications" });
},
routes: {
"": "index",
"edit/:id": "edit",
"create": "create"
},
index: function () {
listView.render();
},
edit: function (id) {
listView.render();
$(notifierView.el).empty();
$(formView.el).empty();
var model = comics.get(id);
formView.model = model;
formView.render();
},
create: function () {
var model = new Comic();
listView.render();
$(notifierView.el).empty();
$(formView.el).empty();
formView.model = model;
formView.render();
}
});
jQuery(function () {
comics.fetch({
success: function () {
window.app = new ComicsAdmin();
Backbone.history.start();
},
error: function () {
}
});
})
So, shouldnt my create be getting validated too? Why isnt it?
When creating a new instance of a model, the validate method isn't called. According to the backbone documentation the validation is only called before set or save.
I am also struggling with this problem and found solutions in related questions:
You could make a new model and then set its attributes (see question 9709968)
A more elegant way is calling the validate method when initializing the model (see question 7923074)
I'm not completely satisfied with these solutions because creating a new instance of the model like described in the backbone documentation shouldn't happen when an error is triggered. Unfortunately, in both solutions you're still stuck with a new instance of the model.
edit: Being stuck with a new instance of the model is actually quite nice. This way you can give the user feedback about why it didn't pass the validator and give the opportunity to correct his/her input.
OK. So, I'm having some mild success here.
First, I wrote my own validation framework, Backbone.Validator since I didn't like any of the ones out there that I found.
Second, I am able to get the validation framework to set off the validation routine by setting silent: false with in the object provided during the new Model creation.
Along with using the use_defaults parameter from my validation framework I am able to override bad data during setup in initial testing. I'm still working on doing some more tests on this, but it seems to be going OK from from the Chrome browser console.
I'm building small one page application with rails 3.1 mongodb and backbonejs.
I have two resources available through json api. I created two models and collections in backbone which look like this
https://gist.github.com/1522131
also I have two seprate routers
projects router - https://gist.github.com/1522134
notes router - https://gist.github.com/1522137
I generated them with backbonejs-rails gem from github so code inside is just template. I initialize my basic router inside index.haml file
#projects
:javascript
$(function() {
window.router = new JsonApi.Routers.ProjectsRouter({projects: #{#projects.to_json.html_safe}});
new JsonApi.Routers.NotesRouter();
Backbone.history.start();
});
I don't want fetch notes when application is starting, because there is big chance that user will never look inside notes. So there isn't good reason to fetch it on start. Inside NotesRouter in all action I rely on #notes variable but without .fetch() method this variable is empty. Also I should can reproduce notes view from url like
/1/notes/5
project_id = 1
note_id = 5
What is best practices in backbonejs to solve this kind of problem ?
Why don't you lazy load the notes when it's requested? Here's an example:
var State = Backbone.Model.extend({
defaults: {
ready: false,
error: null
}
});
var Note = Backbone.Model.extend({
initialize: function () {
this.state = new State();
}
});
var Notes = Backbone.Collection.extend({
model: Note,
initialize: function () {
this.state = new State();
}
});
var NoteCache = Backbone.Model.extend({
initialize: function () {
this._loading = false;
this._loaded = false;
this._list = new Notes();
},
_createDeferred: function (id) {
var note = new Note({ id: id });
this._list.add(note);
this._load();
return note;
},
getNote: function (id) {
return this._list.get(id) || this._createDeferred(id);
},
getNotes: function () {
if (!this._loaded)
this._load();
return this._list;
},
_load: function () {
var that = this;
if (!this._loading) {
this._list.state.set({ ready: false, error: null });
this._loading = true;
$.ajax({
url: '/api/notes',
dataType: 'json',
cache: false,
type: 'GET',
success: function (response, textStatus, jqXHR) {
_.each(response.notes, function (note) {
var n = that._list.get(note.id);
if (n) {
n.set(note);
} else {
that._list.add(note, { silent: true });
n = that._list.get(note.id);
}
n.state.set({ ready: true, error: null });
});
that._list.state.set({ ready: true, error: null });
that._list.trigger('reset', that._list);
that._loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
that._list.state.set({ error: 'Error retrieving notes.' });
that._list.each(function (note) {
note.state.set({ error: 'Error retrieving note.' });
});
},
complete: function (jqXHR, textStatus) {
that._loading = false;
}
});
}
}
});
In this example, I'm defining a NoteCache object that manages the lazy loading. I also add a "state" property to the Note model and Notes collection.
You'll probably want to initialize NoteCache somewhere (probably inside your route) and whenever you want a note or notes, just do this:
var note = noteCache.getNote(5);
var notes = noteCache.getNotes();
Now inside your view, you'll want to listen for state changes in case the note/notes is not loaded yet:
var NoteView = Backbone.View.extend({
initialize: function(){
this.note.state.bind('change', this.render, this);
},
render: function(){
if (this.note.state.get('error') {
// todo: show error message
return this;
}
if (!this.note.state.get('ready') {
// todo: show loader animation
return this;
}
// todo: render view
return this;
}
});
I haven't tested this, so there may be some bugs, but I hope you get the idea.