I've a collection Days actually shared by 2 collections - collection view v1 for adding or removing days (edit) and composite view v2 that displays clickable links to navigate to the individual day's itemViews.
AppointmentManager = new Marionette.Application //app object.
//sharing collection days among two views v1, v2.
var days = new Entities.Days(ApptModel.get("apptDays"));
var v1 = new EditCollectionView({collection: days});
var v2 = new ListCompositeView({collection: days});
To delete say appointments for day 2, user clicks on the delete button on dayItemView2 in collectionview v1, which also causes the day to be deleted from collection view v1, like so:
onDeleteDayClicked() {
this.model.collection.remove(this.model);
}
This deletion also gets reflected in the view v2, as collection is shared across these two views. Day 2's navigation link is deleted from v2 automatically by Marionette.
The collection Days listens on this remove event in initialize. To ensure that these changes are reflected and saved serverside (along with other bits of info stored as part of the larger model that also stores collection of days), I trigger on the application manager a save event:
Entities.Days = Backbone.Collection.extend({
initialize: function(options) {
this.on("remove", function(model,collection,index) {
AppointmentManager.trigger("appts:save");
}
});
However in my ApptController, I receive the apps:save event twice. I've checked that collection Days receives only one delete event and only one model is deleted , hence appts:save trigger is called ones.
ApptManager.listenTo(ApptManager, "appts:save", function() {
console.log("Saving appts!");
appts.set("days", days);
appts.save();
});
"Saving appts" gets printed twice and appts PUT twice!
Any clues?!
Using Marionette v1.1.0.
I've got other modules and apps that display header navbar view and other pages. My application structure is based on Backbone.Marionette.js: A Gentle
Introduction by David Sulc - although I doubt if this packaging structure is of any relevance to this problem.
How exactly you sharing your collection?
Add console log to your collection initialize
Probably intitialize method called twice
Related
I have a list of items. They are stored in backbone pageable collection.
They are displayed like this
|---item1---------------------------|
|---item2---------------------------|
|---item3---------------------------|
|---item4---------------------------|
|---item5---------------------------|
|---item6---------------------------|
|---item7---------------------------|
<< 1,2,3...end >>
User can click on individual item to open detail view in a separate page. Detail view has listeners initialized
when it's created. Those listeners are bound to the item model.
Since the detail view is huge, I cache it in the DOM by toggling the visibility.
The subsequent click on the item will toggle the cached view.
------ here is the problem -----
When item list is switched to another page, the collection is reset (by paginator). And all the models previously stored in the collection is dereferenced and
a new set of models is created. So after the page is switched back and forth, the previously opened item has a different copy of itself stored
in the collection. So when I change the name of the item in the detail view (in the view cache), the name in the item list is not changed.
The views are out of sync! because they are referencing to different models.
Not sure if anyone else encounter this before. If you do, please share with me how you solve it.
Thanks very much.
The most straight-forward way to maintain a fresh reference between your list view items and the corresponding detail view, on page change, is to re-render the detail view. But I'm assuming this options is not acceptable within the scope of your project.
What I often do, when I have the task of forming relationships within logically separate views is use listeners. As long as the views share a unique identifier (for example, they both share a model, or at least identical model ids), I can always send a message that will reach the view I'm interested in.
For this you'll need a centralized event hub, which with Backbone is trivially easy to generate. In some appropiately global variable (like, for example, MyApp) we simply do:
MyApp.EventBus = _.extend({}, Backbone.Events);
Set up the detail view
On the detail view initialize function I would drop this listener,
initialize: function () {
// Listen to a toggle visibility on this view
this.listenTo(MyApp.EventBus, 'detail-view:toggle-view', toggleView);
},
toggleView: function (id) {
if (this.model.id == id) {
// Show this view if I have the passed id
this.$el.show()
// Notify the parent list item view that its detail view exists
MyApp.EventBus.trigger('detail:view:exists', true);
} else {
// Hide all other views
this.$el.hide();
}
},
changeName: function () {
// logic that parses DOM user input to
// local variable name
// We now trigger an event 'detail-view:change:name', and we send as
// parameters our model's id and the new name
MyApp.EventBus.trigger('detail-view:change:name', this.model.id, name);
}
Setting up the list item view
The list item view will want to listen to a name change (or any other model property in the detail view that you want the list item to be aware of). So we'll set up a handler for the 'detail-view:change:name' event.
We'll also want to wire our click handler to toggle the visibility of the list item's detail view. The tricky part is to handle the event that a view has not been rendered yet (I'm assuming you're lazy loading the detail view). So we set up a second listener for the detail:view:exists event the detail view triggers when it catches a detail-view:toggle-view event. If we don't hear the detail:view:exists event from the targeted detail view in a timely manner (I'm using 100 ms, but you can play around with that to suit your needs), then we render the view.
initialize: function () {
// Listen to when the detail associated with this list item changes
// the the list item name
this.listenTo(MyApp.EventBus, 'detail-view:change:name', onNameChange);
// Set a property in this view if its detail view exists
this.listenTo(MyApp.EventBus, 'detail:view:exists',
_.bind(function () { this.detailViewExists = true; }, this));
// Create a debounced function that tests whether this view's
// detail view exists
_.debounce(_.bind(this.handleViewState, this), 100);
},
events {
click: 'toggleDetailView'
},
toggleDetailView: function (id) {
MyApp.EventBus.trigger('detail-view:toggle-view', this.model.id);
this.handleViewState();
},
// Debounced function that will wait 100ms asynchronously for the
// detail view to respond. If the detailViewExists bit is not set to true
// then we assume the view does not exist and we render it
handleViewState: function () {
if (!this.detailViewExists)
// The view does not exist, render and attach the view
// Set the bit to false to allow testing in the event that the detail view
// is destroyed in the future
this.detailViewExists = false;
},
changeName: function (id, newname) {
if (this.model.id == id) {
// Change the name of this list item view
this.$('.item-name').text(newname);
}
The take-away
Now, the reference between these two disparate views is the shared unique identifier. Since, by design, these two identifiers are unique in their scope, and should not change, and assuming the detail view has been rendered and attached to the DOM, then regardless of the rendering its state the list item view will always be able to communicate with its detail view.
I am fetching a collection from the server, lets say I originally start with 30 models and in the database one of these models has some of attributes changed, when I fetch the collection the change is detected and the changes are rendered. Fine works just fine.
But when the model is delete in the database and the collection which had 30 and now has 29 does not fire destroy on the missing model. The model does not exist anymore but the view is still rendered and it does not correspond to any model, because the model is not part of the collection anymore. Need help with this one. And the view is binded to "change" and also "destroy".
I already tried all kinds of stuff, many variations in code and nothing seems to work.
Thanks
var commentCollection = new CommentList;
commentCollection.fetch({ data: $.param({ user_id:id}), success: function(){
Profile_view = new Profile({collection: commentCollection});
$("div.Profile_container").html(this.Profile_view.el);
} });
function fetch_collection(commentCollection, id){
//commentCollection.reset();
commentCollection.fetch({update: true, data: $.param({ user_id:id})});
console.log(commentCollection)
}
setInterval(function(){fetch_collection(commentCollection, id)},10000);
I got it!!!!!
All I had to do was bind the remove event to the view function that removes the actual view from the DOM
this.model.bind("remove", this.close, this)
I'm working on a sample ToDo list project in Backbone and I'd like to understand how the framework would prefer me to organize its views and models in the nested list scenario.
To clarify what I mean by that, my single-page Backbone app should display lists of ToDo lists. From the backend standpoint, there's a List resource and an Item (a single entry in a todo list) resource. Something along the lines of:
Monday chores
Pick up the mail
Do the laundry
Pick up drycleaning
Grocery list
Celery
Beef
You get the idea...
Since mine is a Rails 3.2 app, I'm vaguely following the Railscasts Backbone.js tutorial, so that's where I'm getting the current design from. I would love to know if I'm wildly off the Backbone-prescribed pattern, or if I'm on the right track!
I thus far have:
ListsIndex View //index of all lists
\-- ListsCollection
\-- ListView / Model //individual list
\-- ItemsIndex View //index of items in one list
\-- ItemsCollection
\-- Item View / Model //individual todo item
The flow would be:
On router initialize, fetch() collection of lists on /lists backend route. On the 'reset' event for the collection part of ListsIndex, execute render() on each of the items in the collection, appending to the list index view template.
In the initialize method of each Item View (is this where you'd wire-up the second level fetch?) fetch() the items from the /lists/:id/items backend route into an ItemsCollection specific to that view.
In the same method, instantiate an ItemsIndex object and pass the collection into it. Once again, in ItemsIndex, have a 'reset' event handler for when the collection is populated, at which point it should render each fetched model from the item collection and append them to its own view.
I'm essentially taking the design of the List and mirroring it down one level to its items. The difference is that I no longer have a router to rely on. I therefore use the initialize method of ListView to a similar effect.
Yay / nay? Super wrong? Thanks!
TL:DR; 1) I would bootstrap your initial data instead of a fetch() reset(). 2) You can do a fetch in the initialize of a View as you need it. Or you could load the data at the start. Just remember that if you fetch in the init, the async nature won't have the data ready at render. Not a problem if you have a listener waiting for that sync/add/etc. 3) I don't know what you mean by itemIndex object but you can create objects and add to them collections as you need them. Or you can just bake the in at the start if you know all your lists are going to have a collection eventually. You can reset if you want (fetch automatically does this unless you give it option {add:true}) or just add them in one by one as they come in although reset(), remove prior views, render all views seems to be the common way people do things with a complete fetch().
I think it looks pretty good. The nice thing about Backbone is that you can do it many different ways. For example, your number 2 says to wire up a second fetch() from the view. You could do that if you want to lazy load. Or you could just grab all the data at app start before anything is done. It's really up to you. This is how I might do it.
This is how I might make an app like this (just my preference, I don't know that it's any better or worse or if its the same as you described.)
First I would create a model called ListModel. It would have an id and a name attr. This way, you can create many separate lists, each with their own id that you can fetch individually.
Each ListModel has an ItemsCollection inside of it. This collection has a url based on the ListModel it is a part of. Thus, the collection url for ListModel-1 would be something like /list/1
Finally you have ItemModel which is a resource id and text.
ListCollection
ListModel // Monday Chores
ItemCollection
ItemModel // Mail
ItemModel // Laundry
ItemModel // Drycleaning
ListModel // Grocery
ItemCollection
ItemModel // Celery
ItemModel // Beef
So in this little display you'll notice I didn't put anything to do with views in yet. I don't know if it's more of a conceptual thing but this is what the data hierarchy looks like and your views can be, should be totally independent of it. I wasn't exactly sure how you were including the views up above but I thought this might make it clearer.
As for defining these structures, I think two things.
First, I'd make sure my ListModel is defined in my collection. That way I can use the collection add(hash) to instantiate new models as I produce / add them.
Second, I would define the ListModel so that when one is created, it automatically creates an ItemCollection as a property of that ListModel object (not as an attribute).
So ideally, your ListModels would be like this:
ListModel.ItemCollection
Before the app initializes, I would bootstrap the data in and not fetch(). (This kind of addresses point 1 you make) Ideally, when your Backbone application starts it should have all the necessary data it needs from the get go. I would pass in the head some data like this:
var lists = [listModel-1-hash, listModel-2-hash];
Now when the app fires up, you can instantly create these two lists.
var myLists = new ListCollection();
_.each(lists, function(hash) {
myLists.add(hash); // Assumes you have defined your model in the ListCollection
}
Now your List Collection has all the list models it needs.
Here is where views come in. You can pass in anything to any view. But I might break views down into three things.
AppView, ListModelView, ItemModelView and that's it.
Imagine a structure like this:
<body> // AppView
<ul class="List"> // ListModelView
<li class="Item"></li> // ItemModelView
</ul>
<ul class="List"> // ListModelView
</ul>
</body>
When your start your app and create an AppView, inside AppView you'd generate each ListModelView and append it to the body. Our lists are empty. Maybe when you click on the it lazy loads the items. This is how you'd hook it up.
// In ListModelView
events: {'click':'fetchItems'}
fetchItems: function() {
this.model.itemCollection.fetch(); // Assumes you passed in the ListModel into view
}
So since I bootstrapped the data to begin with, this fetch() call would be your "second" fetch. (I'm addressing point 2 you made.) You can fetch it in your initialize. Just remember that it is an asynchronous function so if you need them at render time, it won't work. But, what you can do is add event listeners to this view that are listening for add events to your itemCollections.
this.model.itemCollection.on('add', this.addItemView, this);
addItemView() will generate new instances of the itemViews and append them.
As for point 3, you can instantiate a collection at that point you need it and throw it into your ListModel. Or you can do what I did and make sure all your models always have an ItemCollection. This depends on your preferences and goals. You probably didn't need all this but I felt like illustrating it out for some reason. I dunno, maybe it helps.
Suppose you are making a music library app.
You have one view with a list on genres and another that shows the contents of the selected genre. When the user clicks a genre on the list, the contents in the other view should be updated accordingly.
What is the best way to do this so that there are minimal dependencies?
I have not found any other place to listen to the mouse clicks than the view that draws the individual genre. I can send an event from there, but what is the optimal way of getting that event to update the other view which draws the genre contents? Who should listen to that event, the genre content view or its collection?
EDIT: I instantiate both views from the router of the app and I did manage to get this to work by making the views aware of each other, but that's not optimal of course.
You could make a simple model to hold the application state, you don't need anything fancy, just a bag of data that implements the usual Backbone event methods:
var AppState = Backbone.Model.extend({});
var app_state = new AppState();
Then the genre list view would listen for click events (as you already have) and set the current genre on the app-state model when someone changes it:
var Genres = Backbone.View.extend({
//...
choose: function(ev) {
// This would be the click handler for the genre,
// `.html()` is just for demonstration purposes, you'd
// probably use a data attribute in real life.
app_state.set({genre: $(ev.target).html() });
},
});
The view for the individual genre would listen for "change:genre" events on the app-state model and react as the genre changes:
var Genre = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'change_genre');
app_state.on('change:genre', this.change_genre);
},
//...
change_genre: function() {
// Refill the genre display...
}
});
Demo: http://jsfiddle.net/ambiguous/mwBKm/1/
You can make models for any data you want and models are a convenient way of working with data events in Backbone. As an added bonus, this approach makes it fairly easy to persist your application's state: just add the usual Backbone persistence support to AppState and away you go.
If you only need a simple event bus to push non-data events around, you can use Backbone's Events methods to build a simple event aggregator:
app.events = _.extend({}, Backbone.Events);
Then, assuming you have a global app namespace, you can say things like this:
app.events.on('some-event', some_function);
and
app.events.trigger('some-event', arg1, arg2, ...);
The best way I have seen is in the method proposed by Derick Bailey. In short you can create an event aggregator, which provides a centralized object for raising events to which different views can subscribe. The beauty of this solution is that it is very simple to implement as it makes use of Backbone's existing event system.
I am very new at using Backbone. Please forgive me in advance as I am struggling to think in new ways building web apps.
I am confused about how to go about using it for items that are never really covered in any of the tutorials. All the tutorials give the basic "here is a model", "here is a collection of models", "here is a view that uses the model", etc. for entities that we all understand, such as a to-do item.
I do have those cases, and I am doing OK with those, but I am having trouble figuring out how to use Backbone for the following situation.
I have a to-do app (of course.) My UI needs to have several menus that allow the user to filter the to-dos by things like priority, due date, and other properties. So, my filter menus might look like this...
All To-Dos (100)
Inbox (15)
Important (10)
Someday (15)
Today (0)
Tomorrow (6)
This Week (7)
These are all somewhat static menus, except that when a filter is clicked, it should highlight, and possibly cause another filter to be turned off. Also, this would trigger an update in my results by performing a search and re-rendering my to-do list.
So, should I represent these items with views only? Are models needed to represent the state of the menus? Should I create a FilterMenuItem model and FilterMenu model, and also the corresponding views?
Again, I understand the samples when it comes to a model for a to-do item and a to-do collection, but I am stumped on how to tackle these seemingly simple items using Backbone.
I appreciate any suggestions or guidance.
The important thing to remember here is that collections in backbone.js inherit a bunch of cool features from underscore.js. Included in these is filter (or select), which allows you to get only those members of a collection which match your perameters. For example:
render: function(){
myCollection.filter(function(item){return item.folder === "inbox"});
}
If the menus are actually static, then you can use a case select statement to determine which page you are on & therefore which filter to use. Otherwise, you can have an array of objects representing the views, which describe how to filter, i.e.:
[
{view: "all", filter: function(item){return item;}}.
{view: "inbox", filter: function(item){return item.foler === "inbox";}},
{view: "important", filter: function(item){return item.type === "important;}}
]
As far as producing the view for your menu items goes, you have to decide if the the menu is static or not. If it is, then you can simply hard-code the items to different controller routes. If it is not, then you should probably use a collection of menuItem models:
var menuItem = Backbone.Model.extend({});
var menuList = Backbone.Collection.extend({
model: menuItem
});
var menu = new menuList([{name: "All To-Dos", url: "#!/all"}, {name: "index", url: "#!/index"}]);
which you can add or remove items to dynamically, as well as having the options built from the server specifications (i.e. the user may be able to save custom folders, which will get added here) use the refresh command to avoid unneccesary http calls. Then, you can pass this collection to your view, and render the items out however you want.
Hope this helps!