Is it possible to hide substates from showing up in the URL when using the Ember Router v2? - ember.js

I would like to have a route substate not show up in the URL, but still be able to take advantage of having a route class on which I can define renderTemplate, model, setupController, etc. hooks. Is this possible with the v2 router? I am using Ember release candidate 2.
Here's an example.
Suppose I have the routes:
/exercise/:exercise_id
/exercise/:exercise_id/correct
/exercise/:exercise_id/incorrect
I would like all of these to show up in the URL as:
/exercise/:exercise_id
As I don't want the student to just directly type in /correct onto the end of the ULR and get to the correct answer. And although I have a way to prevent that from working, the full route still shows up in the URL. From the student's perspective, I only want them to think about the state as /exercise/:exercise_id.
Of course I could just store the state correct vs. incorrect in some controller variable, but then I loose the convenience of having route classes, ExerciseCorrectRoute and ExerciseIncorrectRoute, which I want to behave differently, and so the hooks, like renderTemplate and setupController, are nice to have defined cleanly in separate places.
Thoughts?
Kevin
UPDATE:
I went with Dan Gebhardt's suggestion because I like to keep things as much as possible within the framework's considered design cases, as this seems to reduce headaches given Ember is still evolving. Also I didn't get a chance to try out inDream's hack.
Although I still think it would be nice if the router added a feature to mask substates from the URL.

Every route must be associated with a URL for Ember's current router.
Instead of using multiple routes, I'd recommend that you use conditionals in your exercise template to call the appropriate {{render}} based on the state of the exercise. In this way you can still maintain separate templates and controllers for each state.

You can reference to my answer in Ember.js - Prevent re-render when switching route.
Reopen the location API you're using and set window.suppressUpdateURL to true if you want to handle the state manually.
Ember.HistoryLocation:
Ember.HistoryLocation.reopen({
onUpdateURL: function(callback) {
var guid = Ember.guidFor(this),
self = this;
Ember.$(window).bind('popstate.ember-location-'+guid, function(e) {
if(window.suppressUpdateURL)return;
// Ignore initial page load popstate event in Chrome
if(!popstateFired) {
popstateFired = true;
if (self.getURL() === self._initialUrl) { return; }
}
callback(self.getURL());
});
}
});
Ember.HashLocation:
Ember.HashLocation.reopen({
onUpdateURL: function(callback) {
var self = this;
var guid = Ember.guidFor(this);
Ember.$(window).bind('hashchange.ember-location-'+guid, function() {
if(window.suppressUpdateURL)return;
Ember.run(function() {
var path = location.hash.substr(1);
if (get(self, 'lastSetURL') === path) { return; }
set(self, 'lastSetURL', null);
callback(path);
});
});
}
});

Related

Nested routes: handling failed child find in error action loses parent context

