1

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

kevlarr
  • 1,070
  • 12
  • 24

3 Answers3

1

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;
}
kevlarr
  • 1,070
  • 12
  • 24
RustyToms
  • 7,600
  • 1
  • 27
  • 36
  • 1
    Seems like a neat trick, but I haven't been able to get this to apply the `activeClass` when necessary. I updated it to include the extra boilerplate necessary to even get it to not error out. – kevlarr Aug 21 '17 at 14:53
  • 1
    Nice edit @kevlarr , and thanks for posting the answer you got to work – RustyToms Aug 21 '17 at 18:51
  • I am still curious about using LinkComponent too, because it _should_ work - I was most likely just not implementing it properly – kevlarr Aug 22 '17 at 13:18
1

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.

Alex LaFroscia
  • 961
  • 1
  • 8
  • 24
  • Haven't tried to use the polyfill yet (1.x versions aren't listed as having been tested), but for newer versions of Ember I would think this should **absolutely** be the preferred answer. – kevlarr Aug 21 '17 at 14:51
  • 1
    Yeah, they're probably not tested on that old of an Ember version. Since you mentioned 1.13+ I wasn't sure if a 2.X-compatible solution would be appropriate for you. – Alex LaFroscia Aug 21 '17 at 21:26
  • I might still try the polyfill honestly, yours is by far the most sane method - I'm glad they made the `router` a public service now. – kevlarr Aug 22 '17 at 13:16
  • Out of curiosity, is there a reason you can’t get to Ember 2.8? What version are you on now, 1.13? – Alex LaFroscia Aug 23 '17 at 04:45
  • Yep, 1.13... Given the sheer number of deprecations, "old code" (was originally written for an older version), and old packages, we are not sure how much developer time it will take to update to newest Ember, clean out all the errors, and verify 100% feature parity. So it's on deck for some point, we just can't allocate the time right now while launching another app. – kevlarr Aug 23 '17 at 14:16
1

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);
  }),
});
kevlarr
  • 1,070
  • 12
  • 24
  • `currentArticle: Ember.inject.service('current-article')` its equivalent to `currentArticle: Ember.inject.service()`. refer https://guides.emberjs.com/v2.14.0/applications/services/#toc_accessing-services super easy to write it. I always do like this. – Ember Freak Aug 21 '17 at 15:08
  • 1
    You can even try passing `currentArticle.id` when including `taglines-ticker` component. so that get rid of `didReceiveAttrs` hook code and injection of service in component. – Ember Freak Aug 21 '17 at 15:10