Avoid traliing slash for default routes in Backbone.js - backbone.js

I'm looking for something to accomplish in Backbone. Here is the explanation.
I have backbone routes like
{
'': 'defaultRoute',
'test': 'test',
'testing': 'testing'
}
Assuming my root url is '/routes'
Now when I say 'router.navigate('test', {trigger: true});' url changes to /routes/test.
Similar way when I call 'router.navigate('testing', {trigger: true});' url changes to /routes/testing.
But when I call 'router.navigate('', {trigger: true});' url changes to /routes/.
You know i didn't expect that / at the end. I never passed that. It should have been back to root url i.e. '/routes'.
Adding / at the end makes lot of difference/meaning. Checkout 'http://googlewebmastercentral.blogspot.in/2010/04/to-slash-or-not-to-slash.html'
Any fix for that (i.e. not having / at the end for default route)?

Try to override Backbone.History.prototype.navigate.
Copy default function and change this line :
var url = this.root + fragment;
to :
if(this.root !== '/' && fragment === ''){
var url = this.root.replace(trailingSlash, '');
}else{
var url = this.root + fragment;
}

First things first:
If your application is not being served from the root url / of your
domain, be sure to tell History where the root really is, as an
option: Backbone.history.start({pushState: true, root:
"/public/search/"})
Backbone#History
In your case, it is most likely to be something like root: "/routes". Something you need to keep in mind is that your website is seen in the browser as either a resource or a remote folder, therefore the default route without a trailing slash may not fully work by just leaving it as "" because that means usually "the current folder". You could try to set your root url as root: "/" instead (just as it should be by default) and create a "routes" resource as your default route, something like the following:
{
'test': 'test',
'testing': 'testing',
'routes': 'defaultRoute',
'*': 'catchAll'
}
Another recommendation that I am doing to you (as you can see it above) is to set your a catch all URL at the end in case someone enters a non-existent one, and you can also use it to redirect your users to your default route.
// your code above ...
defaultRoute: function() {
// your default route code...
},
catchAll: function() {
this.navigate('routes', {trigger: true});
}
// your code below ...
Finally, by any reason mess up with Backbone URLs manually, you are highly risking yourself to break the whole thing AND if in the future the API changes it should be easier to update if you just follow the intended use.

Update:-
This has been already fixed in Backbone's latest version. I asked a question on there forum and found the answer. I'm adding it here as an answer if it helps anyone who is looking for the same thing.
https://github.com/jashkenas/backbone/issues/2871

Related

React Router: How to properly append to route/url without replacing anything?

How do I append to the route with react router?
Let's say the current route is /page1/page2 and I want to route to /page1/page2/next-page
first I get the router
const router = useHistory();
When I use push like this
router.push("next-page");
It routes to /page1/next-page.
When I use add a / like this
router.push("/next-page");
It routes to /next-page
I also tried something like this
router.push(`${router.location.pathname}/next-page`)
But the problem this way is, when I'm currently at /page1/page2/, I end up at /page1/page2//next-page with two //.
Is there a good way to solve this without having to write the complete route like router.push("/page1/page2/next-page")?
What I ended up doing is defining a function that removes an / form the url if there
export const removeSlashSuffix = (input) => {
if (input.charAt(input.length - 1) === "/") {
return input.substr(0, input.length - 1)
} else {
return input;
}
}
and then route like this
router.push(`${removeSlashSuffix(router.location.pathname)}/nest-page`)
This way I get no issues with routes that resolve in url with // in it.
This is somewhat of a hacky solution and I wish react-router-dom would support this out of the box but it doesn't seem like it does
If you do router.push("/page1/page2/next-page"), it should work. I think you need to specify the full path, I don't see anything wrong in doing so
if you want to append something to an existing path, you should be able to do something like
const location = useLocation()
const currentPath = location.pathname // to get current route
router.push(`${currentPath}/next-page`)
THis is a sudo-code, so you may need to tweak it a bit for proper syntax

location.path() not redirecting to page

