compony 0.7.1 → 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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +10 -14
  5. data/VERSION +1 -1
  6. data/compony.gemspec +4 -4
  7. data/doc/ComponentGenerator.html +1 -1
  8. data/doc/Components.html +1 -1
  9. data/doc/ComponentsGenerator.html +1 -1
  10. data/doc/Compony/Component.html +193 -457
  11. data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
  12. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
  13. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +3 -3
  14. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
  15. data/doc/Compony/ComponentMixins/Default/Standalone.html +187 -1
  16. data/doc/Compony/ComponentMixins/Default.html +1 -1
  17. data/doc/Compony/ComponentMixins/Resourceful.html +2 -2
  18. data/doc/Compony/ComponentMixins.html +1 -1
  19. data/doc/Compony/Components/Button.html +2 -2
  20. data/doc/Compony/Components/Buttons/CssButton.html +282 -0
  21. data/doc/Compony/Components/Buttons/Link.html +252 -0
  22. data/doc/Compony/Components/Buttons.html +126 -0
  23. data/doc/Compony/Components/Destroy.html +11 -11
  24. data/doc/Compony/Components/Edit.html +14 -14
  25. data/doc/Compony/Components/Form.html +100 -100
  26. data/doc/Compony/Components/Index.html +2 -2
  27. data/doc/Compony/Components/List.html +3 -3
  28. data/doc/Compony/Components/New.html +2 -2
  29. data/doc/Compony/Components/Show.html +24 -24
  30. data/doc/Compony/Components/WithForm.html +3 -3
  31. data/doc/Compony/Components.html +5 -3
  32. data/doc/Compony/ControllerMixin.html +2 -2
  33. data/doc/Compony/Engine.html +1 -1
  34. data/doc/Compony/ExposedIntentsDsl.html +403 -0
  35. data/doc/Compony/Intent.html +1503 -0
  36. data/doc/Compony/MethodAccessibleHash.html +1 -1
  37. data/doc/Compony/ModelFields/Anchormodel.html +1 -1
  38. data/doc/Compony/ModelFields/Association.html +2 -2
  39. data/doc/Compony/ModelFields/Attachment.html +1 -1
  40. data/doc/Compony/ModelFields/Base.html +1 -1
  41. data/doc/Compony/ModelFields/Boolean.html +1 -1
  42. data/doc/Compony/ModelFields/Color.html +1 -1
  43. data/doc/Compony/ModelFields/Currency.html +1 -1
  44. data/doc/Compony/ModelFields/Date.html +1 -1
  45. data/doc/Compony/ModelFields/Datetime.html +1 -1
  46. data/doc/Compony/ModelFields/Decimal.html +1 -1
  47. data/doc/Compony/ModelFields/Email.html +1 -1
  48. data/doc/Compony/ModelFields/Float.html +1 -1
  49. data/doc/Compony/ModelFields/Integer.html +1 -1
  50. data/doc/Compony/ModelFields/Percentage.html +1 -1
  51. data/doc/Compony/ModelFields/Phone.html +1 -1
  52. data/doc/Compony/ModelFields/RichText.html +1 -1
  53. data/doc/Compony/ModelFields/String.html +1 -1
  54. data/doc/Compony/ModelFields/Text.html +1 -1
  55. data/doc/Compony/ModelFields/Time.html +1 -1
  56. data/doc/Compony/ModelFields/Url.html +1 -1
  57. data/doc/Compony/ModelFields.html +1 -1
  58. data/doc/Compony/ModelMixin.html +1 -1
  59. data/doc/Compony/NaturalOrdering.html +1 -1
  60. data/doc/Compony/RequestContext.html +177 -14
  61. data/doc/Compony/Version.html +1 -1
  62. data/doc/Compony/ViewHelpers.html +15 -272
  63. data/doc/Compony/VirtualModel.html +1 -1
  64. data/doc/Compony.html +303 -837
  65. data/doc/ComponyController.html +1 -1
  66. data/doc/_index.html +30 -2
  67. data/doc/class_list.html +1 -1
  68. data/doc/file.README.html +11 -18
  69. data/doc/guide/basic_component.md +12 -8
  70. data/doc/guide/example.md +17 -17
  71. data/doc/guide/feasibility.md +4 -2
  72. data/doc/guide/generators.md +4 -2
  73. data/doc/guide/inheritance.md +4 -2
  74. data/doc/guide/installation.md +4 -2
  75. data/doc/guide/intents.md +167 -0
  76. data/doc/guide/internal_datastructures.md +4 -2
  77. data/doc/guide/model_fields.md +4 -2
  78. data/doc/guide/nesting.md +5 -3
  79. data/doc/guide/ownership.md +5 -3
  80. data/doc/guide/pre_built_components/destroy.md +3 -3
  81. data/doc/guide/pre_built_components/edit.md +1 -1
  82. data/doc/guide/pre_built_components/form.md +1 -1
  83. data/doc/guide/pre_built_components/index.md +1 -1
  84. data/doc/guide/pre_built_components/list.md +1 -1
  85. data/doc/guide/pre_built_components/new.md +2 -2
  86. data/doc/guide/pre_built_components/show.md +1 -1
  87. data/doc/guide/pre_built_components/with_form.md +1 -1
  88. data/doc/guide/pre_built_components.md +4 -3
  89. data/doc/guide/resourceful.md +5 -3
  90. data/doc/guide/standalone.md +10 -2
  91. data/doc/guide/virtual_models.md +4 -2
  92. data/doc/index.html +11 -18
  93. data/doc/method_list.html +273 -161
  94. data/doc/top-level-namespace.html +1 -1
  95. data/lib/compony/component.rb +19 -48
  96. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +2 -2
  97. data/lib/compony/component_mixins/default/standalone.rb +16 -0
  98. data/lib/compony/component_mixins/resourceful.rb +1 -1
  99. data/lib/compony/components/buttons/css_button.rb +32 -0
  100. data/lib/compony/components/buttons/link.rb +31 -0
  101. data/lib/compony/components/destroy.rb +9 -8
  102. data/lib/compony/components/edit.rb +5 -4
  103. data/lib/compony/components/form.rb +7 -1
  104. data/lib/compony/components/index.rb +2 -2
  105. data/lib/compony/components/list.rb +4 -4
  106. data/lib/compony/components/new.rb +1 -1
  107. data/lib/compony/components/show.rb +8 -11
  108. data/lib/compony/components/with_form.rb +1 -1
  109. data/lib/compony/exposed_intents_dsl.rb +29 -0
  110. data/lib/compony/intent.rb +145 -0
  111. data/lib/compony/model_fields/association.rb +1 -1
  112. data/lib/compony/request_context.rb +21 -0
  113. data/lib/compony/view_helpers.rb +5 -48
  114. data/lib/compony.rb +63 -149
  115. metadata +12 -6
  116. data/doc/guide/helpers.md +0 -156
  117. data/doc/guide/pre_built_components/button.md +0 -8
  118. data/doc/guide/root_actions.md +0 -67
  119. data/lib/compony/components/button.rb +0 -61
