Backbone/Marionette: why shouldn't I trigger route handler on navigate? - javascript

I'm reading David Sulc's A gentle introduction to Maionette, and came across the following:
It’s important to note that the route-handling code should get
fired only when a user enters the application by a URL, not each time the
URL changes. Put another way, once a user is within our Marionette app,
the route-handling shouldn’t be executed again, even when the user
navigates around;
What's the problem with triggering a handler on navigate?

There is no difference IF you aren't already in your Marionette app. So say we are first getting into our Marionette app and we want it to initially route to the posts index page. Initially we can either
call navigate({trigger: true) or
call navigate (to update the URL) and then call App.vent to trigger the call.
Both of them will resolve in our controller's API.list function and behave exactly the same way (fetch our list of posts and then display it). So calling trigger: true when initially entering your app/routing to the first page is totally fine. I think David just tries to make it a practice to not do so to re-enforce the power of Marionette's pub/sub infastructure since with it you don't need to pass trigger: true.
However, let's say we're now in the list view displaying a list of posts. We've already spent the time of fetching our list of posts from the server when initially entering our app. Now we click on a post and want to view the show view of that post. The post already exists in memory so we can just do a App.vent.trigger "post:clicked", post to use the post already in memory to display it. If we were to instead utilize the navigate({trigger: true}) route instead we'd end up on the same page but we would have to re-fetch the individual post instead using the one already in memory.
So the main reason is because you don't need to - triggering the page would cause a reload, re-fetch, etc. It would make your app feel slow and kind of defeat the purpose of a responsive web app/single page application.
Here's what your router should look like - you always want it setup so that you can just navigate to the page via a App.vent call when inside your app AND able to handle the manual browser refresh/navigating to the route directly (which is what the trigger would do, but this is the slow load that you'd kind of expect when initially fetching resources/entering the application. When in your app you want it to be the fast responsive piece that the pub/sub infrastructure affords).
#SampleApp.module "PostsApp", (PostsApp, App, Backbone, Marionette, $, _) ->
class PostsApp.Router extends Marionette.AppRouter
appRoutes:
"" : "list"
":id" : "show"
API =
list: ->
new PostsApp.List.Controller
show: (id, post) ->
new PostsApp.Show.Controller
id: id
post: post
App.vent.on "posts:list:clicked", ->
App.navigate "/"
API.list()
App.vent.on "post:clicked", (post) ->
App.navigate "/" + post.id
API.show post.id, post
App.addInitializer ->
new PostsApp.Router
controller: API
Then to navigate there you'd just call App.vent.trigger "posts:list:clicked" from wherever you want (like after clicking a "View all posts" button and bubbling the event up to the controller and active on that event).
#listenTo bannerView, "posts:list:button:clicked", (args) ->
model = args.model
App.vent.trigger "posts:list:clicked"
EDIT:
In the controller handling the show call to avoid the re-fetch:
#SampleApp.module "PostsApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ post, id } = options
post or= App.request "post:entity", id
App.execute "when:fetched", post, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion post
#postRegion post
#bannerRegion post
#show #layout

Related

Redirect to a previously visited URL

