Backbone router.navigate doesn't redirect - javascript

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;

Related

How to get Backbone.js Router to trigger on default/index (no #tag hashtag) in CodeIgniter

I have a CodeIgniter app using .htaccess to remove index.php from the url so they look like this:
http://example.com/admin/
http://example.com/admin/users
http://example.com/admin/reports
http://example.com/admin/files
With http://example.com/admin/files being a single page backbone app, I need to show a list of files by default. Then http://example.com/admin/files#file/123 would edit a specific file.
Here is my Backbone Router:
App.Router = Backbone.Router.extend({
routes: {
'' : 'files',
'file/:id' : 'file'
},
files: function(){
$(document.body).append("Files route has been called..");
},
file: function(id){
$(document.body).append("File route called with id:" + id);
}
});
new App.Router;
Backbone.history.start();
I can get the "file" route to trigger with:
http://example.com/admin/files#file/33
However, http://example.com/admin/files is completely ignored by the Backbone Router.
How can I get the default or index Backbone.js route to trigger when using CodeIgniters RewriteRule that eliminates the index.php file?
UPDATE: I'm very new at Backbone.js so still learning. I discovered if I change:
Backbone.history.start();
to:
Backbone.history.start({root: "/admin/files/" });
It works correctly. :)
The Backbone Router works with the browsers history api where it uses hash names (#something) to represent some state on the client. Initially, /admin/files is a route on the server and ignored on the client so you won't be able to use the Router for the files route. Since your page is rendered for the server route, I would just run whatever code you need to run on page load. I think that is what you want. If you need someway to get back to this initial "files" state, then you probably add an index route so that it would show up like /admin/files#index which would get you back to when the page first initially loaded.

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

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

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.

How to redirect user after successful login?

Update:
After implementing the below suggestion by Rob Sedgwick, it has become apparent that the redirect only works when the user manually "F5" refreshers the browser (Chrome). What I need to achieve is that this happens automatically in the code so the redirect happens without the user having to hot refresh. Thanks for help with this last part.
At the moment ManageTodosView from what I understand is the first action after the user has been logged in. It prints a list of to do items set up by the user. A working example can be found here http://parseplatform.github.io/Todo/ and the code is https://github.com/ParsePlatform/Todo
I'm using to code to really get user logins to work, I'm not to worries about what the output of the rest of the code is because the long term plan will be to remove it, for the time being its helpful to keep in place to show that the app functioning correctly.
I'm using this code as a base to build a web app. At the moment, once the user is logged in, they are displayed data on the same page.
I want to be able to change this so that after they login, the user is redirected to a different page and the information is then displayed to them there.
The reason for this is that the index page is just a landing/login page and I want to redirect them to a more structured HTML page with menus, etc.
Within the JS code, do I just put in a redirect, something like:
self.location="top.htm";
to this area of the code?
// The main view for the app
var AppView = Parse.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#todoapp"),
initialize: function() {
this.render();
},
render: function() {
if (Parse.User.current()) {
new ManageTodosView();
} else {
new LogInView();
}
}
});
I have added the JS code to this JSFiddle
Update:
To address the issue of the page needing a manual fresh before the redirect works, insert
window.location.href="/someurl";
into the following code section within the todoe.js file and comment out the new ManageTodosView(); code.
Parse.User.logIn(username, password, {
success: function(user) {
window.location.href="user_home.html";
//new ManageTodosView();
self.undelegateEvents();
delete self;
},
Try this one also
window.open('url','_parent');
I would suggest a more robust template for integrating Parse Todo samples with real apps. 'Marionette' offers lots of value in real world.
If you take the time to look over the app's structure and then look at the 'loginSuccess' function (scroll to very bottom of link), its pretty straightforward to redirect. You can either use the router as shown OR you can use the Marionette aggregated events which would look like:
vent.trigger('header:loggedIn', _user);
somewhere else in any module within the app....
vent.on('header:loggedIn', function (user) {
that.setViewNew(user);
});
...
setViewNew : function (user) {
User = user;
var viewOptionsUser = {
collection : this.roleList,
events : {
'keypress #new-todo': 'createTaskOnEnter'},
parentUser : User};
this.headerRegion.show(new HeaderView(viewOptionsUser));
this.mainRegion.show(new RoleRoleListCompositeView(viewOptionsUser));
}
Try something like this to redirect your user
window.location = 'www.google.com'

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