@@ -4,55 +4,12 @@ module Compony
4
4
  # Rule of thumb: this holds methods that require a view context and results are rendered immediately.
5
5
  # @see Compony Compony for standalone/pure helpers
6
6
  module ViewHelpers
7
- # Use this in your application layout to render all actions of the current root component.
8
- def compony_actions
9
- return nil unless Compony.root_comp
10
- Compony.root_comp.render_actions(self, wrapper_class: 'root-actions', action_class: 'root-action')
11
- end
12
-
13
- # Renders a link to a component given a comp and model or family. If authentication is configured
7
+ # Renders a button/link to a component given a comp and model or family. If authentication is configured
14
8
  # and the current user has insufficient permissions to access the target object, the link is not displayed.
15
- # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
16
- # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
17
- # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
18
- # `Users`, `'Users'`, `:users`, `User.first`
19
- # @param link_args [Array] Positional arguments that will be passed to the Rails `link_to` helper
20
- # @param label_opts [Hash] Options hash that will be passed to the label method (see {Compony::ComponentMixins::Default::Labelling#label})
21
- # @param link_kwargs [Hash] Named arguments that will be passed to the Rails `link_to` helper
22
- def compony_link(comp_name_or_cst_or_class,
23
- model_or_family_name_or_cst = nil,
24
- *link_args,
25
- label: nil,
26
- label_opts: {},
27
- params: {},
28
- feasibility_action: nil,
29
- feasibility_target: nil,
30
- standalone_name: nil,
31
- **link_kwargs)
32
- model = model_or_family_name_or_cst.respond_to?(:model_name) ? model_or_family_name_or_cst : nil
33
- if comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
34
- target_comp_instance = comp_name_or_cst_or_class.new(data: model)
35
- else
36
- target_comp_instance = Compony.comp_class_for!(comp_name_or_cst_or_class, model_or_family_name_or_cst).new(data: model)
37
- end
38
- return unless target_comp_instance.standalone_access_permitted_for?(self, standalone_name:)
39
- feasibility_action ||= comp_name_or_cst_or_class.to_s.underscore.to_sym
40
- feasibility_target ||= model
41
- label ||= target_comp_instance.label(model, **label_opts)
42
- path ||= Compony.path(target_comp_instance.comp_name, target_comp_instance.family_name, model, standalone_name:, **params)
43
- if feasibility_target && !feasibility_target.feasible?(feasibility_action)
44
- path = '#'
45
- link_kwargs[:class] = link_kwargs[:class].is_a?(String) ? "#{link_kwargs[:class]} disabled" : 'disabled'
46
- link_kwargs[:title] = feasibility_target.full_feasibility_messages(feasibility_action).presence
47
- end
48
- return helpers.link_to(label, path, *link_args, **link_kwargs)
49
- end
50
-
51
- # Given a component and a family/model, this instanciates and renders a button component.
52
- # @see Compony#button Check Compony.button for accepted params
53
- # @see Compony::Components::Button Compony::Components::Button: the default underlying implementation
54
- def compony_button(...)
55
- Compony.button(...).render(helpers.controller)
9
+ # When inside a request context (`content do...`), this is preceded by {RequestContext#render_intent}.
10
+ # @param button [Hash] Parameters that will be given to the button component initializer.
11
+ def render_intent(*, button: {}, **)
12
+ Compony.intent(*, **).render(self, **button)
56
13
  end
