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,117 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: Form
|
|
5
|
+
|
|
6
|
+
This component holds a form and should only be instantiated by the `form_comp` call of a component that inherits from [`WithForm`](./with_form.md).
|
|
7
|
+
|
|
8
|
+
`Compony::Components::Form` is an abstract base class for any components presenting a regular form. This class comes with a lot of tooling for rendering forms and inputs, as well as validating parameters. When the component is rendered, the Gem SimpleForm is used to create the actual form: [https://github.com/heartcombo/simple_form](https://github.com/heartcombo/simple_form).
|
|
9
|
+
|
|
10
|
+
Parameters are structured like typical Rails forms. For instance, if you have a form for a `User` model and the attribute is `first_name`, the parameter looks like `user[first_name]=Tom`. In this case, we will call `user` the `schema_wrapper_key`. Parameters are validated using Schemacop: [https://github.com/sitrox/schemacop](https://github.com/sitrox/schemacop).
|
|
11
|
+
|
|
12
|
+
The following DSL calls are provided by the Form component:
|
|
13
|
+
|
|
14
|
+
- Required: `form_fields` takes a block that renders the inputs of your form. More on that below.
|
|
15
|
+
- Optional: `skip_autofocus` will prevent the first input to be auto-focussed when the user visits the form.
|
|
16
|
+
- Typically required: `schema_fields` takes the names of fields as a whitelist for strong parameters. Together with model fields, this will completely auto-generate a Schemacop schema suitable for validating this form. If your argument list gets too long, you can use multiple calls to `schema_field` instead to declare your fields one by one on separate lines.
|
|
17
|
+
- Optional: `schema_line` takes a single Schemacop line. Use this for custom whitelisting of an argument, e.g. if you have an input that does not have a corresponding model field.
|
|
18
|
+
- Optional: `schema` allows you to instead fully define your own custom Schemacop V3 schema manually. Note that this disables all of the above schema calls.
|
|
19
|
+
- Optional: `disable!` causes generated inputs to be disabled. Alternatively, `disabled: true` can be passed to the initializer to achieve the same result.
|
|
20
|
+
|
|
21
|
+
The `form_fields` block acts much like a content block and you will use Dyny there. Two additional methods are made available exclusively inside the block:
|
|
22
|
+
|
|
23
|
+
- `field` (not to be confused with the model mixin's static method) takes the name of a model field and auto-generates a suitable SimpleForm input as defined in the field's type.
|
|
24
|
+
- `f` gives you direct access to the `simple_form` instance. You can use it to write e.g. `f.input(...)`.
|
|
25
|
+
|
|
26
|
+
Here is a simple example for a form for a sample user:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class Components::Users::Form < Compony::Components::Form
|
|
30
|
+
setup do
|
|
31
|
+
form_fields do
|
|
32
|
+
concat field(:first_name)
|
|
33
|
+
concat field(:last_name)
|
|
34
|
+
concat field(:age)
|
|
35
|
+
concat field(:comment)
|
|
36
|
+
concat field(:role)
|
|
37
|
+
end
|
|
38
|
+
schema_fields :first_name, :last_name, :age, :comment, :role
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Note that the inputs and schema are two completely different concepts that are not auto-inferred from each other. You must make sure that they always correspond. If you forget to mention a field in `schema_fields`, posting the form will fail. Luckily, Schemacop's excellent error messaging will explain which parameter is prohibited.
|
|
44
|
+
|
|
45
|
+
Both calls respect Cancancan's `permitted_attributes` directive. This means that you can safely declare `field` and `schema_field` in a form that is shared among users with different kinds of permissions. If the current user is not allowed to access a field, the input will be omitted automatically. Further, the parameter validation will exclude that field, effectively disallowing that user from submitting that parameter.
|
|
46
|
+
|
|
47
|
+
## Handling password fields
|
|
48
|
+
|
|
49
|
+
When using Rails' `has_secure_password` method, which typically generates the attributes accessors `:password` and `password_confirmation`, do not declare these two as fields in your User model.
|
|
50
|
+
|
|
51
|
+
There are two main reasons for this:
|
|
52
|
+
|
|
53
|
+
- `password` and `password_confirmation` should never show up in lists and show pages, and as these kinds of components tend to iterate over all fields, it's best to have anything that should not show up there declared as a field in the first place.
|
|
54
|
+
- Rails' `authenticate_by` does not work when `password` is declared as a model attribute.
|
|
55
|
+
|
|
56
|
+
Instead of making these accessors Compony fields, ignore them in the User model and use the following methods in your Form:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Components::Users::Form < Compony::Components::Form
|
|
60
|
+
setup do
|
|
61
|
+
form_fields do
|
|
62
|
+
# ...
|
|
63
|
+
concat pw_field(:password)
|
|
64
|
+
concat pw_field(:password_confirmation)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ...
|
|
68
|
+
schema_pw_field :password
|
|
69
|
+
schema_pw_field :password_confirmation
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In contrast to the regular `field` and `schema_field` calls, their `pw_...` pendants do not check for per-field authorization. Instead, they check whether the current user can `:set_password` on the form's object. Therefore, your ability may look something like:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class Ability
|
|
78
|
+
# ...
|
|
79
|
+
can :manage, User # This allows full access to all users
|
|
80
|
+
cannot :manage, User, [:user_role] # This prohibits access to user_role, thus removing the input and making the parameter invalid if passed anyway
|
|
81
|
+
cannot :set_password, User # This prohibits setting and changing passwords of any user
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Dealing with multilingual fields
|
|
85
|
+
|
|
86
|
+
When using Gems such as `mobility`, Compony provides support for multilingual fields. For instance, assuming that a model has the attribute `label` translated in English and German, making `label` a virtual attribute reading either `label_en` and `label_de`, depending on the user's language, Compony automatically generates a multilingual field if the following is used:
|
|
87
|
+
|
|
88
|
+
In the model:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
class Foo < ApplicationRecord
|
|
92
|
+
# No need to write:
|
|
93
|
+
field :label, :string, virtual: true
|
|
94
|
+
I18n.available_locales.each do |locale|
|
|
95
|
+
field :"label_#{locale}", :string
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Instead, write this, which is equivalent:
|
|
99
|
+
field :label, :string, multilang: true
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
In the same mindset, you can simplify your form as follows to generate one input per language:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class Components::Foos::Form < Compony::Components::Form
|
|
107
|
+
setup do
|
|
108
|
+
form_fields do
|
|
109
|
+
# Since `field` only generates an input, you must loop over them and render them as you wish, e.g. with "concat":
|
|
110
|
+
field(:label, multilang: true).each { |inp| concat inp }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Don't forget to mark `schema_field` as multilingual as well, which will accept label_en and label_de:
|
|
114
|
+
schema_field :label, multilang: true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: Index
|
|
5
|
+
|
|
6
|
+
This stanalone component is resourceful, holds a collection of records and corresponds to Rail's `index` controller action. It is a wrapper for the [`List` component](./list.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: List
|
|
5
|
+
|
|
6
|
+
This resourceful component displays a table / list of records. It is meant to be nested within another component, typically [`Index`](./index.md) of the same family or [`Show`](./show.md) of another family. Compony's implementation of this component features:
|
|
7
|
+
|
|
8
|
+
- Inferrence of rows from model fields as well as custom rows
|
|
9
|
+
- Row actions for each displayed record
|
|
10
|
+
- Pagination
|
|
11
|
+
- Sorting: if the Ransack gem is installed and at least one sorting column has been specified, the component can automatically generate a select input for sorting as well as sorting links.
|
|
12
|
+
- Filtering / Searching: if the Ransack gem is installed and at least one filter has been specified, the component can automatically generate a filter / search form that works with Ransack.
|
|
13
|
+
|
|
14
|
+
This component serves as a base block for building powerful management interfaces. Consult the component's class to learn about the various methods you can use in `setup` in order to customize the behavior. You will likely want to implement your own custom base component based on this component and overwrite the `content` blocks that you would like to customize.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: New
|
|
5
|
+
|
|
6
|
+
This component is the Compony equivalent to a typical Rails controller's `new` and `create` actions.
|
|
7
|
+
|
|
8
|
+
`Compony::Components::New` is a resourceful standalone component based on [`WithForm`](./with_form.md) that listens to two verbs:
|
|
9
|
+
|
|
10
|
+
- GET will cause the New component to create a fresh instance of its `data_class` and render the form.
|
|
11
|
+
- POST (equivalent to a `create` action in a controller) will attempt to save the resource. If that fails, the form is rendered again with a HTTP 422 code ("unprocessable entity"). If the creation succeeds, a flash is shown and the user is redirected:
|
|
12
|
+
- if present: the data's Show component
|
|
13
|
+
- otherwise, if the resource is owned by another resource class: the owner's Show component
|
|
14
|
+
- otherwise, the data's Index component
|
|
15
|
+
|
|
16
|
+
Authorization checks for `create` even in GET. The reason is that it makes no sense to present an empty form to a user who cannot create a new record. This also causes any `compony_link` and `compony_button` to New components to be hidden to users lacking the permission.
|
|
17
|
+
|
|
18
|
+
This component follows the [resourceful lifecycle](/doc/guide/resourceful.md#complete-resourceful-lifecycle). `load_data` is set to create a new record and `store_data` attempts to create it. Parameters are validated in `assign_attributes` using a Schemacop schema that is generated from the form. The schema corresponds to Rail's typical strong parameter structure for forms. For example, a user's New component would look for a parameter `user` holding a hash of attributes (e.g. `user[first_name]=Tom`).
|
|
19
|
+
|
|
20
|
+
In case you overwrite `store_data`, make sure to set `@create_succeeded` to true if storing was successful (and to set it to false otherwise).
|
|
21
|
+
|
|
22
|
+
The following DSL calls are implemented to allow for convenient overrides of default logic:
|
|
23
|
+
|
|
24
|
+
- The block `on_create_failed_respond` is run if `@create_succeeded` is not true. By default, it logs all error messages with level `warn` and renders the component again through HTTP 422, causing Turbo to correctly display the page. Error messages are displayed by the form inputs.
|
|
25
|
+
- The block `on_created` is evaluated between successful record creation and responding. By default, it is not implemented and doing so is optional. This would be a suitable location for hooks that update state after a resource was created (like an `after_create` hook, but only executed if a record was created by this component). Do not redirect or render here, use the next blocks instead.
|
|
26
|
+
- The block given in `on_created_respond` is evaluated after successful creation and by default shows a flash, then redirects. Overwrite this block if you need to completely customize all logic that happens after creation. If this block is overwritten, `on_created_redirect_path` will not be called.
|
|
27
|
+
- `on_created_redirect_path` is evaluated as the second step of `on_created_respond` and redirects to the resource's Show, its owner's Show, or its own Index component as described above. Overwrite this block in order to redirect ot another component instead, while keeping the default flash provided by `on_created_respond`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: Show
|
|
5
|
+
|
|
6
|
+
This resourceful component corresponds to a typical Rails controller's `show` action and presents `@data` which is typically a model instance.
|
|
7
|
+
|
|
8
|
+
To use it, create a component of the style `Components::Users::Show` and inherit from `Compony::Components::Show`. By default, this will display all permitted fields along with their labels. Consult the component's class to learn about the methods you can use in `setup` in order to customize the behavior.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: WithForm
|
|
5
|
+
|
|
6
|
+
`Compony::Components::WithForm` is an abstract base class for components that render a form. Those components can further be resourceful, but don't have to be. If a component inherits from WithForm, it is always twinned with another component that will provide the form.
|
|
7
|
+
|
|
8
|
+
WithForm adds the following DSL methods:
|
|
9
|
+
|
|
10
|
+
- `form_comp_class` sets the class that will be instantiated by `form_comp`
|
|
11
|
+
- `form_comp` returns an instance of the Form component twinned with this component. If `form_comp_class` was never set, it will default to loading the component named `Form` in the same family as this component.
|
|
12
|
+
- `submit_verb` takes a symbol containing a verb, e.g. `:patch`. It defines this component's standalone verb that should be called when the twinned Form component is submitted.
|
|
13
|
+
- `submit_path` defaults to this component's standalone path. You can override this to submit the form to another component, should you need it.
|
|
14
|
+
|
|
15
|
+
The following other pre-built components implement `WithForm`:
|
|
16
|
+
|
|
17
|
+
- [`New`](new.md)
|
|
18
|
+
- [`Edit`](edit.md)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Pre-built components shipped with Compony
|
|
4
|
+
|
|
5
|
+
Compony comes with a few pre-built components that cover the most common cases that can be speed up development. They are meant to be inherited from and the easiest way to do this is by using the [provided Rails generators](./generators.md) `rails g component ...`.
|
|
6
|
+
|
|
7
|
+
The pre-built components can be found in the module `Compony::Components`. As you can see, there is no Show and no Index component. The reason is that these will depend a lot on your application's UI framework (e.g. Bootstrap) and thus the benefits a UI-agnostic base component can provide are minimal. Additionally, these components are very easy to implement, as is illustrated in the example at the beginning of this documentation.
|
|
8
|
+
|
|
9
|
+
In the following, the pre-built components currently shipped with Compony are presented:
|
|
10
|
+
|
|
11
|
+
- [Button](./pre_built_components/button.md): This component class gets instanciated whenever using `Compony.button` or `compony_button`.
|
|
12
|
+
- [Show](./pre_built_components/show.md): Compony's equivalent to Rail's `show` controller action
|
|
13
|
+
- [Index](./pre_built_components/index.md): Compony's equivalent to Rail's `index` controller action
|
|
14
|
+
- [List](./pre_built_components/list.md): Compony's equivalent to Rail's `_list` partial
|
|
15
|
+
- [Destroy](./pre_built_components/destroy.md): Compony's equivalent to Rail's `destroy` controller action
|
|
16
|
+
- [WithForm](./pre_built_components/with_form.md): A base class for components containing and submitting forms
|
|
17
|
+
- [Form](./pre_built_components/form.md): Compony's equivalent to Rail's `_form` partial
|
|
18
|
+
- [New](./pre_built_components/new.md): Compony's equivalent to Rail's `new` and `create` controller action
|
|
19
|
+
- [Edit](./pre_built_components/new.md): Compony's equivalent to Rail's `edit` and `update` controller action
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Resourceful components
|
|
4
|
+
|
|
5
|
+
So far, we have mainly seen how to present static content, without considering how loading and storing data is handled. Whenever a component is about data, be it a collection (e.g. index, list) or a single instance (e.g. new, show, edit, destroy, form), that component typically becomes resourceful. In order to implement a resourceful component, include the mixin `Compony::ComponentMixins::Resourceful`.
|
|
6
|
+
|
|
7
|
+
Resourceful components use an instance variable `@data` and provide a reader `data` for it. As a convention, always store the data the component "is about" in this variable.
|
|
8
|
+
|
|
9
|
+
Further, the class of which `data` should be can be specified and retrieved by using `data_class`. By default, `data_class` is inferred from the component's family name, i.e. `Components::User::Show` will automatically return `User` as `data_class`.
|
|
10
|
+
|
|
11
|
+
The mixin adds extra hooks that can be used to store logic that can be executed in the request context when the component is rendered standalone. The formulation of that sentence is important, as the decision which of these blocks are executed depends on the verb DSL. But before elaborating on that, let's first look at all the available hooks provided by the Resourceful mixin:
|
|
12
|
+
|
|
13
|
+
- `load_data`: Important. Specify a block that assigns something to `@data` here. The block will be run before authorization - thus, you can check `@data` for authorizing (e.g. `can?(:read, @data)`).
|
|
14
|
+
- `after_load_data`: Optional. If a block is specified, it is run immediately after `load_data`. This is useful if you inherit from a component that loads data but you need to alter something, e.g. refining a collection.
|
|
15
|
+
- `assign_attributes`: Important for components that alter data, e.g. New, Edit. Specify a block that assigns attributes to your model from `load_data`. The model is now dirty, which is important: **do not save your model here**, as authorization has not yet been performed. Also, **do not forget to validate params before assigning them to attributes**.
|
|
16
|
+
- `after_assign_attributes`: Optional. If a block is specified, it is run immediately after `assign_attributes`. Its usage is similar to that of `after_load_data`.
|
|
17
|
+
- (At this point, your `authorize` block is executed, throwing a `CanCan::AccessDenied` exception causing HTTP 403 not authorized if the block returns false.)
|
|
18
|
+
- `store_data`: Important for components that alter data, e.g. New, Edit. This is where you save your model stored in `@data` to the database.
|
|
19
|
+
|
|
20
|
+
Another important aspect of the Resourceful mixin is that it also **extends the Verb DSL** available in the component. The added calls are:
|
|
21
|
+
|
|
22
|
+
- `load_data`
|
|
23
|
+
- `assign_attributes`
|
|
24
|
+
- `store_data`
|
|
25
|
+
|
|
26
|
+
Unlike the calls above, which are global for the entire component, the ones in the Verb DSL are on a per-verb basis, same as the `authorize` call. If the same hook is both given as a global hook and in the Verb DSL, the Verb DSL hook overwrites the global one. The rule of thumb on where to place logic is:
|
|
27
|
+
|
|
28
|
+
- If multiple verbs use the same logic for a hook, place it in the global hook. For example, let us consider an Edit component: if GET is called on it, the model is loaded and parameters are assigned to it in order to fill the form's inputs. If PATCH is called, the exact same thing is done before attempting to save the model. In this case, you would implement both `load_data` and `assign_attributes` as global hooks.
|
|
29
|
+
- If a hook is specific to a single verb, place it in the verb config.
|
|
30
|
+
|
|
31
|
+
Let's build an example of a simplified Destroy component. In practice, you'd instead inherit from `Compony::Components::Destroy`. However, for the sake of demonstration, we will implement it from scratch:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class Components::Users::Destroy < Compony::Component
|
|
35
|
+
# Make the component resourceful
|
|
36
|
+
include Compony::ComponentMixins::Resourceful
|
|
37
|
+
|
|
38
|
+
setup do
|
|
39
|
+
# Let the path be of the form users/42/destroy
|
|
40
|
+
standalone path: 'users/:id/destroy' do
|
|
41
|
+
verb :get do
|
|
42
|
+
# In the case of a GET request, ask for confirmation, not deleting anything.
|
|
43
|
+
# Nevertheless, we should authorize :destroy, not :read.
|
|
44
|
+
# Reason: this way, buttons pointing to this component will not be shown
|
|
45
|
+
# to users which lack the permission to destroy @data.
|
|
46
|
+
authorize { can?(:destroy, @data) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
verb :delete do
|
|
50
|
+
# In the case of a DELETE request, the record will be destroyed.
|
|
51
|
+
authorize { can?(:destroy, @data) }
|
|
52
|
+
store_data { @data.destroy! }
|
|
53
|
+
# We overwrite the respond block because we want to redirect, not render
|
|
54
|
+
respond do
|
|
55
|
+
flash.notice = "#{@data.label} was deleted."
|
|
56
|
+
redirect_to Compony.path(:index, :users)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Resourceful components have a default `load_data` block that loads the model.
|
|
62
|
+
# Therefore, the default behavior is already set to:
|
|
63
|
+
# load_data { @data = User.find(params[:id]) }
|
|
64
|
+
|
|
65
|
+
label(:short) { |_| 'Delete' }
|
|
66
|
+
label(:long) { |data| "Delete #{data.label}" }
|
|
67
|
+
content do
|
|
68
|
+
h1 "Are you sure to delete #{@data.label}?"
|
|
69
|
+
div compony_button(:destroy, @data, label: 'Yes, delete', method: :delete)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Complete resourceful lifecycle
|
|
76
|
+
|
|
77
|
+
This graph documents a typical resourceful lifecycle according to which Compony's [pre-built components](./pre_built_components.md) are implemented.
|
|
78
|
+
|
|
79
|
+
- `load_data` creates or fetches the resource from the database.
|
|
80
|
+
- `after_load_data` can refine the resource, e.g. add scopes to a relation.
|
|
81
|
+
- `assign_attributes` takes the HTTP parameters, validates them and assigns them to the resource.
|
|
82
|
+
- `after_assign_attributes` can refine the assigned resource, e.g. provide defaults for blank attributes.
|
|
83
|
+
- `authorize` is called.
|
|
84
|
+
- `store_data` creates/updates/destroys the resource.
|
|
85
|
+
- `respond` typically shows a flash and redirects to another component.
|
|
86
|
+
|
|
87
|
+

|
|
88
|
+
|
|
89
|
+
## Nesting resourceful components
|
|
90
|
+
|
|
91
|
+
As mentioned earlier, hooks such as those provided by Resourceful typically run only when a component is accessed standalone. This means that in a nested setting, only the component running those hooks is the root component.
|
|
92
|
+
|
|
93
|
+
When nesting resourceful components, it is therefore best to load all necessary data in the root component. Make sure to include any relations used by sub-components in order to avoid "n+1" queries in the database.
|
|
94
|
+
|
|
95
|
+
`resourceful_sub_comp` is the resourceful sibling of `sub_comp` and both are used the same way. Under the hood, the resourceful call passes two extra parameters to the sub component: `data` and `data_class`.
|
|
96
|
+
|
|
97
|
+
The rule of thumb thus becomes:
|
|
98
|
+
|
|
99
|
+
- When a resourceful component instantiates a resourceful sub-component, use `resourceful_sub_comp` in the parent component.
|
|
100
|
+
- When a resourceful component instantiates a non-resourceful sub-component, use `sub_comp`.
|
|
101
|
+
- The situation where a non-resourceful component instantiates a resourceful component should not occur. Instead, make your parent component resourceful, even if it doesn't use the data itself. By housing a resourceful sub-comp, the parent component's nature inherently becomes resourceful and you should use the Resourceful mixin.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Compony root actions
|
|
4
|
+
|
|
5
|
+
The word "actions" is heavily overused, so here is a disambiguation:
|
|
6
|
+
|
|
7
|
+
- Rails controller actions: a method that is implemented in a Rails controller
|
|
8
|
+
- CanCanCan actions: the first method to CanCanCan's `can?` method
|
|
9
|
+
- Compony root actions: buttons that point to other components
|
|
10
|
+
|
|
11
|
+
At this point, Compony actions are a loose concept, which will likely be refined in the future. Currently, Compony actions are defined as buttons, rendered by the application layout, that point to other components. They provide context-sensitive buttons to your application.
|
|
12
|
+
|
|
13
|
+
## Defining and manipulating root actions
|
|
14
|
+
|
|
15
|
+
In addition to regular buttons that are rendered as part of the content blocks, components can expose root actions with the `actions` call. Root actions will only be rendered if the component they are defined in is currently the root component.
|
|
16
|
+
|
|
17
|
+
To have a component expose a root action, call the method `action` in a `setup` block and return a Compony button:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
setup do
|
|
21
|
+
action :edit do
|
|
22
|
+
Compony.button(:edit, @data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
action :destroy do
|
|
26
|
+
Compony.button(:destroy, @data)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The name of the action ("edit" and "destroy" in the example above) allows you to refer to that action in a component inheriting from this one:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Assuming that this component inherits from the example above
|
|
35
|
+
setup do
|
|
36
|
+
skip_action :destroy
|
|
37
|
+
|
|
38
|
+
action :overview, before: :edit do
|
|
39
|
+
Compony.button(:index, :users, label: 'Overview')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
In this example, two actions will be shown: overview and edit.
|
|
45
|
+
|
|
46
|
+
An action button can be disabled through the [feasibility framework](./feasibility.md). However, it can also instead be hidden completely by returning nil from within the action block:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
action :edit do
|
|
50
|
+
next if @data.locked?
|
|
51
|
+
Compony.button(:edit, @data)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The action in this example will be skipped entirely if `locked?` returns true.
|
|
56
|
+
|
|
57
|
+
## Displaying root actions
|
|
58
|
+
|
|
59
|
+
Root actions are not shown by default in Compony because layouting is up to you. In order to display the root component's actions, add the following view helper call to your layout:
|
|
60
|
+
|
|
61
|
+
```erb
|
|
62
|
+
<%# layouts/application.html.erb %>
|
|
63
|
+
...
|
|
64
|
+
<%= compony_actions %>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If there is currently no root component, or if the root component defines no actions, this does nothing. However, if there are root actions available, the Compony buttons returned by the root component will be rendered.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Standalone (routing to components)
|
|
4
|
+
|
|
5
|
+
As stated earlier, Compony can generate routes to your components. This is achieved by using the standalone DSL inside the setup block. The first step is calling the method `standalone` with a path. Inside this block, you will then specify which HTTP verbs (e.g. GET, PATCH etc.) the component should listen to. As soon as both are specified, Compony will generate an appropriate route.
|
|
6
|
+
|
|
7
|
+
Assume that you want to create a simple component `statics/welcome.rb` that displays a static welcome page. The component should be exposed under the route `'/welcome'` and respond to the GET method. Here is the complete code for making this happen:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# app/components/statics/welcome.rb
|
|
11
|
+
class Components::Statics::Welcome < Compony::Component
|
|
12
|
+
setup do
|
|
13
|
+
label(:all) { 'Welcome' }
|
|
14
|
+
|
|
15
|
+
standalone path: 'welcome' do
|
|
16
|
+
verb :get do
|
|
17
|
+
authorize { true }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
content do
|
|
22
|
+
h1 'Welcome to my dummy site!'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This is the minimal required code for standalone. For security, every verb config must provide an `authorize` block that specifies who has access to this standalone verb. The block is given the request context and is expected to return either true (access ok) or false (causing the request to fail with `Cancan::AccessDenied`).
|
|
29
|
+
|
|
30
|
+
Typically, you would use this block to check authorization using the CanCanCan gem, such as `authorize { can?(:read, :welcome) }`. However, since we skip authentication in this simple example, we pass `true` to allow all access.
|
|
31
|
+
|
|
32
|
+
The standalone DSL has more features than those presented in the minimal example above. Excluding [resourceful](./resourceful.md) features, the full list is:
|
|
33
|
+
|
|
34
|
+
- `standalone` can be called multiple times, for components that need to expose multiple paths, as described below. Inside each `standalone` call, you can call:
|
|
35
|
+
- `skip_authentication!` which disables authentication, in case you provided some. You need to implement `authorize` regardless.
|
|
36
|
+
- `skip_forgery_protection!` which disables CSRF protection for the controller action generated for this standalone configuration.
|
|
37
|
+
- `layout` which takes the file name of a Rails layout and defaults to `layouts/application`. Use this to have your Rails application look differently depending on the component.
|
|
38
|
+
- `verb` which takes an HTTP verb as a symbol, one of: `%i[get head post put delete connect options trace patch]`. `verb` can be called up to once per verb. Inside each `verb` call, you can call (in the non-resourceful case):
|
|
39
|
+
- `authorize` is mandatory and explained above.
|
|
40
|
+
- `respond` can be used to implement special behavior that in plain Rails would be placed in a controller action. The default, which calls `before_render` and the `content` blocks, is usually the right choice, so you will rarely implement `respond` on your own. See below how `respond` can be used to handle different formats or redirecting clients. **Caution:** `authorize` is evaluated in the default implementation of `respond`, so when you override that block, you must perform authorization yourself!
|
|
41
|
+
|
|
42
|
+
## Exposing multiple paths in the same component (calling standalone multiple times)
|
|
43
|
+
|
|
44
|
+
If your component loads data dynamically from a JavaScript front-end (e.g. implemented via Stimulus), you will find yourself in the situation where you need an extra route for a functionality that inherently belongs to the same component. Example use cases would be search fields that load data as the user types, maps that load tiles, dynamic photo galleries etc.
|
|
45
|
+
|
|
46
|
+
In this case, you can call `standalone` a second time and provide a name for your extra route:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
setup do
|
|
50
|
+
# Regular route for rendering the content
|
|
51
|
+
standalone path: 'map/viewer' do
|
|
52
|
+
verb :get do
|
|
53
|
+
authorize { true }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Extra route for loading tiles via AJAX
|
|
58
|
+
standalone :tiles, path: 'map/viewer/tiles' do
|
|
59
|
+
verb :get do
|
|
60
|
+
respond do # Again: overriding `respond` skips authorization! This is why we don't need to provide an `authorize` block here.
|
|
61
|
+
controller.render(json: MapTiler.load(params, current_ability)) # current_ability is provided by CanCanCan and made available by Compony.
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# More code for labelling, content etc.
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Please note that the idea here is to package things that belong together, not to provide different kinds of content in a single component. For displaying different pages, use multiple components and have each expose a single route.
|
|
71
|
+
|
|
72
|
+
## Naming of exposed routes
|
|
73
|
+
|
|
74
|
+
The routes to standalone components are named and you can point to them using Rails' `..._path` and `..._url` helpers. The naming scheme is: `[standalone]_[component]_[family]_comp`. Examples:
|
|
75
|
+
|
|
76
|
+
- Default standalone: `Components::Users::Index` exports `index_users_comp` and thus `index_users_comp_path` can be used.
|
|
77
|
+
- Named standalone: If `standalone :foo, path: ...` is used within `Components::Users::Index`, the exported name is `foo_index_users_comp`.
|
|
78
|
+
|
|
79
|
+
## Handling formats
|
|
80
|
+
|
|
81
|
+
Compony is capable of responding to formats like Rails does. This is useful to deliver PDFs, CSV files etc. to a user from within Compony. This can be achieved by specifying the `respond` block:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
setup do
|
|
85
|
+
standalone path: 'generate/report' do
|
|
86
|
+
verb :get do
|
|
87
|
+
# Respond with a file when generate/report.pdf is GETed:
|
|
88
|
+
respond :pdf do
|
|
89
|
+
file, filename = PdfGenerator.generate(params, current_ability)
|
|
90
|
+
send_data(file, filename:, type: 'application/pdf')
|
|
91
|
+
end
|
|
92
|
+
# If someone visits generate/report, issue a 404:
|
|
93
|
+
respond do
|
|
94
|
+
fail ActionController::RoutingError, 'Unsupported format - please make sure your URL ends with `.pdf`.'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Redirect in `respond` or in `before_render`?
|
|
102
|
+
|
|
103
|
+
Rails controller redirects can be issued both in a verb DSL's `respond` block and in `before_render`. The rule of thumb that tells you which way to go is:
|
|
104
|
+
|
|
105
|
+
- If you want to redirect depending on the HTTP verb, use `respond`.
|
|
106
|
+
- If you want to redirect depending on params, state, time etc. **independently of the HTTP verb**, use `before_render`, as this is more convenient than writing a standalone -> verb -> respond tree.
|
|
107
|
+
|
|
108
|
+
## Path constraints
|
|
109
|
+
|
|
110
|
+
When calling `standalone`, you may specify the keyword `constraints` that will be passed to the route. For example:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# In your component
|
|
114
|
+
standelone path: '/:lang', constraints: { lang: /([a-z]{2})?/i }
|
|
115
|
+
|
|
116
|
+
# This will automatically lead to a route of this form:
|
|
117
|
+
get ':lang', constraints: { lang: /([a-z]{2})?/i }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Passing scopes
|
|
121
|
+
|
|
122
|
+
When calling `standalone`, you may specify the keyword `scope` to wrap the component's Rails route into a route scope. Additionally, you may specify a hash `scope_args`, which will be passed as keyword arguments to the `scope` call in the route:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# In your component
|
|
126
|
+
standalone path: '/welcome', scope: '(:lang)', scope_args: { lang: /([a-z]{2})?/i } do
|
|
127
|
+
verb :get do
|
|
128
|
+
# ....
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# This will automatically lead to a route of this form:
|
|
133
|
+
scope '(:lang)', lang: /([a-z]{2})?/i do
|
|
134
|
+
get 'welcome', to: 'compony#your_component'
|
|
135
|
+
end
|
|
136
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Unleashing virtual models through Compony's `ActiveType` integration
|
|
4
|
+
|
|
5
|
+
Compony explicitely supports using virtual models using the `active_type` gem for its resourceful components. However, when doing so, your model should inherit from `Compony::VirtualModel` rather than from `ActiveType::Object`.
|
|
6
|
+
|
|
7
|
+
Combining Compony with virtual models enables programming patterns that are extremely powerful for use cases such as non-persistent wizards, filter forms, stateless launch forms for generators and much more.
|
|
8
|
+
|
|
9
|
+
For instance, let us consider an application that that generates configurable reports. In this example, the user workflow is to click on a "Generate report" button, fill in a configuration form (what kind of report the generator should produce, what timespan should be considered, which criteria to filter and group by etc.), and, by submitting the form, queueing a job that will perform the report in the background. To realize this, a simple approach would be the following:
|
|
10
|
+
|
|
11
|
+
- Add the `active_type` gem to your `Gemfile` and run `bundle install`.
|
|
12
|
+
- Implement `Components::Reports::Request` which inherits from `Compony::Components::New`.
|
|
13
|
+
- In the top section of the component class, define a class `VirtualModel < Compony::VirtualModel` (within the namespace of `Components::Reports::Request`). Use `active_type`'s `attribute` method to add virtual columns for any kind of information your generator will need. Call Compony's `field` method as you would with a normal Rails model and implement any suitable Rails validations. You may even use Rails associations such as `belongs_to` to a real (database-backed) Rails model by implementing an attribute with the following three lines:
|
|
14
|
+
- `attribute :user_id, :bigint` (provided by `active_type`)
|
|
15
|
+
- `belongs_to :user` (provided by Rails)
|
|
16
|
+
- `field :user, :association` (provided by Compony)
|
|
17
|
+
- Implement `Components::Reports::RequestForm < Compony::Components::Form` and implement your configuration form there.
|
|
18
|
+
- In your `Components::Reports::Request`:
|
|
19
|
+
- Call `standalone path: '/reports/request'` to avoid path conflicts with other components in the `Components::Reports` namespace inheriting from `New`.
|
|
20
|
+
- Call `data_class VirtualModel` to tell the component to use the class you just created within the component's namespace.
|
|
21
|
+
- Call `form_comp_class Components::Reports::RequestForm` to inform the component to use the custom named form.
|
|
22
|
+
- Call something like `label(:all) { ... }` to set a label for your component.
|
|
23
|
+
- Implement `on_created_respond` to create the report job and redirect to a suitable location.
|
|
24
|
+
|
|
25
|
+
Why this works: As your `Components::Reports::Request` inherits from Compony's `New` component, Compony will believe that the user is about to create a new resource, providing the Compony equivalents for the Rails controller actions `new` and `create`. When the user submits the form, Compony will run validations, re-render the form with error messages if they fail, or otherwise call `@data.save` which does nothing since the model is only virtual. This is why you take back control by overriding the `on_created_respond` block, which is called only if all validations have passed.
|
|
26
|
+
|
|
27
|
+
Note: it is even possible to combine this pattern with Rails' `accepts_nested_attributes_for` and `simple_form`'s `f.simple_fields_for` call, where the nested object is a real database-backed model. Even though the component's resource is purely virtual, Rails will create or update the nested model when Compony calls `save` on the parent resource. This allows for very fast implementation of business logic creating multiple objects from a single form post by wrapping the resources in a virtual model.
|
|
28
|
+
|
|
29
|
+
If you intend to use this technique in combination with `ActiveStorage`, you must also override the `store_data` block to just validate the model instead of saving it, as the hook creating the attachment is bound to fail (the virtual model does not exist in the database and thus cannot be referenced from `ActiveStorage::Attachment`). For the same reason, you cannot call `blob.download`, but must find the file's tempfile in the request parameters in order to process the file attached by the user.
|