Add view template and helper guides
This commit is contained in:
Родитель
ad2e80ef26
Коммит
24c64e8b1b
|
@ -0,0 +1,32 @@
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
There is nothing special about ERB templates that can declare data dependencies. ERB templates are just Ruby functions and view helpers are just Ruby functions so they may also declare data dependencies.
|
||||||
|
|
||||||
|
Helpers accessing many or nested object fields may declare a fragment for those requirements.
|
||||||
|
|
||||||
|
``` ruby
|
||||||
|
module MilestoneHelper
|
||||||
|
# Define static query fragment for fetching data for helper.
|
||||||
|
MilestoneProgressFragment = YourApp::Client.parse <<-'GRAPHQL'
|
||||||
|
fragment on Milestone {
|
||||||
|
closedIssueCount
|
||||||
|
totalIssueCount
|
||||||
|
}
|
||||||
|
GRAPHQL
|
||||||
|
|
||||||
|
def milestone_progress(milestone)
|
||||||
|
milestone = MilestoneProgressFragment.new(milestone)
|
||||||
|
percent = (milestone.closed_issue_count / milestone.total_issue_count) * 100
|
||||||
|
content_tag(:span, "#{percent}%", class: "progress", style: "width: #{percent}%")
|
||||||
|
end
|
||||||
|
|
||||||
|
# A simpler version may use keyword arguments to define the functions
|
||||||
|
# requirements. This avoids any dependency on the shape of data result
|
||||||
|
# classes. This maybe a fine alternative if theres only a handful of
|
||||||
|
# arguments.
|
||||||
|
def milestone_progress(closed:, total:)
|
||||||
|
percent = (closed / total) * 100
|
||||||
|
content_tag(:span, "#{percent}%", class: "progress", style: "width: #{percent}%")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
|
@ -0,0 +1,179 @@
|
||||||
|
# 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`:
|
||||||
|
|
||||||
|
``` 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`:
|
||||||
|
|
||||||
|
``` 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`:
|
||||||
|
|
||||||
|
``` 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.
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<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.
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<%graphql
|
||||||
|
fragment IssueFragment on Issue {
|
||||||
|
title
|
||||||
|
...Views::Issues::Header::Issue
|
||||||
|
...Views::Issues::Body::Issue
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Looping over a collection
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<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 { ... }`.
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<%graphql
|
||||||
|
fragment IssueFragment on Issue {
|
||||||
|
title
|
||||||
|
comments {
|
||||||
|
...Views::Issues::Comment::CommentFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branch on associated data presence
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<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.
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<%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.
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<% 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.
|
||||||
|
|
||||||
|
|
||||||
|
``` erb
|
||||||
|
<%graphql
|
||||||
|
fragment Comment on Comment {
|
||||||
|
bodyHTML
|
||||||
|
editableByViewer
|
||||||
|
...Views::Issues::CommentEditToolbar::Comment
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
[github-graphql-rails-example](https://github.com/github/github-graphql-rails-example) template examples:
|
||||||
|
|
||||||
|
* [app/views/repositories/index.html.erb](https://github.com/github/github-graphql-rails-example/blob/master/app/views/repositories/index.html.erb) shows the root template's listing query and composition over subviews.
|
||||||
|
* [app/views/repositories/_repositories.html.erb]( https://github.com/github/github-graphql-rails-example/blob/master/app/views/repositories/_repositories.html.erb) makes use of GraphQL connections to show the first couple items and a "load more" button.
|
||||||
|
* [app/views/repositories/show.html.erb](https://github.com/github/github-graphql-rails-example/blob/master/app/views/repositories/show.html.erb) shows the root template for the repository show page.
|
Загрузка…
Ссылка в новой задаче