57
14
  end
58
15
  end
data/lib/compony.rb CHANGED
@@ -3,17 +3,25 @@
3
3
  # the setters, create an initializer `config/initializers/compony.rb` and call
4
4
  # them from there.
5
5
  # @see Compony::ViewHelpers Compony::ViewHelpers for helpers that require a view context and render results immediately
6
+ # @see Compony::RequestContext Compony::RequestContext for helpers that require a view context and render results immediately
6
7
  module Compony
7
8
  ##########=====-------
8
9
  # Configuration writers
9
10
  ##########=====-------
10
11
 
11
- # Setter for the global button component class. This allows you to implement a
12
- # custom button component and have all Compony button helpers use your custom
13
- # button component instead of {Compony::Components::Button}.
14
- # @param button_component_class [String] Name of your custom button component class (inherit from {Compony::Components::Button} or {Compony::Component})
15
- def self.button_component_class=(button_component_class)
16
- @button_component_class = button_component_class
12
+ # Adds a button style that can be referred to when rendering an intent.
13
+ # @param name [Symbol] Name of the style. If it exists already, will override the style.
14
+ # @param button_component_class [String] String with the class name of the button component that will be instanciated to render the intent.
15
+ def self.register_button_style(name, button_component_class_name)
16
+ @button_component_class_names ||= {}
17
+ @button_component_class_names[name.to_sym] = button_component_class_name
18
+ end
19
+
20
+ # Setter for the default button style. Defaults to :css_button.
21
+ # @param default_button_style [Symbol] Name of the style that should be used as default.
22
+ # @see {Compony#default_button_style}
23
+ def self.default_button_style=(default_button_style)
24
+ @default_button_style = default_button_style
17
25
  end