I’m working on a MVC 5 (asp) application. One of the requirements I get is to have multiple navigation paths to point at the same destination page. The problem is going back to a previous page after a post according to the navigation history of the user.
Let’s consider a basic scenario of three webpages
Customers/ShowAll -> Show a list of all customers
Customers /Search -> Show a list of customers according to a search (name, country …)
Customers /Update/1134 -> Show the update page for a specific customer (i.e. customer_id=1134)
So the navigation path goes like this
ShowAll -> Update
or
Search -> Update
If the user navigates to “Customers /Search” then “Customers /Update/1134”, updates the customer information and saves the data, I want the server to redirect to the page “Customers /Search” since it’s the path the user uses.
This is a very basic case but it can be more complex like going back many pages and always return a previously visited page (or a default one if no pages match the history).
What I’ve done so far
I have created a prototype that keeps track of the navigation history of a user on the server side. It uses session storage on the client side to give a unique id for the current browser tab. On each page unload it adds the browser tab id to the cookies. Then on the server side there’s a dictionary (in the session) with the tab id (extracted from the cookies) as the key and a list of visited URLs as the value. The current URL is added to the list of URLs. I found this solution to be working but it has some flaws.
If JavaScript is disabled this solution won’t work (this is not a very big deal since I can require all the users to turn it on (it’s an intranet for a small company))
If a tab is duplicated the resulting two tabs will have the same id. This is due to the implementation of session storage (at least on Chrome). So the history on the server can get corrupted if the user uses both tabs.
I store the dictionary of history in a session variable so if the session timed out, the history is lost. I thought about keeping the history in the database but I feel it’s a little of overheat for the database.
A last thing is that the dictionary of URLs is limited to the last 30 pages visited since I want to limit the server memory. It’s not important to my question but I feel to mention it since I’m sure some of you may see the problem of keep all pages from all users for all tabs in history.
I also thought of a similar solution using cookies to transmit the last 30 pages visited on each request and have the server parse this history when it needs it. Only the pages from the application domain will be kept. This will resolve the problem of persistence after a session times out but it introduce a little more processing to the server since the history will be parsed in about each request.
I want to know if there a better solution to redirect the user according to navigation history. Maybe there’s a build-in functionality in MVC 5 that I don’t know.
Thanks for any advice.
Regards.
Using session state (as you have discovered) is not a very good solution to this problem because:
It times out, in which case the data is lost.
If the user doesn't navigate to the page the way you expect (for example, coming directly to a page via Google SERP), then it doesn't work.
The only way to make it work 100% of the time is to put all of the navigation identifier information into the URL so the system can determine how to build the navigation links.
There is no built-in functionality for this in MVC 5, but you could use MvcSiteMapProvider to solve your issue. It contains HTML helpers for Menu and SiteMapPath, which acts like a breadcrumb trail.
#Html.MvcSiteMap().Menu()
#Html.MvcSiteMap().SiteMapPath()
It works on a different principle - it loads a single shared hierarchy of nodes (a site map) into memory. Then each request that comes in matches one of the nodes and uses the map to determine how to build the links in the HTML helpers. There is no session state used at all.
The trick to getting it to work in your scenario is to make the Customers/Update/1134 page available on 2 different URLs. Then you can configure 2 different node hierarchies and it will know which one to match based on the routing information.
For example, you could add an additional route value that indicates that the page you are navigating from is the search page.
#Html.ActionLink("Customer 1134", "Update", "Customers", new { source = "Search" }, null)
By default, this will build a URL like Customers/Update/1134?source=Search. You can make it look prettier by adjusting your route configuration.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "SearchSource",
url: "Search/{controller}/{action}/{id}",
defaults: new { source = "Search", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "ShowAllSource",
url: "ShowAll/{controller}/{action}/{id}",
defaults: new { source = "ShowAll", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Now with the same ActionLink shown above, you will get the URL /Search/Customers/Update/1134. That's better. Note that when you put the ActionLink on your ShowAll page, it should be like this instead:
#Html.ActionLink("Customer 1134", "Update", "Customers", new { source = "ShowAll" }, null)
Then when you set up the node configuration in MvcSiteMapProvider, you need to make 2 different parent nodes, like this.
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<!-- Additional nodes here -->
<mvcSiteMapNode title="Search" controller="Customers" action="Search">
<mvcSiteMapNode title="Update Customer" controller="Customers" action="Update" source="Search" preservedRouteParameters="id"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Show All Customers" controller="Customers" action="ShowAll">
<mvcSiteMapNode title="Update Customer" controller="Customers" action="Update" source="ShowAll" preservedRouteParameters="id"/>
</mvcSiteMapNode>
<!-- Additional nodes here -->
</mvcSiteMapNode>
You will then get a complete navigation solution:
/Customers/ShowAll | Home > Show All Customers
/ShowAll/Customers/Update/1134 | Home > Show All Customers > Update Customer
/Customers/Search | Home > Search
/Search/Customers/Update/1134 | Home > Search > Update Customer
Of course, this is just an example. You can make the URLs and navigation links look any way you want.
Redirecting Back
Finally, there is redirecting back to the location the user came from. That's easy because MvcSiteMapProvider keeps track of the parent node.
[HttpPost]
public ActionResult Update(CustomerModel model)
{
// Update customer here...
var currentNode = this.GetCurrentSiteMapNode();
if (currentNode != null)
{
var parentNode = currentNode.ParentNode;
if (parentNode != null)
{
return Redirect(parentNode.Url);
}
}
return View(model);
}
You may wish to store some additional information (sort order, search term, etc) from the original parent page, in which case you will need to pass those parameters through the Update page and back to the parent page somehow.
One way is to use session and then use some logical default behavior if they are missing when you get to the redirect page.
Another (easier) approach is to add them as parameters (query string or route values) to the URL of the Customer Update page so they will automatically be built into the return URL. Since you have 2 different routes, you would just need to add the information to the appropriate route. You just need to ensure both the routes of the Search page and the Customer Update page include them so the parent URL is built appropriately.
Full Disclosure: I am a major contributor of the MvcSiteMapProvider project.
See also:
https://github.com/maartenba/mvcsitemapprovider/wiki/Multiple-Navigation-Paths-to-a-Single-Page
http://www.shiningtreasures.com/post/2013/08/07/MvcSiteMapProvider-40-a-test-drive
http://www.shiningtreasures.com/post/2013/09/02/how-to-make-mvcsitemapprovider-remember-a-user-position

Emberjs: Save persistent data from remote API to local storage

I am still a little confused about the way Ember fetches data from remote API and save them in the browser.
So I have created a REST Adapter to get sets of records with an Ajax call, a serializer, and the corresponding model. Suppose they are posts, and I have successfully made a posts index page where the user can click on any post to get into the post detail page.
The Ajax call happens on the index page, and using the Ember inspector, it is clear that all the records are stored in the local store.
But when I click the "back link" which is available on every post detail page, it does redirect to '/posts/' but it seems to make the ajax call again. So all the posts are fetched from the API once again making the page much less responsive.
Here's my questions:
How does that part of Ember work and how do I make Ember simply get the records from the local store without making Ajax call again and again? (unless the user refresh the browser)
If I make a GET request to 'post/1' , no data will be available since in this route no Ajax call should be made. But how do I let the data show? Should I set up another REST adapter for individual post or is it possible to get the record from the local store if an Ajax call has been made?
Hope that makes sense and thanks in advance!
Update:
My post adapter:
App.PostAdapter = DS.RESTAdapter.extend({
findAll: function(store, type, sinceToken) {
var url = 'THE URL TO GET JSON BACK';
return $.getJSON(url).then(function(data) {
return posts;
})
}
});
My Post and Posts routes:
App.PostRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('post', params.postId);
}
})
App.PostsRoute = Ember.Route.extend({
model: function() {
return this.store.find('post');
}
})
Regarding your first question: It depends on the model callback of your route. If you use the all method of the store, the Ajax Request won't be made again. (But: You'd be responsible to get the data the first time around. You way want to sideload it somewhere or may want to call find if all didn't return anything. It depends on your application.
Regarding your second question: The RESTAdapter should be available for single data items as well as for lists. You can implement a model hook in the route using find. If you link-to this route with an object (instead of an ID), this hook won't be called. So the hook would only be called when needed.

