compony 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +44 -1582
- data/VERSION +1 -1
- data/compony.gemspec +4 -4
- data/doc/ComponentGenerator.html +1 -1
- data/doc/Components.html +1 -1
- data/doc/ComponentsGenerator.html +1 -1
- data/doc/Compony/Component.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone.html +1 -1
- data/doc/Compony/ComponentMixins/Default.html +1 -1
- data/doc/Compony/ComponentMixins/Resourceful.html +1 -1
- data/doc/Compony/ComponentMixins.html +1 -1
- data/doc/Compony/Components/Button.html +1 -1
- data/doc/Compony/Components/Destroy.html +1 -1
- data/doc/Compony/Components/Edit.html +1 -1
- data/doc/Compony/Components/Form.html +1 -1
- data/doc/Compony/Components/Index.html +1 -1
- data/doc/Compony/Components/List.html +1 -1
- data/doc/Compony/Components/New.html +1 -1
- data/doc/Compony/Components/Show.html +1 -1
- data/doc/Compony/Components/WithForm.html +1 -1
- data/doc/Compony/Components.html +1 -1
- data/doc/Compony/ControllerMixin.html +1 -1
- data/doc/Compony/Engine.html +1 -1
- data/doc/Compony/MethodAccessibleHash.html +1 -1
- data/doc/Compony/ModelFields/Anchormodel.html +1 -1
- data/doc/Compony/ModelFields/Association.html +1 -1
- data/doc/Compony/ModelFields/Attachment.html +1 -1
- data/doc/Compony/ModelFields/Base.html +1 -1
- data/doc/Compony/ModelFields/Boolean.html +1 -1
- data/doc/Compony/ModelFields/Color.html +1 -1
- data/doc/Compony/ModelFields/Currency.html +1 -1
- data/doc/Compony/ModelFields/Date.html +1 -1
- data/doc/Compony/ModelFields/Datetime.html +1 -1
- data/doc/Compony/ModelFields/Decimal.html +1 -1
- data/doc/Compony/ModelFields/Email.html +1 -1
- data/doc/Compony/ModelFields/Float.html +1 -1
- data/doc/Compony/ModelFields/Integer.html +1 -1
- data/doc/Compony/ModelFields/Percentage.html +1 -1
- data/doc/Compony/ModelFields/Phone.html +1 -1
- data/doc/Compony/ModelFields/RichText.html +1 -1
- data/doc/Compony/ModelFields/String.html +1 -1
- data/doc/Compony/ModelFields/Text.html +1 -1
- data/doc/Compony/ModelFields/Time.html +1 -1
- data/doc/Compony/ModelFields/Url.html +1 -1
- data/doc/Compony/ModelFields.html +1 -1
- data/doc/Compony/ModelMixin.html +6 -1
- data/doc/Compony/NaturalOrdering.html +1 -1
- data/doc/Compony/RequestContext.html +1 -1
- data/doc/Compony/Version.html +1 -1
- data/doc/Compony/ViewHelpers.html +1 -1
- data/doc/Compony/VirtualModel.html +152 -0
- data/doc/Compony.html +3 -3
- data/doc/ComponyController.html +1 -1
- data/doc/_index.html +8 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +40 -1619
- data/doc/guide/basic_component.md +259 -0
- data/doc/guide/example.md +228 -0
- data/doc/guide/feasibility.md +38 -0
- data/doc/guide/generators.md +13 -0
- data/doc/guide/helpers.md +156 -0
- data/doc/guide/inheritance.md +62 -0
- data/doc/guide/installation.md +45 -0
- data/doc/guide/internal_datastructures.md +45 -0
- data/doc/guide/model_fields.md +62 -0
- data/doc/guide/nesting.md +132 -0
- data/doc/guide/ownership.md +21 -0
- data/doc/guide/pre_built_components/button.md +8 -0
- data/doc/guide/pre_built_components/destroy.md +25 -0
- data/doc/guide/pre_built_components/edit.md +27 -0
- data/doc/guide/pre_built_components/form.md +117 -0
- data/doc/guide/pre_built_components/index.md +6 -0
- data/doc/guide/pre_built_components/list.md +14 -0
- data/doc/guide/pre_built_components/new.md +27 -0
- data/doc/guide/pre_built_components/show.md +8 -0
- data/doc/guide/pre_built_components/with_form.md +18 -0
- data/doc/guide/pre_built_components.md +19 -0
- data/doc/guide/resourceful.md +101 -0
- data/doc/guide/root_actions.md +67 -0
- data/doc/guide/standalone.md +136 -0
- data/doc/guide/virtual_models.md +29 -0
- data/doc/index.html +40 -1619
- data/doc/top-level-namespace.html +1 -1
- data/lib/compony/virtual_model.rb +10 -0
- data/lib/compony.rb +4 -0
- metadata +29 -2
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# What is a component?
|
|
4
|
+
|
|
5
|
+
Compony components are nestable elements that are capable of replacing Rails' routes, views and controllers. They structure code for data manipulation, authentication and rendering into a single class that can easily be subclassed. This is achieved with Compony's DSL that provides a readable and overridable way to store your logic.
|
|
6
|
+
|
|
7
|
+
Just like Rails, Compony is opinionated and you are advised to structure your code according to the examples and explanations. This makes it easier for others to dive into existing code.
|
|
8
|
+
|
|
9
|
+
# A basic (bare) component
|
|
10
|
+
|
|
11
|
+
## Naming
|
|
12
|
+
|
|
13
|
+
Compony components must be named according to the pattern `Components::FamilyName::ComponentName`.
|
|
14
|
+
|
|
15
|
+
- The family name should be pluralized and is analog to naming a Rails controller. For instance, when you would create a `UsersController` in plain Rails, the Compony family equivalent is `Users`.
|
|
16
|
+
- The component name is the Compony analog to a Rails action.
|
|
17
|
+
|
|
18
|
+
Example: If your plain Rails `UsersController` has an action `show`, the equivalent Compony component is `Components::Users::Show` and is located under `app/components/users/show.rb`.
|
|
19
|
+
|
|
20
|
+
If you have abstract components (i.e. components that your app never uses directly, but which you inherit from), you may name and place them arbitrarily.
|
|
21
|
+
|
|
22
|
+
## Initialization, manual instantiation and rendering
|
|
23
|
+
|
|
24
|
+
You will rarely have to override `def initialize` of a component, as most of your code will go into the component's `setup` block as explained below. However, when you do, make sure to forward all default arguments to the parent class, as they are essential to the component's function:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
def initialize(some_positional_argument, another=nil, *args, some_keyword_argument:, yetanother: 42, **kwargs, &block)
|
|
28
|
+
super(*args, **kwargs, &block) # Typically you should call this first
|
|
29
|
+
@foo = some_positional_argument
|
|
30
|
+
@bar = another
|
|
31
|
+
@baz = some_keyword_argument
|
|
32
|
+
@stuff = yetanother
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Typically, your components will be instantiated and rendered by Compony through the ["standalone" feature](./standalone.md). Nonetheless, it is possible to do so manually as well, for instance if you'd like to render a component from within an existing view in your application:
|
|
37
|
+
|
|
38
|
+
```erb
|
|
39
|
+
<% index_users_comp = Components::Users::Index.new %>
|
|
40
|
+
<%= index_users_comp.render(controller) %>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Note that rendering a component always requires the controller as an argument. It also possible to pass an argument `locals` that will be made available to `render` (see below):
|
|
44
|
+
|
|
45
|
+
```erb
|
|
46
|
+
<% index_users_comp = Components::Users::Index.new %>
|
|
47
|
+
<%= index_users_comp.render(controller, locals: { weather: :sunny }) %>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Setup
|
|
51
|
+
|
|
52
|
+
Every component must call the static method `setup` which will contain most of the code of your components. This can be achieved either by a call directly from your class, or by inheriting from a component that calls `setup`. If both classes call the method, the inherited class' `setup` is run first and the inheriting's second, thus, the child class can override setup properties of the parent class.
|
|
53
|
+
|
|
54
|
+
Call setup as follows:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class Components::Users::Show < Compony::Component
|
|
58
|
+
setup do
|
|
59
|
+
# Your setup code goes here
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The code in setup is run at the end the component's initialization. In this block, you will call a number of methods that define the component's behavior and which we will explain now.
|
|
65
|
+
|
|
66
|
+
### Labelling
|
|
67
|
+
|
|
68
|
+
This defines a component's label, both as seen from within the component and from the outside. You can query the label in order to display it as a title in your component. Links and buttons to components will also display the same label, allowing you to easily rename a component, including any parts of your UI that point to it.
|
|
69
|
+
|
|
70
|
+
Labels come in different formats, short and long, with long being the default. Define them as follows if your component is about a specific object, for instance a show component for a specific user:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
setup do
|
|
74
|
+
label(:short) { |user| user.label } # Assuming your User model has a method or attribute `label`.
|
|
75
|
+
label(:long) { |user| "Displaying user #{user.label}" } # In practice, you'd probably use I18n.t or FastGettext here to deal with translations.
|
|
76
|
+
|
|
77
|
+
# Or use this short hand to set both long and short label to the user's label:
|
|
78
|
+
label(:all) { |user| user.label }
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To read the label, from within the component or from outside, proceed as follows:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
label(User.first) # This returns the long version: "Displaying user John Doe".
|
|
86
|
+
label(User.first, format: :short) # This returns the short version "John Doe".
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It is important to note that since your label block takes an argument, you must provide the argument when reading the label (exception: if the component implements the method `data` returning an object, the argument can be omitted and the label block will be provided that object). Only up to one argument is supported.
|
|
90
|
+
|
|
91
|
+
Here is an example on how labelling looks like for a component that is not about a specific object, such as an index component for users:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
setup do
|
|
95
|
+
label(:long) { 'List of users' }
|
|
96
|
+
label(:short) { 'List' }
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
And to read those:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
label # "List of users"
|
|
104
|
+
label(format: :short) # "List"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
If you do not define any labels, Compony will fallback to the default which is using Rail's `humanize` method to build a name from the family and component name, e.g. "index users".
|
|
108
|
+
|
|
109
|
+
Additionally, components can specify an icon and a color. These are not used by Compony directly and it is up to you to to define how and where to use them. Example:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
setup do
|
|
113
|
+
color { '#AA0000' }
|
|
114
|
+
icon { %i[fa-solid circle] }
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
To retrieve them from outside the component, use:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
my_component.color # '#AA0000'
|
|
122
|
+
my_component.icon # [:'fa-solid', :circle]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Providing content
|
|
126
|
+
|
|
127
|
+
Basic components do not come with default content. Instead, you must call the method `content` inside the setup block and provide a block containing your view. It will be evaluated inside a `RequestContext` (more on that later).
|
|
128
|
+
|
|
129
|
+
In this block, provide the HTML to be generated using Dyny: [https://github.com/kalsan/dyny](https://github.com/kalsan/dyny)
|
|
130
|
+
|
|
131
|
+
Here is an example of a component that renders a title along with a paragraph:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
setup do
|
|
135
|
+
label(:all) { 'Welcome' }
|
|
136
|
+
content do
|
|
137
|
+
h1 'Welcome to my basic component.'
|
|
138
|
+
para "It's not much, but it's honest work."
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Naming content blocks, ordering and overriding them in subclasses
|
|
144
|
+
|
|
145
|
+
Content blocks are actually named. The `content` call adds or replaces a previously defined content block, e.g. in an earlier call to `setup` in a component's superclass. When calling `content` without a name, it defaults to `main` and will overwrite any previous `main` content. However, you can provide your own name and refer to other names by using the `before:` keyword.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
setup do
|
|
149
|
+
content do # will become :main
|
|
150
|
+
h1 'Welcome to my basic component.'
|
|
151
|
+
end
|
|
152
|
+
content :thanks do
|
|
153
|
+
para 'Thank you and see you tomorrow.'
|
|
154
|
+
end
|
|
155
|
+
content :middle, before: :thanks do
|
|
156
|
+
para 'This paragraph is inserted between the others.'
|
|
157
|
+
end
|
|
158
|
+
content :thanks do
|
|
159
|
+
para 'Thank you and see you tonight.' # this overwrites "Thank you and see you tomorrow."
|
|
160
|
+
end
|
|
161
|
+
content :first, before: :main do
|
|
162
|
+
para 'This appears first.'
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This results in:
|
|
168
|
+
- This appears first.
|
|
169
|
+
- Welcome to my basic component.
|
|
170
|
+
- This paragraph is inserted between the others.
|
|
171
|
+
- Thank you and see you tonight.
|
|
172
|
+
|
|
173
|
+
As you see, overusing this feature can lead to messy code as it becomes unclear what happens in what order. For this reason, this feature should only be used to decouple the content of your abstract components for allowing surgical overrides in subclasses.
|
|
174
|
+
|
|
175
|
+
It is a good convention to always have one content block named `:main`, as you might want to refer to it in subclasses.
|
|
176
|
+
|
|
177
|
+
#### Nesting content blocks, calling a content block from another
|
|
178
|
+
|
|
179
|
+
In some situations, such as in forms, it can be useful to nest content blocks. This will also allow subclasses to override a wrapper while keeping the content, and vice versa. To make this possible, you can also use the `content` keyword inside a content block. Note that unlike the call in `setup`, this call will render a content block instead of defining it. This happens inside the request context and the content block must be defined inside the current component.
|
|
180
|
+
|
|
181
|
+
Note that you cannot call another component's content block this way.
|
|
182
|
+
|
|
183
|
+
Here is an example on how to use this feature, e.g. to create a bootstrap card that can be overridden with precision:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# Components::Bootstrap::Card
|
|
187
|
+
setup do
|
|
188
|
+
content hidden: true do # hidden: true will cause `render` to skip this content block. You can still use it in the nested fashion.
|
|
189
|
+
div 'I am the default content for the card'
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
content :card do
|
|
193
|
+
div class: 'card card-body' do
|
|
194
|
+
content :main
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The output is:
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<div class="card card-body"><div>I am the default content for the card</div></div>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
So when you subclass this component, you can forget about the card and just overwrite `:main` as follows:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# Components::Hello::HelloCard < Components::Bootstrap::Card
|
|
210
|
+
setup do
|
|
211
|
+
content do # hidden is still true because the old :main content block specified that already.
|
|
212
|
+
h1 'Hello'
|
|
213
|
+
para 'Welcome to my site.'
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The output is:
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<div class="card card-body"><h1>Hello</h1><p>Welcome to my site.</p></div>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Removing content blocks
|
|
225
|
+
|
|
226
|
+
If a component's parent class defines a content block that is undesired in a subclass component, the content block can be removed as follows:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
setup do
|
|
230
|
+
remove_content :some_content_defined_in_parent # This component will now behave as if this content block was never declared in its parent.
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Redirecting away / Intercepting rendering
|
|
235
|
+
|
|
236
|
+
Immediately before the `content` block(s) are evaluated, another chain of blocks is evaluated if present: `before_render`. If on of these blocks creates a reponse body in the Rails controller, the subsequent `before_render` blocks and all `content` blocks are skipped.
|
|
237
|
+
|
|
238
|
+
This is useful for redirecting. Here is an example of a component that provides a restaurant's lunch menu, but redirects to the menu overview page instead if it's not lunch time:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
setup do
|
|
242
|
+
label(:all){ 'Lunch menu' }
|
|
243
|
+
|
|
244
|
+
before_render do
|
|
245
|
+
current_time = Time.zone.now
|
|
246
|
+
if current_time.hour >= 11 && current_time.hour < 14
|
|
247
|
+
flash.notice = "Sorry, it's not lunch time."
|
|
248
|
+
redirect_to all_menus_path
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
content do # This is entirely skipped if it's not lunch time.
|
|
253
|
+
h1 label
|
|
254
|
+
para 'Today we have spaghetti.'
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Similarly to `content`, the `before_render` method also accepts a name, defaulting to `:main`, as well as a `before:` keyword. This allows you to selectively extend and/or override `before_render` blocks in subclasses.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Example
|
|
4
|
+
|
|
5
|
+
To get you a rough idea what working with Compony feels like, let's look at a small dummy application using Compony from scratch, to make this example as explicit as possible. In practice, much of the logic shown here would be moved to abstract components that you can inherit from.
|
|
6
|
+
|
|
7
|
+
The example is meant to be read top-down and information will mostly not be repeated. Comments will give you a rough idea of what's going on on each line. The features are more completely documented in subsequent chapters.
|
|
8
|
+
|
|
9
|
+
Please note that from this example alone, you won't be able to comprehend the underlying concepts - refer to the rest of the [guide](/README.md#guide) for this.
|
|
10
|
+
|
|
11
|
+
Let's implement a simple user management page with Compony. User's have a name, an integer age, a comment, as well as a role (which we will conveniently model using `AnchorModel`: https://github.com/kalsan/anchormodel). We want to be able to list, show, create, edit and destroy users. Users having the role Admin shall not be destroyed.
|
|
12
|
+
|
|
13
|
+
## The User model
|
|
14
|
+
|
|
15
|
+
We'll assume a model that has the standard Rails schema:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
create_table 'users', force: :cascade do |t|
|
|
19
|
+
t.string 'name'
|
|
20
|
+
t.string 'comment'
|
|
21
|
+
t.integer 'age'
|
|
22
|
+
t.datetime 'created_at', null: false
|
|
23
|
+
t.datetime 'updated_at', null: false
|
|
24
|
+
t.string 'role', default: 'guest', null: false
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class User < ApplicationRecord
|
|
30
|
+
# Refer to https://github.com/kalsan/anchormodel
|
|
31
|
+
belongs_to_anchormodel :role
|
|
32
|
+
|
|
33
|
+
# Fields define which attributes are relevant in the GUI and how they should be presented.
|
|
34
|
+
field :name, :string
|
|
35
|
+
field :age, :integer
|
|
36
|
+
field :comment, :string
|
|
37
|
+
field :role, :anchormodel
|
|
38
|
+
field :created_at, :datetime
|
|
39
|
+
field :updated_at, :datetime
|
|
40
|
+
|
|
41
|
+
# The method `label` must be implemented on all Compony models. Instead of this method, we could also rename the column :name to :label.
|
|
42
|
+
def label
|
|
43
|
+
name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# This is how we tell Compony that admins are not to be destroyed.
|
|
47
|
+
prevent :destroy, 'Cannot destroy admins' do
|
|
48
|
+
role == Role.find(:admin)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## The Show component
|
|
54
|
+
|
|
55
|
+
This components loads a user by reading the param `id`. It then displays a simple table showing all the fields defined above.
|
|
56
|
+
|
|
57
|
+
We will implement this component on our own, giving you an insight into many of Compony's mechanisms:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# All components (except abstract ones) must be placed in the `Components` namespace living under `app/components`.
|
|
61
|
+
# They must be nested in another namespace, called "family" (here, `Users`), followed by the component's name (here, `Show`).
|
|
62
|
+
class Components::Users::Show < Compony::Component
|
|
63
|
+
# The Resourceful mixin causes a component to automatically load a model from the `id` parameter and store it under `@data`.
|
|
64
|
+
# The model's class is inferred from the component's name: `Users::Show` -> `User`
|
|
65
|
+
include Compony::ComponentMixins::Resourceful
|
|
66
|
+
|
|
67
|
+
# Components are configured in the `setup` method, which prevents loading order issues.
|
|
68
|
+
setup do
|
|
69
|
+
# The DSL call `label` defines what is the title of the component and which text is displayed on links as well as buttons pointing to it.
|
|
70
|
+
# It accepts different formats and takes a block. Given that this component always loads one model, the block must take an argument which is the model.
|
|
71
|
+
# The argument must be provided by links and buttons pointing to this component.
|
|
72
|
+
label(:short) { |_u| 'Show' } # The short format is suitable for e.g. a button in a list of users.
|
|
73
|
+
label(:long) { |u| "Show user #{u.label}" } # The long format is suitable e.g. in a link in a text about this user.
|
|
74
|
+
|
|
75
|
+
# Actions point to other components. They have a name that is used to identify them (e.g. in the `prevent` call above) and a block returning a button.
|
|
76
|
+
# Compony buttons take the name to an action and either a family name or instance, e.g. a Rails model instance.
|
|
77
|
+
# Whether or not an instance must be passed is defined by the component the button is pointing to (see the comment for `label` earlier in the example).
|
|
78
|
+
action(:index) { Compony.button(:index, :users) } # This points to `Components::Users::Index` without passing a model (because it's an index).
|
|
79
|
+
action(:edit) { Compony.button(:edit, @data) } # This points to `Components::Users::Edit` for the currently loaded model. This also checks feasibility.
|
|
80
|
+
|
|
81
|
+
# When a standalone config is present, Compony creates one or multiple Rails routes. Components without standalone config must be nested within others.
|
|
82
|
+
standalone path: 'users/show/:id' do # This specifies the path to this component.
|
|
83
|
+
verb :get do # This speficies that a GET route should be created for the path specified above.
|
|
84
|
+
authorize { true } # Immediately after loading the model, this is called to check for authorization. `true` means that anybody can get access.
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# After loading the model and passing authorization, the `content` block is evaluated. This is Compony's equivalent to Rails' views.
|
|
89
|
+
# Inside the `content` block, the templating Gem Dyny (https://github.com/kalsan/dyny) is used, allowing you to write views in plain Ruby.
|
|
90
|
+
content do
|
|
91
|
+
h3 @data.label # Display a <h3> title
|
|
92
|
+
table do # Open a <table> tag
|
|
93
|
+
tr do # Open a <tr> tag
|
|
94
|
+
# Iterate over all the fields defined in the model above and display its translated label (this uses Rails' `human_attribute_name`), e.g. "Name".
|
|
95
|
+
@data.fields.each_value { |field| th field.label }
|
|
96
|
+
end # Closing </tr>
|
|
97
|
+
tr do
|
|
98
|
+
# Iterate over the fields again and call `value_for` which formats each field's value according to the field type.
|
|
99
|
+
@data.fields.each_value { |field| td field.value_for(@data) }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Here is what our Show component looks like when we have a layout with the bare minimum and no styling at all:
|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
|
|
111
|
+
It is important to note that actions, buttons, navigation, notifications etc. are handled by the application layout. In this and the subsequent screenshots, we explicitely use minimalism, as it makes the generated HTML clearer.
|
|
112
|
+
|
|
113
|
+
## The Destroy component
|
|
114
|
+
|
|
115
|
+
Compony has a built-in abstract `Destroy` component which displays a confirmation message and destroys the record if the verb is `DELETE`. This is a good example for how DRY code can become for "boring" components. Since everything is provided with an overridable default, components without special logic can actually be left blank:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class Components::Users::Destroy < Compony::Components::Destroy
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Note that this component is fully functional. All is handled by the class it inherits from:
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+
## The New component and the Form component
|
|
127
|
+
|
|
128
|
+
Compony also has a pre-built abstract `New` component that handles routing and resource manipulation. It combines the controller actions `new` and `create`, depending on the HTTP verb of the request. Since it's pre-built, any "boring" code can be omitted and our `New` components looks like this:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
class Components::Users::New < Compony::Components::New
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
By default, this component looks for another component called `Form` in the same directory, which can look like this:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class Components::Users::Form < Compony::Components::Form
|
|
139
|
+
setup do
|
|
140
|
+
# This mandatory DSL call prepares and opens a form in which you can write your HTML in Dyny.
|
|
141
|
+
# The form is realized using the simple_form Gem (https://github.com/heartcombo/simple_form).
|
|
142
|
+
# Inside this block, more DSL calls are available, such as `field`, which automatically generates
|
|
143
|
+
# a suitable simple_form input from the field specified in the model.
|
|
144
|
+
form_fields do
|
|
145
|
+
concat field(:name) # `field` checks the model to find out that a string input is needed here. `concat` is the Dyny equivalent to ERB's <%= %>.
|
|
146
|
+
concat field(:age)
|
|
147
|
+
concat field(:comment)
|
|
148
|
+
concat field(:role) # Compony has built-in support for Anchormodel and as the model declares `role` to be of type `anchormodel`, a select is rendered.
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# This DSL call is mandatory as well and automatically generates strong param validation for this form.
|
|
152
|
+
# The generated underlying implementation is Schemacop V3 (https://github.com/sitrox/schemacop/blob/master/README_V3.md).
|
|
153
|
+
schema_fields :name, :age, :comment, :role
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This is enough to render a fully functional form that creates new users:
|
|
159
|
+
|
|
160
|
+

|
|
161
|
+
|
|
162
|
+
## The Edit component
|
|
163
|
+
|
|
164
|
+
Just like `New`, `Edit` is a pre-built component that handles routing and resource manipulation for editing models, combinding the controller actions `edit` and `update` depending on the HTTP verb. It uses that same `Form` component we wrote above and thus the code is as simple as:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
class Components::Users::Edit < Compony::Components::Edit
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
It then looks like this:
|
|
172
|
+
|
|
173
|
+

|
|
174
|
+
|
|
175
|
+
## The Index component
|
|
176
|
+
|
|
177
|
+
This component should list all users and provide buttons to manage them. We'll build it from scratch and make it resourceful, where `@data` holds the ActiveRecord relation.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
class Components::Users::Index < Compony::Component
|
|
181
|
+
# Making the component resourceful enables a few features for dealing with @data.
|
|
182
|
+
include Compony::ComponentMixins::Resourceful
|
|
183
|
+
|
|
184
|
+
setup do
|
|
185
|
+
label(:all) { 'Users' } # This sets all labels (long and short) to 'Users'. When pointing to this component using buttons, we will not provide a model.
|
|
186
|
+
standalone path: 'users' do # The path is simply /users, without a param. This conflicts with `Resourceful`, which we will fix in `load_data`.
|
|
187
|
+
verb :get do
|
|
188
|
+
authorize { true }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# This DSL call is specific to resourceful components and overrides how a model is loaded.
|
|
193
|
+
# The block is called before authorization and must assign a model or collection to `@data`.
|
|
194
|
+
load_data { @data = User.all }
|
|
195
|
+
|
|
196
|
+
content do
|
|
197
|
+
h4 'Users:' # Provide a title
|
|
198
|
+
# Provide a button that creates a new user. Note that we must write `:users` (plural) because the component's family is `Users`.
|
|
199
|
+
concat compony_button(:new, :users) # The `Users::New` component does not take a model, thus we just pass the symbol `:users`, not a model.
|
|
200
|
+
|
|
201
|
+
div class: 'users' do # Opening tag <div class="users">
|
|
202
|
+
@data.each do |user| # Iterate the collection
|
|
203
|
+
div class: 'user' do # For each element, open another div
|
|
204
|
+
User.fields.values.each do |field| # For each user, iterate all fields
|
|
205
|
+
span do # Open a <span> tag
|
|
206
|
+
concat "#{field.label}: #{field.value_for(user)} " # Display the field's label and apply it to value, as we did in the Show component.
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
# For each user, add three buttons show, edit, destroy. The method `with_button_defaults` applies its arguments to every `compony_button` call.
|
|
210
|
+
# The option `format: :short` causes the button to call the target component's `label(:short) {...}` label function.
|
|
211
|
+
Compony.with_button_defaults(label_opts: { format: :short }) do
|
|
212
|
+
concat compony_button(:show, user) # Now equivalent to: `compony_button(:show, user, label_opts: { format: :short })`
|
|
213
|
+
concat compony_button(:edit, user)
|
|
214
|
+
concat compony_button(:destroy, user)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The result looks like this:
|
|
225
|
+
|
|
226
|
+

|
|
227
|
+
|
|
228
|
+
Note how the admin's delete button is disabled due to the feasibility framework. Pointing the mouse at it causes a tooltip saying: "Cannot destroy admins.", as specified in the model's prevention.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Feasibility
|
|
4
|
+
|
|
5
|
+
When a user has the permission to perform an action in general, but it is currently not feasible (for instance if the concerned object is incomplete, or if right now is not the right time to do the action), buttons pointing to that action should be disabled and a HTML `title` attribute should cause a tooltip explaining why this action cannot be performed right now.
|
|
6
|
+
|
|
7
|
+
This can be easily achieved with the feasibility framework, which allows you to prevent actions on conditions, along with an error message. Formulate the error message similar to Rails validation errors (first letter not capital, no period at the end), as the prevention framework is able to concatenate multiple error messages if multiple conditions prevent an action.
|
|
8
|
+
|
|
9
|
+
The feasibility framework currently only makes sense for resourceful components.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# app/models/user.rb
|
|
15
|
+
# Prevent sending an e-mail to a user that has no e-mail address present
|
|
16
|
+
prevent :send_mail, 'the e-mail address is missing' do
|
|
17
|
+
email.blank?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# app/models/event.rb
|
|
21
|
+
# Multiple actions can be prevented at once:
|
|
22
|
+
# Prevent creating or removing a booking to an event that lies in the past or that is locked
|
|
23
|
+
prevent [:create_booking, :destroy_booking], 'the event is already over' do
|
|
24
|
+
ends_at < Time.zone.now || locked?
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Note that the feasibility framework currently only affects buttons/links pointing to actions, not the action itself.** If a user were to issue the HTTP call manually, the component happily responds and performs the action. This is why you should always back important preventions with an appropriate Rails model validation:
|
|
29
|
+
|
|
30
|
+
- The Rails model validation prevents that invalid data can be saved to the database.
|
|
31
|
+
- The feasibility framework disables buttons and links and explains to guide the user.
|
|
32
|
+
- Links are disabled by changing the href to `'#'` and adding the `.disabled` class, which is useful when bootstrap is used.
|
|
33
|
+
- Authorization is orthogonal to this, limiting the actions of a specific user.
|
|
34
|
+
- If an action is both prevented and not authorized, the authorization "wins" and the action button is not shown at all.
|
|
35
|
+
|
|
36
|
+
Compony has a feature that auto-detects feasibility of some actions. In particular, it checks for `dependent` relations in the `has_one`/`has_many` relations and disables delete buttons that point to objects that have dependent objects that cannot automatically be destroyed.
|
|
37
|
+
|
|
38
|
+
To disable auto detection, call `skip_autodetect_feasibilities` in your model.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Rails Generators provided by Compony
|
|
4
|
+
|
|
5
|
+
To make your life easier and coding faster, Compony comes with two generators:
|
|
6
|
+
|
|
7
|
+
- `rails g component Users::New` will create `app/components/users/new.rb` and, since the component's name coincides with a a pre-built component, automatically inherit from that. If the name is unknown, the generated component will inherit form `Compony::Component` instead. The generator also equips generated components with the boilerplate code that wil be required to make the component work.
|
|
8
|
+
- The generator can also be called via its alternative form `rails g component users/new`.
|
|
9
|
+
- `rails g components Users` will generate a set of the most used components.
|
|
10
|
+
|
|
11
|
+
### Support for custom base components
|
|
12
|
+
|
|
13
|
+
Generators will automatically detect your `BaseComponents` (see [Inheritance: best practice](./doc/guide/inheritance.md#best-practice)).
|