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,156 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Compony helpers, links and buttons
|
|
4
|
+
|
|
5
|
+
When pointing to or instantiating a component, writing the whole class name would be cumbersome. For this reason, Compony has several helpers that will retrieve the correct class for you. The most important ones are explained in this subsection. The terms are defined as follows:
|
|
6
|
+
|
|
7
|
+
- Component name or constant: For a component `Components::Users::Show`, this would be `'Show'`, `'show'`, or `:show`
|
|
8
|
+
- Family name or constant: For a component `Components::Users::Show`, this would be `'Users'`, `'users'`, or `:users`
|
|
9
|
+
- Model: an instance of a class that implements the `model_name` method in the same way as `ActiveRecord::Base` does. For helpers that support giving models, Compony will use `model_name` to auto-infer the family name. This requires you to name the component according to convention, i.e. the family name must match the model's pluralized camelized `model_name`.
|
|
10
|
+
|
|
11
|
+
## Getting the class of a component
|
|
12
|
+
|
|
13
|
+
- `Compony.comp_class_for(comp_name_or_cst, model_or_family_name_or_cst)` returns the class or nil if not found.
|
|
14
|
+
- `Compony.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst)` returns the class. If the class is not found, an error will be raised.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
my_component = Compony.comp_class_for!(:show, User.first).new
|
|
20
|
+
my_component.class # Components::Users::Show
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Getting a path to a component
|
|
24
|
+
|
|
25
|
+
- `Compony.path(comp_name_or_cst, model_or_family_name_or_cst)` returns the route to a component. Additional positional and keyword arguments will be passed to the Rails helper.
|
|
26
|
+
|
|
27
|
+
If a model is given, its ID will automatically be added as the `id` parameter when generating the route. This means:
|
|
28
|
+
|
|
29
|
+
- To generate a path to a non-resourceful component, pass the family name.
|
|
30
|
+
- To generate a path to a resourceful component, prefer passing an instance instead of a family name.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
link_to 'User overview', Compony.path(:index, :users) # -> 'users/index'
|
|
36
|
+
link_to 'See user page', Compony.path(:show, User.first) # -> 'users/show/1'
|
|
37
|
+
link_to 'See user page', Compony.path(:show, :users, id: 1) # -> 'users/show/1'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Note that the generated paths in the example are just for illustration purposes. The paths point to whatever path you configure in the target component's default standalone config. Also, this example is not how you should generate links to components, as is explained in the next subsection.
|
|
41
|
+
|
|
42
|
+
### Customizing path generation
|
|
43
|
+
|
|
44
|
+
By implementing `path do ... end` inside the `setup` method of a component, you can override the way paths to that component are generated. Customizing the path generation will affect all mentioned methods mentioned here involving paths, such as `Compony.path`, `compony_link`, `Compony.button`, `compony_button` etc.
|
|
45
|
+
|
|
46
|
+
This is an advanced usage. Refer to the default implementation of `Component`'s `path_block` to see an exmple.
|
|
47
|
+
|
|
48
|
+
## Generating a link to a component
|
|
49
|
+
|
|
50
|
+
In order to allow a user to visit another component, don't implement your links and buttons manually. Instead, use Compony's links and buttons, as those extract information from the target component, avoiding redundant code and making refactoring much easier.
|
|
51
|
+
|
|
52
|
+
Compony comes with the view helper `compony_link` that is available in any of your views, including a component's `content` blocks. The link's label is inferred from the component the link points to. `compony_link` is used as follows:
|
|
53
|
+
|
|
54
|
+
- To generate a link to a non-resourceful component, pass the family name.
|
|
55
|
+
- To generate a link to a resourceful component, prefer passing an instance instead of a family name. More precisely, you must pass an instance if the component's label requires an argument.
|
|
56
|
+
|
|
57
|
+
Any additional arguments passed to `compony_link` will be given to Rails' `link_to` method, allowing you to set parameters, HTTP method, terget, rel etc.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
compony_link(:index, :users) # "View all users" -> 'users/index'
|
|
63
|
+
compony_link(Components::Users::Index) # same as above
|
|
64
|
+
compony_link(:index, :users, label_opts: { format: :short }) # "All" -> 'users/index'
|
|
65
|
+
compony_link(:show, User.first) # "View John Doe" -> 'users/show/1'
|
|
66
|
+
compony_link(:destroy, User.first, method: :delete) # "Delete John Doe" -> 'users/destroy/1'
|
|
67
|
+
|
|
68
|
+
# NOT working:
|
|
69
|
+
compony_link(:show, :users, id: 1) # Error: The label for the Users::Show component takes an argument which was not provided (the user's label)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Generating a button to a component
|
|
73
|
+
|
|
74
|
+
Compony buttons are components that render a button to another component. While the view helper `compony_button` works similar to `compony_link`, you can also manually instantiate a button and work with it like with any other component.
|
|
75
|
+
|
|
76
|
+
Similar to links, Compony buttons take a component name and either a family or model. The label, path, method and title (i.e. tooltip) can be overwritten by passing the respective arguments as shown below.
|
|
77
|
+
|
|
78
|
+
Compony buttons have a type that is either `:button` or `:submit`. While the first works like a link redirecting the user elsewhere, the second is used for submitting forms. It can be used inside a `form_for` or `simple_form_for`.
|
|
79
|
+
|
|
80
|
+
A compony button figures out on it's own whether it's clickable or not:
|
|
81
|
+
|
|
82
|
+
- Buttons can be disabled explicitly by passing `enabled: false` as a parameter.
|
|
83
|
+
- If a user is not authorized to access the component a button is pointing to, the button is not displayed.
|
|
84
|
+
- If the target component should not be accessible due to a prevention in the [feasibility framework](./feasibility.md), the button is disabled and a tooltip is shown explaining why the button is not clickable.
|
|
85
|
+
|
|
86
|
+
Do not directly instantiate `Compony::Components::Button`. Instead, use `Compony.button`:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
my_button = Compony.button(:index, :users) # "View all users" -> 'users/index'
|
|
90
|
+
my_button = Compony.button(:index, :users, label_opts: { format: :short }) # "All" -> 'users/index'
|
|
91
|
+
my_button = Compony.button(:index, :users, label: 'Back') # "Back" -> 'users/index'
|
|
92
|
+
my_button = Compony.button(:show, User.first) # "View John Doe" -> 'users/show/1'
|
|
93
|
+
my_button = Compony.button(:new, :users, label: 'New customer', params: { user: { type: 'customer' } }) # "New customer" -> 'users/new?user[type]=customer'
|
|
94
|
+
my_button = Compony.button(:new, :users, label: 'New customer', params: { user: { type: 'customer' } }, method: :post) # Instantly creates user.
|
|
95
|
+
my_button = Compony.button(label: 'I point to a plain Rails route', path: 'some/path') # Specifying a custom path
|
|
96
|
+
my_button = Compony.button(label: 'Nothing happens if you click me') # javascript:void()
|
|
97
|
+
my_button = Compony.button(label: 'Not implemented yet', enabled: false) # Disabled button
|
|
98
|
+
|
|
99
|
+
# `enabled` and `path` can also be provided with a callable (block or lambda) to defer evaluation until when the button is rendered.
|
|
100
|
+
# The lambdas will be called in the button's `before_render` and given the controller, allowing you to query request specific data.
|
|
101
|
+
my_button = Compony.button(label: 'I point to a plain Rails route', path: ->{ |controller| controller.helpers.some_rails_path })
|
|
102
|
+
my_button = Compony.button(:index, :users, enabled: -> { |controller| controller.current_ability.can?(:read, :index_pages) })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A Compony button can be rendered like any other component:
|
|
106
|
+
|
|
107
|
+
```erb
|
|
108
|
+
<%= my_button.render(controller) %>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
However, it is much easier to just use the appropriate view helper instead, which takes the same arguments as `Compony.button`:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
compony_button(:index, :users)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If you need to render many buttons that share a parameter, the call `Compony.with_button_defaults` allows you to DRY up your code:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Assuming this is inside a Dyny view context and each button should be inside a div.
|
|
121
|
+
# Without with_button_defaults:
|
|
122
|
+
div compony_button(:new, :documents, label_opts: { format: :short }, method: :post)
|
|
123
|
+
div compony_button(:new, :letters, label_opts: { format: :short }, method: :post)
|
|
124
|
+
div compony_button(:new, :articles, label_opts: { format: :short }, method: :post)
|
|
125
|
+
|
|
126
|
+
# Equivalent using with_button_defaults:
|
|
127
|
+
Compony.with_button_defaults(label_opts: { format: :short }, method: :post) do
|
|
128
|
+
div compony_button(:new, :documents)
|
|
129
|
+
div compony_button(:new, :letters)
|
|
130
|
+
div compony_button(:new, :articles)
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Implementing custom buttons
|
|
135
|
+
|
|
136
|
+
Plain HTML buttons are not exactly eye candy, so you will likely want to implement your button kind with black jack and icons. For this reason, the button instantiated by Compony's button helpers can be customized.
|
|
137
|
+
|
|
138
|
+
To build your own button class, inherit as follows:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
class MyButton < Compony::Components::Button
|
|
142
|
+
def initialize(*args, **kwargs, &block) # Add extra arguments here
|
|
143
|
+
super(*args, **kwargs, &block)
|
|
144
|
+
# Add extra initialization code here
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Add/replace before_render/content here. Be careful to not overwrite code you depend on. Check Compony's button's code for details.
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Then, in the Compony initializer, register your custom button class to have Compony instantiate it whenever `Compony.button` or another helper is called:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# config/initializers/compony.rb
|
|
155
|
+
Compony.button_component_class = 'MyButton'
|
|
156
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Inheritance
|
|
4
|
+
|
|
5
|
+
Compony's key advantage is that you can write DRYer code with it. To achieve this, you are encouraged to create abstract components, implement common functionality there and inherit from them in other components.
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
|
|
9
|
+
- Perhaps you have code shared in all of your `New` components. In this case, create `BaseComponents::New` and inherit from `Compony::Components::New`. In `setup` of your base component, you can now perform all the configurations needed. Now you may inherit from it: `class Components::Users::New < BaseComponents::New`.
|
|
10
|
+
- Perhaps you often implement the same kind of component, for instance an index component displaying a filterable list. In this case, create `BaseComponents::Index` and inherit as follows: `class Components::Users::Index < BaseComponents::Index`.
|
|
11
|
+
|
|
12
|
+
## Behavior
|
|
13
|
+
|
|
14
|
+
When inheriting from another component class, `setup` can be called in the child as well in order to overwrite specified configurations. The parent's `setup` block will be run first, then the child's, then the grand-child's and so on.
|
|
15
|
+
|
|
16
|
+
Omit any configuration that you want to keep from the parent class. For instance, if your parent's setup looks like this:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
setup do
|
|
20
|
+
standalone path: 'foo/bar' do
|
|
21
|
+
layout 'funky'
|
|
22
|
+
verb :get do
|
|
23
|
+
authorize { true }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
content do
|
|
27
|
+
h1 'Test'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Assuming you want to implement a child class that only differs by layout and adds more content below "test", you can implement:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
setup do
|
|
36
|
+
standalone do
|
|
37
|
+
layout 'dark'
|
|
38
|
+
end
|
|
39
|
+
content :below do
|
|
40
|
+
para 'This will appear below "Test".'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Un-exposing a component
|
|
46
|
+
|
|
47
|
+
If a component's parent class is [standalone](./standalone.md) but the child should not be, use `clear_standalone!`:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
setup do
|
|
51
|
+
clear_standalone!
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Best practice
|
|
56
|
+
|
|
57
|
+
Compony has the following convention:
|
|
58
|
+
|
|
59
|
+
- implement a custom base component in the directory `app/compony/base_components/your_component.rb`
|
|
60
|
+
- name the class `BaseComponents::YourComponent` where `BaseComponents` is typically a module simple meant for namespacing
|
|
61
|
+
|
|
62
|
+
When respecting these conventions, compony's generators will automatically make generated classes inherit from the suitable base component if one is available. In the example above, `rails g component Users::Index` will automatically make the generated class inherit from `BaseComponent::Index`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
2
|
+
|
|
3
|
+
# Installation
|
|
4
|
+
|
|
5
|
+
## Installing Compony
|
|
6
|
+
|
|
7
|
+
First, add Compony to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'compony'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run `bundle install`.
|
|
14
|
+
|
|
15
|
+
Create the directory `app/components`.
|
|
16
|
+
|
|
17
|
+
In `app/models/application_record.rb`, add the following line below `primary_abstract_class`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
include Compony::ModelMixin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Installing CanCanCan
|
|
24
|
+
|
|
25
|
+
Create the file `app/models/ability.rb` with the following content:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class Ability
|
|
29
|
+
include CanCan::Ability
|
|
30
|
+
|
|
31
|
+
def initialize(_user)
|
|
32
|
+
can :manage, :all
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This is an initial dummy ability that allows anyone to do anything. Most likely, you will want to adjust the file. For documentation, refer to [https://github.com/CanCanCommunity/cancancan/](https://github.com/CanCanCommunity/cancancan/).
|
|
38
|
+
|
|
39
|
+
## Optional: installing anchormodel
|
|
40
|
+
|
|
41
|
+
To take advantage of the anchormodel integration, follow the installation instructions under [https://github.com/kalsan/anchormodel/](https://github.com/kalsan/anchormodel/).
|
|
42
|
+
|
|
43
|
+
## Optional: installing `active_type`
|
|
44
|
+
|
|
45
|
+
To take advantage of [virtual models](./virtual_models.md) through the `active_type` integration, follow the instructions under [https://github.com/makandra/active_type](https://github.com/makandra/active_type)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
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.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
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.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
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 `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 `sub_comp` (or its [resourceful](./resourceful.md) 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.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[Back to the guide](/README.md#guide)
|
|
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 (more on them later) offer root actions 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
|
+
```
|
|
@@ -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: Button
|
|
5
|
+
|
|
6
|
+
As stated earlier, buttons are just regular components that rendered in-place. They don't make use of nesting logic (and presumably never will), and thus they are rendered as-is, without `sub_comp`.
|
|
7
|
+
|
|
8
|
+
You will rarely (or probably never) instantiate a button on your own, but use helpers like `Compony.button` or `compony_button`. For this reason, the documentation for instantiating buttons is located in the [section documenting helpers](/doc/guide/helpers.md).
|
|
@@ -0,0 +1,25 @@
|
|
|
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: 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 `compony_link` and `compony_button` 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` root action 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)
|
|
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`.
|