Ember: Back button broken - javascript

I'm working on a simple web app using Ember. I am rendering a nested resource into the application template rather than it's parent resource.
This works fine except if I press the back button I go back to the parent resource but the parent template is not rendered into the application outlet. I can refresh the page and bingo it renders then.
Router:
Movies.Router.map(function () {
this.resource('list', { path: '/list' }, function() {
this.route('add');
// Nested resource example
this.resource('movies', { path: '/:list_id/movies' }, function() {
});
});
this.route('boxoffice');
});
Movies Route:
Movies.MoviesRoute = Ember.Route.extend({
model: function(params) {
return Movies.List.find(params.list_id);
},
renderTemplate: function() {
this.render('movies', {
// template outlet to render into (will mess up your back btn!)
into: 'application'
});
}
});
Thanks in advance!

By default the ember Router uses the browser's hash to load routes of your application and will keep it in sync. This relies on a hashchange event existing in the browser.
But you can setup ember to use the browser history API instead of hash which is the default. This can be accomplished in different way's. For example like this:
App.Router = Ember.Router.extend({
location : Ember.Location.create({
implementation : 'history' // can be hash, history or none
})
});
Or by a more simpler approach by reopening the router like this:
App.Router.reopen({
location: 'history'
});
This way using the browser back & forward buttons would work as expected.
For more info on the history API see here.
Hope it helps

Related

What is the best practice to register routes based on permissions in Ember.js?