Backbone router.navigate doesn't redirect

Honestly I feel a bit stupid asking this question because should be a very simple thing...
In my app init, I have:
window.myApp =
Models: {}
Collections: {}
Views: {}
Routers: {}
initialize: ->
window.router = new Backbone.Router
Backbone.history.start(pushState: true)
And then in my View, when I click a button, I do:
router.navigate('/profile')
I have tried also with
router.navigate('/profile', true)
Now...I know that this isn't the best way of use backbone routes, but I need that my rails app manage the routes and I use backbone routes only for have routes history...
If I do window.href = '/profile' it redirects correctly.
What I'm doing wrong with backbone routes ? I've used this way time ago, but I don't find out why now it doesn't work anymore.
EDIT: The new url is always correctly show in the navigation bar, but it doesn't redirect to the new page...only show the new url in the navigation bar.
Backbone router.navigate doesn't redirect
No, it doesn't. That's not what router.navigate even does. It's has nothing to do with redirecting, it's for updating the address bar.
The new url is always correctly show in the navigation bar, but it doesn't redirect to the new page...only show the new url in the navigation bar.
Yes, this is exactly what router.navigate is documented to do. That is its express purpose. You use it to update the URL to reflect the current state of the page, not to redirect the page to something else.
If you want to redirect to a new page, you're supposed to use window.location.href = '/profile'.
If you want to update the address bar and then trigger a fresh round of Backbone routing, use router.navigate('/profile', { trigger: true });, but again, that's not how Backbone is meant to work.
We had an issue with this, and found out that sometimes even window.location.href does not work.
We were lucky to find this post Backbone: Refresh the same route path for twice and we were actually only able to make it work using a combination of the presented solution :
Navigate to an inexistent route : Backbone.history.navigate('xpto');
Then use: window.location.href = location;