In node this is how I define my details route (render as jade and send).
app.get('/details', function(req, res){
jade.renderFile('details.jade', function(err, html){
if(err){
console.log(err);
}
res.send(html);
});
});
In jade with 'blah' is clicked then calls navigateToPath function with params.
a(ng-click="navigateToPath(date_obj_key, part)") blah
In angular, this function should go to this path. The url path changes in the browser but it does not REDIRECT to the page. Help (yes I am injecting the location service)
$scope.navigateToPath = function(date, part){
var path = '/details?date='+date+'&part_type='+part;
$location.path('/details').replace(); //also tried $location.url(path)
$scope.apply(); //also tried $scope.$apply and $rootScope.$apply as func
console.log($location.path());
}
I am using Fire Fox developer tools(F12) and put a break point on where I used $window.location in my project and looked at the values in $window.location and this what it shows:
It seems like this would work. for both a location in the same folder or sub-folder as well as going to a completely different web site.
$window.location = $window.location.origin + path
or
$window.location = <whatever website you want to go to>
In my case I was just using the $window.location to call a rest service to download a file that the user selected from a ui-grid while still staying on the same page. and this may have worked for me because my scenario is a bit different then what yours is I think. so all I had to do was
$window.location = "../../services" + "<path to my rest service>" + $scope.shortCode + "/" + $scope.wireInstSelectedRow.entity.fileName;
#shapiro I am not sure why this does not work
$location.path('/details').replace();
I tried the same thing originally in my project and based on the documentation: https://docs.angularjs.org/api/ng/service/$location It seems like that would work and what its supposed to be used for but what I noticed is it would put a '#' character in the url before it put the path I wanted it to take which was preventing it from going to that page. Anyhow for me it seems like as long as you are going to an html page that is in the same folder or sub-folder just doing
$window.location = <the path you want to go to>;
is a good solution... at least it did the trick for me. Hope this helps.

Router in backbone not handling route after initialization

I have multiple routers in my app, in general way it looks like this:
// Start backbone.js
if (!Backbone.History.started) {
Backbone.history.start({pushState: true, hashChange: false});
}
// Perform some RPC requests ...
// Depending on user role, received from the server should be created suitable router:
var router;
if (typeof app.user.role === 'manager') {
router = new routers.manager();
} else {
router = new routers.guest();
}
Problem is that after page is loaded and script is executed routers do not do. anything. They do not load route for current url automatically. So, i had to fix it this way (i am not sure that it is a right way):
routers.guest.initialize = routers.manager.initialize = function() {
var defaultRoute = 'default';
if (typeof this.routes[Backbone.history.fragment] !== 'undefined') {
this[this.routes[Backbone.history.fragment]]();
} else {
this.navigate(defaultRoute, true);
}
};
It is working fine, except one bug: when i use route with params, for example /reset-password-confirm/:code - it is unable to find in in routes property. I could write some more code to fix it, but i suppose that i am doing something wrong, if i have to write such things - as i understand router should handle routes just after it was created.
So, questions:
Why my router(s) does not handle routes for current url after it is being created? Perhaps i need to start backbone history later? (but this bug will happen again later then)
How it is possible to make routes with params like /user/:id work there?
Perhaps it is bad idea to re-create routers? Perhaps it is better to create all of them one time?
P.S. I've tried to create both routers and keep them, also i've trie to call backbone history start method after all routers were created.. but this didn't help :/
Assuming you route is declared as the following:
routes : {
'/user/:id' : 'user'
}
Your initialize code is not working because when you initialize your router with a url such as: /user/1234. Backbone.history.fragment will be /user/1234 (not /user/:id). Since the this.routes object doesn't have a key of /user/1234, your else clause calls the default route.
If you first instantiate your router then call Backbone.history.start(), you will be able to remove your router initialize code. When you navigate to a url as /user/1234 your router will match the /user/:id route and call the user function.
The following should work for you without adding your initialize code:
var router = (app.user.role === 'manager') ? new routers.manager()
: new routers.guest();
Backbone.history.start({pushState: true, hashChange: false});
Looking at the code, seems like you're starting the backbone history before initializing any routes. That's most likely not goning to work.
The correct way of doing this type of seperation is by creating all the routes based on the role received from the server and then start the backbone history. Here's an SO thread that talks about it with code samples as well : How to protect routes for different user groups

Backbone.history.navigate disregard trigger:false