In my Ember application, I have a map of routes and CRUD permissions that is returned from the server. If a user doesn't have read access to a page, I can easily exclude the menu item but for Create and Update operations I need to make some changes in router.js.
So currently this is the router I have:
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function () {
this.route('product-types', function () {
this.route('view', {path: '/:product-type_id'});
this.route('edit', {path: '/:product-type_id/edit'});
});
this.route('products');
this.route('members', function () {
this.route('view', {path: '/:member_id'}, function () {
this.route('member-accounts', function () {
this.route('new');
});
});
this.route('edit', {path: '/:member_id/edit'});
this.route('new');
});
this.route('tasks', function () {
this.route('view', {path: '/:task_id'});
});
});
export default Router;
So I wish somehow to be able to simply not register the route to :new and or :edit if the user doesn't have the right permissions:
this.route('product-types', function () {
if(permission['product-types'].edit()) {
this.route('edit', {path: '/:product-type_id/edit'});
}
});
But I'm looking for a better solution as the routes are growing in huge numbers. So I'd like to perhaps customize this Ember's router to do this automatically. Is that possible?
The other problem is Delete. Because Delete doesn't have any specific route I'd like to be able to pass that permission to each model by default automatically so that each model checks if delete is possible or not and then hide the delete button.
I am not worry about a user hacks the js files and enables the pages and tries to access the forbidden pages because they can't do anything as the server will stop them and will check all the permissions but I want here a mechanism to hide/display pages, buttons based on permissions. Any help is appreciated.
I think you'll be able to solve a lot of your problems with mixins. For your routing permissions, I recommend you have a look at ember-simple-auth's UnauthenticatedRouteMixin. Here is some of the related code:
export default Ember.Mixin.create({
/**
The session service.
#property session
#readOnly
#type SessionService
#public
*/
session: service('session'),
/**
Checks whether the session is authenticated and if it is aborts the current
transition and instead transitions to the
{{#crossLink "Configuration/routeIfAlreadyAuthenticated:property"}}{{/crossLink}}.
__If `beforeModel` is overridden in a route that uses this mixin, the route's
implementation must call `this._super(...arguments)`__ so that the mixin's
`beforeModel` method is actually executed.
#method beforeModel
#param {Transition} transition The transition that lead to this route
#public
*/
beforeModel(transition) {
if (this.get('session').get('isAuthenticated')) {
transition.abort();
Ember.assert('The route configured as Configuration.routeIfAlreadyAuthenticated cannot implement the UnauthenticatedRouteMixin mixin as that leads to an infinite transitioning loop!', this.get('routeName') !== Configuration.routeIfAlreadyAuthenticated);
this.transitionTo(Configuration.routeIfAlreadyAuthenticated);
} else {
return this._super(...arguments);
}
}
});
The delete button issue you should be able to solve with either another mixin where you check permission in a delete action, or add a mixin to components that inject permission related logic, or a combination of sorts..
You can try using a service that will contain the permissions for the current user, and some calculations based on those, which you inject into the necessary component. Which could be a delete button or a form component or something, and toggle the delete button display on the form component. That would look something like this:
//services/permissions.js
export default Ember.Service.extend({
hasPermission: Ember.computed('permissions', function(){
return true/false // permission logic here
})
})
//components/component-permissions
export default Ember.Component.extend({
permissions: Ember.inject.service(),
hasPermission: Ember.computed.alias('permissions.hasPermission')
});
//templates/components/components-permissions
{{#if hasPermission}}
<button class='i-am-a-delete-button'>Delete</button>
{{/if}}

Ember Modal on a route accessible from anywhere

I'm building an app that has a user settings panel that pops up in a modal dialog. The panel should be accessible from any page in the app. I'm trying to figure out the best way to build this in Ember.
I would like to build it in such a way that when the app redirects to the "/settings" route, the modal dialog appears with the current route in the background as you would expect. Then when the modal is closed the app redirects back to that route.
If the user goes directly to "/settings" from her browser then the modal will appear with a default page in the background.
Here is what I have so far:
export default Ember.Route.extend({
defaultParentRoute: "project.index",
beforeModel: function(transition){
// get the name of the current route or if
// user came directly to this url, use default route name
var parent = this.get("defaultParentRoute");
var application = this.controllerFor('application');
if(application && application.get('currentRouteName')) {
parent = application.get('currentRouteName');
}
this.set("parentRoute", parent);
},
renderTemplate: function(){
// make sure the current route is still rendered in the main outlet
this.render(this.get("parentRoute"));
// render this route into the 'modal' outlet
this.render({
into: 'application',
outlet: 'modal'
});
},
actions: {
removeModal: function(page){
// go back to the previous route
this.transitionTo(this.get("parentRoute"));
}
}
});
This works pretty well when navigating to the the route from a link in the app. However if a user goes straight to "myapp/settings" in her browser then the default page template gets rendered but without any data or it tries to use the 'model' data from my settings route.
How do I make sure the template underneath the modal gets rendered with the appropriate data?
Here's a JS Bin to demonstrate. Try clicking on 'settings' from any page in the app, then refresh the browser while the settings modal is open.
This organization seems a bit unnatural given Ember conventions. Generally the URL is supposed to represent a serialized version of state sufficient to reconstruct where the user was (see this old discussion).
It seems you want to put the modal state and the current route into the URL. It might be more natural for the settings modal panel to be accessible from other routes but to not change the URL, and then have another separate route which is dedicated to settings and shows only the settings.
Modal panels seem more like a drop-down menu, the opening and closing of which do not change the URL, even though they represent a minor state change.
If the reason you want to have the settings modal reflected in the URL is so that people can bookmark it or share the link, one option would be to have a permalink available on the settings page that gets them to the other dedicated route that is shareable.
The tricky bit with not having the settings as a route is that there is not an obvious place to load the model which behaves as nicely or works as easily as a route's model hook (i.e., which waits until the promise is resolved to complete the transition).
One way around this is to put functionality to load the settings model in a settings service, which can be injected anywhere:
SomeController = Ember.Controller.extend({
settings: Ember.inject.service()
});
And have the settings service only show itself once the model has loaded.
SettingsService = Ember.Service.extend({
settingsLoaded: false,
ensureLoaded: function() {
var _this = this;
return new Ember.RSVP.Promise (resolve) {
if (_this.get('settingsLoaded')) {
resolve()
} else {
_this.store.find('settings', someId).then(function(model) {
_this.set('model', model);
_this.set('settingsLoaded', true);
resolve();
});
}
};
}
});
Finally you can have a function on some controller that wants to show the settings modal only show it once the settings are loaded:
showSettings: function() {
this.get('settings').ensureLoaded().then(function() {
... # something to show the modal pane.
});
}

Backbone Router initializes but dont fire the events

I have a complex, multi level inherited app and i wanted to use Backbone.Router for navigation but it dont work as i expected.
The address of the application is not under root directory
Like this:
http://www.domain.com/App
and I wanted to use the BB's routing
Here is some code:
$(function () {
var SayfaController = Backbone.Router.extend({
routes: {
"": "home",
"sayfa/:sayfaNo": "sayfa"
},
initialize: function () {
console.log('THIS WORKS');
},
home: function () {
console.log('THIS DONT FIRE');
},
sayfa: function (sayfa) {
console.log("NEITHER THIS FIRES");
console.log(sayfa);
}
});
var sayfaController = new SayfaController();
Backbone.history.start({pushState: true});
....
});
initialize method works but
The events wont fire when i click a link like this:
Click
or change the browser navigation bar
What am I doing wrong
From a glance, it looks like you need to specify the root of your app, as it's not served from the root of your domain. See Backbone docs RE: Backbone.history:
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/"})
try Backbone.history.start({pushState: true, root: "/App/"});
I am not sure of how to get it work for urls of kind
http://www.domain.com/App/#/sayfa/6
But for kind below it works.
http://www.domain.com/App#sayfa/6
http://www.domain.com/App
Use
Backbone.history.start({
root : "/App"
});
Here is the jsfiddle http://jsfiddle.net/nEmeL/4/

Ember - Automatically redirect to firstObject

I'd like an Ember path /clinic/1 to automatically redirect to show the first doctor: /clinic/1/doctor/1.
Each clinic has many doctors.
Unfortunately if I use this code:
var doctor = clinicController.get('content.doctors.firstObject');
router.transitionTo('clinic.doctor.index', doctor);
... it does not work, as content.doctors.length is still 0 when this code runs in app_router.js.
Any ideas?
You should be able to do this:
App.DoctorsRoute = Ember.Route.extend({
model: function() {
return App.Doctor.find();
},
redirect: function() {
var doctor = this.modelFor('doctors').get('firstObject');
this.transitionToRoute('doctor', doctor);
}
});
This will work because:
If the model hook returns an object that hasn't loaded yet, the rest of the hooks won't run until the model is fully loaded.
If the redirect hook transitions to another route, the rest of the hooks won't run.
Note that as of 2426cb9, you can leave off the implicit .index when transitioning.
Redirecting on the Route doesn't work for me in my ember-data based app as the data isn't loaded at the point of redirection, but this does work for me...
In the roles controller I transition to the role route for the firstObject loaded.
Application.RolesController = Ember.ArrayController.extend({
selectFirstObject: function () {
if (this.get('content').get('isLoaded')) {
var role = this.get('firstObject');
this.transitionToRoute('role', role);
}
}.observes('content.isLoaded')
});
HTH, gerry
As an update, if you don't want to redirect because you have a nested route, you'll want to use conditional redirect based on the intended route.
redirect has two arguments passed to it, the model and the transition object. The transition has the property targetName where you can conditionally redirect based on its value.
redirect: function(model, transition){
if(transition.targetName ==='doctors.index'){
this.transitionTo('doctor', model.get('firstObject'));
}
}
For EmberCLI users, you'd achieve the same by doing the following:
//app/routes/clinics/show.js
import Ember from 'ember';
export default Ember.Route.extend({
redirect: function(model) {
var firstDoctor = model.get('doctors.firstObject');
this.transitionTo('doctor', firstDoctor);
}
});

Backbone.js how do i define a custom set of routes depending on a certain application state

Currently building an app that runs on mobile phones
not related to the issue at hand, but through a certain event
the app lands in a state, either online or offline (internet available on the phone or not)
the offline app is very limited, only a few screens available)
now stop me if you catch me doing something stupid or something that i could to a lot better,
but my first thought was to have the Router have a dynamic set of routes,
much like you can define a dynamic url property on a collection.
so instead of this:
var router = Backbone.Router.extend({
routes: {
'' : 'homeAction',
'categories' : 'categoriesAction',
...
},
homeAction: function(){ },
categoriesAction: function(){ }
});
i was thinking of this:
var router = Backbone.Router.extend({
initialize: function(){
this.on('app:togglestate', this.toggleRoutes, this);
},
toggleRoutes: function () {
var router = this;
if(App.onlineModus)
router.routes = { /* hash with online routes here */ };
else
router.routes = { /* hash with offline routes here */ };
},
routes: {
'' : 'homeAction',
'categories' : 'categoriesAction',
...
},
homeAction: function(){ },
categoriesAction: function(){ }
});
though that aparently breaks the whole app,
as the Backbone.history.start(); throws an error, cannot call function start from undefined.
leading me to believe i the routes object is somehow used upon initialization and cannot be changed on the fly.
am i possibly thinking to far?
should i achieve this some other way?
other idea's i had were:
having the routes exactly like url, where the routes argument is a function returning a hash, that didn't work either
and now i'm thinking totally differnt, something along the lines of testing if the app is in online or offline modus in every route's Action. though that seems too mutch i'd probably have to relay them all through a single Action, which only passes to the actual action if the route is accessible in offline modus? but i would not really have a clear idea on how to start with such a relay action without writing too mutch boilerplate code...
In order to dynamically update the routes you will need to make a call to _bindRoutes() after updating the routes.
For example:
toggleRoutes: function () {
var router = this;
if(App.onlineModus)
router.routes = { /* hash with online routes here */ };
else
router.routes = { /* hash with offline routes here */ };
// Get rid of previous navigation history
if(Backbone.history){
Backbone.history == null;
}
// Bind the new routes
router._bindRoutes();
}
Note that when you dynamically change the routes the history is no longer valid so you need to delete the previous history. When _bindRoutes is called it automatically instantiates a new Backbone.history when is called this.route.
I had to do something very similar. I don't have the code in front of me, but this should be right around what I did: (Edit: fleshed it out a bit so you can actually run it now)
ApplicationRouter = Backbone.Router.extend({
//some stuff
constructor: function (routes) {
this.routes = routes;
Backbone.Router.prototype.constructor.call(this);
}
});
routeObject = {
"help": "help"
}
ApplicationRouter.instance = function(routes) {
if (!this._instance) {
this._instance = new ApplicationRouter(routes);
}
return this._instance;
}
ApplicationRouter.instance(routeObject);
ApplicationRouter.instance().on('route:help', function() {
console.log('helped');
});
Backbone.history.start();
//now go to page#help - the console will say "helped"
From then on out, I just referenced ApplicationRouter.instance() when I needed access to the application router.

Categories