Implementing conditional linking

Implementing conditional linking

In the previous post, I outlined what I need and a possible solution to make it work.

Here, I'm going to briefly write about how I implemented it and include the screencast which gives you a glimpse into the process.

If you prefer to watch the screencast first (or just watch that), scroll down to the bottom.

Customizing serialization in Mirage

As we established that most of the work needs to happen in the API layer, I looked at how to customize the response in ember-cli-mirage, the library I use to stand in for the eventual "real" API.

I've found that a recommended way is to amend the serialize method in the (Mirage) serializer, modifying the default response.

Let me show you a few code snippets from the current implementation. What I leave out is either code that's not too relevant for comprehension or very similar to the code snippets I do show, but for other resources in the app.

The Game resource class has lots of relationships where the related resource might not be in the database, so that's what I'm going to show.

I customized the application serialized, found under mirage/serializers/application.js.

export default JSONAPISerializer.extend({
  serialize(resource, request) {
    let json = Serializer.prototype.serialize.apply(this, arguments);
    let { modelName } = resource;
    if (modelName === 'tournament') {
      this.serializeTournaments(resource, request, json);
    }
    if (modelName === 'game') {
      this.serializeGames(resource, request, json);
    }
    return json;
  },
  (...)
});

Here, resource can either be a singular resource, in response to a request like GET /games/5, or a collection, for the GET /games request. So we have to cover both cases in serializeGames:

  serializeGames(resource, request, json) {
    if (this.isCollection(resource)) {
      for (let game of resource.models) {
        this.serializeGame(game, request, json);
      }
    } else {
      this.serializeGame(resource, request, json);
    }
  },

We need both the resources and the original request to be able to decide whether to modify the response (and how to modify it). For example, we should only meddle with the included, related records, if the request asked for those records and if the related record is missing. That's what I check in serializeGame:

serializeGame(game, request, json) {
    let { include } = request.queryParams;

    if (ifIncludes(include, 'whitePlayer') && !game.whitePlayer) {
      this.createPlayerRelationship('white', game.id, game.whiteRawName, json);
    }
    if (ifIncludes(include, 'blackPlayer') && !game.blackPlayer) {
      this.createPlayerRelationship('black', game.id, game.blackRawName, json);
    }
    if (ifIncludes(include, 'tournament') && !game.tournament) {
      this.createTournamentRelationship(game.id, game.tournamentRawName, json);
    }
  },

In the screencast, I implement the createPlayerRelationship so let me show the other method, createTournamentRelationship, here:

  createTournamentRelationship(gameId, rawName, json) {
   let tournamentLite = {
      id: nextTournamentId++,
      type: 'tournament-lites',
      attributes: {
        name: rawName
      }
    }
    let gameItem = json.data.find(item => item.id === gameId);
    gameItem.attributes['has-related-tournament'] = false;
    delete gameItem.attributes['tournament-raw-name'];
    gameItem.relationships.tournament = {
      data: {
        id: tournamentLite.id,
        type: tournamentLite.type
      }
    }
    json.included = json.included || [];
    json.included.push(tournamentLite);
  },

The TournamentLite resource is the one that stands in for a missing tournament relationship. We're setting it in the response as the game's tournament relationship, and include the tournament (lite) in the included part of the response.

(I'm now realizing that the above code only works if we're returning the response to GET /games/. For a single resource, json.data will be an object and thus `json.data.find` will crash. The app currently doesn't make requests of the type `GET /games/:id` but I'll need to fix that.)

hasRelatedTournament is needed so that the client app knows whether to render a link for the game's tournament. We could also check the related tournament's type (is it a Tournament or a TournamentLite) but I feel this method is cleaner.

All we need to do in the template is checking whether we need to show a link:

{{#if game.hasRelatedTournament}}
  {{#link-to "tournaments.tournament" game.tournament.id class="link"}}
    {{game.tournament.name}}
  {{/link-to}}
{{else}}
  {{game.tournament.name}}
{{/if}}

Watch the video

The screencast goes through implementing the linking for the game's players:

As usual, I welcome any questions or feedback below.