0
votes

I want to create an interface in Ember that resembles something like Netflix's wherein upon clicking on a movie title the content reveals itself below the poster image. My think is the create a nested route w/

Router.map(function() {
  this.route('movies', {path: '/' }, function() {
    this.route('movie', { path: '/:id'});
  });
});

The problem is that I don't know how I can have a {{outlet}} inside of each movie component and tell the nested route to render within the corresponding one. Has anyone created something similar to this w/ nested routes?

enter image description here

1

1 Answers

3
votes

Depending on whether you don't care about the url, want query params, or want the url to be a full-on route there are different directions you can take.

Method 1: No URL State

Turn movie-preview and movie-detail into components.

You can use focus here to your advantage. That way, selecting a different movie preview will un-focus one component and focus another without you needing to update any bindings.

In movie-preview.js

export default Component.extend({
  attributeBindings: ['tabindex'],
  tabindex: 0,
  isFocused: false,
  movie: undefined,

  focusIn() {
    this.set('isFocused', true);
  },

  focusOut() {
      this.set('isFocused', false);
  }
});

In the template for movie-preview, we add a conditional.

{{#if isFocused}}
  {{movie-detail movie=movie}}
{{/if}}

However, perhaps we want to reflect this state into the URL? Let's start with how we would do this for query params.

Method 2: query param

On the controller for the route, add a query param for selected.

In the template for this route, we will use eq (a helper, one possible implementation of which is here) to set isFocused based on whether the movie for that preview is the currently selected one. We also pass in an action for setting focus.

{{#each movies as |movie|}} {{movie-preview movie=movie isFocused=(eq movie.id selected) select=(action "selectMovie" movie.id) }} {{/each}}

movie-preview.js will be altered to trigger select on click.

export default Component.extend({
  isFocused: false,
  movie: undefined,
  select: undefined,

  click() {
    this.sendAction('select');
  }
});

Lastly, we'll need to handle this action in the controller for this route.

export default Controller.extend({
  queryParams: ['selected'],
  selected: undefined,

  actions: {
    selectMovie(movieId) {
      this.set('selected', movieId);
    }
  }
});

But maybe we wanted this to have a pretty url? This too is easy.

Method 3: Pretty URLs

The first step is to setup nested routes, as you showed.

this.route('movies', {path: '/' }, function() {
  this.route('movie-detail', { path: '/:id'});
});

For this, we do need an outlet as you initially suggested, but we don't need to make that many changes to our code from method 2. Instead of a movie-detail component, we now have a movie-detail route. Since we didn't need to know the details of this before, I'll skip past that now too. We only care about the mechanics of "activating" the route.

In the template for movie-preview, we change the conditional to wrap an outlet.

{{#if isFocused}}
  {{outlet}}
{{/if}}

In the controller, instead of modifying the query param, we need to trigger a transition.

export default Controller.extend({
  selected: undefined,

  actions: {
    selectMovie(movieId) {
      this.set('selected', movieId);

      this.transitionToRoute('movies.movie-detail', movieId);
    },

    clearSelection() {
      this.set('selected', undefined);
      this.transitionToRoute('movies');
    }
  }
});