compony 0.7.0 → 0.8.0
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 +39 -0
- data/Gemfile.lock +1 -1
- data/README.md +49 -1591
- 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 +193 -457
- 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 +3 -3
- data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone.html +187 -1
- data/doc/Compony/ComponentMixins/Default.html +1 -1
- data/doc/Compony/ComponentMixins/Resourceful.html +2 -2
- data/doc/Compony/ComponentMixins.html +1 -1
- data/doc/Compony/Components/Button.html +2 -2
- data/doc/Compony/Components/Buttons/CssButton.html +282 -0
- data/doc/Compony/Components/Buttons/Link.html +252 -0
- data/doc/Compony/Components/Buttons.html +126 -0
- data/doc/Compony/Components/Destroy.html +11 -11
- data/doc/Compony/Components/Edit.html +14 -14
- data/doc/Compony/Components/Form.html +100 -100
- data/doc/Compony/Components/Index.html +2 -2
- data/doc/Compony/Components/List.html +3 -3
- data/doc/Compony/Components/New.html +2 -2
- data/doc/Compony/Components/Show.html +24 -24
- data/doc/Compony/Components/WithForm.html +3 -3
- data/doc/Compony/Components.html +5 -3
- data/doc/Compony/ControllerMixin.html +2 -2
- data/doc/Compony/Engine.html +1 -1
- data/doc/Compony/ExposedIntentsDsl.html +403 -0
- data/doc/Compony/Intent.html +1503 -0
- data/doc/Compony/MethodAccessibleHash.html +1 -1
- data/doc/Compony/ModelFields/Anchormodel.html +1 -1
- data/doc/Compony/ModelFields/Association.html +2 -2
- 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 +177 -14
- data/doc/Compony/Version.html +1 -1
- data/doc/Compony/ViewHelpers.html +15 -272
- data/doc/Compony/VirtualModel.html +152 -0
- data/doc/Compony.html +303 -837
- data/doc/ComponyController.html +1 -1
- data/doc/_index.html +37 -2
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +49 -1635
- data/doc/guide/basic_component.md +263 -0
- data/doc/guide/example.md +228 -0
- data/doc/guide/feasibility.md +40 -0
- data/doc/guide/generators.md +15 -0
- data/doc/guide/inheritance.md +64 -0
- data/doc/guide/installation.md +47 -0
- data/doc/guide/intents.md +167 -0
- data/doc/guide/internal_datastructures.md +47 -0
- data/doc/guide/model_fields.md +64 -0
- data/doc/guide/nesting.md +134 -0
- data/doc/guide/ownership.md +23 -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 +20 -0
- data/doc/guide/resourceful.md +103 -0
- data/doc/guide/standalone.md +144 -0
- data/doc/guide/virtual_models.md +31 -0
- data/doc/index.html +49 -1635
- data/doc/method_list.html +273 -161
- data/doc/top-level-namespace.html +1 -1
- data/lib/compony/component.rb +19 -48
- data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +2 -2
- data/lib/compony/component_mixins/default/standalone.rb +16 -0
- data/lib/compony/component_mixins/resourceful.rb +1 -1
- data/lib/compony/components/buttons/css_button.rb +32 -0
- data/lib/compony/components/buttons/link.rb +31 -0
- data/lib/compony/components/destroy.rb +9 -8
- data/lib/compony/components/edit.rb +5 -4
- data/lib/compony/components/form.rb +7 -1
- data/lib/compony/components/index.rb +2 -2
- data/lib/compony/components/list.rb +4 -4
- data/lib/compony/components/new.rb +1 -1
- data/lib/compony/components/show.rb +8 -11
- data/lib/compony/components/with_form.rb +1 -1
- data/lib/compony/exposed_intents_dsl.rb +29 -0
- data/lib/compony/intent.rb +145 -0
- data/lib/compony/model_fields/association.rb +1 -1
- data/lib/compony/request_context.rb +21 -0
- data/lib/compony/view_helpers.rb +5 -48
- data/lib/compony/virtual_model.rb +10 -0
- data/lib/compony.rb +67 -149
- metadata +36 -3
- data/lib/compony/components/button.rb +0 -61
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
|
|
3
|
+
# Intents
|
|
4
|
+
|
|
5
|
+
An intent is a gateway to a component, along with relevant context. It encapsulates tools used to generate paths, checking [feasibility](/doc/guide/feasibility.md) and rendering links and buttons pointing other components within your application.
|
|
6
|
+
|
|
7
|
+
To instanciate an intent, use either the raw `Compony.intent` method or one of the [helpers](#helpers). All methods that use intents under the hood have a similar interface, especially regarding the first two positional arguments. The most commonly used forms are:
|
|
8
|
+
|
|
9
|
+
- `Compony.intent(:index, :users)`: point to a component by its comp and family name
|
|
10
|
+
- `Compony.intent(:show, User.first)`: pass the intent a single model and let it figure out the family name from `model_name`
|
|
11
|
+
- `Compony.intent(:list, current_user.quotes)`: pass the intent an active record collection and let it figure out the family name from `model_name`
|
|
12
|
+
- `Compony.intent(Components::Users::Index)`: point to a component by giving its class as a single argument
|
|
13
|
+
|
|
14
|
+
The returned intent can then be used to:
|
|
15
|
+
|
|
16
|
+
- Retrieve the target component class using `intent.comp_class`
|
|
17
|
+
- Build an instance of the target component using `intent.comp`
|
|
18
|
+
- If a model was given when building the intent, such as in the second form above, the comp instance will contain it as `@data`.
|
|
19
|
+
- Retrieve the `path` to the target component (only works for standalone components; will automatically set ID parameter if a model was given)
|
|
20
|
+
- Retrieve the `name` that can be used to store the intent in a hash or similar
|
|
21
|
+
- Retrieve the `label` that can be used to refer to the component (if a model was given, passes it to the target component's label block)
|
|
22
|
+
- Check for [feasibility](/doc/guide/feasibility.md) using `feasible?`
|
|
23
|
+
- Render a [button](#buttons-and-styles) to the target component which automatically includes all of the above along with the suitable behavior using `render` and passing a controller
|
|
24
|
+
|
|
25
|
+
An intent's behavior can be customized by passing some of the following keyword arguments when building it:
|
|
26
|
+
|
|
27
|
+
- `standalone_name` allows you to point to another endpoint within your target component, which is especially useful for generating paths. Keep in mind that within each standalone name, multiple HTTP methods can exist. This argument defaults to `nil`, which is the main endpoint created by `standalone`.
|
|
28
|
+
- `method` defines the HTTP verb within the standalone configuration that should be addressed. Defaults to `:get`, but can be overriden to be `:patch`, `:put`, `:post` or `:delete`. When generating a button, the `turbo_method` will be automatically be derived from this argument.
|
|
29
|
+
- `name` overrides the auto-generated name of the intent, affecting the result of the reader of the same name.
|
|
30
|
+
- `label` accepts two forms:
|
|
31
|
+
- When passing a String, it will be returned as-is when returning the label, also affecting buttons generated by this intent.
|
|
32
|
+
- When passing a Hash, any included key-value pair will be used as keyword arguments to the `label` reader method.
|
|
33
|
+
- `path` allows altering generated paths and accepts either a String or a Hash just like `label`.
|
|
34
|
+
- `data` and `data_class` will be given to the target component if/when it gets instanciated. This is used to point to [resourceful](/doc/guide/resourceful.md) components. If a model was passed as the second positional argument (instead of a family name), it will become `data` and this argument can be omitted. If `data` responds to `model_name`, it will be considered a model-like class.
|
|
35
|
+
- `feasibility_target` and `feasibility_action` can be given to alter the behavior of the `feasible?` reader.
|
|
36
|
+
- Any further arguments are passed to the initializer of the button if/when the intent gets rendered.
|
|
37
|
+
|
|
38
|
+
## Helpers
|
|
39
|
+
|
|
40
|
+
In practice, you will rarely call `Compony.intent` directly, and likely never `Compony::Intent.new`. Instead, you will be interacting with one of the following helpers that will instanciate an intent under the hood and thus all accept similar arguments as those described above:
|
|
41
|
+
|
|
42
|
+
### `Compony.path`
|
|
43
|
+
|
|
44
|
+
This helper is useful when generating paths without rendering any HTML, such as when redirecting. Internally, this builds an intent which will in turn use the target component's `path` block to generate a Rails path (String) pointing to the correct location. Any keyword arguments are passed to the intent's `path` method, allowing you to override the model (to refer to [resourceful](/doc/guide/resourceful.md) target components), as well as specifying a `standalone_name` or pass extra arguments to the target component's `path` block.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
redirect_to Compony.path(:index, :users) # Redirects to /users
|
|
50
|
+
redirect_to Compony.path(:show, @data.author) # Redirects to something like /authors/42
|
|
51
|
+
redirect_to Compony.path(:thank_you, @data) # Redirects to something like /feedbacks/42/thank_you
|
|
52
|
+
redirect_to Compony.path(:step_2, :registrations, accept_terms: :yes) # Redirects to something like /registrations/step_2?accept_terms=yes
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `render_intent`
|
|
56
|
+
|
|
57
|
+
This is the preferred way of quickly rendering [links or buttons to components](#buttons-and-styles) from other parts of your application. The method is implemented twice:
|
|
58
|
+
|
|
59
|
+
- When called from within a `content` block of a component, the method implemented in the `RequestContext` is used, which automatically detects the component from which it is called and passes it as `parent_comp` to the rendered button.
|
|
60
|
+
- When called from a regular Rails view, the Rails helper method is used, which instanciates a button without passing a `parent_comp`.
|
|
61
|
+
|
|
62
|
+
Use the `button` argument to customize the generated button. Example:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
setup do
|
|
66
|
+
content do
|
|
67
|
+
div render_intent(:show, User.first, button: { style: :link, label: { format: :short } })`
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
In the example above, there is a lot more going on than it seems. Due to the [specified style](#buttons-and-styles), a plain HTML link will be generated, pointing to the component `Components::Users::Show` and instanciating it with the first user as `@data`. Assuming that component inherits from `Compony::Components::Show` and did not override label or path, the link will be labelled "Show" (which is the default short format label generated by Compony's default [pre-built Show component](/doc/guide/pre_built_components/show.md)), and the `href` will be `/users/:id` where ID will automatically be `User.first`'s ID (e.g. `/users/1`). However, if `:show` was [prevented](/doc/guide/feasibility.md), the link will be strikethrough, non-clickable, greyed out and have a title explaining why it can't be clicked. Further, if the current user does not have [authorization](/doc/guide/standalone.md) to display the target user, the link will not show up at all, and no HTML will be generated within the `div`.
|
|
73
|
+
|
|
74
|
+
### `render_sub_comp`
|
|
75
|
+
|
|
76
|
+
This is used within a component's `content` block to instanciate another component and [nest it within](/doc/guide/nesting.md). Internally, the current component's `sub_comp` method is used and all arguments are passed to that.
|
|
77
|
+
|
|
78
|
+
For example, let us consider you want to build your user management's Show component to include the list of quotes belonging to the displayed user. Assuming you have already built `Components::Quotes::List` for quotes' Index component, this helper allows you to simply write:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class Components::Users::Show < Compony::Components::Show
|
|
82
|
+
setup do
|
|
83
|
+
# ...
|
|
84
|
+
content :quotes do
|
|
85
|
+
concat render_sub_comp(:list, @data.quotes)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This implicitely builds an intent which auto-detects the family name `:quotes` from `@data.quotes`, which is an active record collection and thus implements `model_name`. The List component is then instanciated with its `@data` being that very collection and the users's Show component as `parent_comp`, resulting in the proper nesting and display of the desired resources.
|
|
92
|
+
|
|
93
|
+
### `Compony.comp_class_for`
|
|
94
|
+
|
|
95
|
+
This helper is useful for checking whether a component is implemented. For instance, when implementing [abstract components to inherit from later](/doc/guide/inheritance.md), you can check for `if Compony.comp_class_for(:destroy, family_name)` to only provide some functionality of a Destroy component exists for the current family.
|
|
96
|
+
|
|
97
|
+
This method also has its sibling `Compony.comp_class_for!`, which will fail if no such component could be found. It is however mostly used internally.
|
|
98
|
+
|
|
99
|
+
## Buttons and styles
|
|
100
|
+
|
|
101
|
+
Button components are used as presenters for intents, hyperlinks to other components or submit buttons. They are a central way to define how buttons all over the application should look like. Their interface is adapted to intents, creating a standardized "slim waist" that greatly simplifies linking between components. Note that the term "button" refers to how they look and not how they are actually implemented - actual HTML buttons have several disadvantages (e.g. requiring drop-in forms and not responding to Ctrl+Click or middle-click when the user would prefer a new tab).
|
|
102
|
+
|
|
103
|
+
Compony comes with two button styles:
|
|
104
|
+
|
|
105
|
+
- `:css_button` is the default button style and creates a div that looks similar to a HTML button rendered in Firefox. If the class `disabled` is given, it is greyed out.
|
|
106
|
+
- `:link` is mostly just a regular `<a>` tag, but will appear greyed out and strikethrough if the class `disabled` is given.
|
|
107
|
+
|
|
108
|
+
All button styles support the following keyword arguments to their initializer:
|
|
109
|
+
|
|
110
|
+
- `label` is a String which will be displayed as the text of the link or button.
|
|
111
|
+
- `href` is the url/path that the button points to. If `nil`, will change to `javascript:void(0)`.
|
|
112
|
+
- `method` takes a HTTP verb will generate a suitable `turbo_method` data attribute.
|
|
113
|
+
- `class` will mostly let be as-is, but checked for the `disabled` class - if given, the style will be ovewritten.
|
|
114
|
+
|
|
115
|
+
Note that since buttons are full components, they can be [nested](/doc/guide/nesting.md) into another component by providing `parent_comp`. If called from within a component's `content` block, the helper `render_intent` does this automatically.
|
|
116
|
+
|
|
117
|
+
As implicitely mentioned above, Compony buttons are referenced to by a name called a style (`:css_button` actually points to `Compony::Components::Buttons::CssButton`). When rendering an intent, the style can be passed as an argument: `render_intent(:show, User.first, button: { style: :link })` and the intent will automatically instanciate the desired component class.
|
|
118
|
+
|
|
119
|
+
Note: it is possible to use a button component to submit a form. In order to achieve this, you must implement a hidden submit button (for handling keyboard Enter and Return), as well as pass `onclick: "this.closest('form').requestSubmit(); return false;"` as an argument. See the pre-built Form component's implementation for an example.
|
|
120
|
+
|
|
121
|
+
### Adding your own styles
|
|
122
|
+
|
|
123
|
+
In your application, you will likely want to implement your own button styles. Create a component (e.g. `Components::Commons::MyButton`) and inherit from `Compony::Components::Buttons::Link`. Override the method `prepare_opts!` and don't forget to call `super` first. Then, go through any `@comp_args` that might be of interest to you and mutate `@comp_args[:style]` and/or `@comp_args[:class]` to suit your needs. Make sure to handle the class `disabled`, as intents will set them if the intent is not feasible. Note that if a user is lacking authorization to perform an intent, the intent will not even instanciate the button.
|
|
124
|
+
|
|
125
|
+
Once your button class is ready, register it in `config/initializers/compony.rb` with: `Compony.register_button_style :my_button, '::Components::Commons::MyButton'`. You can also change the default button style there using: `Compony.default_button_style = :my_button`.
|
|
126
|
+
|
|
127
|
+
If you have multiple kinds of buttons (e.g. dropdown items, pill-style buttons, compact forms etc.), you should create a separate style and button component class for every kind. This will make it easy to refer to them by supplying something like `button: { style: :dropdown_item }` in `render_intent`.
|
|
128
|
+
|
|
129
|
+
## Exposed intents
|
|
130
|
+
|
|
131
|
+
Components can expose a set of intents to be displayed elsewhere. Those can either be rendered by the parent comp, or by the application layout itself in case the component exposing them is currently `root_comp` (see the chapter about [standalone](/doc/guide/standalone.md)). This is useful if you have something like an actions toolbar that changes depending on the currently contained component.
|
|
132
|
+
|
|
133
|
+
To expose an intent, proceed as shown in the following example:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# ...
|
|
137
|
+
class Components::Quotes::Show < Compony::Components::Show
|
|
138
|
+
setup do
|
|
139
|
+
exposed_intents do
|
|
140
|
+
add :index, family_name, label: 'Show all', name: :index
|
|
141
|
+
remove :destroy
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
In this example, the shown component exposes an intent pointing to the Index component of it's own family (`:users`). The `:name` argument causes the new intent to be just named `:index` rather than `:index_users`. This is for the sake of the example and would allow a component inheriting from this component to use `exposed_intents { remove :index }` rather than `exposed_intents { remove :index_users }`.
|
|
148
|
+
|
|
149
|
+
Similarly, this component removes the exposed intent `:destroy` which it whould otherwise inherit from `Comopony::Components::Show`. Every call to `exposed_intents` overrides properties set in previous calls by the same component, primarly useful for [reusing components by inheritance](/doc/guide/inheritance.md).
|
|
150
|
+
|
|
151
|
+
In order to replace an existing intent defined by a previous call to `exposed_intents` (e.g. in a parent class), simply call `add` again and make sure the intent's name matches that of the one to override. `add` also accepts the `before:` keyword, allowing you to reorder intents or insert a new one into a specific place in the intent list.
|
|
152
|
+
|
|
153
|
+
Note that the `add` method has full intent argument support and thus also accepts parameters related to the button (e.g. `style`), path generation, feasibility etc.
|
|
154
|
+
|
|
155
|
+
### Rendering exposed intents
|
|
156
|
+
|
|
157
|
+
You can render exposed intents in the parent component or in the application layout. To do so, call `component.exposed_intents`, loop across them and call `.render(controller)` on each (perhaps inside a `div` tag or whatever suits your needs).
|
|
158
|
+
|
|
159
|
+
Example in `layouts/application.html.erb`
|
|
160
|
+
|
|
161
|
+
```erb
|
|
162
|
+
<% Compony.root_comp&.exposed_intents&.each do |intent| %>
|
|
163
|
+
<div class="root-intent"><%= intent.render(controller) %>
|
|
164
|
+
<% end %>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
[Guide index](/README.md#guide--documentation)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
|
|
3
|
+
# Internal datastructures
|
|
4
|
+
|
|
5
|
+
Compony has a few internal data structures that are worth mentioning. Especially when building your own UI framework on top of Compony, these might come in handy.
|
|
6
|
+
|
|
7
|
+
## MethodAccessibleHash
|
|
8
|
+
|
|
9
|
+
This is a simpler and safer version of [OpenStruct](https://github.com/ruby/ostruct), allowing you to access a hash's keys via method accessors.
|
|
10
|
+
|
|
11
|
+
Usage example:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
default_options = { foo: :bar }
|
|
15
|
+
options = Compony::MethodAccessibleHash.new(default_options)
|
|
16
|
+
options[:color] = :green
|
|
17
|
+
options.foo # => :bar
|
|
18
|
+
options.color # => green
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This part of Compony is also made available under the MIT license at: [https://gist.github.com/kalsan/87826048ea0ade92ab1be93c0919b405](https://gist.github.com/kalsan/87826048ea0ade92ab1be93c0919b405).
|
|
22
|
+
|
|
23
|
+
## RequestContext
|
|
24
|
+
|
|
25
|
+
The content blocks, as well as Form's `form_fields` block all run within a `Compony::RequestContext`, which encapsulates useful methods for accessing data within a request. RequestContext is a Dslblend object and contains all the magic described in [https://github.com/kalsan/dslblend](https://github.com/kalsan/dslblend).
|
|
26
|
+
|
|
27
|
+
The main provider (refer to the Dslblend documentation to find out what that means) is set to the component. Additional providers are controller's helpers, the controller itself, as well as custom additional providers that can be fed to RequestContext in the initializer.
|
|
28
|
+
|
|
29
|
+
To instantiate a RequestContext, the following arguments must be given:
|
|
30
|
+
|
|
31
|
+
- The first argument must be the component instantiating the RequestContext.
|
|
32
|
+
- The second argument must be the controller holding the current HTTP request.
|
|
33
|
+
- Optional: any further arguments will be given to Dslblend as additional providers.
|
|
34
|
+
- Optional: the keyword argument `helpers` can be given to overwrite the `helpers` context. If not given, the helpers will be extracted from the controller.
|
|
35
|
+
- Optional: the keyword argument `locals` can be given a hash of local assigns to be made available within the context.
|
|
36
|
+
|
|
37
|
+
RequestContext further provides the following methods on its own:
|
|
38
|
+
|
|
39
|
+
- `controller` returns the controller.
|
|
40
|
+
- `helpers` returns the helpers (either from the initializer or the controller).
|
|
41
|
+
- `local_assigns` returns the locals that can be given to the RequestContext on instantiation through the `locals` keyword argument.
|
|
42
|
+
- `evaluate_with_backfire` is `evaluate` with enabled backfiring.
|
|
43
|
+
- `component` returns the component the RequestContext was instantiated with.
|
|
44
|
+
- `request_context` returns self. This is for disambiguation purposes.
|
|
45
|
+
- Any call to an unknown method will first be evaluated as a potential hit in `locals`. Only if no matching local is found, Dslblend takes over.
|
|
46
|
+
|
|
47
|
+
[Guide index](/README.md#guide--documentation)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
|
|
3
|
+
# Model fields
|
|
4
|
+
|
|
5
|
+
Compony fields are your models' attributes that you wish to expose in your application's UI. They are a central place to store important information about those attributes, accessible from everywhere and without the need for a database connection.
|
|
6
|
+
|
|
7
|
+
Every Compony field must define at least a name and type. Compony types and ActiveRecord types are similar but not equivalent. While ActiveRecord uses types for storing data in the DB, Compony fields use them for presenting it. For instance, the Compony "string" type covers any kind of string, including ActiveRecord's "string", "text" etc. Similarly, Compony has no "numeric" type - use "integer" or "decimal" instead, depending on whether or not you want to show decimals or not. There are additional field types like "color", "url" etc. You can find a complete list of all Compony field types in the module `Compony::ModelFields`.
|
|
8
|
+
|
|
9
|
+
Compony fields support Postgres arrays (non-nested).
|
|
10
|
+
|
|
11
|
+
A particularly interesting model field is `Association` which handles `belongs_to`, `has_many` and `has_one` associations, automatically resolving the association's nature and providing links to the appropriate component.
|
|
12
|
+
|
|
13
|
+
Every Compony field can further take an arbitrary amount of additional named arguments. Those can be retrieved by calling `YourRailsModel.fields[:field_name].extra_attrs`.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Here is an example call to fields for a User model:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# app/models/user.rb
|
|
20
|
+
class User < ApplicationRecord
|
|
21
|
+
field :first_name, :string
|
|
22
|
+
field :last_name, :string
|
|
23
|
+
field :user_role, :anchormodel
|
|
24
|
+
field :website, :url
|
|
25
|
+
field :created_at, :datetime
|
|
26
|
+
field :updated_at, :datetime
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
All fields declared this way are automatically exported as Rails Model attributes. Note that this also means that you should never declare `password` and `password_confirmation` as a Compony field, as you will get the ArgumentError "One or more password arguments are required" otherwise. Read more about handling password fields in the section about `Compony::Components::Form`.
|
|
31
|
+
|
|
32
|
+
Compony fields provide the following features:
|
|
33
|
+
|
|
34
|
+
- a label that lets you generate a name for the column: `User.fields[:first_name].label`
|
|
35
|
+
- `value_for`: given a model instance, formats the data (e.g. a field of type "url" will produce a link).
|
|
36
|
+
- Features for forms:
|
|
37
|
+
- `simpleform_input` auto-generates in input for a simple form (from the `simple_form` gem).
|
|
38
|
+
- `simpleform_input_hidden` auto-generates a hidden input.
|
|
39
|
+
- `schema_line` auto-generates a DSL call for Schemacop v3 (from the `schemacop` gem), which is useful for parameter validation.
|
|
40
|
+
|
|
41
|
+
You can then use these fields in other components, for instance a list as described in the example at the top of this guide:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
User.fields.values.each do |field|
|
|
45
|
+
span do
|
|
46
|
+
concat "#{field.label}: #{field.value_for(user)} " # Display the field's label and apply it to value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Implementing your own fields
|
|
52
|
+
|
|
53
|
+
You can implement your own model fields. Make sure they are all within the same namespace and inherit at least from `Compony::ModelFields::Base`. To enable them, write an initializer that overwrites the array `Compony.model_field_namespaces`. Namespaces listed in the array are prioritized from first to last. If a field (e.g. `String`) exists in multiple declared namespaces, the first will be used. This allows you to overwrite Compony fields.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# config/initializers/compony.rb
|
|
59
|
+
Compony.model_field_namespaces = ['MyCustomModelFields', 'Compony::ModelFields']
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You can then implement `MyCustomModelFields::Animal`, `MyCustomModelFields::String` etc. You can then use `field :fav_animal, :animal` in your model.
|
|
63
|
+
|
|
64
|
+
[Guide index](/README.md#guide--documentation)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
|
|
3
|
+
# Nesting
|
|
4
|
+
|
|
5
|
+
Components can be arbitrarily nested. This means that any component exposing content can instantiate an arbitrary number of sub-components that will be rendered as part of its own content. This results in a component tree. Sub-components are aware of the nesting and even of their position within the parent. The topmost component is called the **root component** and it's the only component that must be standalone. If you instead render the topmost component from a custom view, there is conceptually no root component, but Compony has no way to detect this special case.
|
|
6
|
+
|
|
7
|
+
Nesting is orthogonal to inheritance, they are two entirely different concepts. For disambiguating "parent component", we will make an effort to apply that term to nesting only, while writing "parent component class" if inheritance is meant.
|
|
8
|
+
|
|
9
|
+
Sub-components are particularly useful for DRYing up your code, e.g. when a visual element is used in multiple places of your application or even multiple times on the same page.
|
|
10
|
+
|
|
11
|
+
Nesting occurs when a component is being rendered. It is perfectly feasible to use an otherwise standalone component as a sub-component. Doing so simply plugs it into the content of another component and any arguments can be given to its constructor.
|
|
12
|
+
|
|
13
|
+
Note that only the root component runs authentication and authorization. Thus, be careful which components you nest.
|
|
14
|
+
|
|
15
|
+
To create a sub-component, use `render_sub_comp` in a component's content block. Any keyword arguments given will be passed to the sub-component. It is strictly recommended to exclusively use `render_sub_comp`, `sub_comp` or its [resourceful](./resourceful.md#nesting-resourceful-components) pendent to nest components, as this method makes a component aware of its exact nesting.
|
|
16
|
+
|
|
17
|
+
Here is a simple example of a component that displays numbers as binary:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# app/components/numbers/binary.rb
|
|
21
|
+
class Components::Nestings::Binary < Compony::Component
|
|
22
|
+
def initialize(*args, number: nil, **kwargs, &block)
|
|
23
|
+
@number = nil # If this component is initialized with the argument `number`, it will be stored in the component instance.
|
|
24
|
+
end
|
|
25
|
+
setup do
|
|
26
|
+
# standalone and other configs are omitted in this example.
|
|
27
|
+
content do
|
|
28
|
+
# If the initializer did not store `number`, check whether the Rails request contains the parameter `number`:
|
|
29
|
+
# Note: do not do that, as we will demonstrate below.
|
|
30
|
+
@number ||= params[:number].presence&.to_i || 0
|
|
31
|
+
# Display the number as binary
|
|
32
|
+
para "The number #{@number} has the binary form #{@number.to_s(2)}."
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If used standalone, the number can be set by using a GET parameter, e.g. `?number=5`. The result is something like this:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
The number 5 has the binary form 101.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Now, let's write a component that displays three different numbers side-by-side:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# app/components/numbers/binary_comparator.rb
|
|
48
|
+
class Components::Nestings::BinaryComparator < Compony::Component
|
|
49
|
+
setup do
|
|
50
|
+
# standalone and other configs are omitted in this example.
|
|
51
|
+
content do
|
|
52
|
+
concat sub_cop(Components::Nestings::Binary, number: 1).render(controller)
|
|
53
|
+
concat sub_cop(Components::Nestings::Binary, number: 2).render(controller)
|
|
54
|
+
concat sub_cop(Components::Nestings::Binary, number: 3).render(controller)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The result is something like this:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
The number 1 has the binary form 1.
|
|
64
|
+
The number 2 has the binary form 10.
|
|
65
|
+
The number 3 has the binary form 11.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
However, this is static and no fun. We cannot use the HTTP GET parameter any more because all three `Binary` sub-components listen to the same parameter `number`. To fix this, we will need to scope the parameter using the `param_name` as explained in the next subsection.
|
|
69
|
+
|
|
70
|
+
## Proper parameter naming for (nested) components
|
|
71
|
+
|
|
72
|
+
As seen above, components can be arbitrarily nested, making it harder to identify which HTTP GET parameter in the request is intended for which component. To resolve this, Compony provides nesting-aware scoping of parameter names:
|
|
73
|
+
|
|
74
|
+
- Each component has an `index`, given to it by the `sub_comp` call in the parent, informing it witch n-th child of the parent it is.
|
|
75
|
+
- For instance, in the example above, the three `Binary` components have indices 0, 1 and 2.
|
|
76
|
+
- Each component has an `id` which corresponds to `"#{family_name}_#{comp_name}_#{@index}"`.
|
|
77
|
+
- For instance, the last `Binary` component from the example above has ID `nestings_binary_2`.
|
|
78
|
+
- The `BinaryComparator` has ID `nestings_binary_comparator_0`.
|
|
79
|
+
- Each component has a `path` indicating its exact position in the nesting tree as seen from the root component.
|
|
80
|
+
- In the example above, the last `Binary` component has path `nestings_binary_comparator_0/nestings_binary_2`.
|
|
81
|
+
- `BinaryComparator` has path `nestings_binary_comparator_0`.
|
|
82
|
+
- Each component provides the method `param_name` that takes the name of a parameter name and prepends the first 5 characters of the component's SHA1-hashed path to it.
|
|
83
|
+
- For instance, if `param_name(:number)` is called on the last `Binary` component, the output is `a9f3d_number`.
|
|
84
|
+
- If the same method is called on the first `Binary` component, the output is `f6e86_number`.
|
|
85
|
+
|
|
86
|
+
In short, `param_name` should be used to prefix every parameter that is used in a component that could potentially be nested. It is good practice to apply it to all components. `param_name` has two important properties:
|
|
87
|
+
|
|
88
|
+
- From the param name alone, it is not possible to determine to which component the parameter belongs. However:
|
|
89
|
+
- `param_name` is consistent across reloads of the same URL (given that the components are still the same) and thus each component will be able to identify its own parameters and react to them.
|
|
90
|
+
|
|
91
|
+
With that in mind, let's adjust our `Binary` component. In this example, we will assume that we have implemented yet another component called `NumberChooser` that provides a number input with a Stimulus controller attached. That controller is given the parameter as a String value, such that the it can set the appropriate HTTP GET param and trigger a full page reload to the `BinaryComparator` component.
|
|
92
|
+
|
|
93
|
+
Further, we can drop the custom initializer from the `Binary` component, as the number to display is exclusively coming from the HTTP GET param. The resulting code looks something like:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# app/components/numbers/binary_comparator.rb
|
|
97
|
+
class Components::Nestings::BinaryComparator < Compony::Component
|
|
98
|
+
setup do
|
|
99
|
+
# standalone and other configs are omitted in this example.
|
|
100
|
+
content do
|
|
101
|
+
3.times do
|
|
102
|
+
concat sub_cop(Components::Nestings::Binary).render(controller)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# app/components/numbers/binary.rb
|
|
109
|
+
class Components::Nestings::Binary < Compony::Component
|
|
110
|
+
setup do
|
|
111
|
+
# standalone and other configs are omitted in this example.
|
|
112
|
+
content do
|
|
113
|
+
# This is where we use param_name to retrieve the parameter for this component, regardless whether it's standalone or used as a sub-comp.
|
|
114
|
+
@number ||= params[param_name(:number)].presence&.to_i || 0
|
|
115
|
+
# Display the number as binary
|
|
116
|
+
para "The number #{@number} has the binary form #{@number.to_s(2)}."
|
|
117
|
+
# Display the number input that will reload the page to adjust to the user input. We give it the param_name such that it can set params accordingly.
|
|
118
|
+
concat sub_comp(Components::Nestings::NumberChooser, param_name: param_name(:number))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The result for the URL `path/to/binary_comparator?a9f3d_number=2&e70b4_number=4&a9f3d_number=8` is something like this:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
The number 2 has the binary form 10. Enter a number and press ENTER: [2]
|
|
128
|
+
The number 4 has the binary form 100. Enter a number and press ENTER: [4]
|
|
129
|
+
The number 8 has the binary form 1000. Enter a number and press ENTER: [8]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Note that this example is completely stateless, as all the info is encoded in the URL.
|
|
133
|
+
|
|
134
|
+
[Guide index](/README.md#guide--documentation)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
|
|
3
|
+
# Ownership
|
|
4
|
+
|
|
5
|
+
Ownership is a concept that captures the nature of data to be presented by Compony. It means that an object only makes sense within the context of another that it belongs to. Owned objects have therefore no index component, because they don't have meaning on their own. For instance:
|
|
6
|
+
|
|
7
|
+
- typically NOT owned: visitors and vouchers: while a voucher can `belong_to` a visitor, the voucher can be managed on it's own. Vouchers can have their own index page which makes it possible to search for a given voucher code across all vouchers.
|
|
8
|
+
- typically owned: users and their permissions: a permission only makes sense with respect to its associated user and having a list of all permissions across the system would rarely be a use case. In this case, we consider the `Permission` model to be conceptually **owned by** the `User` model.
|
|
9
|
+
|
|
10
|
+
In Compony, if a model class is owned by another, it means that:
|
|
11
|
+
|
|
12
|
+
- The owned model has a non-optional `belongs_to` relation ship to its owner.
|
|
13
|
+
- The owned model class has no Index component.
|
|
14
|
+
- [Pre-built components](/doc/guide/pre_built_components.md) offer [exposed intents](/doc/guide/intents.md#exposed-intents) to the owner model and redirect to its Show component instead of to the current object's Index component.
|
|
15
|
+
|
|
16
|
+
To mark a model as owned by another, write the following code **in the model**:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# app/models/permission.rb
|
|
20
|
+
owned_by :user
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
[Guide index](/README.md#guide--documentation)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: Destroy
|
|
5
|
+
|
|
6
|
+
This component is the Compony equivalent to a typical Rails controller's `destroy` action.
|
|
7
|
+
|
|
8
|
+
`Compony::Components::Destroy` is a resourceful standalone component that listens to two verbs:
|
|
9
|
+
|
|
10
|
+
- GET will cause the Destroy component to ask if the resource should be destroyed, along with a button pointing to the DELETE verb. If the record does not exist, a HTTP 404 code is returned.
|
|
11
|
+
- DELETE will `destroy!` the resource, show a flash and redirect to:
|
|
12
|
+
- if present: the data's Show component
|
|
13
|
+
- otherwise: the data's Index component
|
|
14
|
+
|
|
15
|
+
Authorization checks for `destroy` even in GET. The reason is that users that aren't able to destroy a resource shouldn't even arrive at the page asking them whether they want to do so, unable to click the only button due to lacking permissions. This also causes any [intents](/doc/guide/intents.md) to Destroy components to be hidden if the user is unable to destroy the corresponding resource.
|
|
16
|
+
|
|
17
|
+
This component largely follows the [resourceful lifecycle](/doc/guide/resourceful.md#complete-resourceful-lifecycle). As can be expected, the resource is loaded by `Resourceful`'s default load block and `store_data` is implemented to destroy the resource.
|
|
18
|
+
|
|
19
|
+
If the resource is [owned](/doc/guide/ownership.md), the component provides a `:back_to_owner` [exposed intent](/doc/guide/intents.md#exposed-intents) in the form of a cancel button.
|
|
20
|
+
|
|
21
|
+
The following DSL methods are implemented to allow for convenient overrides of default logic:
|
|
22
|
+
|
|
23
|
+
- The block `on_destroyed` is evaluated between successful record destruction 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 destroyed (like an `after_destroy` hook, but only executed if a record was destroyed by this component). Do not redirect or render here, use the next blocks instead.
|
|
24
|
+
- The block given in `on_destroyed_respond` is evaluated after destruction and by default shows a flash, then redirects. The redirection is performed with HTTP code 303 ("see other") in oder to force a GET request. This is required for the component to work with Turbo. Overwrite this block if you need to completely customize all logic that happens after destruction. If this block is overwritten, `on_destroyed_redirect_path` will not be called.
|
|
25
|
+
- `on_destroyed_redirect_path` is evaluated as the second step of `on_destroyed_respond` and redirects to the resource's Show or Index component as described above. Overwrite this block in order to redirect to another component instead, while keeping the default flash provided by `on_destroyed_respond`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide--documentation)
|
|
2
|
+
- [List of pre-built components](/doc/guide/pre_built_components.md)
|
|
3
|
+
|
|
4
|
+
# Pre-built components: Edit
|
|
5
|
+
|
|
6
|
+
This component is the Compony equivalent to a typical Rails controller's `edit` and `update` actions.
|
|
7
|
+
|
|
8
|
+
`Compony::Components::Edit` is a resourceful standalone component based on [`WithForm`](./with_form.md) that listens to two verbs:
|
|
9
|
+
|
|
10
|
+
- GET will cause the Edit component to load a record given by ID and render the form based on that record. If the record does not exist, a HTTP 404 code is returned.
|
|
11
|
+
- PATCH (equivalent to a `update` 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 update 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
|
+
Unlike in New and Destroy, Edit's authorization checks for `edit` in GET and for `update` in PATCH. This enables you to "abuse" an Edit component to double as a Show component. Users having only `:read` permission will not see any links or buttons pointing to an Edit component. Users having only `:edit` permissions can see the form (including the data) but not submit it. Users having `:write` permissions can edit and update the Resource, in accordance to CanCanCan's `:write` alias.
|
|
17
|
+
|
|
18
|
+
This component follows the [resourceful lifecycle](/doc/guide/resourceful.md#complete-resourceful-lifecycle). 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 Edit 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 `@update_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_update_failed_respond` is run if `@update_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_updated` 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 updated (like an `after_update` hook, but only executed if a record was updated by this component). Do not redirect or render here, use the next blocks instead.
|
|
26
|
+
- The block given in `on_updated_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_updated_redirect_path` will not be called.
|
|
27
|
+
- `on_updated_redirect_path` is evaluated as the second step of `on_updated_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_updated_respond`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
- [Back to the guide](/README.md#guide--documentation)
|
|
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--documentation)
|
|
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).
|