Access current route name from a controller or component - ember.js

I needs to apply an "active" class to a bootstrap tab depending on the current route name. The route object contains "routeName" but how to I access this from a controller or component?

Use this this.controllerFor('application').get('currentRouteName');

In fact, you don't need to apply active class by yourself. A link-to helper will do it for you.
See here:
{{link-to}} will apply a CSS class name of 'active' when the application's current route matches the supplied routeName. For example, if the application's current route is 'photoGallery.recent' the following use of {{link-to}}:
{{#link-to 'photoGallery.recent'}}
Great Hamster Photos
{{/link-to}}
will result in
<a href="/hamster-photos/this-week" class="active">
Great Hamster Photos
</a>

In the absolutely desperate case, you can look up the router, or the application controller (which exposes a 'currentRouteName' property) via this.container.lookup("router:main") or this.container.lookup("controller:application") from within the component.
If it was a common trend for me, I would make a CurrentRouteService and inject it into my component(s) so that I can mock things more easily in my tests.
There may also be a better answer to come along - but the container.lookup() should knock down your current blocker.

Since Ember 2.15 you can do this through the public Router service.
router: service(),
myRouteName: computed('router.currentRouteName', function () {
return this.get('router.currentRouteName') + 'some modification';
}
https://www.emberjs.com/api/ember/release/classes/RouterService
Which worked really well for me since I wanted something computed off of the current route. The service exposes currentRouteName, currentURL, location, and rootURL.
currentURL has the query params, but you would need to parse them from the URL.

For Ember 2, from a controller you can try :
appController: Ember.inject.controller('application'),
currentRouteName: Ember.computed.reads('appController.currentRouteName')
Then you can pass it to component.

Try this.
export default Ember.Route.extend({
routeName: null,
beforeModel(transition){
//alert(JSON.stringify(transition.targetName) + 'typeof' + typeof transition.targetName);
this.set('routeName', transition.targetName);
},
model(){
// write your logic here to determine which one to set 'active' or pass the routeName to controller or component
}
`

Using insights from #maharaja-santhir's answer, one can think of setting the routeName property on the target controller to use, e.g., in the target's template. This way there's no need for defining the logic in multiple locations and hence code-reusability. Here's an example of how to accomplish that:
// app/routes/application.js
export default Ember.Route.extend({
...
actions: {
willTransition(transition) {
let targetController = this.controllerFor(transition.targetName);
set(targetController, 'currentRouteName', transition.targetName);
return true;
}
}
});
Defining this willTransition action in the application route allows for propagating the current route name to anywhere in the application. Note that the target controller will retain the currentRouteName property setting even after navigating away to another route. This requires manual cleanup, if needed, but it might be acceptable depending on your implementation and use case.

Related

Make nested route inside AuthenticatedRouteMixin accessible

I'd like to "free" one nested route, so that also users who are not even logged in can access this route.
For example:
posts
- /create
- /edit
- /show
On the posts route I used the AuthenticatedRouteMixin.
With this, all sub routes are automatically protected. Now I only want to make /show accessable. I know that I could use a mixin on /create and /edit and remove it from posts route, but if you have 10+ nested routes and only 1 of them should be available also for not logged in users, it's kind of inconvenient.
Do you know any other solution to that challenge?
If not, I think I have to write an additional mixin for that...
Thanks!
ember-simple-auth's AuthenticatedRouteMixin uses beforeModel hook to check whether session.isAuthenticated or not. You need to override the beforeModel in 'show' route to either skip the Auth check by bypassing AuthenticatedRouteMixin's super() implementation all together.
beforeModel (transition, skipAuthCheck) {
if (!skipAuthCheck) {
return this._super(...arguments, true);
}
}
Check if the 'show' beforeModel has dependency with parent route .i.e 'posts', implement this check at the parent route.
You could fake a nested route, by using the path parameter:
this.route('posts', function() {
this.route('create');
this.route('edit');
});
this.route('posts-show', { path: '/posts/show' });

Clean Ember 1.13+ way of knowing if child route is activated

Assume we have an Article model as follows:
export default DS.Model.extend({
author: DS.belongsTo('user'),
tagline: DS.attr('string'),
body: DS.attr('string'),
});
Assume also that we have a lot of pages, and on every single page we want a ticker that shows the taglines for brand new articles. Since it's on every page, we load all (new) articles at the application root level and have a component display them:
{{taglines-ticker articles=articles}}
{{output}}
That way we can visit any nested page and see the taglines (without adding the component to every page).
The problem is, we do not want to see the ticker tagline for an article while it's being viewed, but the root-level taglines-ticker has no knowledge of what child route is activated so we cannot simply filter by params.article_id. Is there a clean way to pass that information up to the parent route?
Note:
This is not a duplicate of how to determine active child route in Ember 2?, as it does not involve showing active links with {{link-to}}
Ember is adding a proper router service in 2.15; this exposes information about the current route as well as some methods that allow for checking the state of the router. There is a polyfill for it on older versions of Ember, which might work for you depending on what version you're currently using:
Ember Router Service Polyfill
Based on the RFC that introduced that service, there is an isActive method that can be used to check if a particular route is currently active. Without knowing the code for tagline-ticker it's hard to know exactly how this is used. However, I would imaging that you're iterating over the articles passed in, so you could do something like:
export default Ember.Component.extends({
router: Ember.inject.service(),
articles: undefined,
filteredArticles: computed('articles', 'router.currentRoute', function() {
const router = this.get('router');
return this.get('articles').filter(article => {
// Return `false` if this particular article is active (YMMV based on your code)
return !router.isActive('routeForArticle', article);
});
})
});
Then, you can iterate over filteredArticles in your template instead and you'll only have the ones that are not currently displayed.
You can still use the link-to component to accomplish this, and I think it is an easy way to do it. You aren't sharing your taglines-ticker template, but inside it you must have some sort of list for each article. Make a new tagline-ticker component that is extended from the link-to component, and then use it's activeClass and current-when properties to hide the tagline when the route is current. It doesn't need to be a link, or look like a link at all.
tagline-ticker.js:
export default Ember.LinkComponent.extend({
// div or whatever you want
tagName: 'div',
classNames: ['whatever-you-want'],
// use CSS to make whatever class you put here 'display: none;'
activeClass: 'hide-ticker',
// calculate the particular route that should hide this tag in the template
'current-when': Ember.computed(function() {
return `articles/${this.get('article.id')}`;
}),
init() {
this._super(arguments);
// LinkComponents need a params array with at least one element
this.attrs.params = ['articles.article'];
},
});
tagline-ticker being used in taglines-ticker.hbs:
{{#tagline-ticker}}
Article name
{{/tagline-ticker}}
CSS:
.hide-ticker {
display: none;
}
I tried to extend the LinkComponent, but I ran into several issues and have still not been able to get it to work with current-when. Additionally, if several components need to perform the same logic based on child route, they all need to extend from LinkComponent and perform the same boilerplate stuff just to get it to work.
So, building off of #kumkanillam's comment, I implemented this using a service. It worked perfectly fine, other than the gotcha of having to access the service somewhere in the component in order to observe it.
(See this great question/answer.)
services/current-article.js
export default Ember.Service.extend({
setId(articleId) {
this.set('id', articleId);
},
clearId() {
this.set('id', null);
},
});
routes/article.js
export default Ember.Route.extend({
// Prefer caching currently viewed article ID via service
// rather than localStorage
currentArticle: Ember.inject.service('current-article'),
activate() {
this._super(arguments);
this.get('currentArticle').setId(
this.paramsFor('articles.article').article_id);
},
deactivate() {
this._super(arguments);
this.get('currentArticle').clearId();
},
... model stuff
});
components/taglines-ticker.js
export default Ember.Component.extend({
currentArticle: Ember.inject.service('current-article'),
didReceiveAttrs() {
// The most annoying thing about this approach is that it
// requires accessing the service to be able to observe it
this.get('currentArticle');
},
filteredArticles: computed('currentArticle.id', function() {
const current = this.get('currentArticle.id');
return this.get('articles').filter(a => a.get('id') !== current);
}),
});
UPDATE:
The didReceiveAttrs hook can be eliminated if the service is instead passed through from the controller/parent component.
controllers/application.js
export default Ember.Controller.extend({
currentArticle: Ember.inject.service('current-article'),
});
templates/application.hbs
{{taglines-ticker currentArticle=currentArticle}}
... model stuff
});
components/taglines-ticker.js
export default Ember.Component.extend({
filteredArticles: computed('currentArticle.id', function() {
const current = this.get('currentArticle.id');
return this.get('articles').filter(a => a.get('id') !== current);
}),
});

Tracking the current URL in a template

I have a "copy link" input in my template that should display the currently loaded URL (with query params) to the user for them to copy & paste.
The input with the URL is part of the template belonging to a parent route that has a number of children:
- Parent route <-- Copy URL link in this template
- Child route
- Child route
- Child route
This means that as you navigate the child routes, the URL should update in the parent route. This is turning out to be very difficult to implement.
AFAIK there is no native event for URL change so I can't create a component and simply track any browser event. I tried hashchange on the window but this obviously only tracks the #and not URL or query params.
I can't use window.location.href when the page loads as any subsequent transitions to the child routes will not be reflected in the input.
I can't get the URL with window.location.href in the didTransition action (or any other route hooks) on the parent route because at that point the URL hasn't updated yet (i.e. I get the previous URL)
EDIT:
At the moment, this is the only approach that seems to work:
// In the parent route:
actions: {
didTransition() {
this._super(...arguments);
Ember.run.schedule('afterRender', () => {
this.set('controller.currentURL', window.location.href);
});
}
}
but seems pretty hacky
Since Ember 2.15 you can use the route service for this.
Example:
import { inject as service } from '#ember/service';
import { alias } from '#ember/object/computed';
export default Component.extend({
router: service(),
currentRoute: alias('router.currentURL')
});
I think you can benefit from router's location's path property. What I mean is, you can inject router to the controller you like with an instance-initializer and define a computed property to watch the current path. Please check out the following twiddle. I wrote an instance initializer named instance-initializers\routerInjector to inject application's router to every controller. I defined a computed property named location within application.js controller as follows:
location: Ember.computed.oneWay('router.location.path')
I added this to application.hbs. If I got what you want correctly; this is what you want.

When creating a new data object in Ember that relies on another object how do you pass it along?

I am on a page where I can see a specific customer, part of my router.js is:
this.route('customers');
this.route('customer', {path: "customers/:customer_id"});
this.route('customer.order.create', { path: "customers/:customer_id/order/create" });
customer.order.create needs to load in my main view and so is not nested. An order 'has a' customer.
I've setup my /customer/order/create controller to have
needs: "customer"
I want to access the customer in my /customer/order/create.hbs template like this:
<h3>New Order for {{controllers.customer.name}}</h3>
When I end up creating the order I will also want to set newOrder.customer = customer.
customer.hbs links like so
<div>
{{#link-to 'customer.order.create' model}}Create Order{{/link-to}}
</div>
Currently {{controllers.customer.name}} renders nothing, what piece of the puzzle am I missing to get to the customer in my order/create route?
Or putting it more generally, what route/controller/etc code do I need when I have a parent object which belongs to my child object in a /parentObject/parent_id/childObject/create type scenario.
There are many points to fix:
1) {{controllers.customer}} is Controller Object, {{controllers.customer.name}} it's name property. I think you want {{controllers.customer.model.name}}.
2) "..newOrder.customer = customer.." should be
newOrder.set('customer', this.get('controllers.customer.model'));
3) your customer.order.create route model hook shoudn't be empty, since you are using dynamic segment customer_id:
//route
model: function(params) {
return this.find('customer', params.customer_id);
}
4) Your routes are not nested, so {{controllers.customer.model.name}} would be empty if your customer route is not activated. So you should use: {{model.name}} instead of {{controllers.customer.model.name}}
When you click link you passes model directly, and model hook is not fired, so all looks good. When you visit url directly, model hook is fired. If it is empty you will see nothing.
General concept: it is dependancy injection concept, you could read here: http://guides.emberjs.com/v1.12.0/understanding-ember/dependency-injection-and-service-lookup/
You should be able to get the customer from the store. Give the following code a try:
The route:
export default Ember.Route.extend({
model: function(params) {
return Ember.RSVP.hash({
customer: this.store.find('customer', params.customer_id)
});
}
});
The controller:
export default Ember.Controller.extend({
customer: Ember.computed.alias('model.customer')
});
And it should be directly accessible as customer in your template, like so:
<h3>New order for {{customer.name}}</h3>
I changed needs: "customer" to needs: ["customer"] and then used {{model.name}} in my template. Seems that needs requires an array of strings and not just a string, after I fixed that Ember took compare of the rest without the need to create a /customers/order/create.js route.
For a more complete answer see Artych's answer if you don't want everything taken care of.

How Can The Current Route Be Observed for Changes?

I want to update the <title> tag for the page whenever the location (route) changes. I'm particularly interested in observing App.Router's current route changing - I then want to access the View associated with that route and update the <title> tag from the observer based on a property of the View.
For example, if going to /about
What do I set the Observer to observe? I'm having trouble finding a currentState property or equivalent to observe.
Within the Observer, how can I access the App.AboutView associated with the route?
I'm using 1.0.0pre4
My goal is to have a title property and an authorization property on each View that the Observer on currentPath will work with. I want to use the title property to update the document.title, and the authorization property object to check wither my current user can access the route.
Now that I say this and with #Michael Grassotti's answer, perhaps these properties belong on the Controller and not the View. The goal is to gain access to these properties associated with the current Route's context to modify the document.title and check whether or not my App.CurrentUserController (that stores a User object model for the logged in user) is authorized to acess the current route.
What do I set the Observer to observe? I'm having trouble finding a currentState property or equivalent to observe.
You can observe the ApplicationController.currentPath. For example:
App.ApplicationController = Ember.Controller.extend({
currentPathDidChange: function() {
path = this.get('currentPath');
console.log('path changed to: ', path);
window.document.title = path;
}.observes('currentPath')
});
Within the Observer, how can I access the App.AboutView associated with the route?
Trying to access App.AboutView from this observer would be tricky and probably not a good idea. I'm not clear on why a view's property would be useful in this scenario. Can you elaborate?
So the properties I'm trying to work with belong on the Controller for the route, not the View.
Given some contactUs route, it's Controller might look like
App.ContactUsController = App.AppController.extend({
title: "Contact Us"
});
My App.ApplicationController can now look like
App.ApplicationController = Ember.Controller.extend({
updateTitle: function() {
window.document.title = this.controllerFor(this.currentPath).get('title');
}.observes('currentPath')
});
So the API methods/properties I was looking for are
App.ApplicationController.get('currentPath') (currently appears to be undocumented on the site)
controllerFor (also seems undocumented, but is found within a reopen implementation for Ember.ControllerMixin)