18
26
 
19
27
  # Setter for the global field namespaces. This allows you to implement custom
@@ -58,12 +66,31 @@ module Compony
58
66
  # Configuration readers
59
67
  ##########=====-------
60
68
 
61
- # Getter for the global button component class.
62
- # @see Compony#button_component_class= Explanation of button_component_class (documented in the corresponding setter)
63
- def self.button_component_class
64
- @button_component_class ||= Components::Button
65
- @button_component_class = const_get(@button_component_class) if @button_component_class.is_a?(String)
66
- return @button_component_class
69
+ # Getter for the button component class for a given style.
70
+ # @param style [Symbol] Style for which the matching button component class should be returned. Defaults to {Compony.default_button_style}.
71
+ # @see {Compony#register_button_style}
72
+ # @see {Compony#default_button_style}
73
+ def self.button_component_class(style = default_button_style)
74
+ # Lazy initialize
75
+ if @button_component_classes.nil?
76
+ @button_component_classes = {
77
+ css_button: Compony::Components::Buttons::CssButton,
78
+ link: Compony::Components::Buttons::Link
79
+ }.merge(@button_component_class_names&.transform_values(&:constantize) || {})
80
+ @button_component_classes.each_value do |button_component_class|
81
+ unless button_component_class.is_a?(Class) && button_component_class < Compony::Component
82
+ fail("Expected a button component class, got #{button_component_class.inspect}")
83
+ end
84
+ end
85
+ end
86
+ # Retrieval
87
+ return @button_component_classes[style&.to_sym] || fail("Unknown button style #{style.inspect}. Use one of: #{@button_component_classes.keys.inspect}")
88
+ end
89
+
90
+ # Getter for the default button style, defaults to `:css_button`.
91
+ # @see {Compony#default_button_style=}
92
+ def self.default_button_style
93
+ return @default_button_style || :css_button
67
94
  end
68
95
 
69
96
  # Getter for the global field namespaces.
@@ -94,123 +121,38 @@ module Compony
94
121
  # Application-wide available pure helpers
95
122
  ##########=====-------
96
123
 
124
+ # Pure helper to create a Compony Intent. If given an intent, will return it unchanged. Otherwise, will give all params to the intent initializer.
125
+ def self.intent(intent_or_comp_args, ...)
126
+ if intent_or_comp_args.is_a?(Intent)
127
+ return intent_or_comp_args
128
+ else
129
+ return Intent.new(intent_or_comp_args, ...)
130
+ end
131
+ end
132
+
97
133
  # Generates a Rails path to a component. Examples: `Compony.path(:index, :users)`, `Compony.path(:show, User.first)`
98
- # @param comp_name_or_cst_or_class [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
99
- # or can also pass a component class (such as Components::Users::Show)
100
- # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
101
- # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
102
- # `Users`, `'Users'`, `:users`, `User.first`
103
- # @param standalone_name [Symbol,nil] Name of the standalone config to point to (defaults to nil the default standalone config).
104
- # @param args_for_path_helper [Array] Positional arguments passed to the Rails helper
105
- # @param kwargs_for_path_helper [Hash] Named arguments passed to the Rails helper. If a model is given to `model_or_family_name_or_cst`,
106
- # the param `id` defaults to the passed model's ID.
107
- def self.path(comp_name_or_cst_or_class, model_or_family_name_or_cst = nil, *args_for_path_helper, standalone_name: nil, **kwargs_for_path_helper)
108
- # Extract model if any, to get the ID
109
- model = model_or_family_name_or_cst.respond_to?(:model_name) ? model_or_family_name_or_cst : nil
110
- comp_class = if comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
111
- comp_name_or_cst_or_class
112
- else
113
- comp_class_for!(comp_name_or_cst_or_class, model_or_family_name_or_cst)
114
- end
115
- return comp_class.new.path(model, *args_for_path_helper, standalone_name:, **kwargs_for_path_helper)
134
+ # The first two arguments are given to create an {Intent} and all subsequend args and all kwargs are given to {Intent#path}
135
+ def self.path(comp_name_or_cst_or_class, model_or_family_name_or_cst = nil, ...)
136
+ intent(comp_name_or_cst_or_class, model_or_family_name_or_cst).path(...)
116
137
  end
