Edit List using Backbone.js/Marionette - javascript

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

Related

Backbone views which don't know about their container, models to be fetched via AJAX, no UI/UX trade-offs and maintainable code

Since I'm not totally sure on which level my issue actually is to be solved best, I'd like to summarise the path I went and the things I tried first:
It's more or less about $el (I think).
As most basic backbone examples state, I started with having the $el defined within its view, like
Invoice.InvoiceView = Backbone.View.extend({
el: $('#container'),
template: ..,
..
});
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
Following this article's advice, I switched to passing $el over to the view while calling the render()-method. Example:
$('#container').html( new WineListView({model: app.wineList}).render().el );
So far so good - but now render() gets called, while it maybe shouldn't (yet).
For example the View asynchronously fetches a model in its initialize()-routine. Adding a binding to reset or sync (e.g. like this.model.bind('sync', this.render, this)) makes sure, render() gets definitely called once the model is fetched, however above stated way, render() still might get called while the model isn't fetched yet.
Not nice, but working(TM), I solved that by checking for the model's existence of its primary key:
render: function() {
if(this.model.get('id')) {
...
}
However, what I didn't expect - and if it really isn't documented (at least I didn't find anything about it) I think it really should be - the fetch operation doesn't seem to be atomic. While the primary key ('id') might be already part of the model, the rest might not, yet. So there's no guarantee the model is fetched completely that way. But that whole checking seemed wrong anyway, so I did some research and got pointed to the deferred.done-callback which sounded exactly what I was looking for, so my code morphed into this:
render: render() {
var self = this;
this.model.deferred.done(function() {
self.model.get('..')
};
return this;
}
..
$('#container').html( new WineListView({model: app.wineList}).render().el);
It works! Nice, hu? Ehrm.. not really. It might be nice from the runtime-flow's point of view, but that code is quite cumbersome (to put it mildly..). But I'd even bite that bullet, if there wouldn't be that little, tiny detail, that this code sets (=replaces) the view instantly, but populates it later (due to the deferred).
Imagine you have two (full-page) views, a show and an edit one - and you'd like to instantly switch between the two (e.g. after hitting save in the edit-view it morphs into the show-view. But using above code it sets (=resets) the view immediately and then renders its content, once the deferred fires (as in, once fetching the model is completed).
This could be a short flickering or a long blank transition page. Either way, not cool.
So, I guess my question is: How to implement views, which don't know about their container, involve models which need to be fetched, views which should be rendered on demand (but only once the model is fetched completely), having no need to accept UI/UX trade-offs and - the cherry on the cake - having maintainable code in the end.
First of all, the first method you found is terrible (hard coding selector in view's constructor)
The second: new WineListView({model: app.wineList}).render().el is very common and ok. This requires you to return the reference to view from render method, and everyone seems to follow this, which is unnecessary.
The best method (imo) is to simply attach the views element to the container, like this
$('#container').html(new WineListView({model: app.wineList}).el);
The WineListView doesn't need to know about where it's going to be used, and whatever is initializing WineListView doesn't need to worry about when to render the WineListView view instance:
because the el is a live reference to an HTML Element, the view instance can modify it anytime it wants to, and the changes will reflect wherever it is attached in DOM/ when it gets attached in DOM.
For example,
WineListView = Backbone.View.extend({
initialize: function(){
this.render(); // maybe call it here
this.model.fetch({
success: _.bind(this,function(){
this.render(); // or maybe here
})
});
}
});
Regarding flickering: this hardly has to do anything with rendering or backbone, it's just that you're replacing one element with another and there will be an emptiness for a tiny bit of time even if your new view renders instantly. You should handle this using general techniques like transitions, loaders etc, or avoid having to switch elements (For example convert labels into inputs in the same view, without switching view)
First off, the linked example is outdated. It's using version 0.9.2,
whereas the current version (2016-04-19) is 1.3.3. I recommend
you have look at the change log and note the differences, there are many.
Using the el property is fine. Like everything though, there's a time and place.
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
I wouldn't define an el property on every view, but sometimes it makes sense, such as your example. Which is why, I'm assuming, Backbone allows the use of the el property. If you know container is already in the DOM, why not use it?
You have a few options:
The approach outlined in my original answer, a work-around.
fetch the model, and in the success callback, insert the view element into the DOM:
model.fetch({
success:function() {
$('#container').html(new View({model:model}).render().el);
}
});
Another work-around.
Define an el property on the view and fetch the model in the view initialize function. The new content will be rendered in the container element (also the view), when the content/model data is ready, by ready, I mean when the model has finished fetching from the server.
In short,
If you don't want to define an el property, go with number 1.
If you don't want to let the view fetch the model, go with number 2.
If you want to use the el property, go with number 3.
So, I guess my question is: How to implement views, which don't know about their container
In your example, I would use the el property, it's simple a solution with the least amount of code. Not using the el property here, turns into hacky work-arounds that involve more code (complexity) without adding any value (power).
Here's what the code looks like using el:
var Model = Backbone.Model.extend({url:'/model_url'});
var model = new Model();
// set-up a view
var View = Backbone.View.extend({
el:'#container',
template:'model_template',
initialize:function() {
this.model.fetch();
this.listenTo(this.model,'sync',this.render);
},
render:function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var view = new View({model:model});
Check out the documentation for el.
Here is an updated working example.
If there is an obvious flicker because, your model takes a noticeable amount of time
to be fetched from the server...maybe you should think about displaying a loading bar/variation thereof
while fetching the model. Otherwise instead of seeing the flicker, it will appear the
application is slow, delayed, or hanging..but in reality - it's waiting to render the next view,
waiting for the model to finish fetching from the server. Sitting on old content, just waiting for
the model to load new data to show new content.

Count or Select Backbone View Instances

Let's say I'm trying to create a toDo application, where clicking each toDo opens an edit form for each toDoItem. I only want a maximum of one edit form open at any one time, so right now I am doing this in the edit method of the toDoItem view:
edit: function (e) {
e.preventDefault();
if ($('.editForm').length == 0) {
//create form model and view
}
}
That works, but doesn't seem very Backbone-y. Is there are way to select or count all instances of a particular view (in this case, the form-view)?
AFAIK, there are no utilities method in Backbone.View to count instances of a particular Views. Here are some ideas...
Maybe each of your TODO form is tied to a Model? In that case, you can have a model.set/get 'editing' and a collection.isAlreadyEditing() which would filter the models on this field:
(collection.filter(function(model){ return model.get("editing") }).length > 0
That would allow you to use on change:editing events throughout your views to control the logic and have convenient helpers functions in the collection to define some behavior of all those TODO as a whole. This would be one way to implement something closer to a Controller pattern in Backbone.
Another common thing in backbone is to keep an array of all the subviews when you instanciate them, so you could just do a:
_.any(subViews, function(view){return view.editing; })
Assuming that you keep a editing flag in your subviews when it gets toggled.
You can have your views listen to a toggleEdit event with the id or the model or something identifying what is being edited, sometimes the event handler can be as simple as a toggleClass("open", model==this.model)...
I am sure there are millions of other ideas. But counting jQuery selected elements is probably not very high on the list!

Ember.js does not auto bind model RESTful changes

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

Collection sorting with comparator only works after fetch is complete?

I have a simple collection of messages that I want to reverse sort on time (newest on top), using comparator:
...
this.comparator = function(message) {
var time = new Date(message.get("time")).getTime();
return time;
}
...
In my view, I use fetch and add event:
messages = new MessageCollection();
messages.fetch({update: true});
messages.on("add", this.appendMessage);
...
appendMessage: function(message) {
var messageView = new MessageView({
model: message
});
this.$el.prepend(messageView.render().el);
}
Sadly, the messages are not rendered in the order I am looking for, but in the original order they were in coming from the server.
Now, after some testing I found out that when I add all the messages at once (using reset), the order is as I expected.
messages.fetch();
messages.on("reset", this.appendCollection);
...
appendCollection: function(messages) {
messages.each(function(message) {
this.appendMessage(message);
}, this);
}
Even though I can understand this process since a collection probably can only figure out how it's supposed to be sorted after all models are added, this (the on("add") configuration) used to work in Backbone 0.9.2.
Am I missing something? Did the comparator method change, or the event model in regard to add? Or am I going at it the wrong way? Thanks!
You call appendMessage method when you add a model in collection. the appendMessage is being called in the order of adding models and not the actual order in the collection.
In the "add" case, the model is inserted in the right position in the collection, as it should be by "comparator" documentation). But then you are doing
this.$el.prepend(messageView.render().el);
which will put the html from the MessageView rendering at the top of the $el (which I assume is the CollectionView container).
The best way to also keep the Html respecting the sorted order would be to re-render the collection view, or scroll the collection view children and insert the added messageView at the right place (a bit more difficult to do).

A Backbone.js view that is a simple select list

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

Categories