I'm in disagreement with a co-worker about the responsiblities of a Router. Inside of our single page application, it's my impression to have network requests (AJAX) handled via events being triggered (i.e. Backbone.Events), like so:
events : {
'click a#getUsers' : 'updateModels'
}
updateModels: function() {
$.ajax(); or this.Model.fetch();
}
Yet it is his understanding that network requests should be handled via the router on URL change (depending on a click which changes the url like a[href="#getThings"] like so:
var App = Backbone.Router.extend({
routes: {
"" : "main",
"thing" : "getThings"
}
getThings: function() {
this.newView = new NewView();
$.ajax(); // which populates the view with data
}
});
I wanted to know what the backbone reason is for doing network requests.
The vast majority of your AJAX requests should start with a backbone model or collection and be handled via the backbone.sync function. The key methods are model.fetch(), collection.fetch(), and model.save().
Backbone is flexible and only handles a very primitive set of use cases, so there are cases when you need to step outside these boundaries, but any call to $.ajax in a view or a router is a strong code smell of MVC failure.
To address some of your specifics.
Can a view call model.fetch() to load data?
Yes, this is perfectly fine and idiomatic backbone.
Can a view emit an event which will eventually load data?
Yes, this is perfectly fine. This is only necessary beyond just calling model.fetch() as your application grows in complexity and the decoupling that event emitting allows is valuable and warranted. For a todo list, it's usually overkill. For a huge application, this can be a clean approach.
Does every model fetch cause a change in the browser URL?
No. In fact many single page applications simply live at "/" forever. Routing to unique URLs is optional. Some applications lend themselves easily to it, others not so much. Don't equate "the application needs data X" with "the browser URL needs to be X". These are orthogonal. If a user clicks on the view for "Bill Clinton" in a list of presidents and you want to change the URL to "/presidents/clinton", then go ahead and fire a new route and that makes perfect sense, but sometimes you just want some data without changing the URL.
To summarize the responsibilities of the router, I think of it as follows:
Based on the URL, which View is supposed to be displayed?
Also based on the URL, are there model IDs that need to be extracted and instantiated into model instances?
Wire things up and render the view
So a typical router method pseudocode might be:
when the URL matches /presidents/:name, respond as follows
grab the :name parameter and make a President model
instantiate a PresidentView, passing it the model
call presidentView.render() and swap the view's element into the DOM at the appropriate spot in the overall page layout
With Backbone, you should let Backbone be doing most of the ajax requests for you, which will be more often than not triggered from your view, not the router. And I would never use a jQuery ajax request when you could use a Backbone request instead. this.model.fetch() is the right way to get the model. I see the router as a way to orient your page, but the finer details should be left to the views. However, choosing between logic in the router vs. in the views/collections/models is more art than science, and I would take a look at other Backbone examples to get more guidance. I think, in a lot of cases, the network requests are handled in the views because it's easier.
In response to your comment below, whenever data is returned from the server, Backbone uses parse to get the information it needs, so rewriting fetch would be a poor decision. You can overwrite the parse method for your model to get the information that you want, and nothing more. i.e.:
var YourModel = Backbone.Model.extend({
parse: function(response) {
//You can manipulate the object in other ways as well,
//but here's how you'd delete info
delete response.junk;
return response;
}
});
Similarly, every time you do this.model.save(), it uses toJSON() before the model gets sent to the server, which you can also overwrite:
toJSON: function() {
// default -> return _.clone(this.attributes);
return _new code here_;
}
Related
With Ember 2.2.0, I have a Route:
Dash.BacklogReportsRoute = Ember.Route.extend
model: ->
#store.queryRecord "report", newest: true
and a Component:
Dash.BacklogReportComponent = Ember.Component.extend
report: null
actions:
generate: ->
# Send the request.
$.ajax
type: "post",
url: "/reports/generate/#{new Date().getFullYear()}",
success: =>
# Update the model.
# TODO - how to reload the model in place? Or at least just refresh
# the Route so we don't have to reload the entire app.
window.location.reload()
which includes as part of the template:
<p>
Report was generated {{report.timestamp}}.
</p>
where timestamp is a computed property on the Report model.
The Route's model is the most recent Report object, and via the POST the generate action causes the server to create a newer (thus more recent) Report. I want to update the timestamp of that [newer] Report.
My question is, in Ember 2, how can I cause the Route to refresh itself? I'm new to Ember so I'm not sure if it's possible to access the Route directly from within a Component (or if I can, if it's a good idea). I don't think just reloading the model would work, because after a successful POST a different model (i.e. a record with a different id) will be returned by #store.queryRecord "report", newest: true than when the page loaded. The existing model will be the same after the POST.
Alternatively, I'd be perfectly happy completely rerendering the Route (it's a very simple page) or transitioning to "this" page again, but I can't figure out how to do that.
For now I'm simply forcing an old-school page window.location.reload().
Looking at this another way, perhaps there's a more idiomatic way to approach this problem with Ember.
Should I be loading in all (or some) Reports with the Route, and forcing a refresh on that Collection after the POST? I see Enumerable has a .firstObject property - is that a computed property (in other words, if my Routes model is a Collection of Reports and I push on to the front of that Collection, would that change propagate through .firstObject references automatically?)
Well you can't actually access the route directly from the component. But that is no problem since you can use actions to achieve the same effect.
So first you need to send an action from your generate action in your component:
Dash.BacklogReportComponent = Ember.Component.extend
report: null
actions:
generate: ->
# Send the request.
$.ajax
type: "post",
url: "/reports/generate/#{new Date().getFullYear()}",
success: =>
#sendAction('didGenerate')
Next you need to hook up the action in the template, so your controller/route gets notified when it is called:
{{backlog-report report=report didGenerate='didGenerateReport'}}
And now you can "catch" that action in your controller or route. We will do it in ther route so we can refresh it:
Dash.BacklogReportsRoute = Ember.Route.extend
actions:
didGenerateReport: ->
#refresh()
model: ->
#store.queryRecord "report", newest: true
Sorry if I made any mistakes with CoffeScript (didn't use it for quiet some time). Also there are other things you could improve. Generally I wouldn't put the ajax call in the component, I would put it in the controller/route if possible and call it with an action (I just have the experience that it's best to avoid putting ajax calls in a component, though there are usecases for it).
I would generally propose to read all of the ember guides if you didn't yet do so. They are pretty good and while you won't understand everything from the first read, you will at least more or less know what to look for, or what is possible.
If you need to manually reload the page, there really should be a better way. Ember as other similar frameworks are an SPA (single page application) and do their magic when updating only parts of the page very efficiently.
that being said, if you want to access the router from the controller/component by sending an action to the router and handling the changes in that action.
see action bubbling
https://guides.emberjs.com/v1.10.0/templates/actions/
So, this is not that simple to explain, but I have my Ember app, with my routes, controllers and views etc, it's about jobs listings, this is the most important part of the app, but I also have other models, for users and companies. So, in the ApplicationRoute I'm doing this thing here:
renderTemplate: function() {
this.render({ controller: 'jobs' });
}
because on my jobs controller I need to count the amount of listings I have, to display in a menu that shows up at every page, like this:
I'm getting this number by doing this at the jobsController:
jobsCount: function() {
return this.get('model.length');
}.property('#each')
which works fine because my homepage is the listing of jobs, which calls /api/jobs/ with all of them. The problem happens when I start the app in any other page, like accessing /jobs/:id/ directly, /admins/ or anything else, the count would be zero, since the jobsController is not loaded and I don't get the number of jobs from the api (accessing /api/jobs/:id/ directly, for example). After accessing the homepage the number of ads load, since it calls /api/jobs/ with all the jobs on it. So, one of the workarounds for this was setting ApplicationsRoute's model to be jobs as well, which resulted in two requests every time the application loads for the first time, that was working fine but I don't like the idea of having to do two requests just to get a number, especially when most of the time the first page to be loaded is the homepage, resulting in two requests to the same URL (/api/jobs/).
Is there anyway to do this without doing two requests, like if I visit a job listing (/jobs/:id/) to force it to load /api/jobs/ and then look up for the id, instead of loading /api/jobs/:id/, or this is not a good solution as well? Should I have a specific URL on the API just to load this numbers?
Is the question clear enough?
Thanks.
I had a similar problem and ultimately I decided to break DRY. In your example this would mean rendering the job_count in the json rendered by all routes that could possibly be called when the user enters the application. This makes the app's design slightly messy, but means that you never have to make multiple calls.
However, if your app gets to where there is a lot of unrelated data that ember needs access to when the user first loads the app, then you should make a url specifically for loading that data and yes, make two requests upon initial load. As far as UX goes, a marginally longer initial load time is irrelevant because of the improvement ember gives for every route entered there-after. Remember, by rendering client-side ember will always be slower upon initial load than a traditional server-rendered app anyway.
I'm afraid that's trade-off that has to be made.
I would do it something more like this. In your JobsRoute, you can do something like this:
App.JobsRoute = Ember.Route.extend({
model: function(params) {
var jobs = this.store.find('job');
jobs.then(function() {
this.store.find('job', params.job_id);
});
}
});
First, we'd find all the jobs by calling this.store.find('job'). This will return a promise, which we wait for it to resolve and populate the store with all the jobs. Then, we find the job in particular we're looking for. This way all the jobs are loaded, in just one request, and only one job is represented by your job controller.
I am trying to use knockout and I wonder myself how it works REALLY from a network point of view : where can I see the "calls" from the browser client to the server to retrieve data?
I mean : I used to use Ajax calls to populate forms, tables... => we can see the ajax calls to the server. Same thing when I need to submit a form : I can see the ajax calls.
That means I can debug with Firefbug and see the parameters sent/ the response received, inluding the headers (request/response).
With knocknout, the data in the form are "binding" automatically by the framework ko. So, does someone know how it works really? how the calls are done? is there any way to "see" the data flow?
From a network point of view, nothing changes when using knockout. You'll still need to make AJAX calls to populate your view models, but they're outside the framework, not part of it. This means that you can still put a breakpoint on your AJAX calls and observe the stuff being sent and received.
A major code departure is that your network calls will probably now exist within a knockout viewmodel.
var someVm = function(data) {
var self = this;
self.Id = ko.observable(data.Id);
// ...
self.getItems = function() {
// AJAX call here, now method on a vm
}
}
However, as TJ Crowder points out - the key mechanic of knockout is binding a client side view model to a user interface, either for data population or visibility control in a single page application. Networking is something you'll have to handle, but it's not part of knockout. Most likely, you'll make small changes in your placement of AJAX calls.
It's based on the publish-subscribe pattern.
Whenever something is changed it notifies about it.
Here's some info about it http://msdn.microsoft.com/en-us/magazine/hh201955.aspx
i'm building a app with backbone.js, now i wanna adding a global loading effect when backbone.js is making http request to the server, this is a single page webapp, so there are lots of asynchronous http requests. I think i can modify the Backbone.sync method to make this easy. How can i do this?
Post a sample of code
// let's say there is a function to generate the loading and remove it
// mask.create();
// mask.remove();
var BookList = Backbone.Collection.extend({
model:Book,
url:'/api/list/1',
});
var list = new BookList();
list.bind('reset', function(){
$('.content').html('');
list.each(function(book){
self.addOne(book);
})
});
list.fetch();
It sounds like you're looking to connect your display to the sync for UI feedback. You don't want to muddling concerns though so you don't want the sync itself to care about the presentation. The default sync implementation provides some events (and there's likely more covered in the docs):
Whenever a model or collection begins a sync with the server, a
"request" event is emitted. If the request completes successfully
you'll get a "sync" event, and an "error" event if not.
So you could start with binding to those events, but either way you should stick to some kind of approach where "sync" remains focused on its responsibility.
#MattWhipple is absolutely correct about separation of concerns: you shouldn't mix UI logic to the persistence layer. However that doesn't mean that you shouldn't override the Backbone.sync method, as long as you provide a decoupling mechanism to separate the concerns.
One such mechanism is the mediator pattern, i.e. using a global event bus to publish and consume messages. As it happens, Backbone provides just such a mechanism with the evented global Backbone object. You can override your sync to trigger an event:
Backbone.trigger("request:start");
And listen to it in the UI layer:
Backbone.on("request:start", callback);
In this way neither layer has to know of each other, and you can still achieve the global request handling.
Furthermore, if all you want is to catch all AJAX requests and you don't care whether they originated from Backbone, you can drop one level lower to listen to jQuery's global events and leave Backbone out of it altogether.
I have just begun to port a layered single page js app onto backbone.js and was trying to understand how to handle composite url parameters with routes and spalts in backbone.js. The backend has rails and sends JSON.
There are various entities (models) like filters, dimensions, features, questions which can be passed via request parameters.
URL 1
/display/#widget?id=42&fon=1,2,4&foff=6,9,19&q=1a2bc3abc4d
URL 2
/display/#widget?id=42&compare=345,567,90&fon=1,2,4&foff=6,9,19&q=1a2bc3abc4d
How to i structure these non-restful urls, keep the same functionality and allow bookmarkability.
Thanks
Backbone's router, for the purpose of invoking views, cares only about the hash portion of window.location. However, it does keep track of the search portion for the purpose of maintaining the browser history.
Therefore, the decision about bookmarkability is your responsibility: the hash will invoke a specific route, and what views that route hides or shows is up to you. How those views parse the search string and react is also up to you.
I can see what you want to do: change a model through the search function, then render it. It's a bit of a two-step trigger: hash-change -> model-sync -> show-view. Structuring that sounds like it'll be fun. But Backbone is capable.