I have a backbone.js app, whose views have multiple states, which differ substantially from each other ("View","Edit", etc). There are at least 2 different templates for every view. This is OK. My problem is with the JS view managing code.
I rely on an initalize-thin-render-thick approach (which, I think is pretty bad), where the render method is where 80%-90% of the logic occurs. When I want to change the state, I simply call the render method with a specific parameter ("view","edit"). On the basis of that, the view decides what to show and what not, to which events to bind, etc.
I think this is bad, because, on one side it puts bottlenecks on the rendering process, on another, it is not proper state machine, which means that I am not carrying about possible callbacks that might have been bound previously. When I receive the view, I simply clean the view and that's it.
I also observed, that I am not using the delegated event system, provided by backbone, which I think is another minus, because I think, it is very well implemented (BTW, does it make sure to unbind callbacks, when a certain DOM element is removed?)
I think I need some serious refactoring. Please, help with some advice, as to what the best approach for a multi-state Backone view would be.
What I tend to do for these cases is to make a toplevel view that manages a subview for each individual state (index, show, edit, etc.). When a user action is invoked, e.g. "edit this user", "delete this user", "save my changes", the active state view signals the router (directly, or through a hyperlink), and the router will tell the toplevel view to update its state.
Continuing the user editor example, let's say that I have a top level view called UserEditorView. It renders a basic container for the user editor (title bars, etc.) and then, by default, instantiates and renders Users.IndexView inside that container.
Users.IndexView renders the list of users. Next to each user is an edit icon, which is a link to "#users/555/edit". So, when the user clicks it, that event goes to the router, which tells UserEditorView, "hey, I want to edit user #555". And then UserEditorView will remove the IndexView (by calling its .remove() method), instantiate Users.EditView for the appropriate user model, and put the EditView into the container.
When the user is done editing the user, she clicks on "Save", and then EditView saves the model. Now we need to get back to the IndexView. EditView calls window.router.navigate('users', { trigger: true }), so the URL gets updated and the router gets invoked. The router then calls .showIndex() on the UserEditorView, and the UserEditorView does the swap back to IndexView from EditView.
On a simple way to manage unloading of events, I've found this article on zombie views quite useful.
Basically, I don't have a toplevel view, but I render all the views using a view handler that takes care of the views for a given container.
To make your renderer thinner, I would recommend using routes. They are easy to setup, and you can have different views for each route. Or, what I'm used to do is just to have different templates. Using a general Backbone.View overwrite:
Backbone.View = Backbone.View.extend({
initialize: function(attrs) {
attrs = attrs || {}
if(!_.isUndefined(attrs.template)) {
this.template = attrs.template;
}
}
});
I've noticed that I reuse views in two ways:
1. edit views differ only in the underlying model and template, but not the associated logic (clicking the submit validates and saves the model)
2. the same view can be reused in several places with different templates (like a list of users as a ranking or you accounts)
With the above extension, I can pass {template: '/my/current/template/} to the view, and it will be rendered as I want. Together with routes, I finally got a flexible, easy to understand and thin setup.
Related
Should Backbone routers be used for nested views also, as it it is used for navigating in between more complete page views and some other sorts? When to use a router and when to use inner views?
For example a home view page has tabs and each tab for showing another view with its model and collection. This home view is similar to Twitter or Facebook. How should this complete page be rendered:
By navigating via router to subviews on tab clicks and rendering by subviews render function and placing them in home view page by homeview render, or routers is not and not good for this purpose. Advantage: a.bookmark-able subviews, b. ?more maintainable code. Disadvantage: on page refresh homeview part not rendered, only subview does.
Instead, on tab clicks subviews should be created in homeview and
rendered in themselves and placed in the home view page by homeview
render(). Advantage: no disadvantage above. Disadvantage: no advantage a, ?b above.
UPDATE:
A hybrid solution to refresh problem in 1. To have each subview to render homeview parts, tabs etc., by depending on a seperte single little template for that or from their templates having written those parts code to have those homeview parts. Disadvantage here is like separation of modules decreases a bit by subviews requiring (as dependancy) or including (in their template) something that is not sub at all but something like sup, main, belongs to an upper level module.
Or is there another, better, way?
Ultimately it's subjective; there is no rule for when you should start a new page View triggered by a Backbone.Router route and when you should just re-render a sub-view for a part of the DOM without involving a route. Really, it's just a matter of whether you want the user to feel like they've gone to a new page or not. Ask yourself:
do you want them to be able to click back/forward in their browser?
do you want them to be able to bookmark the "page"?
is most of the DOM changing or just a small part?
To put it another way, when a user goes to a Backbone.Router page it indicates that a significant change in state has occurred on your site. Really all the bookmarking/history entry/significant DOM changes are just reflections of that. So if you feel that a significant change, whatever that means to you and your site, is happening, make a route for it. Otherwise just re-render a View.
I'm using Backbone for a webapp in which I fetch a model from an API call and populate a view with that. Within that view, there are a couple things, among which is a textarea and some sort of chat area (which is populated with other views). So far so good.
I now want to call the API again, and if the initial model changed I want to update parts of that view. My problem is now, that I am afraid that if I reload the view, the things that people might have written in the textarea are gone.
I guess I can make a call and update parts of the view using jQuery, but I think that's not really the idea behind Backbone.
So my question is now; how can I use Backbone to update parts of the initial view without reloading the whole view? All tips are welcome?
Typically a view is bound to a model and it represents a part of the DOM.
When you say that the view should update when the model changes, my first thought is that this would be a separate view from the inputbox.
For example, there could be one parent view that is represented as a wrapper element, and multiple subviews that represent different models.
You can still manage events from your parent view, because events will bubble up.
It is important to take into account proper view cleanup when you're working with subviews though. Marionette.js is a library that manages this well.
Alternatively, you can temporarily cache the user input and populate the view back after it's rendered.
Your view needs to listen to model changes. If you render your view you pass your model to the template. And in your initialize function of the view you tell yout view the listen to model changes. See here.
var view = Backbone.View.extend({
initialize: function(){
this.model.on('change', this.render, this);
},
render: function() {
// ...
}
});
In the application we are developing, we have a CollectionView whose every ItemView contains a link to the item-details page. Also, every ItemView contains a checkbox, because items can be selected in the CollectionView to perform bulk actions on them.
When switching to the ItemDetails view, we want to keep the state of the CollectionView, ideally without having to redraw it (a bit like GMail when switching from inbox to mail and back). Our solution is to render the two views in two different regions and to hide one when switching from one to the other.
My perplexity about this solution is that
Marionette doesn't seem to be meant for this kind of use.
It is not very memory-friendly, since all the DOM elements are never deleted.
Is there any better solution to achieve this goal?
Storing the state somewhere, close the CollectionView and redraw it later is another possible solution, but would it imply a heavy computation overhead? (we are quite scared about redrawing views).
Marionette allows for showing a view in a region without closing the view that was already rendered. You simply pass in {preventClose: true} to the show() method of the region. You would still need to maintain a reference to the collection view though so you can later re-show it or close it yourself.
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#showing-a-view
Unless your collection is very large, there's not a problem with just rerendering the collectionView when switching back from a itemDetail view. You do need to store the state of the checkboxes indeed.
However, I don't see what's really wrong with your other approach. It's probably even faster and there's nothing wrong with just hiding one region and showing another. If that works for you, go ahead.
Regarding the memory issue, as long as you're looking at the collection or itemDetail there isn't much to gain by closing either one of the views (especially if your itemDetail views are not very large). Once you move away from that section (thus not looking at the collection or itemDetail view anymore) you could just close the layout that contains these two regions. That'll free up any memory used by these regions.
I've assembled a modestly sized application and I am in the process of factoring code to reduce the overall number of maintained lines, as well as performance tuning. The use case that has me posting this question is that I have a button embedded in a menu that invokes (or needs to invoke) a method on a controller that displays a form. This currently is implemented using a direct reference to the specific button control, creating a panel, and putting that panel inside of my viewport.
The question at: ExtJS 4.1 Call One Controller From Another begins to address the issue of best-practices near the end of responses, but doesn't really settle on a base-case that can be reproduced or extended to cover more complex implementations (which is the aim of my question.)
Given the two controllers:
A "main menu" controller.
// controller/Menu.js
Ext.define("App.controller.Menu", {
extend: "Ext.app.Controller",
init: function () {
this.control({
"viewport > mainmenu > button": function (ctl, evt) {
}
});
}
});
A user account controller
// controller/User.js
Ext.define("App.controller.User", {
extend: "Ext.app.Controller",
stores: ["User"],
views: ["user.Edit", "user.List"],
init: function () {
}
});
The Question
What would be the (best) way to implement a crosswise connection between the two controllers to properly delegate the responsibility of responding to a click event on the menu button for "Create a New Account?"
One Possible Solution
Using componentquery I can easily narrow down the focus of the button in the main menu view using a tag property such that the User controller is responding directly to the event:
// controller/User.js
"viewport > mainmenu > button [tag=user.create]": function () {}
An unknown alternative
Or I could possible winnow my way through the object graph to find the reference to the User controller from the Menu controller and invoke it that way.
// controller/Menu.js
// switch on tag case "user.create"
App.controller.User.createUserForm()
The real question
The question that results from all of this is what the "most acceptable" solution is here. One could imagine the use of a third, mediating controller, to "route" requests from controls to controllers but I think that goes against what the remainder of the framework is attempting to do. Using a variation of either of these methods currently works however neither feels completely clean and reliable; or ultimately maintainable long-term (as code gets spread out rather quickly.) Additionally the thought had occurred to us to drop into raw events but we run into the same kind of maintainability issues there.
Some short lines:
A thing that I don't understand is that Sencha Touch has routing but no eventbus where ExtJS has a event bus but no routing... (and there are more points where the MVC implementation differ) Whatsoever, because I am using ExtJS most of the time I created a custom routing to fill this gap for me. Maybe sencha will add this in version 5.
The easiest and quickest solution: use the getController() of the Ext.app.Application controller to invoke the responsible controller from your menu controller.
The (imo) best solution: write yourself a router where each controller register it's routes to and use both; routing and eventbus. This gets really handy if your app have shared components that are used by more than one dev team.
The question, in brief:
In MVC, how do you distinguish between a checkbox click (or a selectbox or listbox change) from a human meaning "Controller, modify the model", and a checkbox click (or a selectbox or listbox change) from the Controller meaning "I'm updating the view because the model has changed"?
The example:
I have a JS app (all one big HTML+JS page; there's a server behind it, and AJAX going on, but it's not important to the example) which has the notion of "Vertices" connected by "Edges". The UI lets you add and remove Vertices on a map, and enable or disable Edges between pairs of Vertices.
There are two ways to disable an Edge from Vertex A to Vertex B:
click on the Edge to make the "Edge Details" window provide you with a "Disable This Edge" button; or
click on Vertex A (or B) to make the "Vertex Details" window provide you with a checklist of nearby Vertices, from which you can uncheck Vertex B (or A).
Here's how this works under the hood in MVC (but see the end of this post for an update, where I correct problems in my understanding):
Model: a list of Vertex objects and a list of Edge objects.
View: a GMaps UI, with markers and polylines, plus checkboxes and buttons and "Vertex Details" and "Edge Details" DIVs.
Controller:
JS functions that update the model when events on the checkboxes and buttons fire; and
JS functions that update the view when events on the models fire.
Here's the specific inelegance:
The user has the Vertex Details Window focused on Vertex A, and the Edge Details Window focused on the Edge from Vertex A to Vertex B.
The user clicks "Disable This Edge" in the Edge Details window.
Controller function 1 gets the click event, and calls disable() on the Edge model object.
The Edge model object fires the "I just got disabled" event.
Controller function 2 receives the "I just got disabled" event, and
redraws the Edge Details Window to say "I'm disabled!" and
unchecks Vertex B in the Vertex Details Window.
Crap! This fires Controller function 1 again, which was listening for UI events that mean an edge was disabled!
So there's an unnecessary re-update of the Model, and re-update of the View. In a more complex view with events that fire events that fire events, this can make for dozens of extraneous updates!
Update: a great answer.
I misunderstood MVC a bit. I don't have just one View, as I described above: I have several Views into several Models. In particular, I have a checkbox-list View of Edges to a particular Node, and a separate, "detailed window-style" View of an Edge.
Furthermore, I shouldn't have one controller function updating all views when the Model changes: each View should modify itself when the Model changes.
So if each View registers for "state updated" events on the Model, and each View updates itself upon receipt of those events, then the answer to my circular events question is simply this:
The checkbox-list View will disable checkbox events for the moment that it is updating the checkboxes after a Model state change.
Now if a user disables an Edge via the Edge Detail window, the Controller updates the Edge Model, the checkbox-list View receives notification of the update, and the checkbox-list View is smart enough to silence checkbox events while changing the status of the appropriate checkbox.
This is much more palatable than my original solution, where one Controller updates ALL Views -- and thus has to know which views need special care and feeding to avoid loops. Instead, only the single View with troublesome UI elements has to deal with the problem.
Thanks to those who answered my question!
Just to recap the MVC model. Views should generally update themselves. Here's how it works: a controller changes the state of the model, the model sends updates to its views, the views pull in new state from the model and update themselves. While controllers and views are generally bundled (i.e. drilling down on data in a graphic representation) they should never interact directly, only through the model. This in general of course.
So the JS functions that update your views are not actually controllers, which is an important distinction. They should be considered part of your view. This might not be helpful to the problem at hand but I thought it merited pointing out.
You can also not delete your model, I assume you mean you're deleting someting from your model, since no views or controllers can actually exist (or be in a functional state) if they're not backed by a model.
Not being a JS code jockey and not having used gmaps I don't really see where the problem is. Does changing the state of a checkbox(checked property) fire the onClick() event? It really shouldn't IMHO but perhaps they implemented it that way, otherwise you could just attach your controller to the onClick() and add some logic to the checkbox (or, this being JS, in a function somewhere) to change the checkbox state. If that's not possible, option 1 and 2 are definitely your best bet.
addition: user interacting with a view
So what happens when a user wants to interact with a view? Frequently a widget will include both a view and the controller. A checkbox has a view (you can see if it's checked or not) and also a controller (you can click it). When you click the checkbox, in principle the following should happen:
checkbox controller receives the event
checkbox controller changes the state for the value this checkbox represents in the model
model updates listeners (including the checkbox)
checkbox updates its look to reflect that that value has changed
The first step, how the controller receives the event is somewhat language dependent, but in OOP languages it's likely a listener object attached to user interface events on this particular widget which either is the controller or notifies the controller of the user interaction.
This is a tough one. If I understand correctly, the problem results because you've exposed a click handler on your model, and the model's click event is caught by the controller. The controller updates the view, which in turn toggles the same event.
From my point of view, I would consider it inappropriate for the Controller to attach itself to the Edge's Click event, because it exposes too much detail about how the Edge is implemented and used. Controller does not care about how the Edge is used or any other implementation details.
In fact, canonical MVC style doesn't require the Controller to hook onto any Model events at all, generally because the Model's state is not mutated by the View or any other Controllers. Its not necessary for the Model to notify the Controller that it's been changed.
To fix your problem, you should define View's interface to have a single method, such as ToggleEdge:
public interface GraphView
{
event Action ToggleEdge;
}
Its tempting to want to create two methods, EdgeClicked and CheckboxClicked, but insisting on two independent methods like that violates the principle of encapsulation. It exposes too many implementation details to your Controller or anyone else who wants to hook onto those events. Remember, the Controller only cares that the View's state changed, it doesn't care how it changed.
When you implement the View interface onto your user interface, you should take care to ensure that the ToggleEdge event is invoked from one location. You can do this by hooking onto the Edge.Clicked event in your View and using it to toggle your checkbox; this makes your checkbox responsible for raising the Toggle vent up to the controller:
public class UI : UserControl, GraphView
{
public event Action ToggleEdge;
void OnToggleEdge(Edge edge)
{
if (ToggleEdge != null)
ToggleEdge(edge);
}
protected void Edge_Clicked(object sender, EventArgs e)
{
CheckBox chkbox = FindCheckBoxThatCorrespondsToEdge((Edge)sender);
chkbox.Checked = !chkbox.Checked;
}
protected void chkEdge_CheckChanged(object sender, EventArgs e)
{
Edge edge = FindEdgeThatCorrespondsToCheckbox((CheckBox)sender);
OnToggleEdge(edge);
}
}
You can make an argument that the View knows too much about its implementation now: its aware that edges and checkboxes are fundamentally connected. Maybe this is another hack, but it can probably be dismissed as "UI logic" need to keep the View's display syncronized.