Using Backbone.js, is it possible to make the router navigate to the page where it came from? I'd like to use that for the case where I change my URL when a popup appears, and I want go change it back, when I hide the popup. I don't want to simply go back, because I want to keep the background page in precisely the same position as I left it before I showed the popup
You can solve this problem by extending Backbone.Router and storing all the routes during navigation.
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
Then, when you have to dismiss your modal, simply call the MyRouter.previous() method that it will redirect you back to the last "hash".
I think mateusmaso's answer is mostly right but requires some tweaks to guarentee that you always get the right URL you are looking for.
First you need to override the route method to have a beforeRoute method fire:
route: (route, name, callback) =>
Backbone.Router.prototype.route.call(this, route, name, =>
#trigger('beforeRoute')
callback.apply(this, arguments)
)
Then you bind the event and initialize the history instance variable:
initialize: (options) ->
#history = []
#on "beforeRoute", #storeRoute
Next create helper methods for storing and retrieving the fragment:
storeRoute: =>
#history.push Backbone.history.fragment
previousFragment: =>
#history[#history.length-2]
Finally, you need one final helper method that can be used to change the URL without reloading and store the resulting fragment. You need to use this when closing the pop up or you won't have the expected fragment in your history if the user gets the pop up again without navigating anywhere else. This is because calling navigate without "trigger: true" won't trigger the event handler to store the fragment.
changeAndStoreFragment: (fragment) =>
#navigate(fragment)
#storeRoute()
This answer may not address the question, but it's preety much the same issue. I couldn't navigate silently to a previous route, or a custom route because of the trailing slash. In order for route functions to NOT be triggered, use {trigger:false}, or don't use trigger at all since false is the default behaviour, and make sure your route begins with #something instead of '#/something' (notice the slash), or change the regEx inside Backbone.js, the router part.
You can trigger a route in the onCloseEvent of your popup or overlay with:
router.navigate('/lasturl/');
This will set the url. If you pass true as the second param, you also will execute the routes action. Otherwise the page will be left unchanged.
http://documentcloud.github.com/backbone/#Router-navigate
Related
I have a page with an ion-navbar, when I navigate to the next page I use the following:
this.nav.push(SubCategoryPage, {
employeeModel: this.employeeModel
});
It passes the param parameter successfully to the next page.
When I am in the next page (SubCategoryPage), I update the parameter object (employeeModel). My question is, when I click the back arrow to return to the previous page, how do I pass the modified parameter object (employeeModel) back?
When back is clicked, the ngOnDestroy is called, do I use this, but then how do I pass the parameter object?
OR, is there some other scope I should put the object in? (How do you pass objects from one page to another, including back?) What's best practice?
Thanks
I recommending caching the object in a provider. That way, its properties will remain the same when navigating between your components. By caching, all I mean its a simple get and setter.
SOLVED:
in app.ts I need to set the object, then it is shared between pages
ionicBootstrap(MyApp, [EmployeeModel]);
ref: https://www.joshmorony.com/an-in-depth-explanation-of-providers-in-ionic-2/
I have a Durandal application, and I use router.mapUnknownRoutes to display a user-friendly error page if the URL does not correspond to a known route. This works fine -- if I go to, say /foo, and that doesn't match a route, then the module specified by mapUnknownRoutes is correctly displayed.
However I cannot find any way to display that same error page when I have a parameterised route, and the parameter does not match anything on the backend.
For example, say I have a route like people/:slug where the corresponding module's activate method looks like this:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
});
};
If I go to, say /people/foo, then the result depends on whether dataService.load('foo') returns data or an error:
If foo exists on the backend then no problem - the observables are set and the composition continues.
If foo doesn't exist, then the error is thrown (because there is no catch). This results in an unhandled error which causes the navigation to be cancelled and the router to stop working.
I know that I can return false from canActivate and the navigation will be cancelled in a cleaner way without borking the router. However this isn't what I want; I want an invalid URL to tell the user that something went wrong.
I know that I can return { redirect: 'not-found' } or something similar from canActivate. However this is terrible because it breaks the back button -- after the redirect happens, if the user presses back they go back to /people/foo which causes another error and therefore another redirect back to not-found.
I've tried a few different approaches, mostly involving adding a catch call to the promise definition:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
}).catch(function (err) {
// ... do something to indicate the error ...
});
};
Can the activate (or canActivate) notify the router that the route is in fact invalid, just as though it never matched in the first place?
Can the activate (or canActivate) issue a rewrite (as opposed to a redirect) so that the router will display a different module without changing the URL?
Can I directly compose some other module in place of the current module (and cancel the current module's composition)?
I've also tried an empty catch block, which swallows the error (and I can add a toast here to notify the user, which is better than nothing). However this causes a lot of binding errors because the observables expected by the view are never set. Potentially I can wrap the whole view in an if binding to prevent the errors, but this results in a blank page rather than an error message; or I have to put the error message into every single view that might fail to retrieve its data. Either way this is view pollution and not DRY because I should write the "not found" error message only once.
I just want an invalid URL (specifically a URL that matches a route but contains an invalid parameter value) to display a page that says "page not found". Surely this is something that other people want as well? Is there any way to achieve this?
I think you should be able to use the following from the activate or canActivate method.
router.navigate('not-found', {replace: true});
It turns out that Nathan's answer, while not quite right, has put me on the right track. What I have done seems a bit hacky but it does work.
There are two options that can be passed to router.navigate() - replace and trigger. Passing replace (which defaults to false) toggles between the history plugin using pushState and replaceState (or simulating the same using hash change events). Passing trigger (which defaults to true) toggles between actually loading the view (and changing the URL) vs only changing the URL in the address bar. This looks like what I want, only the wrong way around - I want to load a different view without changing the URL.
(There is some information about this in the docs, but it is not very thorough: http://durandaljs.com/documentation/Using-The-Router.html)
My solution is to navigate to the not-found module and activate it, then navigate back to the original URL without triggering activation.
So in my module that does the database lookup, in its activate, if the record is not found I call:
router.navigate('not-found?realUrl=' + document.location.pathname + document.location.hash, { replace: true, trigger: true });
(I realise the trigger: true is redundant but it makes it explicit).
Then in the not-found module, it has an activate that looks like:
if (params.realUrl) {
router.navigate(params.realUrl, { replace: true, trigger: false });
}
What the user sees is, it redirects to not-found?realUrl=people/joe and then immediately the URL changes back to people/joe while the not-found module is still displayed. Because these are both replace style navigations, if the user navigates back, they go to the previous entry, which is the page they came from before clicking the broken link (i.e. what the back button is supposed to do).
Like I said, this seems hacky and I don't like the URL flicker, but it seems like the best I can do, and most people won't notice the address bar.
Working repo that demonstrates this solution
This is a simple question, but I am new to routing and haven't been able to find an answer to this.
I have a Marionette Router (if I intimidate you, its really the same thing as a Backbone Router. Very simple).
sys.routes = {
"app/:id": "onAppRoute",
};
sys.Router = new Marionette.AppRouter({
controller: {
onAppRoute: function(route) {
console.log('Called app route!');
},
},
appRoutes: sys.routes,
});
Backbone.history.start({pushState: true})
This works - if you hit the back button my browser, the url will change within my Single Page Application and will call the onAppRoute function.
However, let's say I open a new browser window and paste in my page url to a certain 'app':
http://localhost/app/myApplication
This doesn't call the onAppRoute function. It doesn't even seem like it should, though, but I really don't know.
I want it to.
I don't know if I am doing it wrong, or if I should just manually fire it by grabbing my page url on page load, parsing it, then 'navigating' to that route. Seems hacky.
Contrary to your intuition, backbone's default behaviour is to trigger matching routes on page load! cf. http://backbonejs.org/#Router - look for the option silent: true. You'd have to specify that for the router to IGNORE your matching routes on page load, i.e. not trigger the corresponding callbacks.
So your problem lies somewhere else: your routes do NOT match the url you have stated as an example. Clearly, you require an :id parameter, trailing http://localhost/app/myApplication. Therefore, http://localhost/app/myApplication/213 would cause your callback to be triggered on page load, given you didn't pass silent: true as an option to backbone.history.start().
If you want to match the 'root' url, i.e. no params, you would define the following route:
routes: {
'/': someFunction
}
The :id part is a parameter, which will be extracted by Backbone.Router and sent as an argument to onAppRoute.
But in your URL you don't have any parameters /localhost/app/myApplication
I have an action:
{{action create target="controller"}}
which I have targeted to the bound controller (rather than the router) like this:
App.AddBoardController = Ember.Controller.extend
create: ->
App.store.createRecord App.Board, {title: #get "boardName"}
App.store.commit()
//TODO: Redirect to route
How do I redirect back to a route from the controller action?
Use transitionToRoute('route') to redirect inside an Ember controller action:
App.AddBoardController = Ember.Controller.extend({
create: function(){
...
//TODO: Redirect to route
this.transitionToRoute('route_name');
}
...
In fact, this is not Ember idiomatic. From what I know, and what I have learnt from Tom Dale himself, here are some remarks about that code:
First, you should not transitionTo from elsewhere than inside the router: by doing so, you are exposing yourself to serious issues as you don't know in which state is the router, so to keep stuff running, you will quickly have to degrade your design, and by the way the overall quality of you code, and finally the stability of your app,
Second, the action content you are showing should be located inside the router to avoid undesired context execution. The router is indeed a way to enforce a coherent behavior for the whole app, with actions being processed only in certain states. While you are putting the actions implementation into Controllers, those actions can be called at anytime, any including wrong...
Finally, Ember's controllers are not aimed to contain behavior as they rather are value-added wrappers, holding mainly computed properties. If you nevertheless want to factorize primitives, maybe the model can be a good place, or a third party context, but certainly not the Controller.
You should definitely put the action inside the router, and transitionTo accordingly.
Hope this will help.
UPDATE
First example (close to your sample)
In the appropriated route:
saveAndReturnSomewhere: function (router, event) {
var store = router.get('store'),
boardName = event.context; // you pass the (data|data container) here. In the view: {{action saveAndReturnSomewhere context="..."}}
store.createRecord(App.Board, {
title: boardName
});
store.commit();
router.transitionTo('somewhere');
}
Refactored example
I would recommend having the following routes:
show: displays an existing item,
edit: proposes to input item's fields
Into the enclosing route, following event handlers:
createItem: create a new record and transitionTo edit route, e.g
editItem: transitionTo edit route
Into the edit route, following event handlers:
saveItem: which will commit store and transitionTo show route, e.g
EDIT: Keep reading, Mike's answer discusses some of the problems with this approach.
You can just call transitionTo directly on the router. If you are using defaults this looks like App.router.transitionTo('route', context).
I'm just starting to play with Backbone JS as we are starting a very JS intensive application and I want to follow a convention to keep the project from looking horrific in later months and years.
This is probably a simple question but I can't figure it out.
I have the normal RESTful actions/views etc. and have added another call info just to play with. This renders the index page correctly when the respective link is clicked.
:javascript
$(document).ready(function() {
$("#client-clicker").click( function() {
window.router = new Pm.Routers.ClientsRouter({clients: #{#clients.to_json.html_safe}});
Backbone.history.start();
});
});
Now for the questions:
How does this know to go to the index action of the ClientsRouter?
How do I specify the edit, show or info actions?
Thanks in advance!
I have never used a "convention" for js but backbone js just makes sense!
Update
I did find that if I delete this line in the Clients Router, the default to index breaks.
routes:
"/new" : "newClient"
"/index" : "index"
"/:id/edit" : "edit"
"/:id" : "show"
".*" : "index" <----- #deleted and it broke
"/info" : "info"
I'm still not sure how to hit a specific view.
When you call Backbone.history.start Backbone starts watching for changes to the location, either using pushstate or hashchange events or in older browsers polling the location. It also immediately triggers the route for the current location by comparing window.location.pathname with the route table.
In your case it matches on the ".*" route. I am not sure what the location is at the time this happens but this route will match any location so that's why it is triggering the index action.
You can change location and optionally trigger an action by calling the navigate method on your router. e.g.
window.router.navigate('/new', true);
should change the location and (because you are passing true in the second parameter) trigger the newClient action.