I'm new to backbone.js, and trying to figure something out, I have the following objects currently:
A TodoItem model
A TodoItemView view
A TodoCollection collection
I add a bunch of TodoItems to the TodoCollection, which creates TodoItemViews for each, this renders a basic list of todo items. Now, when I click on a todo item, I want to open a new tab with all the data for that todo item, in a form (i.e, editable), and a Save button.
I'm trying to figure out how to model this.. should the TodoItemView have a click event which:
opens a tab and fills up all the info and somehow binds events
from that new tab to functions within it? (almost certainly wrong)
create a new EditableTodoItemView, whose render opens a new tab,
click on the TodoItemView creates a new EditableTodoItemView and then
forgets about it (better, I think)
I'm assuming the EditableTodoItemView should reference the original model, i.e, the TodoItem should be shared between EditableTodoItemView and TodoItemView. No new collection needs to be created, the EditableTodoItemView calls backbone.sync() when the user hits save.
Likewise, I assume that when I create a new TodoItem, I push it into the TodoCollection, which creates a TodoItemView for it and possibly automatically creates an EditableTodoItemView if the item is new (i.e, has default data).
Does this make sense? Anything else I should be thinking about?
The easiest way to switch between multiple views is a Backbone.Router. You can use it like this:
var TodoRouter = Backbone.Router.extend({
routes: {
"edit/:id": "edit", // matches http://yourapp.com/#edit/1234
".*": "index", // matches http://yourapp.com/#everything-else
},
edit: function(id) {
item = TodoCollection.get(id);
this.view = EditableTodoItemView({model: item});
$("#main").html(this.view.render().el);
},
index: function() {
//...
}
});
Then just run window.router = new TodoRouter; Backbone.history.start(); where you start the application. Make sure, your TodoCollection is fetched, before you run this. You can use TodoCollection.bind("reset", _.once(function(){Backbone.history.start()}));.
Related
I am new to Backbone. I see this in every backbone app:
var List = Backbone.collection.extend({
model: model
});
var myList = new List();
I am a bit confused about this. This script is included in a page, and when the page is reloaded or opened again and again, it will keep instantiate new collection doesn't it?
Whenever I save some models into this collection, things are still fine. But when I start to reload the page or open the page again, it will instantiate new collection with the same name again and the collection becomes empty again.
Any suggestions to prevent this? I want collection keep the models even if reloaded.
Use myList.fetch() in your view to load data from your api resource.
Some more info at BB site
Edit:
You can save model by using Collection create
So first instantiate new collection, then use
Collection.create({
name: 'John'
});
You can observe your network log to see what was posted to the api.
For your example:
var List = Backbone.collection.extend({
model: model
});
var myList = new List();
myList.create({
name: 'John'
});
Correct me if I'm wrong, but I thought Ember should most of the model - view binding for you?
What would be the case when you have to manually track model changes and update/refresh the view accordingly?
The app I'm working have nested routes and models associated with them.
App.Router.map(function() {
this.resource('exams', {path: "/exams"}, function() {
this.resource('exam', {path: ":exam_id"}, function(){
this.resource('questions', {path: "/questions"}, function() {
this.route("question", {path: ":question_id" });
this.route("new");
})
})
});
});
Everything works fine and I'm able to get exams and questions separately from the rest server.
For each model I have appropriate Ember.ArrayController and Ember.ObjectController to deal with list and single model items in the view. Basically for both models the way I handle things is IDENTICAL except for the fact that one is nested within the other. One more difference is that to display the nested route data I'm using another {{outlet}} - the one that is inside the first template.
Now the problem is that the top level model binding to the views is handled automatically by Ember without any special observers, bindings etc.. - e.g. When I add new item it is saved and the list view is refreshed to reflect the change or when the item is deleted it is auto removed from the view. "It just works (c)"
For second model (question), on the other hand, I'm able to reproduce all the crud behaviour and it works fine, but the UI is not updated automatically to reflect the changes.
For instance I had to something like this when adding new entry (the line in question has a comment):
App.QuestionsController = Ember.ArrayController.extend({
needs: ['exam'],
actions: {
create: function () {
var exam_id = this.get('controllers.exam.id')
var title = this.get('newQuestion');
if (!title.trim()) { return; }
var item = this.store.createRecord('question', {
title: title,
exam_id: exam_id
});
item.save();
this.set('newQuestion', '');
this.get('content').pushObject(item); // <-- this somehow important to update the UI
}
}
});
Whereas it was handled for me for (exam model)
What am I doing wrong? How do I get Ember.js to track and bind model and change the UI for me?
With
this.get('content').pushObject(item);
you push your new question to questions controller array. I think it would be better if you push the new question directly to the exam has_many relation.
exam = this.modelFor('exam');
exam.get('questions').pushObject(item);
I'll start off with I'm new to these two frameworks but I'm really excited with what I've learned so far with them. Big improvement with the way I've been doing things.
I want to display a collection of items (trip itineraries) in say a table. I only want to display a couple of the itinerary fields in the table because there are many fields. When you edit/add an item, I would like to display a form to edit all the fields in a different region or in a modal and obviously when you save the list/table is updated. I'm unsure of the best way to structure this and am hoping someone could point me in the right direction or even better an example of something similar. My searches so far have came up short. It seems I should use a composite view for the list but how to best pass the selected item off to be edited is where I'm kinda stuck at. Any pointers would be much appreciated.
I'd use a CompositeView for the table, and an ItemView for the form. Clearly this is incomplete, but it should get you started. Actually... I kind of got carried away.
What follows are some ideas for the basic structure & flow. The implementation details, including templates, I'll leave to you.
The table/row views:
// each row in the table
var RowView = Backbone.Marionette.ItemView.extend({
tagName: "tr"
});
// This could be a table tag itself, or a container which holds the table.
// You want to use a CompositeView rather than a CollectionView so you can
// render the containing template (the table tag, headers), and also, it works
// if you have an actual collection model (ItineraryList, etc).
var TableView = Backbone.Marionette.CompositeView.extend({
itemView: RowView,
collectionEvents: {
"change": "render"
}
});
The form view:
// This would be a simple form wrapper that pulls default values from
// its model. There are some plugins in this space to help with
// forms in backbone (e.g. backbone-forms), but they may or may not
// be worth the bloat, or might be tricky to work with Marionette.
var FormView = Backbone.Marionette.ItemView.extend({
events: {
"submit form": "onFormSubmit"
},
data: function () {
// here you'd need to extract your form data, using `serializeArray`
// or some such, or look into a form plugin.
return {};
},
// Clearly this is incomplete. You'd need to handle errors,
// perhaps validation, etc. You probably also want to bind to
// collection:change or something to close the formview on success.
//
// Re-rendering the table should be handled by the collection change
// event handler on the table view
onFormSubmit: function (e) {
e.preventDefault();
if (this.model.isNew()) {
this.collection.create(this.data());
} else {
this.model.save(this.data());
}
}
});
Somewhere in your load process you'd instantiate a collection and show it:
// first off, somewhere on page load you'd instantiate and probably fetch
// a collection, and kick off the tableview
var itineraries = new Itineraries();
itineraries.fetch();
// For simplicity I'm assuming your app has two regions, form and table,
// you'll probably want to change this.
app.table.show(new TableView({collection: itineraries}));
Making routes for the edit and new itinerary links makes sense, but if your form is in a modal you might want to bind to button clicks instead. Either way, you'd open the form something like this:
// in the router (/itineraries/edit/:id) or a click handler...
function editItinerary(id) {
var itinerary = itineraries.get(id);
// then show a view, however you do that. If you're using a typical
// marionette region pattern it might look like
app.form.show(new FormView({
collection: itineraries,
model: itinerary
});
}
// once again, a route (/itineraries/new), or a click handler...
function newItinerary() {
app.form.show(new FormView({
collection: itineraries,
model: new Itinerary()
}));
}
I have a Backbone view that renders a calendar, which in turn renders a sub-view for each day. Each day has a single model and a single click event that either selects or de-selects the day. If a day is selected, the model is saved, and if it's de-selected, the model is destroyed.
Once a view's model is destroyed (because the date was de-selected), I'm not sure how to save a new model in the collection of calendar dates if the date is re-selected. The view only knows about the model — nothing about the collection. Should the calendar view handle creating and attaching a new model to the date view when the model is destroyed? Or should the date view be passed the collection and do that on its own? Or is there a better solution?
Here are some snippets of my code for clarity:
var CalendarView = Backbone.View.extend({
initialize: function () {
this.model.dates.on('reset', this.renderDates, this);
},
renderDates: function () {
// Loop through the number of days to display and create a view for each.
// Find a model for the date. If one doesn't exist, this returns a new model.
model = this.model.dates.completedOn(date.format('YYYY-MM-DD'));
view = new DateView({
model: model
});
$dates.append(view.render().el);
// End loop.
}
});
var DateView = Backbone.View.extend({
events: {
'click .date': 'toggleDate'
},
toggleDate: function () {
if (this.model.selected()) {
this.model.destroy();
}
else {
this.model.save();
}
}
});
Thanks for any help!
Sounds like you should have a set up like this:
A Backbone.Model for the selected dates. We'll call this M.
A Backbone.Collection to hold all the M models. We'll call this C.
A Backbone.View, VM, that represents a single selected date, the model property will be an M.
A Backbone.View, VC, to represent a whole month (or year or whatever you're showing), the collection property will be a C.
Then VC can listen for 'add' events on its collection and insert a VM in the appropriate place when a new selected date is added to the C.
Removing an M from a C is also fairly straight forward. Any event on a model will also be triggered on its collection (if it has one). So to unselect a date, simply destroy the model. The VC can listen to 'destroy' events on its collection clean up the VM and its overall display as needed.
This way spend most of your time talking to the collection to manage your selected dates and everything else sorts itself out by responding to the appropriate events.
Here's a quick'n'dirty demo that should show you how the pieces fit together: http://jsfiddle.net/ambiguous/TYMTM/
I've built a Backbone-powered library that allows a user to add/remove items, much like the Todos example.
Every time an item is add or removed - or the entire collection is refreshed - I need two other select elements that are on other areas of the page to re-populate with the latest items as options. How would this be implemented, do I simply re-populate the select element in the render function of the view which holds a reference to the collection?
I'm tempted to create a view just for the select options but this seems like overkill, especially when considering the view doesn't need to re-act to any events. The select options are used by other views to populate form data.
You're correct: create a unique view for each select option. It's not overkill at all; that's what Views are for. They listen for events from their models, in this case the item list, and redraw themselves upon receiving an event. They have container designations, so once you've established those in the parameters for the View subclass, you never need to think about them again. You can style them independently.
That's the whole point of the Views being the way they are.
More importantly, you could also abstract out "view of a list of things," and then each of your specific views can inherit from that view, and add two features: the filter ("latest"), and the renderer. You have to write the renderer anyway; you may as well exploit a little syntatic sugar to make it clear what you're rendering where. It's better than writing comments.
Not to distract from Elf Sternberg's already excellent answer, but to add a little context:
Don't loop over collections in your templates
If you want to do this, you might as well just use HTML partials and
AJAX. Instead, use a Backbone View that renders its own views (the
granularity is what minimizes server syncs and page refreshes). This
is recursive, you can repeat this pattern until there is no more
associated data to loop over.
— Jonathan Otto in A Conceptual Understanding of Backbone.js For The Everyman
Of course, there are a few gotchas when you want to render subviews.
I did a code search to try and find some examples of how to do this. Turns out that the TodoMVC example is a good model for doing this. From the Strider-CD source (MIT License):
var UserView = Backbone.View.extend({
//... is a class. not sure how to put that here
tagName: "option",
// Cache the template function for a single item.
template: _.template($('#user-item-template').html()),
// The DOM events specific to an item.
// maybe could put links here? but then user couldn't see on mouse-over
// The UserView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between a **User** and a **UserView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.bind('destroy', this.remove);
},
// Re-render the contents of the User item.
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
// Remove the item, destroy the model.
clear: function() {
this.model.clear();
}
});
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
var UsersView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#user-form"),
// no events here either at this time
// At initialization we kick things off by
// loading list of Users from the db
initialize: function() {
_.bindAll(this, 'addAll', 'addOne','render');
Users.bind('add', this.addOne);
Users.bind('reset', this.addAll);
Users.bind('all', this.render);
console.log("fetching Users");
Users.fetch();
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function() {
console.log("rendering User AppView");
// might want to put some total stats for the Users somewhere on the page
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function(User) {
console.log("adding one User: " + User.get("id") + "/" + User.get("email"));
var view = new UserView({model: User});
this.$("#user-list").append(view.render().el);
},
// Add all items in the **Users** collection at once.
addAll: function() {
console.log("adding all Users");
console.log(Users.length + " Users");
Users.each(this.addOne);
}
});
// Finally, we kick things off by creating the **App**.
console.log("starting userapp now");
var UsersApp = new UsersView();
});
Additional examples of a select list view with option sub-views can be found in:
Zipkin source
reviewboard source