117
138
 
118
139
  # Given a component and a family/model, this returns the matching component class if any, or nil if the component does not exist.
119
- # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
120
- # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
121
- # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
122
- # `Users`, `'Users'`, `:users`, `User.first`
123
- def self.comp_class_for(comp_name_or_cst, model_or_family_name_or_cst)
124
- family_cst_str = family_name_for(model_or_family_name_or_cst).camelize
125
- comp_cst_str = comp_name_or_cst.to_s.camelize
126
- return nil unless ::Components.const_defined?(family_cst_str)
127
- family_constant = ::Components.const_get(family_cst_str)
128
- return nil unless family_constant.const_defined?(comp_cst_str)
129
- return family_constant.const_get(comp_cst_str)
140
+ # @see Intent for allowed parameters.
141
+ def self.comp_class_for(...)
142
+ return intent(...).comp_class
143
+ rescue NameError
144
+ return nil
130
145
  end
131
146
 
132
147
  # Same as Compony#comp_class_for but fails if none found
148
+ # @see Intent for allowed parameters.
133
149
  # @see Compony#comp_class_for
134
- def self.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst)
135
- comp_class_for(comp_name_or_cst, model_or_family_name_or_cst) || fail(
150
+ def self.comp_class_for!(...)
151
+ comp_class_for(...) || fail(
136
152
  "No component found for [#{comp_name_or_cst.inspect}, #{model_or_family_name_or_cst.inspect}]"
137
153
  )
138
154
  end
139
155
 
140
- # Given a component and a family, this returns the name of the Rails URL helper returning the path to this component.<br>
141
- # The parameters are the same as for {Compony#rails_action_name}.<br>
142
- # Example usage: `send("#{path_helper_name(:index, :users)}_url)`
143
- # @see Compony#path
144
- # @see Compony#rails_action_name rails_action_name for the accepted params
145
- def self.path_helper_name(...)
146
- "#{rails_action_name(...)}_comp"
147
- end
148
-
149
- # Given a component and a family or a component class, this returns the name of the ComponyController action for this component.<br>
150
- # Optionally can pass a name for extra standalone configs.
151
- # @param comp_name_or_cst [String,Symbol] Name of the component the action points to.
152
- # @param model_or_family_name_or_cst [String,Symbol] Name of the family the action points to.
153
- # @param name [String,Symbol] If referring to an extra standalone entrypoint, specify its name using this param.
154
- # @see Compony#path
155
- def self.rails_action_name(comp_name_or_cst_or_class, model_or_family_name_or_cst, name = nil)
156
- if comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
157
- comp_class = comp_name_or_cst_or_class
158
- comp_name_or_cst_or_class = comp_class.comp_name
159
- model_or_family_name_or_cst = comp_class.family_name
160
- end
161
- [name.presence, comp_name_or_cst_or_class.to_s.underscore, family_name_for(model_or_family_name_or_cst)].compact.join('_')
162
- end
163
-
164
- # Given a component and a family/model, this instanciates and returns a button component.
165
- # @param comp_name_or_cst_or_class [String,Symbol,Class] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
166
- # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
167
- # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
168
- # `Users`, `'Users'`, `:users`, `User.first`
169
- # @param label_opts [Hash] Options hash that will be passed to the label method (see {Compony::ComponentMixins::Default::Labelling#label})
170
- # @param params [Hash] GET parameters to be inclued into the path this button points to. Special case: e.g. format: :pdf -> some.url/foo/bar.pdf
171
- # @param feasibility_action [Symbol] Name of the feasibility action that should be checked for this button, defaults to the component name
172
- # @param feasibility_target [Symbol] Name of the feasibility target (subject) that the feasibility should be checked on, defaults to the model if given
173
- # @param standalone_name [Symbol,nil] Name of the standalone config to point to (defaults to nil the default standalone config).
174
- # @param override_kwargs [Hash] Override button options, see options for {Compony::Components::Button}
175
- # @see Compony::ViewHelpers#compony_button View helper providing a wrapper for this method that immediately renders a button.
176
- # @see Compony::Components::Button Compony::Components::Button: the default underlying implementation
177
- def self.button(comp_name_or_cst_or_class,
178
- model_or_family_name_or_cst = nil,
179
- label_opts: nil,
180
- params: nil,
181
- feasibility_action: nil,
182
- feasibility_target: nil,
183
- method: nil,
184
- standalone_name: nil,
185
- **override_kwargs)
186
- label_opts ||= button_defaults[:label_opts] || {}
187
- params ||= button_defaults[:params] || {}
188
- model = model_or_family_name_or_cst.respond_to?(:model_name) ? model_or_family_name_or_cst : nil
189
- if comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
190
- target_comp_instance = comp_name_or_cst_or_class.new(data: model)
191
- else
192
- target_comp_instance = Compony.comp_class_for!(comp_name_or_cst_or_class, model_or_family_name_or_cst).new(data: model)
193
- end
194
- feasibility_action ||= button_defaults[:feasibility_action] || comp_name_or_cst_or_class.to_s.underscore.to_sym
195
- feasibility_target ||= button_defaults[:feasibility_target] || model
196
- options = {
197
- label: target_comp_instance.label(model, **label_opts),
198
- icon: target_comp_instance.icon,
199
- color: target_comp_instance.color,
200
- path: Compony.path(target_comp_instance.comp_name, target_comp_instance.family_name, model, standalone_name:, **params),
201
- method:,
202
- visible: ->(controller) { target_comp_instance.standalone_access_permitted_for?(controller, standalone_name:, verb: method) }
203
- }
204
- if feasibility_target
205
- options.merge!({
206
- enabled: feasibility_target.feasible?(feasibility_action),
207
- title: feasibility_target.full_feasibility_messages(feasibility_action).presence
208
- })
209
- end
210
- options.merge!(override_kwargs.symbolize_keys)
211
- return Compony.button_component_class.new(**options.symbolize_keys)
212
- end
213
-
214
156
  # Returns the current root component, if any
215
157
  def self.root_comp
216
158
  RequestStore.store[:compony_root_comp]
@@ -228,37 +170,6 @@ module Compony
228
170
  end
229
171
  end
230
172
 
231
- # Getter for current button defaults
232
- # @todo document params
233
- def self.button_defaults
234
- RequestStore.store[:button_defaults] || {}
235
- end
236
-
237
- # Overwrites the keys of the current button defaults by the ones provided during the execution of a given block and restores them afterwords.
238
- # This method is useful when the same set of options is to be given to a multitude of buttons.
239
- # @param keys_to_overwrite [Hash] Options that should be given to the buttons within the block, with their values
240
- # @param block [Block] Within this block, all omitted button options point to `keys_to_overwrite`
241
- def self.with_button_defaults(**keys_to_overwrite, &block)
242
- # Lazy initialize butto_defaults store if it hasn't been yet
243
- RequestStore.store[:button_defaults] ||= {}
244
- keys_to_overwrite.transform_keys!(&:to_sym)
245
- old_values = {}
246
- newly_defined_keys = keys_to_overwrite.keys - RequestStore.store[:button_defaults].keys
247
- keys_to_overwrite.each do |key, new_value|
248
- # Assign new value
249
- old_values[key] = RequestStore.store[:button_defaults][key]
250
- RequestStore.store[:button_defaults][key] = new_value
251
- end
252
- return_value = block.call
253
- # Restore previous value
254
- keys_to_overwrite.each_key do |key|
255
- RequestStore.store[:button_defaults][key] = old_values[key]
256
- end
257
- # Undefine keys that were not there previously
258
- newly_defined_keys.each { |key| RequestStore.store[:button_defaults].delete(key) }
259
- return return_value
260
- end
261
-
262
173
  # Goes through model_field_namespaces and returns the first hit for the given constant
263
174
  # @param constant [Constant] The constant that is searched, e.g. RichText -> would return e.g. Compony::ModelFields::RichText
264
175
  def self.model_field_class_for(constant)
@@ -279,6 +190,7 @@ require 'request_store'
279
190
  require 'schemacop'
280
191
  require 'simple_form'
281
192
 
193
+ require 'compony/intent'
282
194
  require 'compony/engine'
283
195
  require 'compony/model_fields/base'
284
196
  require 'compony/model_fields/anchormodel'
@@ -306,8 +218,10 @@ require 'compony/component_mixins/default/standalone/verb_dsl'
306
218
  require 'compony/component_mixins/default/standalone/resourceful_verb_dsl'
307
219
  require 'compony/component_mixins/default/labelling'
308
220
  require 'compony/component_mixins/resourceful'
221
+ require 'compony/exposed_intents_dsl'
309
222
  require 'compony/component'
310
- require 'compony/components/button'
223
+ require 'compony/components/buttons/link'
224
+ require 'compony/components/buttons/css_button'
311
225
  require 'compony/components/index'
312
226
  require 'compony/components/list'
313
227
  require 'compony/components/show'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compony
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Kalbermatter
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-11-20 00:00:00.000000000 Z
12
+ date: 2025-11-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: yard
@@ -203,6 +203,9 @@ files:
203
203
  - doc/Compony/ComponentMixins/Resourceful.html
204
204
  - doc/Compony/Components.html
205
205
  - doc/Compony/Components/Button.html
206
+ - doc/Compony/Components/Buttons.html
207
+ - doc/Compony/Components/Buttons/CssButton.html
208
+ - doc/Compony/Components/Buttons/Link.html
206
209
  - doc/Compony/Components/Destroy.html
207
210
  - doc/Compony/Components/Edit.html
208
211
  - doc/Compony/Components/Form.html
@@ -213,6 +216,8 @@ files:
213
216
  - doc/Compony/Components/WithForm.html
214
217
  - doc/Compony/ControllerMixin.html
215
218
  - doc/Compony/Engine.html
219
+ - doc/Compony/ExposedIntentsDsl.html
220
+ - doc/Compony/Intent.html
216
221
  - doc/Compony/MethodAccessibleHash.html
217
222
  - doc/Compony/ModelFields.html
218
223
  - doc/Compony/ModelFields/Anchormodel.html
@@ -254,15 +259,14 @@ files:
254
259
  - doc/guide/example.md
255
260
  - doc/guide/feasibility.md
256
261
  - doc/guide/generators.md
257
- - doc/guide/helpers.md
258
262
  - doc/guide/inheritance.md
259
263
  - doc/guide/installation.md
264
+ - doc/guide/intents.md
260
265
  - doc/guide/internal_datastructures.md
261
266
  - doc/guide/model_fields.md
262
267
  - doc/guide/nesting.md
263
268
  - doc/guide/ownership.md
264
269
  - doc/guide/pre_built_components.md
265
- - doc/guide/pre_built_components/button.md
266
270
  - doc/guide/pre_built_components/destroy.md
267
271
  - doc/guide/pre_built_components/edit.md
268
272
  - doc/guide/pre_built_components/form.md
@@ -272,7 +276,6 @@ files:
272
276
  - doc/guide/pre_built_components/show.md
273
277
  - doc/guide/pre_built_components/with_form.md
274
278
  - doc/guide/resourceful.md
275
- - doc/guide/root_actions.md
276
279
  - doc/guide/standalone.md
277
280
  - doc/guide/virtual_models.md
278
281
  - doc/imgs/intro-example-destroy.png
@@ -297,7 +300,8 @@ files:
297
300
  - lib/compony/component_mixins/default/standalone/standalone_dsl.rb
298
301
  - lib/compony/component_mixins/default/standalone/verb_dsl.rb
299
302
  - lib/compony/component_mixins/resourceful.rb
300
- - lib/compony/components/button.rb
303
+ - lib/compony/components/buttons/css_button.rb
304
+ - lib/compony/components/buttons/link.rb
301
305
  - lib/compony/components/destroy.rb
302
306
  - lib/compony/components/edit.rb
303
307
  - lib/compony/components/form.rb
@@ -308,6 +312,8 @@ files:
308
312
  - lib/compony/components/with_form.rb
309
313
  - lib/compony/controller_mixin.rb
310
314
  - lib/compony/engine.rb
315
+ - lib/compony/exposed_intents_dsl.rb
316
+ - lib/compony/intent.rb
311
317
  - lib/compony/method_accessible_hash.rb
312
318
  - lib/compony/model_fields/anchormodel.rb
313
319
  - lib/compony/model_fields/association.rb
data/doc/guide/helpers.md DELETED
@@ -1,156 +0,0 @@
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
- ```
@@ -1,8 +0,0 @@
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).
@@ -1,67 +0,0 @@
1
- [Back to the guide](/README.md#guide)
2
-
3
- # Compony root actions
4
-
5
- The word "actions" is heavily overused, so here is a disambiguation:
6
-
7
- - Rails controller actions: a method that is implemented in a Rails controller
8
- - CanCanCan actions: the first method to CanCanCan's `can?` method
9
- - Compony root actions: buttons that point to other components
10
-
11
- At this point, Compony actions are a loose concept, which will likely be refined in the future. Currently, Compony actions are defined as buttons, rendered by the application layout, that point to other components. They provide context-sensitive buttons to your application.
12
-
13
- ## Defining and manipulating root actions
14
-
15
- In addition to regular buttons that are rendered as part of the content blocks, components can expose root actions with the `actions` call. Root actions will only be rendered if the component they are defined in is currently the root component.
16
-
17
- To have a component expose a root action, call the method `action` in a `setup` block and return a Compony button:
18
-
19
- ```ruby
20
- setup do
21
- action :edit do
22
- Compony.button(:edit, @data)
23
- end
24
-
25
- action :destroy do
26
- Compony.button(:destroy, @data)
27
- end
28
- end
29
- ```
30
-
31
- The name of the action ("edit" and "destroy" in the example above) allows you to refer to that action in a component inheriting from this one:
32
-
33
- ```ruby
34
- # Assuming that this component inherits from the example above
35
- setup do
36
- skip_action :destroy
37
-
38
- action :overview, before: :edit do
39
- Compony.button(:index, :users, label: 'Overview')
40
- end
41
- end
42
- ```
43
-
44
- In this example, two actions will be shown: overview and edit.
45
-
46
- An action button can be disabled through the [feasibility framework](./feasibility.md). However, it can also instead be hidden completely by returning nil from within the action block:
47
-
48
- ```ruby
49
- action :edit do
50
- next if @data.locked?
51
- Compony.button(:edit, @data)
52
- end
53
- ```
54
-
55
- The action in this example will be skipped entirely if `locked?` returns true.
56
-
57
- ## Displaying root actions
58
-
59
- Root actions are not shown by default in Compony because layouting is up to you. In order to display the root component's actions, add the following view helper call to your layout:
60
-
61
- ```erb
62
- <%# layouts/application.html.erb %>
63
- ...
64
- <%= compony_actions %>
65
- ```
66
-
67
- If there is currently no root component, or if the root component defines no actions, this does nothing. However, if there are root actions available, the Compony buttons returned by the root component will be rendered.