Authentication with devise rails using backbone

I am new to backbone.js. I am developing a rails application using "backbone-on-rails" gem. I have included 3 models and rendering views uisng backbone. It worked fine. Now i want to add authentication to my app using devise, after the user has signed in only my app needs to be rendered otherwise i need to redirect to login page.
I have added devise gem for it.
Can someone please help me on how can i check whether user has signed in or not, if user hasn't logged in need to redirect to devise sign_in page using backbone?
Thanks in Advance
Backbone's a frontend-only framework, so it doesn't have a concept of authentication. All the source code is sent to the web browser, and all the network connections are plain to see, so a malicious user can trick your app into thinking it's logged in, even if it isn't. So you'll still need to check access permissions on the server.
What you can do, though, is have your Backbone app detect whether it thinks it's logged in, and change its display based on that. For instance, you could use Devise's user_signed_in? helper to add a data attribute on your body tag, and hook into that. Something like this in your app/views/layouts/application.html.erb:
<body data-user-signed-in="<%= user_signed_in? ? "true" : "false" %>">
And then, maybe your Backbone router is going to look something like this:
myApp.Routers.Router = Backbone.Router.extend({
routes: {"": "showFrontPage"},
isSignedIn: function() {
return $('body').data('user-signed-in') === "true";
},
showFrontPage: function() {
var view;
if (this.isSignedIn()) {
view = new myApp.Views.MainAppView();
} else {
view = new myApp.Views.SignInView();
}
view.render();
}
});
Alternatively, you could look directly for a session cookie. That seems a bit more brittle to me, though; if the name of your application changes, or Rails changes how it names its cookies, your app's going to break. But in that case, your isSignedIn() function is going to look more like this:
isSignedIn: function() {
return document.cookie.indexOf("_Railsappname_session") > -1;
}
If you want to check your user at various points of your app, you could easily write a controller method that returns the result of user_signed_in? as a JSON object. But it's better not to rely on this; rather than calling /user/is_signed_in and then /posts/create, far better to do one call to /posts/create and have that return a 401 Unauthorized if the user's not logged in.
As for logging in itself, you can adapt Devise to work via JS so you can login via AJAX, but it's not as straightforward as you might hope. There's a tutorial here.
I needed to implement a backbone login in rails with devise. Note that for my purposes I did not need user registration as well, since I wanted to have just one admin user, created manually in the terminal by me.
Basically as long as you make an AJAX post request to the right devise route, devise will handle the login for you (assuming of course you went through the devise setup process correctly).
In Backbone you can make this post request using a new model save.
This tutorial helped me set up my Backbone model and view (FYI: the tutorial also goes over what you need to do in order to add registration functionality).
The tutorial had some more advance backbone setup (for example it uses backbone.marionette and backbone.modelbinder) which although very useful, I did not want to get into. Below is my simplified version of the tutorial to the bare core of what you need.
Create a model with the urlRoot that matches your devise login route. For most people that go with the standard User model, the urlRoot route below should work. Note my code is written in coffeescript
class MyCoolBackboneApp.Models.UserSession extends Backbone.Model
urlRoot: '/users/sign_in.json'
defaults:
email: ""
password: ""
toJSON: ->
{ user: _.clone(#attributes) }
Note that devise expects the params to be wrapped inside 'user' which is why we had to overwrite the toJSON method
Then in your view, all you need to do is save the model together with the login credentials. Of course every person might have a different success and failure callback, but here is a very basic implementation:
events:
'submit form': 'login'
initialize: =>
#model = new MyCoolBackboneApp.Models.UserSession()
render: =>
$(#el).html( #template() )
#
credentials: ->
{
email: #$('#email').val(),
password: #$('#password').val(),
remember_me: 1
}
login: (event)->
event.preventDefault()
#model.save(#credentials(),
success: (userSession, response) =>
window.location.href = "/"
error: (userSession, response) =>
message = $.parseJSON(response.responseText).error
alert(message)
)
You should also read this tutorial about how to set up devise ajax authentication.
After you complete the above tutorial, you should be able to save your UserSession model with the right credentials (as I do in the view) and login successfully (assuming you have a saved existing user in your Database). You'll know you've logged in successfully when you get redirected to your success callback.
Then in the controllers in the rest of your app, you should be able to use the devise helpers: user_signed_in? or current_user etc etc.
(If you are logged in but get an undefined method error for these helpers, try to add: include Devise::Controllers::Helpers to your controllers).
Finally Alex P's response can then walk you through how to use the user_signed_in? boolean in your Backbone views.

Backbone fetch failure notification

We're having an issue with our backbone application. We want to provide a user with a notification when a fetch fails (timeout or general error), but we want to display a dialog over the previous page's content rather than showing an error message in the new page (how Facebook/LinkedIn etc. do it)
To trigger a request for the new content, we have to navigate to the new URL first. We can't really change this without a rework, so we want to avoid this if possible. What we need to do is send the user back to the previous URL when there is a connection error, which would cause the route to fire, re-requesting the previous content. We really want to avoid doing this however.
We're aware that we can send a user back using a navigate without triggering a route, but this will mess up the browser history, making backwards become forwards in this case. We could also force a browser back, keeping the history trail correctly, but this would force a re-fetch of the content.
We've also investigated setting a flag of some kind telling our router not to re-request data on the next route change, but this would cause issues when browser back is used to go to a previous screen on which the fetch fails. In this instance we'd need to send the user 'forwards' in their journey instead. As far as we know, this isn't possible using the browser's history manager.
Is there any way of having a dialog how we want, or will we have to go the same way as Facebook/LinkedIn and co.?
Do you have an example of your code / what you have tried?
Going off what you have said, if there is an error fetching the model data after your URL has changed you can silently redirect the user back to the previous URL using the router, e.g:
window.product_v = Backbone.View.extend({
render: function() {
this.model.fetch({
processData: true,
data: this.model.attributes,
success : function(d){
},
error : function(d) {
MyRouter.previous();
}
})
}
});
Then in your router could keep an array of your history so that the route isn't 'triggered' on redirect. or by simply doing:
Backbone.history.navigate(route, {trigger: false, replace: true});
The below question/answer describes this perfectly:
Silently change url to previous using Backbone.js
class MyRouter extends Backbone.Router
constructor: (options) ->
#on "all", #storeRoute
#history = []
super options
storeRoute: ->
#history.push Backbone.history.fragment
previous: ->
if #history.length > 1
#navigate #history[#history.length-2], true

Categories

Resources