I try to study and use Backbone/Marionette in my project. Now I stuck with Router navigation which work not as I though it should.
class MyApp.Router extends Marionette.AppRouter
appRoutes :
'info/:place/(:what)' : 'places_page'
MyApp.Controller = ->
places_page: (place,what)->
console.log 'Triggered places_page'
MyApp.addInitializer( ->
controller = new MyApp.Controller()
new MyApp.Router
controller: controller
Backbone.history.start( pushState: false )
)
MyApp.vent.on('do:search', ->
console.log 'triggered do:search'
place = 'Moscow'
what = 'Пицца'
info_model.set place: place, item:what
new_url = 'info/'+where+'/'+what
if new_url != decodeURIComponent(Backbone.history.fragment)
Backbone.history.navigate(new_url, {trigger: false})
On initial load of site.com/#info/Budapest/Vine page or reload it, I get Triggered places_page message as I expect.
But when I fire do:search event which update url to site.com/#info/Moscow/Пицца, I get Triggered places_page again! So it reload all my views from scratch instead of just change url and re-render one model.
What I can do wrong here?
Update 2:
Found strange thing. If I use latin letters in new url, everything work like it should.
But if I use cyrillic in new url path, it will trigger route function.
Backbone: 1.0, Marionette:v1.0.3, jquery: 1.9.1
Mystery solved!
That happens because of non-latin symbols in url.
Correct code:
new_url = 'info/'+encodeURIComponent(where)+'/'+encodeURIComponent(what)
if new_url != Backbone.history.fragment
Backbone.history.navigate(new_url, {trigger: false})
Because Backboune.navigate don't execute navigate if url didn't change and trigger is false by default, I can write it simple like that:
new_url = 'info/'+encodeURIComponent(where)+'/'+encodeURIComponent(what)
Backbone.history.navigate(new_url)
Backbone.history takes an object when starting. Try this syntax in CoffeeScript:
Backbone.history.start
pushState: false
In addition, pushState is false by default, so you can just have Backbone.history.start()
Does this solve your issue?
Proper URL encoding is required. I couldn't find this in the docs of Backbone related to the router functionality.
I had the same issue, alas with spaces in the query part, i.e.:
#app/terms?filter=java ee
Together with the encodeURIComponent solution as described in your answer, I also found the following lines of comments in backbone.js (1.0.0), pertaining to the navigate function:
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.

AngularJS HTML5 mode degrade to full page reloads in lieu of hashbang

By enabling HTML5 mode in AngularJS, the $location service will rewrite URLs to remove the hashbang from them. This is a great feature that will help me with my application, but there is a problem with its fallback to hashbang mode. My service requires authentication, and I am forced to use an external authentication mechanism from my application. If a user attempts to go to a URL for my app with a hashbang in it, it will first redirect them to the authentication page (won't ever touch my service unless successfully authenticated), and then redirect them back to my application. Being that the hash tag is only seen from the client side, it will drop off whatever parts of the routes come after by the time they hit my server. Once they are authenticated, they may re-enter the URL and it will work, but its that one initial time that will cause a disruption to the user experience.
My question is then, is there any way to go from $location.html5Mode(true) to the fallback of full page reloads for un-supportive browsers, skipping the hashbang method of routing entirely in AngularJS?
The best comparison of available implementations of what I'm aiming for would be something such as browsing around folders on github.com. If the browser supports rewriting the URL without initiating a page refresh, the page will asynchronously load the necessary parts. If the browser does not support it, when a user clicks on a folder, a full-page refresh occurs. Can this be achieved with AngularJS in lieu of using the hashbang mode?
DON'T overwrite the core functionality.
Use Modernizr, do feature detection, and then proceed accordingly.
check for history API support
if (Modernizr.history) {
// history management works!
} else {
// no history support :(
// fall back to a scripted solution like History.js
}
Try to wrap $location and $routeProvider configuration in browser's HTML5 History API checking, like this:
if (isBrowserSupportsHistoryAPI()) {
$location.html5Mode(true)
$routeProvider.when(...);
}
Also may be you need to create a wrapper to $location if you use it to change path.
(Sorry for terrible english)
Why not handle the un-authenticated redirect on the client side for this situation? I'd need to know a bit more about exactly how your app works to give you a more specific solution but essentially something like:
User goes to a route handled by AngularJS, server serves up the AngularJS main template and javascript
User is not authenticated, AngularJS detects this and redirects to the authentication page
You could have something in the module's run block for when the AngularJS application starts:
module('app',[])
.configure(...yadda...yadda...yadda...)
.run(['$location', 'authenticationService', function($location, auth) {
if (!auth.isAuthenticated()) {
$location.url(authenticationUrl)
}
});
I've subbed in a service which would find out if you were authenticated somehow, up to you how, could be checking a session cookie, could be hitting your API to ask. Really depends on how you want to continue to check authentication as the client application runs.
You can try and override the functionality of the $location service. The general idea would be to rewrite the URL according to whether someone is already authenticated or not, or just use a single approach (without hashbangs) for all URLs, regardless to whether html5mode is on or not.
I'm not sure that I fully understand the use-case so I can't write the exact code that you need. Here is a sample implementation of how to overrides/implements and registers the $location service, just making sure that hashbang is always eliminated:
app.service('$location', [function() {
var DEFAULT_PORTS = {
ftp: 21,
http: 80,
https: 443
};
angular.extend(this, {
absUrl: function() {
return location.href;
},
hash: function(hash) {
return location.hash.substr(1);
},
host: function() {
return location.host;
},
path: function(path) {
if (!path) {
return location.pathname;
}
location.pathname = path;
return this;
},
port: function() {
return location.port ? Number(location.port) : DEFAULT_PORTS[this.protocol()] || null;
},
protocol: function() {
return location.protocol.substr(0, location.protocol.length - 1);
},
replace: function() {
return this;
},
search: function(search, paramValue) {
if (search || paramValue) {
return this;
}
var query = {};
location.search.substr(1).split("&").forEach(function(pair) {
pair = pair.split("="); query[pair[0]] = decodeURIComponent(pair[1]);
});
return query;
},
url: function(url, replace) {
return this.path();
}
});
}]);

Resources