I've recently been porting a number of Ember apps I maintain to RC 8 and ran into this.
Before the router facelift landed I would sometimes manage control flow via promises returned by Ember Data find calls.
For example:
SomeRoute = Ember.Route.extend({
model: function(params) {
var resolve = function(model) { return model; };
var route = this;
var reject = function() { this.transitionTo('someOtherRoute'); };
return SomeModel.find(params.some_model_id).then(resolve, reject);
}
...
});
With the recent changes, it is now possible to handle errors created in model callbacks via the error action:
SomeRoute = Ember.Route.extend({
// note: model callback no longer needed--default suffices
actions: {
error: function(reason, transition) {
// check the reason object to determine how/if to handle this error
this.transitionTo('someOtherRoute');
}
}
...
});
I much prefer the latter approach as it makes the code easier to read and better separates concerns.
This works well in most cases but I encountered an issue in an app that uses nested routes. I've included a simplified example followed by a jsbin that demonstrates the issue.
Lets say we want to show Articles that belong to Authors and the URLs look like: /authors/:author_slug/articles/:article_slug. We want to redirect to a Not Found page when someone tries to view an article that doesn't exist.
When managing control flow in the model callback as above, you can browse to /authors/some_author/articles/some_invalid_slug and be redirected to /authors/some_author/articles/not_found as expected.
However, if the redirect to the Not Found page is instead managed via the error action, the parent context is lost at some point and you end up at /authors/undefined/articles/not_found.
You can see this in the following jsbins:
http://jsbin.com/eJOXifo/1#/authors/schneier/articles/12345
(redirects to http://jsbin.com/eJOXifo/1#/authors/schneier/articles/not_found)
http://jsbin.com/oNaWelo/1#/authors/schneier/articles/12345
(redirects to http://jsbin.com/oNaWelo/1#/authors/undefined/articles/not_found)
Does anyone know why this happens or how to avoid it?
Notes:
I know this doesn't have anything to do with Ember Data. However, implementing something equivalent without Ember Data just makes the example more complicated without adding anything
There are a few small hacks to make Ember Data work as expected in jsbin:
I'm preloading the parent model to avoid having to load it from anywhere.
I'm not doing anything special to provide data for the child model. The app just makes a request to http://jsbin.com/articles/12345. This actually returns a 200 but bombs anyway because the response is html. An API that correctly returns a 404 response gives the same behvaiour.
I remember a while ago reading about some service that could be used to build fake API responses for use with services like jsfiddle or jsbin. If anyone knows what it is please comment.
Your right that the parent context is getting lost. The trick is to extract that context from the transition and pass it as an argument when calling transitionTo. So:
App.ArticlesArticleRoute = Em.Route.extend({
actions: {
error: function(reason, transition) {
console.log('in error handler');
this.transitionTo('articles.notFound', transition.resolvedModels.authors);
}
}
});
With this change, visiting the url:
http://jsbin.com/iVOYEvA/2#/authors/schneier/articles/my-fake-article
will redirect to:
http://jsbin.com/iVOYEvA/2#/authors/schneier/articles/not_found

Detect model/URL change for Ember Route

My EmberJS application has a ProjectRoute (/project/:project_id) and a corresponding ProjectController. When viewing a particular project, users can edit its properties, and I'd like the project to automatically be saved when the user stops looking at it.
Currently, what I'm doing is something like this:
Application.ProjectRoute = Ember.Route.extend({
...
exit: function() {
this.get('controller').saveProject();
}
...
});
This works when the user simply closest the project view. However, if the user simply switches to viewing a different project (e.g. goes directly from /project/1 to /project/2), the same route is used (it just uses a different model), and exit is not called.
What I need is a way to detect this transition and call the saveProject function before it happens. Any ideas?
I "solved" it by adding the following to my controller:
Application.ProjectController = Ember.ObjectController.extend({
...
saveOnChange: function() {
var previousProject = this.get('target.projectToSave');
if (previousProject && previousProject.get('isDirty')) {
Application.store.commit();
}
this.set('target.projectToSave', this.get('content'));
}.observes('content')
...
});
This seems to work well. Note that I'm storing the projectToSave property in the route (target) as opposed to the controller, because the controller gets wiped every time the model changes. It feels a little weird/hacky to store this in the route, but the only alternative I could think of was to store it in ApplicationController, which seemed overly broad.

Detect URL change and grab URL in EmberJS (using Discourse)

I'm using Discourse (http://www.discourse.org/), which is built on EmberJS, and trying to observe any time the URL changes, e.g. when opening a new topic. I've seen the answer for observing the currentPath, for example here:
Detect route transitions in EmberJS 1.0.0-pre.4
App.ApplicationController = Ember.Controller.extend({
routeChanged: function(){
// the currentPath has changed;
}.observes('currentPath');
});
But I'm trying to detect any URL change, not just a path change. As mentioned in the comments for that answer:
This observer doesn't fire when transitioning from for example
/pages/1 to /pages/2 because the path is staying the same:
pages.page.index
What I'd like to do is actually detect those aforementioned transitions which don't get triggered by observes('currentPath'). Along those lines, if I do this.get('currentPath'); inside of my function, I get something like topic.fromParams but I actually am interested in the URL path e.g. /t/this-is-my-url-slug.
To put it simply, I'd like to detect when the app goes from:
/t/this-is-my-url-slug
to
/t/another-url-slug
and be able to capture the path: /t/another-url-slug
Sorry but I'm a bit of an Ember n00b and my only experience with it is through Discourse. Any ideas?
You don't need anything Ember-specific to do this. Depending on whether you are using hash or pushstate, you can use...
$(window).on('hashchange', function(){
console.log("Hash URL is " + location.hash.substr(1));
// Do stuff
});
or
$(window).on('popstate', function(e) {
console.log("Hash URL is " + window.location.pathname);
// Do stuff
});
The solution is pretty specific to Discourse (and not as general to EmberJS), but Discourse has a URL namespace which is called for URL related functions (/components/url.js). There is a routeTo(path) function in there which gets called every time a new route is loaded. So I was able to add my own function inside of there, which ensures that:
my function will be called every time a Discourse route changes
I can capture the path itself (i.e. the URL)
With Luke Melia's answer you are not doing any teardown to prevent memory leaks without causing issues when using the browsers back button.
If this is needed globally for your app, and you only want to use this event to call one function, then ok. But if you want to call off() when you leave the route (which you should tear it down when you don't need it) you will cause bugs with ember. Specifically when trying to use the browsers back button.
A better approach would be to leverage the event bus and proxy the event to one that will not cause issues with the back button.
$(window).on('hashchange', function(){
//Light weight, just a trigger
$(window).trigger('yourCustomEventName');
});
Then When you want to listen to hash changes you listen to your custom event, then tear it down when it is not needed.
Enter Route A:
$(window).on('yourCustomEventName', function(){
// Do the heavy lifting
functionforA();
});
Leave Route A:
$(window).off('yourCustomEventName');
Enter Route B:
$(window).on('yourCustomEventName', function(){
// Do the heavy lifting maybe it's different?
functionforB();
});
Leave Route B:
$(window).off('yourCustomEventName');

ember new router

I have a question about Ember routing and controllers. I've just written a small App to get familiar with the new router. Therefore I've built a button that transitions to another state by clicking on it.
App.PostsView = Em.View.extend({
click: function() {
var router;
this.get('controller').transitionTo('about');
}
});
My question now is: what does the get method return?. Obviously an instance of the PostController but on the one hand the controller doesn't have a transitionTo() method and on the other hand that would not make any sense.
this.get('foo') returns a property of your Ember object. Since Views can have a "controller" property, this.get('controller') returns the controller bound to your view's controller property (by default the postsController).
this.get('controller').transitionTo() works because as sly7_7 mentioned, transitionTo() is also defined on the controller and delegates to the router. Note, it's probably going to be deprecated and one should use
this.get('controller').transitionToRoute('about');
instead.
You should not do this at the view level, this is what the router is designed for so instead of capturing a click event in the view, rather implement an action on the button and let the router handle it. I suggest you learn the best practices from the get-go, chances are your application will evolve, requiring more elaborate concepts such as handling transactions commits/rollbacks, creating/updating records. So here's my suggestion to you
In your view
<button type="button" {{action onSaveClick}} />Save</button>
In your router
App.FooRoute = App.Route.extend({
events: {
onSaveClick: function() {
this.transitionTo('bar');
}
}
})
If for any other reasons, say styling or animation, you find yourself forced to capture the click event in the view, then i suggest to, capture the event, perform your styling and finally send an event to the controller. The event can then be handled the same way in the router
App.FooView = Ember.View.extend({
click: function() {
// Do some styling
this.get('controller').send('onSaveClick')
}
})
Finally speaking of best practices, try to learn when working with ember to think of your application as a series of states interacting with each other. You'll find yourself bound to implement the concept right. Have a look at this
the controller has a transitionTo: https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/ext/controller.js#L36, it basically delegate to it's target (which is the router)

Ember.js 1.0-pre4 + jQ UI sortable + localstorage adapter

Day 2 learning ember.js...
I'm working on a offline app that needs to save draggable/sortable tile positions to localstorage, and if there is no existing data, load & save from a fixture.
Using: ember 1.0.0-pre4, ember-data rev11, ember-localstorage-adapter, jQ 1.9, jQ UI 1.9
https://github.com/rpflorence/ember-localstorage-adapter
It's working, but I'm a bit of a novice, feel it's not pretty and could use some community advice.
http://jsfiddle.net/Nsbcu/4/
Questions
What is the proper way to check if your DS.Store has loaded and is empty? My method of looking directly at localstorage didn't feel right.
After I createRecords from the App.Tile.DEFAULTS I feel I should commit them, but an error is thrown. I don't have to commit the known defaults, but curious what causes the error and how I should go about committing properly. Also is the App.ready() callback the right place for loading defaults? Error only happens when localstorage is empty
Uncaught Error: Attempted to handle event loadedData on <App.Tile:ember231:1> while in state rootState.loaded.created.inFlight. Called with undefined
On the TilesController I'm using sortProperties which works great until jQ UI Sortable changes the DOM and Ember wants to update my tile order, before I get a chance to set the new order. My current solution is to turn off sortProperties temporarily while updating the model. Again this feels hacky, suggestions on proper way to do this?
=== Edit Feb 3 ===
If I do an async commit the initial error in question #2 is avoided.
App.TilesRoute = Ember.Route.extend({
model: function() {
return App.Tile.find();
},
setupController: function(controller) {
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
// Commit async, else generates error
var _this = this;
setTimeout(function() {
_this.store.commit();
}, 1);
}
}
});
I would put any initial code inside the application or the index Route within the setupController method
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
//*** WARNING: Generates Error ***/
App.Tile.find().get('store').commit();
}
Once you move the code inside the route, replace App.Tile.find().get('store').commit(); by App.store.commit() inside your route
Create your own transaction instead of using the default one, each time you make a call to the store directly you're using the default transaction. You can create a transaction this way
var transaction = App.store.transaction()
transaction.createRecord(App.Foo);
transaction.commit()
transaction.rollback();
Any call to App.store assumes you already created a store, right now you're only extending the DS.Store. Try instead
App.Store = DS.Store.create({
revision: 11,
adapter: 'App.LSAdapter'
});
I would suggest that you do any event handling or transaction management in the router unless it's purely for styling or animation. In that case, the view is the right place for it. I like the router to orchestrate communication between all the assets (controllers, routes, models, views)
A good pattern to remember is a view talks only to a controller, a controller is a mere proxy to a model, a router orchestrates communication between controllers and manages routes.