2017-01-06 06:30:19 -05:00
|
|
|
|
# Presenters
|
|
|
|
|
|
|
|
|
|
This type of class is responsible for giving the view an object which defines
|
|
|
|
|
**view-related logic/data methods**. It is usually useful to extract such
|
|
|
|
|
methods from models to presenters.
|
|
|
|
|
|
|
|
|
|
## When to use a presenter?
|
|
|
|
|
|
|
|
|
|
### When your view is full of logic
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
|
|
|
|
|
time to create a presenter!
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
|
|
|
|
### When your model has a lot of view-related logic/data methods
|
|
|
|
|
|
|
|
|
|
When your model has a lot of view-related logic/data methods, you can easily
|
|
|
|
|
move them to a presenter.
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
## Why are we using presenters instead of helpers?
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
|
|
|
|
We don't use presenters to generate complex view output that would rely on helpers.
|
|
|
|
|
|
|
|
|
|
Presenters should be used for:
|
|
|
|
|
|
|
|
|
|
- Data and logic methods that can be pulled & combined into single methods from
|
|
|
|
|
view. This can include loops extracted from views too. A good example is
|
|
|
|
|
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7073/diffs.
|
2017-01-09 15:47:32 -05:00
|
|
|
|
- Data and logic methods that can be pulled from models.
|
2017-01-06 06:30:19 -05:00
|
|
|
|
- Simple text output methods: it's ok if the method returns a string, but not a
|
|
|
|
|
whole DOM element for which we'd need HAML, a view context, helpers etc.
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
## Why use presenters instead of model concerns?
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
|
|
|
|
We should strive to follow the single-responsibility principle, and view-related
|
|
|
|
|
logic/data methods are definitely not the responsibility of models!
|
|
|
|
|
|
|
|
|
|
Another reason is as follows:
|
|
|
|
|
|
|
|
|
|
> Avoid using concerns and use presenters instead. Why? After all, concerns seem
|
|
|
|
|
to be a core part of Rails and can DRY up code when shared among multiple models.
|
|
|
|
|
Nonetheless, the main issue is that concerns don’t make the model object more
|
|
|
|
|
cohesive. The code is just better organized. In other words, there’s no real
|
|
|
|
|
change to the API of the model.
|
|
|
|
|
|
|
|
|
|
– https://www.toptal.com/ruby-on-rails/decoupling-rails-components
|
|
|
|
|
|
|
|
|
|
## Benefits
|
|
|
|
|
|
|
|
|
|
By moving pure view-related logic/data methods from models & views to presenters,
|
|
|
|
|
we gain the following benefits:
|
|
|
|
|
|
|
|
|
|
- rules are more explicit and centralized in the presenter => improves security
|
2017-01-09 15:47:32 -05:00
|
|
|
|
- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
|
|
|
|
|
- views are more readable and maintainable
|
2017-01-06 06:30:19 -05:00
|
|
|
|
- decreases number of CE -> EE merge conflicts since code is in separate files
|
|
|
|
|
- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve)
|
|
|
|
|
|
|
|
|
|
## What not to do with presenters?
|
|
|
|
|
|
|
|
|
|
- Don't use helpers in presenters. Presenters are not aware of the view context.
|
|
|
|
|
- Don't generate complex DOM elements, forms etc. with presenters. Presenters
|
2017-01-09 15:47:32 -05:00
|
|
|
|
can return simple data as texts, and URLs using URL helpers from
|
2017-01-06 06:30:19 -05:00
|
|
|
|
`Gitlab::Routing` but nothing much more fancy.
|
|
|
|
|
|
|
|
|
|
## Implementation
|
|
|
|
|
|
|
|
|
|
### Presenter definition
|
|
|
|
|
|
2017-01-10 17:41:04 -05:00
|
|
|
|
Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
|
2017-01-09 15:47:32 -05:00
|
|
|
|
provides a `.presents` method which allows you to define an accessor for the
|
|
|
|
|
presented object. It also includes common helpers like `Gitlab::Routing` and
|
2017-01-06 06:30:19 -05:00
|
|
|
|
`Gitlab::Allowable`.
|
|
|
|
|
|
|
|
|
|
```ruby
|
2017-01-09 15:47:32 -05:00
|
|
|
|
class LabelPresenter < Gitlab::View::Presenter::Simple
|
2017-01-06 06:30:19 -05:00
|
|
|
|
presents :label
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
def text_color
|
2017-01-10 17:41:04 -05:00
|
|
|
|
label.color.to_s
|
2017-01-06 06:30:19 -05:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def to_partial_path
|
|
|
|
|
'projects/labels/show'
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
In some cases, it can be more practical to transparently delegate all missing
|
2017-01-06 06:30:19 -05:00
|
|
|
|
method calls to the presented object, in these cases, you can make your
|
2017-01-09 15:47:32 -05:00
|
|
|
|
presenter inherit from `Gitlab::View::Presenter::Delegated`:
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
2017-01-09 15:47:32 -05:00
|
|
|
|
class LabelPresenter < Gitlab::View::Presenter::Delegated
|
2017-01-06 06:30:19 -05:00
|
|
|
|
presents :label
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
def text_color
|
2017-01-06 06:30:19 -05:00
|
|
|
|
# color is delegated to label
|
2017-01-10 17:41:04 -05:00
|
|
|
|
color.to_s
|
2017-01-06 06:30:19 -05:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def to_partial_path
|
|
|
|
|
'projects/labels/show'
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Presenter instantiation
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which
|
|
|
|
|
detects the presenter based on the presented subject's class.
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class Projects::LabelsController < Projects::ApplicationController
|
|
|
|
|
def edit
|
2017-01-09 15:47:32 -05:00
|
|
|
|
@label = Gitlab::View::Presenter::Factory
|
2017-01-20 12:13:14 -05:00
|
|
|
|
.new(@label, current_user: current_user)
|
2017-01-06 06:30:19 -05:00
|
|
|
|
.fabricate!
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
You can also include the `Presentable` concern in the model:
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class Label
|
2017-01-09 15:47:32 -05:00
|
|
|
|
include Presentable
|
2017-01-06 06:30:19 -05:00
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
and then in the controller:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class Projects::LabelsController < Projects::ApplicationController
|
|
|
|
|
def edit
|
2017-01-20 12:13:14 -05:00
|
|
|
|
@label = @label.present(current_user: current_user)
|
2017-01-06 06:30:19 -05:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Presenter usage
|
|
|
|
|
|
|
|
|
|
```ruby
|
2017-01-09 15:47:32 -05:00
|
|
|
|
%div{ class: @label.text_color }
|
|
|
|
|
= render partial: @label, label: @label
|
2017-01-06 06:30:19 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You can also present the model in the view:
|
|
|
|
|
|
|
|
|
|
```ruby
|
2017-01-20 12:13:14 -05:00
|
|
|
|
- label = @label.present(current_user: current_user)
|
2017-01-06 06:30:19 -05:00
|
|
|
|
|
2017-01-09 15:47:32 -05:00
|
|
|
|
%div{ class: label.text_color }
|
|
|
|
|
= render partial: label, label: label
|
2017-01-06 06:30:19 -05:00
|
|
|
|
```
|