compony 0.11.12 → 0.11.14
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/.rspec +2 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +145 -106
- data/README.md +12 -1
- data/Rakefile +10 -0
- data/TODO.md +2 -1
- data/VERSION +1 -1
- data/compony.gemspec +5 -3
- 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/Buttons/CssButton.html +1 -1
- data/doc/Compony/Components/Buttons/Link.html +1 -1
- data/doc/Compony/Components/Buttons.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 +41 -41
- 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/Intent.html +1 -1
- data/doc/Compony/ManageIntentsDsl.html +2 -2
- 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 +8 -2
- 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 +13 -13
- 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 +1 -1
- data/doc/Compony.html +276 -92
- data/doc/ComponyController.html +1 -1
- data/doc/_index.html +1 -1
- data/doc/file.CHANGELOG.html +89 -77
- data/doc/file.README.html +21 -11
- data/doc/file.basic_component.html +14 -14
- data/doc/file.cookbook.html +27 -27
- data/doc/file.destroy.html +2 -2
- data/doc/file.dsl_reference.html +199 -199
- data/doc/file.edit.html +3 -3
- data/doc/file.example.html +13 -13
- data/doc/file.example_advanced.html +10 -10
- data/doc/file.feasibility.html +2 -2
- data/doc/file.form.html +20 -8
- data/doc/file.generators.html +3 -3
- data/doc/file.glossary.html +44 -44
- data/doc/file.gotchas.html +35 -35
- data/doc/file.index.html +10 -10
- data/doc/file.inheritance.html +5 -5
- data/doc/file.installation.html +9 -9
- data/doc/file.integrations.html +46 -46
- data/doc/file.intents.html +11 -11
- data/doc/file.internal_datastructures.html +6 -6
- data/doc/file.list.html +35 -35
- data/doc/file.maintaining.html +21 -21
- data/doc/file.model_fields.html +3 -3
- data/doc/file.nesting.html +5 -5
- data/doc/file.new.html +3 -3
- data/doc/file.ownership.html +2 -2
- data/doc/file.patterns.html +50 -50
- data/doc/file.pre_built_components.html +2 -2
- data/doc/file.resourceful.html +5 -5
- data/doc/file.show.html +17 -17
- data/doc/file.standalone.html +12 -12
- data/doc/file.virtual_models.html +2 -2
- data/doc/file.with_form.html +23 -23
- data/doc/guide/pre_built_components/form.md +21 -2
- data/doc/index.html +21 -11
- data/doc/method_list.html +16 -0
- data/doc/top-level-namespace.html +1 -1
- data/lib/compony/components/form.rb +5 -4
- data/lib/compony/model_fields/base.rb +3 -0
- data/lib/compony/model_mixin.rb +3 -2
- data/lib/compony.rb +19 -0
- data/spec/README.md +22 -0
- data/spec/compony/component_spec.rb +80 -0
- data/spec/compony/components/form_spec.rb +31 -0
- data/spec/compony/compony_spec.rb +130 -0
- data/spec/compony/intent_spec.rb +94 -0
- data/spec/compony/method_accessible_hash_spec.rb +32 -0
- data/spec/compony/model_fields_spec.rb +70 -0
- data/spec/compony/model_mixin_spec.rb +109 -0
- data/spec/compony/natural_ordering_spec.rb +61 -0
- data/spec/dummy/app/components/statics/home.rb +17 -0
- data/spec/dummy/app/components/statics/secret.rb +15 -0
- data/spec/dummy/app/components/users/destroy.rb +2 -0
- data/spec/dummy/app/components/users/edit.rb +2 -0
- data/spec/dummy/app/components/users/form.rb +14 -0
- data/spec/dummy/app/components/users/index.rb +29 -0
- data/spec/dummy/app/components/users/new.rb +2 -0
- data/spec/dummy/app/components/users/show.rb +26 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/models/ability.rb +7 -0
- data/spec/dummy/app/models/application_record.rb +4 -0
- data/spec/dummy/app/models/user.rb +22 -0
- data/spec/dummy/app/views/layouts/application.html.erb +7 -0
- data/spec/dummy/config/application.rb +20 -0
- data/spec/dummy/config/database.yml +3 -0
- data/spec/dummy/config/environment.rb +3 -0
- data/spec/dummy/config/initializers/simple_form.rb +9 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/db/schema.rb +14 -0
- data/spec/rails_helper.rb +23 -0
- data/spec/requests/authorization_spec.rb +16 -0
- data/spec/requests/plain_component_spec.rb +9 -0
- data/spec/requests/routing_spec.rb +20 -0
- data/spec/requests/users_crud_spec.rb +99 -0
- metadata +63 -1
data/doc/file.with_form.html
CHANGED
|
@@ -63,22 +63,22 @@
|
|
|
63
63
|
<p><a href="/doc/guide/pre_built_components_md.html">List of pre-built components</a></p>
|
|
64
64
|
</li></ul>
|
|
65
65
|
|
|
66
|
-
<h1 id="
|
|
66
|
+
<h1 id="pre-built-components-withform">Pre-built components: WithForm</h1>
|
|
67
67
|
|
|
68
|
-
<p><code>Compony::Components::WithForm</code> is the abstract base for components that render and submit a form. It is <strong>twinned</strong> with a href="./form_md.html"></a> component: WithForm provides the route, authorization and resource handling; the Form provides the inputs and param schema. href="./new_md.html"></a> and href="./edit_md.html"></a> both inherit from WithForm — you rarely subclass it directly, but understanding the twinning explains how they work.</p>
|
|
68
|
+
<p><code>Compony::Components::WithForm</code> is the abstract base for components that render and submit a form. It is <strong>twinned</strong> with a <a href="./form_md.html"><code>Form</code></a> component: WithForm provides the route, authorization and resource handling; the Form provides the inputs and param schema. <a href="./new_md.html"><code>New</code></a> and <a href="./edit_md.html"><code>Edit</code></a> both inherit from WithForm — you rarely subclass it directly, but understanding the twinning explains how they work.</p>
|
|
69
69
|
|
|
70
70
|
<p>A WithForm component may be resourceful (New/Edit are) but does not have to be.</p>
|
|
71
71
|
|
|
72
|
-
<h2 id="
|
|
72
|
+
<h2 id="how-the-twinning-works">How the twinning works</h2>
|
|
73
73
|
<ul><li>
|
|
74
|
-
<p><code>form_comp</code> returns the Form instance, built lazily. It defaults to the component named
|
|
74
|
+
<p><code>form_comp</code> returns the Form instance, built lazily. It defaults to the component named <code>Form</code> in the <strong>same family</strong>, instantiated with this component as <code>parent_comp</code> and passed <code>submit_verb</code>, <code>submit_path</code> and <code>cancancan_action</code>.</p>
|
|
75
75
|
</li><li>
|
|
76
|
-
<p>The Form renders inside this component’s <code>content</code> (e.g. New/Edit do
|
|
76
|
+
<p>The Form renders inside this component’s <code>content</code> (e.g. New/Edit do <code>concat form_comp.render(controller, data: @data)</code>).</p>
|
|
77
77
|
</li><li>
|
|
78
|
-
<p>The form’s <code><form></code> posts back to <code>submit_path</code> using <code>submit_verb</code>; that same component
|
|
78
|
+
<p>The form’s <code><form></code> posts back to <code>submit_path</code> using <code>submit_verb</code>; that same component handles the submit verb (POST for New, PATCH for Edit) and runs the resourceful lifecycle (<code>assign_attributes</code> → <code>store_data</code> → <code>respond</code>).</p>
|
|
79
79
|
</li></ul>
|
|
80
80
|
|
|
81
|
-
<h2 id="
|
|
81
|
+
<h2 id="dsl-methods">DSL methods</h2>
|
|
82
82
|
|
|
83
83
|
<table role="table">
|
|
84
84
|
<thead>
|
|
@@ -90,34 +90,34 @@
|
|
|
90
90
|
</thead>
|
|
91
91
|
<tbody>
|
|
92
92
|
<tr>
|
|
93
|
-
<td
|
|
94
|
-
<td
|
|
95
|
-
<td>HTTP verb the twinned form submits with. Mandatory (New sets
|
|
93
|
+
<td><code>submit_verb</code></td>
|
|
94
|
+
<td><code>submit_verb(:patch)</code></td>
|
|
95
|
+
<td>HTTP verb the twinned form submits with. Mandatory (New sets <code>:post</code>, Edit <code>:patch</code>).</td>
|
|
96
96
|
</tr>
|
|
97
97
|
<tr>
|
|
98
|
-
<td
|
|
99
|
-
<td
|
|
100
|
-
<td>Use a specific Form class instead of the same-family
|
|
98
|
+
<td><code>form_comp_class</code></td>
|
|
99
|
+
<td><code>form_comp_class(Components::Users::SignupForm)</code></td>
|
|
100
|
+
<td>Use a specific Form class instead of the same-family <code>Form</code>.</td>
|
|
101
101
|
</tr>
|
|
102
102
|
<tr>
|
|
103
|
-
<td
|
|
104
|
-
<td
|
|
103
|
+
<td><code>submit_path</code></td>
|
|
104
|
+
<td><code>submit_path { Compony.path(:create, :users) }</code></td>
|
|
105
105
|
<td>Override where the form submits. Block is given the controller; defaults to this component’s own path.</td>
|
|
106
106
|
</tr>
|
|
107
107
|
<tr>
|
|
108
|
-
<td
|
|
109
|
-
<td
|
|
110
|
-
<td>CanCanCan action used by the Form for per-field
|
|
108
|
+
<td><code>form_cancancan_action</code></td>
|
|
109
|
+
<td><code>form_cancancan_action(:edit)</code></td>
|
|
110
|
+
<td>CanCanCan action used by the Form for per-field <code>permitted_attributes</code> (New sets <code>:new</code>, Edit <code>:edit</code>). Pass <code>nil</code> to disable per-field auth.</td>
|
|
111
111
|
</tr>
|
|
112
112
|
<tr>
|
|
113
|
-
<td
|
|
114
|
-
<td
|
|
113
|
+
<td><code>form_comp</code></td>
|
|
114
|
+
<td><code>form_comp</code></td>
|
|
115
115
|
<td>(Not DSL — a reader.) The Form instance; override in a subclass for full control.</td>
|
|
116
116
|
</tr>
|
|
117
117
|
</tbody>
|
|
118
118
|
</table>
|
|
119
119
|
|
|
120
|
-
<h2 id="
|
|
120
|
+
<h2 id="example-a-custom-non-default-form">Example: a custom non-default form</h2>
|
|
121
121
|
|
|
122
122
|
<pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Users</span><span class='op'>::</span><span class='const'>Signup</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/New.html" title="Compony::Components::New (class)">New</a></span></span>
|
|
123
123
|
<span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
|
|
@@ -143,11 +143,11 @@
|
|
|
143
143
|
<span class='kw'>end</span>
|
|
144
144
|
</code></pre>
|
|
145
145
|
|
|
146
|
-
<p>For the full submit/redirect behavior see href="./new_md.html"></a> and href="./edit_md.html"></a>; for the form input/schema DSL see href="./form_md.html"></a>; for the lifecycle hooks see <a href="/doc/guide/resourceful_md.html">Resourceful</a>. Multi-step and clone flows that reuse this machinery are in <a href="../patterns_md.html#11-non-crud-job-dispatch-toggles-clone">Real-world patterns</a>.</p>
|
|
146
|
+
<p>For the full submit/redirect behavior see <a href="./new_md.html"><code>New</code></a> and <a href="./edit_md.html"><code>Edit</code></a>; for the form input/schema DSL see <a href="./form_md.html"><code>Form</code></a>; for the lifecycle hooks see <a href="/doc/guide/resourceful_md.html">Resourceful</a>. Multi-step and clone flows that reuse this machinery are in <a href="../patterns_md.html#11-non-crud-job-dispatch-toggles-clone">Real-world patterns</a>.</p>
|
|
147
147
|
</div></div>
|
|
148
148
|
|
|
149
149
|
<div id="footer">
|
|
150
|
-
Generated on
|
|
150
|
+
Generated on Wed Oct 7 10:48:18 2026 by
|
|
151
151
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
152
152
|
0.9.34 (ruby-3.4.9).
|
|
153
153
|
</div>
|
|
@@ -91,7 +91,7 @@ In the model:
|
|
|
91
91
|
class Foo < ApplicationRecord
|
|
92
92
|
# No need to write:
|
|
93
93
|
field :label, :string, virtual: true
|
|
94
|
-
|
|
94
|
+
Compony.content_locales.each do |locale|
|
|
95
95
|
field :"label_#{locale}", :string
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -114,4 +114,23 @@ class Components::Foos::Form < Compony::Components::Form
|
|
|
114
114
|
schema_field :label, multilang: true
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
|
-
```
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Content locales
|
|
120
|
+
|
|
121
|
+
By default, `multilang: true` generates one field per locale in `I18n.available_locales`. If your app's locales differ
|
|
122
|
+
from the languages your content is stored in, for instance because it uses regional locales such as `:"de-CH"` and
|
|
123
|
+
`:"de-DE"` while content is stored per language, configure the content locales in an initializer:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# config/initializers/compony.rb
|
|
127
|
+
Compony.content_locales = %i[de en fr]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
With `I18n.available_locales = %i[de de-CH en fr]`, `field :label, :string, multilang: true` then generates `label`
|
|
131
|
+
(virtual), `label_de`, `label_en` and `label_fr`, without a `label_de-CH`. The form's `field` and `schema_field` follow
|
|
132
|
+
the same setting.
|
|
133
|
+
|
|
134
|
+
Model fields are generated when the model class loads, so the setting must be in place before any model loads. An
|
|
135
|
+
initializer guarantees that. If the locales are only known later, pass a callable instead, which is evaluated every time
|
|
136
|
+
the content locales are needed: `Compony.content_locales = -> { MyApp.content_locales }`.
|
data/doc/index.html
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<div id="content"><div id='filecontents'>
|
|
61
61
|
<p><img src=“logo.svg” height=250 alt=“Compony logo”/></p>
|
|
62
62
|
|
|
63
|
-
<h1 id="
|
|
63
|
+
<h1 id="where-to-read-what">Where to read what</h1>
|
|
64
64
|
<ul><li>
|
|
65
65
|
<p>Get to know what Compony is and which concepts it uses in the <a href="https://github.com/kalsan/compony">README on GitHub</a>.</p>
|
|
66
66
|
</li><li>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
<p>There is also a <a href="https://www.rubydoc.info/github/kalsan/compony.git">RubyDoc page for Compony</a> available.</p>
|
|
70
70
|
</li></ul>
|
|
71
71
|
|
|
72
|
-
<h1 id="
|
|
72
|
+
<h1 id="about-compony">About Compony</h1>
|
|
73
73
|
|
|
74
74
|
<p>Compony is a Gem that allows you to write your Rails application in <strong>component-style</strong> fashion. It combines a controller action and route along with its view into a single Ruby class. Along with the DSL approach and a powerful model mixin, Compony <strong>makes your application’s code more semantic</strong>, allows writing <strong>much DRYer code</strong>, using inheritance even in views and <strong>much easier refactoring</strong> for your Rails applications, helping you to <strong>keep the code clean as the application evolves</strong>.</p>
|
|
75
75
|
|
|
@@ -95,15 +95,15 @@
|
|
|
95
95
|
<p>Compony uses <a href="https://github.com/CanCanCommunity/cancancan">CanCanCan</a> for authorization but does not provide an authentication mechanism. You can easily build your own by creating login/logout components that manage cookies, and configure Compony to enforce authentication using the <code>Compony.authentication_before_action</code> setter. I have also successfully tested Compony to work with <a href="https://github.com/heartcombo/devise">Devise</a>.</p>
|
|
96
96
|
</li></ul>
|
|
97
97
|
|
|
98
|
-
<h2 id="
|
|
98
|
+
<h2 id="state-of-the-project">State of the project</h2>
|
|
99
99
|
|
|
100
|
-
<p>I am actively using this framework in various applications and both performance and reliability are good. However, the project is experimental and lacking peer reviews and especially automatic testing, such as unit and integration tests. Also, expect there to be (<a href="/CHANGELOG_md.html">documented</a>) breaking changes in the future, as the API will likely be further refined, resulting in renamings and deprecation of various methods.</p>
|
|
100
|
+
<p>I am actively using this framework in various applications and both performance and reliability are good. However, the project is experimental and lacking peer reviews and especially extensive automatic testing, such as unit and integration tests. Also, expect there to be (<a href="/CHANGELOG_md.html">documented</a>) breaking changes in the future, as the API will likely be further refined, resulting in renamings and deprecation of various methods.</p>
|
|
101
101
|
|
|
102
|
-
<h2 id="
|
|
102
|
+
<h2 id="other-projects-exploring-similar-concepts">Other projects exploring similar concepts</h2>
|
|
103
103
|
|
|
104
104
|
<p>A project with a similar aim, but a different approach, is <a href="https://github.com/phlex-ruby/phlex">Phlex</a>.</p>
|
|
105
105
|
|
|
106
|
-
<h1 id="
|
|
106
|
+
<h1 id="guide--documentation">Guide / Documentation</h1>
|
|
107
107
|
|
|
108
108
|
<p>The following topics should help you get started. Please note that the links will not work in yard doc - read this on <a href="https://github.com/kalsan/compony">Compony’s GitHub repo</a>.</p>
|
|
109
109
|
<ul><li>
|
|
@@ -181,18 +181,28 @@
|
|
|
181
181
|
</li></ul>
|
|
182
182
|
</li></ul>
|
|
183
183
|
|
|
184
|
-
<h1 id="
|
|
184
|
+
<h1 id="contributing">Contributing</h1>
|
|
185
185
|
|
|
186
186
|
<p>Compony is Free Software under the LGPLv3 and you are most welcome to contribute to it.</p>
|
|
187
187
|
<ul><li>
|
|
188
|
-
<p>If you spotted a security vulnerability, <strong>do not open an issue</strong> but instead use the contact form at <a href="https://kalsan.ch/#contact">kalsan.ch/#contact</a> instead (English is just fine, even if the website is in German).</p>
|
|
188
|
+
<p>If you spotted a security vulnerability, <strong>do not open an issue</strong> but instead use the contact form at <a href="https://kalsan.ch/#contact">https://kalsan.ch/#contact</a> instead (English is just fine, even if the website is in German).</p>
|
|
189
189
|
</li><li>
|
|
190
190
|
<p>If you’d like to contribute feedback or discuss something, please open an issue.</p>
|
|
191
191
|
</li><li>
|
|
192
192
|
<p>If you have an idea that is worth implementing, please fork the repo, implement your changes in your own fork, and open a pull request.</p>
|
|
193
193
|
</li></ul>
|
|
194
194
|
|
|
195
|
-
<
|
|
195
|
+
<h2 id="running-the-tests">Running the tests</h2>
|
|
196
|
+
|
|
197
|
+
<p>The gem ships an RSpec suite backed by a minimal dummy Rails app (in-memory SQLite, no setup needed):</p>
|
|
198
|
+
|
|
199
|
+
<pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_bundle'>bundle</span> <span class='id identifier rubyid_install'>install</span>
|
|
200
|
+
<span class='id identifier rubyid_bundle'>bundle</span> <span class='id identifier rubyid_exec'>exec</span> <span class='id identifier rubyid_rspec'>rspec</span> <span class='comment'># or: bundle exec rake spec (also the default rake task)
|
|
201
|
+
</span></code></pre>
|
|
202
|
+
|
|
203
|
+
<p>See <a href="./spec/README_md.html">spec/README.md</a> for the suite’s layout and the reasons behind the Gemfile’s <code>rails</code> and <code>rack</code> version pins.</p>
|
|
204
|
+
|
|
205
|
+
<h1 id="caveats">Caveats</h1>
|
|
196
206
|
<ul><li>
|
|
197
207
|
<p>The API is not yet as consistent as I’d like it. Examples:</p>
|
|
198
208
|
</li><li>
|
|
@@ -212,7 +222,7 @@
|
|
|
212
222
|
<p>Compony is not compatible with <code>tailwindcss-rails</code>. This is likely due to Tailwind automatically removing any CSS that is not used by the application and the usage detection not picking up Compony components, as their content is not provided in views.</p>
|
|
213
223
|
</li></ul>
|
|
214
224
|
|
|
215
|
-
<h1 id="
|
|
225
|
+
<h1 id="acknowledgements">Acknowledgements</h1>
|
|
216
226
|
|
|
217
227
|
<p>A big thank you to Alex and Koni who have patiently listened to my weird ideas and helped me developing them further, resulting in a few of the key concepts of Compony, such as <code>param_name</code>, or the way forms are structured.</p>
|
|
218
228
|
|
|
@@ -220,7 +230,7 @@
|
|
|
220
230
|
</div></div>
|
|
221
231
|
|
|
222
232
|
<div id="footer">
|
|
223
|
-
Generated on
|
|
233
|
+
Generated on Wed Oct 7 10:48:16 2026 by
|
|
224
234
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
225
235
|
0.9.34 (ruby-3.4.9).
|
|
226
236
|
</div>
|
data/doc/method_list.html
CHANGED
|
@@ -364,6 +364,22 @@
|
|
|
364
364
|
</li>
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
<li class="odd ">
|
|
368
|
+
<div class="item">
|
|
369
|
+
<span class='object_link'><a href="Compony.html#content_locales-class_method" title="Compony.content_locales (method)">content_locales</a></span>
|
|
370
|
+
<small>Compony</small>
|
|
371
|
+
</div>
|
|
372
|
+
</li>
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
<li class="even ">
|
|
376
|
+
<div class="item">
|
|
377
|
+
<span class='object_link'><a href="Compony.html#content_locales=-class_method" title="Compony.content_locales= (method)">content_locales=</a></span>
|
|
378
|
+
<small>Compony</small>
|
|
379
|
+
</div>
|
|
380
|
+
</li>
|
|
381
|
+
|
|
382
|
+
|
|
367
383
|
<li class="odd ">
|
|
368
384
|
<div class="item">
|
|
369
385
|
<span class='object_link'><a href="Compony/RequestContext.html#controller-instance_method" title="Compony::RequestContext#controller (method)">#controller</a></span>
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
</div>
|
|
103
103
|
|
|
104
104
|
<div id="footer">
|
|
105
|
-
Generated on
|
|
105
|
+
Generated on Wed Oct 7 10:48:19 2026 by
|
|
106
106
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
107
107
|
0.9.34 (ruby-3.4.9).
|
|
108
108
|
</div>
|
|
@@ -106,7 +106,8 @@ module Compony
|
|
|
106
106
|
# DSL method (inside `form_fields`). Renders a simple_form input inferred from the model field `name`.
|
|
107
107
|
# Respects per-field CanCanCan authorization; skipped fields render nothing.
|
|
108
108
|
# @param name [Symbol,String] The model field (use the association name, not the `_id`, for associations).
|
|
109
|
-
# @param multilang [Boolean] If true, generates one suffixed input per
|
|
109
|
+
# @param multilang [Boolean] If true, generates one suffixed input per content locale and returns the array (useful with the "mobility" gem).
|
|
110
|
+
# See {Compony.content_locales}.
|
|
110
111
|
# @param input_opts [Hash] Passed to simple_form. Notable keys: `as:` (input type), `hidden: true`, `autofocus:`.
|
|
111
112
|
# @return [String,Array<String>] The input HTML (array when `multilang`).
|
|
112
113
|
# @api public
|
|
@@ -114,7 +115,7 @@ module Compony
|
|
|
114
115
|
fail("The `field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
|
|
115
116
|
|
|
116
117
|
if multilang
|
|
117
|
-
|
|
118
|
+
Compony.content_locales.map { |locale| field("#{name}_#{locale}", **input_opts) }
|
|
118
119
|
else
|
|
119
120
|
name = name.to_sym
|
|
120
121
|
|
|
@@ -215,12 +216,12 @@ module Compony
|
|
|
215
216
|
# DSL method, whitelists a single field of `data_class` in the param schema, auto-generating the correct schema line.
|
|
216
217
|
# Respects per-field CanCanCan authorization.
|
|
217
218
|
# @param field_name [Symbol,String] The model field (association name, not `_id`, for associations).
|
|
218
|
-
# @param multilang [Boolean] If true, whitelists one suffixed field per
|
|
219
|
+
# @param multilang [Boolean] If true, whitelists one suffixed field per content locale, see {Compony.content_locales} (useful with the "mobility" gem).
|
|
219
220
|
# @return [void]
|
|
220
221
|
# @api public
|
|
221
222
|
def schema_field(field_name, multilang: false)
|
|
222
223
|
if multilang
|
|
223
|
-
|
|
224
|
+
Compony.content_locales.each { |locale| schema_field("#{field_name}_#{locale}") }
|
|
224
225
|
else
|
|
225
226
|
# This runs upon component setup.
|
|
226
227
|
@schema_lines_for_data << proc do |data, controller|
|
|
@@ -72,6 +72,9 @@ module Compony
|
|
|
72
72
|
data = data.compact.map(&transform_block) if block_given?
|
|
73
73
|
return controller.helpers.safe_join(data.compact, ', ')
|
|
74
74
|
else
|
|
75
|
+
# A nil value has nothing to transform, and the transform blocks are written for actual values, thus they are
|
|
76
|
+
# not required to be nil safe. This mirrors the compact applied to enumerables above.
|
|
77
|
+
return nil if data.nil?
|
|
75
78
|
data = transform_block.call(data) if block_given?
|
|
76
79
|
return data
|
|
77
80
|
end
|
data/lib/compony/model_mixin.rb
CHANGED
|
@@ -19,11 +19,12 @@ module Compony
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# DSL method, defines a new field which will be translated and can be added to field groups
|
|
22
|
-
# If multilang is true, a suffixed field is generated for every
|
|
22
|
+
# If multilang is true, a suffixed field is generated for every content locale, along with a non-suffixed virtual field (useful with gem "mobility").
|
|
23
|
+
# See Compony.content_locales.
|
|
23
24
|
def field(name, type, multilang: false, **extra_attrs)
|
|
24
25
|
if multilang
|
|
25
26
|
field(name, type, virtual: true, **extra_attrs)
|
|
26
|
-
|
|
27
|
+
Compony.content_locales.each { |locale| field("#{name}_#{locale}", type, **extra_attrs) }
|
|
27
28
|
else
|
|
28
29
|
name = name.to_sym
|
|
29
30
|
self.fields = fields.dup
|
data/lib/compony.rb
CHANGED
|
@@ -35,6 +35,17 @@ module Compony
|
|
|
35
35
|
@model_field_namespaces = model_field_namespaces
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Setter for the content locales, i.e. the languages in which `multilang: true` fields exist (useful with gem "mobility").
|
|
39
|
+
# Set this when your app's `I18n.available_locales` differ from the languages your content is stored in, e.g. when
|
|
40
|
+
# using regional locales such as `:"de-CH"` while content is stored per language (`label_de`, `label_en`, ...).
|
|
41
|
+
# Model fields are generated when the model class loads, so set this in an initializer, before any model is loaded.
|
|
42
|
+
# Alternatively, pass a callable, which is evaluated every time the content locales are needed.
|
|
43
|
+
# @param content_locales [Array<Symbol,String>,#call] The content locales, or a callable returning them. Defaults to `I18n.available_locales`.
|
|
44
|
+
# @see {Compony#content_locales}
|
|
45
|
+
def self.content_locales=(content_locales)
|
|
46
|
+
@content_locales = content_locales
|
|
47
|
+
end
|
|
48
|
+
|
|
38
49
|
# Setter for the name of the Rails `before_action` that should be called to
|
|
39
50
|
# ensure that users are authenticated before accessing the component. For
|
|
40
51
|
# instance, implement a method `def enforce_authentication` in your
|
|
@@ -99,6 +110,14 @@ module Compony
|
|
|
99
110
|
return @model_field_namespaces ||= ['Compony::ModelFields']
|
|
100
111
|
end
|
|
101
112
|
|
|
113
|
+
# Getter for the content locales, defaults to `I18n.available_locales`.
|
|
114
|
+
# @return [Array<Symbol>] The locales for which `multilang: true` fields generate a suffixed field.
|
|
115
|
+
# @see Compony#content_locales= Explanation of content_locales (documented in the corresponding setter)
|
|
116
|
+
def self.content_locales
|
|
117
|
+
content_locales = @content_locales.respond_to?(:call) ? @content_locales.call : @content_locales
|
|
118
|
+
return (content_locales || I18n.available_locales).map(&:to_sym)
|
|
119
|
+
end
|
|
120
|
+
|
|
102
121
|
# Getter for the name of the Rails `before_action` that enforces authentication.
|
|
103
122
|
# @see Compony#authentication_before_action= Explanation of authentication_before_action (documented in the corresponding setter)
|
|
104
123
|
def self.authentication_before_action
|
data/spec/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Compony test suite
|
|
2
|
+
|
|
3
|
+
Run with `bundle exec rspec` (or `rake spec`).
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
- `spec/dummy` — a minimal Rails app (in-memory SQLite) hosting the gem, with a `User`
|
|
8
|
+
model, CRUD components under `app/components/users`, and plain components under
|
|
9
|
+
`app/components/statics`. The schema lives in `spec/dummy/db/schema.rb` and is loaded
|
|
10
|
+
fresh on boot by `spec/rails_helper.rb`.
|
|
11
|
+
- `spec/compony` — unit specs for the plain-Ruby parts (module API, intents, model mixin,
|
|
12
|
+
model fields, `MethodAccessibleHash`, `NaturalOrdering`, component DSL basics).
|
|
13
|
+
- `spec/requests` — request specs exercising routing, rendering, the pre-built CRUD
|
|
14
|
+
components (Index/Show/New/Edit/Destroy/Form), feasibility and authorization end-to-end.
|
|
15
|
+
|
|
16
|
+
## Version pins (Gemfile)
|
|
17
|
+
|
|
18
|
+
- `rack >= 3.1`: the pre-built components respond with the `:unprocessable_content`
|
|
19
|
+
status, which only exists from Rack 3.1.
|
|
20
|
+
- `rails ~> 7.2.1`: Rails 8.1 renamed the form helper `text_area` to `textarea`, which
|
|
21
|
+
collides with Dyny 0.0.3's `textarea` HTML tag helper on the view context and breaks
|
|
22
|
+
every simple_form textarea. Remove the pin once Dyny is fixed for Rails >= 8.1.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
RSpec.describe Compony::Component do
|
|
2
|
+
describe 'naming' do
|
|
3
|
+
it 'knows its comp and family name' do
|
|
4
|
+
expect(Components::Users::Show.comp_name).to eq('show')
|
|
5
|
+
expect(Components::Users::Show.family_name).to eq('users')
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe 'labelling' do
|
|
10
|
+
let(:user) { User.create!(name: 'Alice') }
|
|
11
|
+
|
|
12
|
+
it 'returns the long label by default' do
|
|
13
|
+
expect(Components::Users::Show.new.label(user)).to eq('Show user Alice')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the short label on demand' do
|
|
17
|
+
expect(Components::Users::Show.new.label(user, format: :short)).to eq('Show')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'applies label(:all) to both formats' do
|
|
21
|
+
comp = Components::Users::Index.new
|
|
22
|
+
expect(comp.label(format: :long)).to eq('Users')
|
|
23
|
+
expect(comp.label(format: :short)).to eq('Users')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe 'setup inheritance' do
|
|
28
|
+
it 'runs the parent setup before the child setup, letting children override' do
|
|
29
|
+
parent = Class.new(Compony::Component) do
|
|
30
|
+
setup do
|
|
31
|
+
label(:all) { 'Parent' }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
child = Class.new(parent) do
|
|
35
|
+
setup do
|
|
36
|
+
label(:all) { 'Child' }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
stub_const('Components::Tests::Parent', parent)
|
|
40
|
+
stub_const('Components::Tests::Child', child)
|
|
41
|
+
expect(child.new.label).to eq('Child')
|
|
42
|
+
expect(parent.new.label).to eq('Parent')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe 'standalone config' do
|
|
47
|
+
it 'stores path and verbs' do
|
|
48
|
+
config = Components::Users::Show.new.standalone_configs[nil] || Components::Users::Show.new.standalone_configs.values.first
|
|
49
|
+
expect(config[:path]).to eq('users/show/:id')
|
|
50
|
+
expect(config[:verbs].keys).to eq([:get])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'marks components without standalone as not standalone-routed' do
|
|
54
|
+
comp_class = Class.new(Compony::Component) do
|
|
55
|
+
setup do
|
|
56
|
+
label(:all) { 'Nested' }
|
|
57
|
+
content { div 'hi' }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
stub_const('Components::Tests::Nested', comp_class)
|
|
61
|
+
expect(comp_class.new.standalone_configs.values.pluck(:path).compact).to be_empty
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'records skip_authentication!' do
|
|
65
|
+
config = Components::Statics::Home.new.standalone_configs.values.first
|
|
66
|
+
expect(config[:skip_authentication]).to be(true)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe 'resourceful' do
|
|
71
|
+
it 'marks resourceful components' do
|
|
72
|
+
expect(Components::Users::Show.new.resourceful?).to be(true)
|
|
73
|
+
expect(Components::Statics::Home.new.resourceful?).to be(false)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'infers the data class from the family name' do
|
|
77
|
+
expect(Components::Users::Show.new.data_class).to eq(User)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
RSpec.describe Compony::Components::Form do
|
|
2
|
+
describe 'multilang' do
|
|
3
|
+
around do |example|
|
|
4
|
+
original_available_locales = I18n.available_locales
|
|
5
|
+
I18n.available_locales = %i[en de de-CH fr es]
|
|
6
|
+
Compony.content_locales = %i[de en fr es]
|
|
7
|
+
example.run
|
|
8
|
+
ensure
|
|
9
|
+
I18n.available_locales = original_available_locales
|
|
10
|
+
Compony.content_locales = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:form) { described_class.new }
|
|
14
|
+
|
|
15
|
+
it 'renders one field per content locale' do
|
|
16
|
+
form.instance_variable_set(:@simpleform, Object.new) # `field` refuses to run outside `form_fields`
|
|
17
|
+
allow(form).to receive(:field).and_call_original
|
|
18
|
+
allow(form).to receive(:field).with(a_string_starting_with('label_')) { |name| name }
|
|
19
|
+
expect(form.field(:label, multilang: true)).to eq(%w[label_de label_en label_fr label_es])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'whitelists one schema field per content locale' do
|
|
23
|
+
allow(form).to receive(:schema_field).and_call_original
|
|
24
|
+
form.send(:schema_field, :label, multilang: true) # DSL method, protected
|
|
25
|
+
%w[label_de label_en label_fr label_es].each do |name|
|
|
26
|
+
expect(form).to have_received(:schema_field).with(name)
|
|
27
|
+
end
|
|
28
|
+
expect(form).not_to have_received(:schema_field).with('label_de-CH')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
RSpec.describe Compony do
|
|
2
|
+
describe '.family_name_for' do
|
|
3
|
+
it 'underscores strings and symbols' do
|
|
4
|
+
expect(described_class.family_name_for('Users')).to eq('users')
|
|
5
|
+
expect(described_class.family_name_for(:Users)).to eq('users')
|
|
6
|
+
expect(described_class.family_name_for(:users)).to eq('users')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'derives the family from a model instance' do
|
|
10
|
+
expect(described_class.family_name_for(User.new)).to eq('users')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'derives the family from a model class' do
|
|
14
|
+
expect(described_class.family_name_for(User)).to eq('users')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '.comp_class_for' do
|
|
19
|
+
it 'resolves a component from comp and family symbols' do
|
|
20
|
+
expect(described_class.comp_class_for(:show, :users)).to eq(Components::Users::Show)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'resolves a component from a model instance' do
|
|
24
|
+
expect(described_class.comp_class_for(:show, User.new)).to eq(Components::Users::Show)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns nil for a missing component' do
|
|
28
|
+
expect(described_class.comp_class_for(:missing, :users)).to be_nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '.comp_class_for!' do
|
|
33
|
+
it 'fails for a missing component' do
|
|
34
|
+
expect { described_class.comp_class_for!(:missing, :users) }.to raise_error(RuntimeError, /No component found/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'resolves an existing component' do
|
|
38
|
+
expect(described_class.comp_class_for!(:edit, :users)).to eq(Components::Users::Edit)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '.intent' do
|
|
43
|
+
it 'builds an intent' do
|
|
44
|
+
intent = described_class.intent(:show, :users)
|
|
45
|
+
expect(intent).to be_a(Compony::Intent)
|
|
46
|
+
expect(intent.comp_class).to eq(Components::Users::Show)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'passes through an existing intent' do
|
|
50
|
+
intent = described_class.intent(:show, :users)
|
|
51
|
+
expect(described_class.intent(intent)).to be(intent)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '.path' do
|
|
56
|
+
it 'generates the path for a component with a model' do
|
|
57
|
+
user = User.create!(name: 'Alice')
|
|
58
|
+
expect(described_class.path(:show, user)).to eq("/users/show/#{user.id}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'generates the path for a component without model' do
|
|
62
|
+
expect(described_class.path(:index, :users)).to eq('/users')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '.button_component_class' do
|
|
67
|
+
it 'returns the default css button class' do
|
|
68
|
+
expect(described_class.button_component_class).to eq(Compony::Components::Buttons::CssButton)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns the link class for :link' do
|
|
72
|
+
expect(described_class.button_component_class(:link)).to eq(Compony::Components::Buttons::Link)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'fails for an unknown style' do
|
|
76
|
+
expect { described_class.button_component_class(:nope) }.to raise_error(RuntimeError, /Unknown button style/)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe '.default_button_style' do
|
|
81
|
+
it 'defaults to :css_button' do
|
|
82
|
+
expect(described_class.default_button_style).to eq(:css_button)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '.model_field_class_for' do
|
|
87
|
+
it 'resolves built-in field classes' do
|
|
88
|
+
expect(described_class.model_field_class_for('String')).to eq(Compony::ModelFields::String)
|
|
89
|
+
expect(described_class.model_field_class_for('Date')).to eq(Compony::ModelFields::Date)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'fails for unknown field types' do
|
|
93
|
+
expect { described_class.model_field_class_for('Nope') }.to raise_error(RuntimeError, /No `model_field_namespace`/)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '.model_field_namespaces' do
|
|
98
|
+
it 'defaults to the built-in namespace' do
|
|
99
|
+
expect(described_class.model_field_namespaces).to eq(['Compony::ModelFields'])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '.content_locales' do
|
|
104
|
+
around do |example|
|
|
105
|
+
original_available_locales = I18n.available_locales
|
|
106
|
+
example.run
|
|
107
|
+
ensure
|
|
108
|
+
I18n.available_locales = original_available_locales
|
|
109
|
+
described_class.content_locales = nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'defaults to I18n.available_locales' do
|
|
113
|
+
I18n.available_locales = %i[en de]
|
|
114
|
+
expect(described_class.content_locales).to eq(%i[en de])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns the configured locales as symbols' do
|
|
118
|
+
described_class.content_locales = %w[de en]
|
|
119
|
+
expect(described_class.content_locales).to eq(%i[de en])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'evaluates a callable every time' do
|
|
123
|
+
locales = %i[de]
|
|
124
|
+
described_class.content_locales = -> { locales }
|
|
125
|
+
expect(described_class.content_locales).to eq(%i[de])
|
|
126
|
+
locales = %i[de fr]
|
|
127
|
+
expect(described_class.content_locales).to eq(%i[de fr])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|