123456789_123456789_123456789_123456789_123456789_

Templates

ERB templates access data in a similar way as traversing an ActiveRecord object graph. Simple object fields maybe accessed, as well as parent or child has-one and has-many associations.

All data passing is done explicitly through locals: just as you would pass arguments to a function. By convention, this object may be a raw data Hash received from GraphQL or a wrapped Ruby struct-like object. For consistency, the argument should be casted into a nice Ruby friendly object first thing in the template.

app/views/issues/show.html.erb:

<%# cast issue data hash into ruby friendly struct %>
<% issue = Issues::Show::Issue.new(issue) %>

<h1><%= issue.repository.name %>: <%= issue.title %></h1>
<%= issue.body_html # bodyHTML is snakecasified %>
by <%= issue.author.login %>

<% issue.comments.each do |comment| %>
  <%# Pass comment to subview %>
  <%= render "issues/comment", comment: comment %>
<% end %>

This is all pretty traditional Ruby and Rails so far.

However, since the views can not access ActiveRecord objects directly anymore, a static query is defined inline .erb file declaring the views data dependencies.

app/views/issues/show.html.erb:

<%graphql
  fragment on Issue {
    title
    repository {
      name
    }
    bodyHTML
    author {
      login
    }
    comments {
      # issues/show is only concerned with rendering a collection of
      # comments, not the comment itself. However, we do need to statically
      # include the data dependencies of the issues/comment partial we
      # intend to render.
      ...Views::Issues::Comment::Comment
    }
  }
%>

Our GraphQL fragment definition includes all the fields we want to access just in the show.html.erb file itself, nothing more, nothing less.

However, we do render a subview and hand off a comment. Since we composed rendered calls, we'll need to compose our fragment query as well. This works by including the subview's ...Views::Issues::Comment::Comment into the comments collection we requested.

app/views/issues/_comment.html.erb:

<%graphql
  fragment on Comment {
    bodyHTML
    author {
      login
    }
  }
%>

<%# cast comment data hash into ruby friendly struct %>
<%# this casting also allows us to accessing any fields that were opaque to %>
<%# our parent view. %>
<% comment = Issues::Comment::CommentFragment.new(comment) %>

<%= comment.body_html %>
by <%= comment.author.login %>

Composing fragments

Static

Many views will always render a set of subviews.

<div class="issue-container">
  <h1><%= issue.title %></h1>
  <%= render "issues/header", issue: issue %>
  <%= render "issues/body", issue: issue %>
</div>

The fragment should declare all the data dependencies used by just this partial. In this case, only the issue's title is explicitly used, then include any subview fragments.

<%graphql
  fragment IssueFragment on Issue {
    title
    ...Views::Issues::Header::Issue
    ...Views::Issues::Body::Issue
  }
%>

Looping over a collection

<h1><%= issue.title %></h1>

<% issue.comments.each do |comment| %>
  <%= render "issues/comment", comment: comment %>
<% end %>

The fragment declares the view's own data dependencies as before. As well as the comments collection. Since a comment is passed to the issues/comment partial, not the issue, we'll include the fragment inside comments { ... }.

<%graphql
  fragment IssueFragment on Issue {
    title
    comments {
      ...Views::Issues::Comment::CommentFragment
    }
  }
%>

Branch on associated data presence

<h1><%= issue.title %></h1>

<% if milestone = issue.milestone %>
  <%= render "issues/milestone", milestone: milestone
<% end %>

Similar to embedding a collection's fragment, the partial defines the data for the milestone itself, not the issue. We include the fragment in the milestone { ... } connection.

<%graphql
  fragment Issue on Issue {
    title
    milestone {
      ...Views::Issues::Milestone::Milestone
    }
  }
%>

Branch on arbitrary flag

More generally, UI may only be visible if a flag is set on the data object.

<% if comment.editable_by_viewer? %>
  <%= render "issues/comment_edit_toolbar", comment: comment
<% end %>

<%= comment.body_html %>

Since the view may conditionally need the edit toolbars data, the view's fragment must always be included. This is an acceptable place where overfetching data is okay.

<%graphql
  fragment Comment on Comment {
    bodyHTML
    editableByViewer
    ...Views::Issues::CommentEditToolbar::Comment
  }

See also

github-graphql-rails-example template examples: