kevlarr
kevlarr

Reputation: 1162

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}}

Upvotes: 1

Views: 215

Answers (3)

kevlarr
kevlarr

Reputation: 1162

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);
  }),
});

Upvotes: 1

RustyToms
RustyToms

Reputation: 7810

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;
}

Upvotes: 1

Alex LaFroscia
Alex LaFroscia

Reputation: 971

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.

Upvotes: 1

Related Questions