BigW Consortium Gitlab

README.md 4.42 KB
Newer Older
Rémy Coutable committed
1 2 3 4 5 6 7 8 9 10
# 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

11 12
When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
time to create a presenter!
Rémy Coutable committed
13 14 15 16 17 18

### 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.

19
## Why are we using presenters instead of helpers?
Rémy Coutable committed
20 21 22 23 24 25 26 27

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.
28
- Data and logic methods that can be pulled from models.
Rémy Coutable committed
29 30 31
- 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.

32
## Why use presenters instead of model concerns?
Rémy Coutable committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

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
53 54
- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
- views are more readable and maintainable
Rémy Coutable committed
55 56 57 58 59 60 61
- 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
62
  can return simple data as texts, and URLs using URL helpers from
Rémy Coutable committed
63 64 65 66 67 68
  `Gitlab::Routing` but nothing much more fancy.

## Implementation

### Presenter definition

69
Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
70 71
provides a `.presents` method which allows you to define an accessor for the
presented object. It also includes common helpers like `Gitlab::Routing` and
Rémy Coutable committed
72 73 74
`Gitlab::Allowable`.

```ruby
75
class LabelPresenter < Gitlab::View::Presenter::Simple
Rémy Coutable committed
76 77
  presents :label

78
  def text_color
79
    label.color.to_s
Rémy Coutable committed
80 81 82 83 84 85 86 87
  end

  def to_partial_path
    'projects/labels/show'
  end
end
```

88
In some cases, it can be more practical to transparently delegate all missing
Rémy Coutable committed
89
method calls to the presented object, in these cases, you can make your
90
presenter inherit from `Gitlab::View::Presenter::Delegated`:
Rémy Coutable committed
91 92

```ruby
93
class LabelPresenter < Gitlab::View::Presenter::Delegated
Rémy Coutable committed
94 95
  presents :label

96
  def text_color
Rémy Coutable committed
97
    # color is delegated to label
98
    color.to_s
Rémy Coutable committed
99 100 101 102 103 104 105 106 107 108
  end

  def to_partial_path
    'projects/labels/show'
  end
end
```

### Presenter instantiation

109 110
Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which
detects the presenter based on the presented subject's class.
Rémy Coutable committed
111 112 113 114

```ruby
class Projects::LabelsController < Projects::ApplicationController
  def edit
115
    @label = Gitlab::View::Presenter::Factory
116
      .new(@label, current_user: current_user)
Rémy Coutable committed
117 118 119 120 121
      .fabricate!
  end
end
```

122
You can also include the `Presentable` concern in the model:
Rémy Coutable committed
123 124 125

```ruby
class Label
126
  include Presentable
Rémy Coutable committed
127 128 129 130 131 132 133 134
end
```

and then in the controller:

```ruby
class Projects::LabelsController < Projects::ApplicationController
  def edit
135
    @label = @label.present(current_user: current_user)
Rémy Coutable committed
136 137 138 139 140 141 142
  end
end
```

### Presenter usage

```ruby
143 144
%div{ class: @label.text_color }
  = render partial: @label, label: @label
Rémy Coutable committed
145 146 147 148 149
```

You can also present the model in the view:

```ruby
150
- label = @label.present(current_user: current_user)
Rémy Coutable committed
151

152 153
%div{ class: label.text_color }
  = render partial: label, label: label
Rémy Coutable committed
154
```