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);
}
});
Related
So, I have two paths in my route. I created the two routes as the doc recommends.
My router is the following:
// router.js
Router.map(function() {
this.route('photos');
this.route('photo', { path: '/photo/:photo_id' });
});
If I visit firstly the route /photo/ID and then go to /photos, it will only show one object on the latter. (wrong)
If I visit /photos first it shows all the objects and I can go to /photo/ID later on and it will be fine. (right)
I want to make it work both ways. How to do this? You can see my code for each route down below:
// photo.js
export default Ember.Route.extend({
model(params) {
return this.get('store').findRecord('photo', params.photo_id);
}
});
// photos.js
export default Ember.Route.extend({
setupController(controller, model) {
let photos = this.get('store').findAll('photo');
console.log('add all...');
// convert to an array so I can modify it later
photos.then(()=> {
controller.set('photos', photos.toArray());
});
},
});
I can always call the findAll() function regardless where the user goes, but I don't think this is smart.
The way I am dealing with the page transitions:
To go to photos I use:
{{#link-to 'photos'}}All{{/link-to}}
To go to /photo/ID I inject the service '-routing' and I use in one event click like this:
routing: Ember.inject.service('-routing'),
actions() {
selectRow(row) {
this.get("routing").transitionTo('photo', [row.get('id')]);
}
}
findAll will get it from a store and return immediately and later on it will request the server and update the store. but in your case, as you are not using route model hook, so this live array will not be updated so it will not reflect it in the template.
If I visit firstly the route /photo/ID and then go to /photos, it will
only show one object on the latter.
In the above case, store will contain only one reocrd, so when you ask for store data using findAll it will return the existing single record.
Another option is,
avoiding this photos.toArray() - It will break live-array update, I am not sure why do you need it here. since photos is DS.RecordArray.
Note: It's important to note that DS.RecordArray is not a JavaScript
array, it's an object that implements Ember.Enumerable. This is
important because, for example, if you want to retrieve records by
index, the [] notation will not work--you'll have to use
objectAt(index) instead.
Having a route like 'dogs': 'process', I need to rewrite it to 'animals': 'process'.
Now, I need the router to recognize both routes, but always display the url like /animals, it is sort of aliasing, but could not find any info on how to solve this without placing an url redirect in 'process' handler.
I'm assuming that the real need for aliases is different than dogs to animals, so I'll answer regardless of if the use-case here is good or not. But if you don't want to change the hash but want to trigger different behaviors in the app, using the router is probably not the route to go.
Route aliases don't really exist in Backbone, other than defining different routes using the same callback. Depending on your exact use-case, there are multiple ways to handle similar routes.
Replace the hash
To display the same hash for a generic route coming from different routes, use the replace option of the navigate function.
routes: {
'lions': 'animalsRoute',
'animals': 'animalsRoute'
},
animalsRoute: function() {
this.navigate("#/animals", { replace: true });
// or using the global history object:
// Backbone.history.navigate("#/animals", { replace: true });
}
then handle the animals route, regardless of which route was initially used to get in this callback.
Some other answers or tutorials will say to use window.location.hash but don't. Manually resetting the hash will trigger the route regardless and may cause more trouble than it'll help.
Different behaviors but showing the same route
Just use different callbacks, both using the replace trick above.
routes: {
'lions': 'lionsRoute',
'tigers': 'tigersRoute'
},
showGenericRoute: function() {
this.navigate("#/animals", { replace: true });
},
tigersRoute: function() {
this.showGenericRoute();
// handle the tigers route
},
lionsRoute: function() {
this.showGenericRoute();
// handle the lions route
}
Notice the inexistent animalsRoute. You could add the route if there's a generic behavior if no specific animal is chosen.
Use the route params
If you want to know which animal was chosen but still use the same callback and remove the chosen animal from the hash, use the route params.
routes: {
'animals/:animal': 'animalsRoute',
},
animalsRoute: function(animal) {
// removes the animal from the url.
this.navigate("#/animals", { replace: true });
// use the chosen animal
var view = new AnimalView({ type: animal });
}
Redirect to the generic route
If you want a different behavior but always show the same route, use different callbacks, then redirect. This is useful if the generic route is in another router instance.
var Router = Backbone.Router.extend({
routes: {
'animals': 'animalsRoute'
},
animalsRoute: function() {
// handle the generic behavior.
}
});
var PussyRouter = Backbone.Router.extend({
routes: {
'lions': 'lionsRoute'
// ...
},
lionsRoute: function() {
// handle lions, then redirect
this.navigate("#/animals", { trigger: true, replace: true });
}
});
Using the trigger options will call the animalsRoute in the other router and the replace option will avoid making an entry in the history, so pushing the back button won't go to lions to get back to animals and being caught in the animals route.
I am using ember 2.7.0.while manually refreshing the page ember clears the ember-data as well us query parameters, so i am unable to load the page in setupController while refreshing. Is there any possible way to retain both model & query parameters, at least retaining query parameter would be fine to reload my page.
route.js
model(params) {
return this.store.peekRecord("book",params.book_id);
},
setupController(controller,model,params){
if(!model){
//fetch book record again if the model is null
}
controller.set('isdn',params.queryParams.isdn);
controller.set('book',model);
}
Any help should be appreciable.
Edited setupController as per Adam Cooper comment :
setupController(controller,model,params){
var isdn = params.queryParams.msisdn;
controller.set('isdn',isdn);
if(!model){
this.store.findRecord('book', isdn).then((customer) => {
this.set('book',customer);
},(resp,status) => {
this.set('errorMessage', `Book with this ${isdn} does not exist.`);
this.set('book', []);
});
}else{
controller.set('device',model);
}
}
Page gets rendered before "findRecord" returning promise.Is there any way to stop page rendering till find record resolves the promise?
You are setting in route properties instead of controller..
setupController(controller, model, params){
var isdn = params.queryParams.msisdn;
controller.set('isdn', isdn);
if(!model){
this.store.findRecord('book', isdn).then((customer) => {
controller.set('book', customer);
}, (resp, status) => {
controller.set('errorMessage', `Book with this ${isdn} does not exist.`);
controller.set('book', []);
});
}else{
controller.set('device', model);
}
}
Only the controller properties will decorate template.
You can even try the below, why don't you give opportunity to model hook to resolve since that will wait for the Promises to resolve.
model(params) {
var result = this.store.peekRecord("book",params.book_id);
if(result !== null){
result= this.store.findRecord('book', params.book_id)
}
return result;
}
setupController(controller,model){
controller.set('book',model);
}
You will need to generate an actual controller for your route and then define a queryParams property in the controller. It looks like the query param you're trying to hold onto is isdn so your controller should look something like:
export default Ember.Controller.extend({
queryParams: ['isdn']
});
"manually refreshing the page ember clears the ember-data as well us query parameters"
Once you completely refresh the browser, a new ember app instance is created and hence ember-data cannot be retained. Ember-data is just for the app on the UI, once ember is exited it will not be retained.
"as well us query parameters"
your query params are part of your url and it should not get cleared. Make sure the below two are present
Include queryParams in ur controller i.e.
queryParams: ['param1', 'param2']
And in your route make sure you have done
queryParams : {
param1: {
refreshModel: true
},
param2: {
refreshModel: true
}
}
"Page gets rendered before "findRecord" returning promise"
You are not doing something right, is the adapter, model, serializer etc defined correctly(if required) in order to use findRecord? Just to debug return a plain object and make sure ur setupController is called before rendering. i.e.
model() {
return {dummy: 'dummy'};
}
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}}
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.