Motivation

In the approach with routable modal described in previous posts, there is one drawback. Closing link located in the modal needs to be handled with additional logic if we want to achieve the same effect when we don’t use a routable approach. In the routable approach we open modal for a specific route so close action needs to make a redirect to the previous route from the history.

We can handle closing modal by simple link-to pointing to the main application route.

1
2
3
{{#full-screen-modal-dialog}}
{{#link-to "application"}}Close{{/link-to}}
{{/full-screen-modal-dialog}}

But what when we want to redirect the user to the previous view, similarly like when the user clicks on the back button in the browser?

There is a problem with how to track the history of visited routes. I was researching a long time and found a few solutions:

  1. Use dedicated add-ons:

    • ember-cli-history-mixin
    • ember-route-history

    Disadvantage: Seems to be not maintained and problems with recording the history of nested routes. Generally fetching ids from the router object is hard.

  2. Use window.history.back()

That looks fine but doesn’t work the same way in all browsers. Also, it would redirect the user outside the app.

  1. Some code snippets on Stackoverflow which hacks router to extract previous route state.

The code seems quite complicated and hacky.

Finally, I chose a solution on this blog: http://kiprosh.com/blog/ember-js-how-to-track-last-visited-route

Although, it wasn’t perfect, because it still didn’t work with nested routes.

On the occasion, I approached it with TDD believing that I will discover something new.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test('click on close redirects back to the previous route from nested routes', function(assert) {
server.createList('note', 1);
server.createList('comment', 1);

visit('/note/1/comment/1');

andThen(function() {
assert.equal(currentURL(), '/note/1/comment/1');
});

click('a#search');

andThen(function() {
assert.equal(currentURL(), '/search');
});

click('a.search-close');

andThen(function() {
assert.equal(currentURL(), '/note/1/comment/1');
});
});

…and started debugging and looking at router object, to retrieve some info current route path.

Happily, I found something new - router.location.path which stores the string. I was surprised. It looks like something new in Ember and doesn’t need to hack it to make a transition back.

So updated recording last visited route in beforeModel hook:

routes/search.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Ember from 'ember';

export default Ember.Route.extend({

beforeModel() {
let previousRoutes = this.router.router.currentHandlerInfos;
let previousRoute = previousRoutes && previousRoutes.pop();

if (previousRoute && previousRoute.name !== 'search') {
localStorage['lastVisitedRoute'] = previousRoute.name;

// use location path to store last visited route
localStorage['lastVisitedRoutePath'] = this.router.location.path;
}
},
});

routes/search.hbs

1
2
3
{{#full-screen-modal-dialog}}
<a class='search-close' {{action "close"}}>Close</a>
{{/full-screen-modal-dialog}}

controllers/search.js

1
2
3
4
5
6
7
8
9
10
import Ember from 'ember';

export default Ember.Controller.extend({
actions: {
close() {
let lastVisitedRoute = localStorage['lastVisitedRoutePath'] || "index";
this.transitionToRoute(lastVisitedRoutePath);
}
}
});

I was happy that I finally found a solution to the problem I was struggling for a long time. However I discovered something unexpected when I tested the app manually!! That didn’t work. Again using debugger I investigated that path is undefined and the router has a problem with transition. Say what!? The tests are green.

Ember.Location API

I learned that Ember uses different strategies for routing location in test and development mode by default. You can see it when you look at config/environment.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = function(environment) {
var ENV = {
// (...)
locationType: 'auto',
};

// (...)

if (environment === 'test') {
// Testem prefers this...
ENV.locationType = 'none';
}

// (...)
};

Started digging in the docs (https://www.emberjs.com/api/ember/2.14.1/namespaces/Ember.Location) and source code to find more.

Generally, 4 types of location can be used by Ember:

Ember.NoneLocation

It’s used in tests. That makes sense because when test is running there is URL like:
I switched to this type in the development and then router.location.path was again accessible and redirection worked as expected. But URL was blank all the time, so I had to give up this solution.

That type could be also usable when you embed your Ember application on a larger page.

Ember.HashLocation

This is the default. The characteristic thing is that it adds # on the beginning of the path in the URL.
I noticed that in the code introduced router.location.lastSetPath which sounds familiar and contains the same string with a full path like router.location.path in Ember.NoneLocation.

Note: the value is set only when a transition is performed by a router. In other words it won’t work on copy-paste an URL and hit Enter.

Ember.HistoryLocation

Using HistoryLocation results in URLs that are indistinguishable from a standard URL. This relies upon the browser’s history API.

Ember.AutoLocation

Using AutoLocation, the router will use the best Location class supported by the browser it is running in.

Summary

I resolved this problem by removing acceptance tests temporarily and in development / production - use hash location combined with storing last visited route like here:

1
localStorage['lastVisitedRoutePath'] = this.router.location.lastSetPath;