compony 0.2.3 → 0.3.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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +90 -13
  4. data/VERSION +1 -1
  5. data/compony.gemspec +4 -4
  6. data/doc/ComponentGenerator.html +1 -1
  7. data/doc/Components.html +1 -1
  8. data/doc/ComponentsGenerator.html +1 -1
  9. data/doc/Compony/Component.html +396 -334
  10. data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
  11. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
  12. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +1 -1
  13. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
  14. data/doc/Compony/ComponentMixins/Default/Standalone.html +1 -1
  15. data/doc/Compony/ComponentMixins/Default.html +1 -1
  16. data/doc/Compony/ComponentMixins/Resourceful.html +1 -1
  17. data/doc/Compony/ComponentMixins.html +1 -1
  18. data/doc/Compony/Components/Button.html +3 -3
  19. data/doc/Compony/Components/Destroy.html +15 -15
  20. data/doc/Compony/Components/Edit.html +19 -19
  21. data/doc/Compony/Components/Form.html +87 -87
  22. data/doc/Compony/Components/New.html +15 -15
  23. data/doc/Compony/Components/WithForm.html +4 -4
  24. data/doc/Compony/Components.html +1 -1
  25. data/doc/Compony/ControllerMixin.html +1 -1
  26. data/doc/Compony/Engine.html +1 -1
  27. data/doc/Compony/MethodAccessibleHash.html +1 -1
  28. data/doc/Compony/ModelFields/Anchormodel.html +1 -1
  29. data/doc/Compony/ModelFields/Association.html +1 -1
  30. data/doc/Compony/ModelFields/Attachment.html +1 -1
  31. data/doc/Compony/ModelFields/Base.html +1 -1
  32. data/doc/Compony/ModelFields/Boolean.html +1 -1
  33. data/doc/Compony/ModelFields/Color.html +1 -1
  34. data/doc/Compony/ModelFields/Currency.html +1 -1
  35. data/doc/Compony/ModelFields/Date.html +1 -1
  36. data/doc/Compony/ModelFields/Datetime.html +1 -1
  37. data/doc/Compony/ModelFields/Decimal.html +1 -1
  38. data/doc/Compony/ModelFields/Email.html +1 -1
  39. data/doc/Compony/ModelFields/Float.html +1 -1
  40. data/doc/Compony/ModelFields/Integer.html +1 -1
  41. data/doc/Compony/ModelFields/Percentage.html +1 -1
  42. data/doc/Compony/ModelFields/Phone.html +1 -1
  43. data/doc/Compony/ModelFields/RichText.html +1 -1
  44. data/doc/Compony/ModelFields/String.html +1 -1
  45. data/doc/Compony/ModelFields/Text.html +1 -1
  46. data/doc/Compony/ModelFields/Time.html +1 -1
  47. data/doc/Compony/ModelFields/Url.html +1 -1
  48. data/doc/Compony/ModelFields.html +1 -1
  49. data/doc/Compony/ModelMixin.html +1 -1
  50. data/doc/Compony/NaturalOrdering.html +300 -0
  51. data/doc/Compony/RequestContext.html +84 -1
  52. data/doc/Compony/Version.html +1 -1
  53. data/doc/Compony/ViewHelpers.html +1 -1
  54. data/doc/Compony.html +3 -3
  55. data/doc/ComponyController.html +1 -1
  56. data/doc/_index.html +8 -1
  57. data/doc/class_list.html +1 -1
  58. data/doc/file.README.html +82 -16
  59. data/doc/index.html +82 -16
  60. data/doc/method_list.html +169 -161
  61. data/doc/top-level-namespace.html +1 -1
  62. data/lib/compony/component.rb +40 -52
  63. data/lib/compony/components/destroy.rb +9 -1
  64. data/lib/compony/components/edit.rb +2 -4
  65. data/lib/compony/components/form.rb +16 -5
  66. data/lib/compony/components/new.rb +5 -6
  67. data/lib/compony/components/with_form.rb +1 -1
  68. data/lib/compony/natural_ordering.rb +60 -0
  69. data/lib/compony/request_context.rb +14 -0
  70. data/lib/compony.rb +1 -0
  71. metadata +4 -2
@@ -102,7 +102,7 @@
102
102
  </div>
103
103
 
104
104
  <div id="footer">
105
- Generated on Sat Apr 27 10:22:11 2024 by
105
+ Generated on Fri May 31 14:28:29 2024 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.2.2).
108
108
  </div>
@@ -7,6 +7,7 @@ module Compony
7
7
 
8
8
  attr_reader :parent_comp
9
9
  attr_reader :comp_opts
10
+ attr_reader :content_blocks # needed in RequestContext for nesting
10
11
 
11
12
  # root comp: component that is registered to be root of the application.
12
13
  # parent comp: component that is registered to be the parent of this comp. If there is none, this is the root comp.
@@ -24,9 +25,9 @@ module Compony
24
25
  @sub_comps = []
25
26
  @index = index
26
27
  @comp_opts = comp_opts
27
- @before_render_block = nil
28
- @content_blocks = []
29
- @actions = []
28
+ @before_render_blocks = NaturalOrdering.new
29
+ @content_blocks = NaturalOrdering.new
30
+ @actions = NaturalOrdering.new
30
31
  @skipped_actions = Set.new
31
32
 
32
33
  init_standalone
@@ -111,46 +112,51 @@ module Compony
111
112
  comp_cst.to_s.underscore
112
113
  end
113
114
 
114
- # @todo deprecate (check for usages beforehand)
115
- def comp_class_for(...)
116
- Compony.comp_class_for(...)
117
- end
118
-
119
- # @todo deprecate (check for usages beforehand)
120
- def comp_class_for!(...)
121
- Compony.comp_class_for!(...)
122
- end
123
-
124
115
  # DSL method
125
- def before_render(&block)
126
- @before_render_block = block
116
+ # Adds or overrides a before_render block.
117
+ # You can use controller.redirect_to to redirect away and halt the before_render/content chain
118
+ # @param [Symbol,String] name The name of the before_render block, defaults to `:main`
119
+ # @param [nil,Symbol,String] before If nil, the block will be added to the bottom of the before_render chain. Otherwise, pass the name of another block.
120
+ # @param [Proc] block The block that should be run as part of the before_render pipeline. Will run in the component's context.
121
+ def before_render(name = :main, before: nil, **kwargs, &block)
122
+ fail("`before_render` expects a block in #{inspect}.") unless block_given?
123
+ @before_render_blocks.natural_push(name, block, before:, **kwargs)
127
124
  end
128
125
 
129
126
  # DSL method
130
- # Overrides previous content (also from superclasses). Will be the first content block to run.
131
- # You can use dyny here.
132
- def content(&block)
133
- fail("`content` expects a block in #{inspect}.") unless block_given?
134
- @content_blocks = [block]
127
+ # Adds or overrides a content block.
128
+ # @param [Symbol,String] name The name of the content block, defaults to `:main`
129
+ # @param [nil,Symbol,String] before If nil, the block will be added to the bottom of the content chain. Otherwise, pass the name of another block.
130
+ # @param [Hash] kwargs If hidden is true, the content will not be rendered by default, allowing you to nest it in another content block.
131
+ # @param [Proc] block The block that should be run as part of the content pipeline. Will run in the component's context. You can use Dyny here.
132
+ def content(name = :main, before: nil, **kwargs, &block)
133
+ # A block is required here, but if this is an override (e.g. to hide another content block), we can tolerate the missing block.
134
+ if !block_given? && @content_blocks.find { |b| b.name == name }.nil?
135
+ fail("`content` expects a block in #{inspect}.")
136
+ end
137
+ @content_blocks.natural_push(name, block || :missing, before:, **kwargs)
135
138
  end
136
139
 
137
140
  # DSL method
138
- # Adds a content block that will be executed after all previous ones.
139
- # It is safe to use this method even if `content` has never been called
140
- # You can use dyny here.
141
- def add_content(index = -1, &block)
142
- fail("`content` expects a block in #{inspect}.") unless block_given?
143
- @content_blocks ||= []
144
- @content_blocks.insert(index, block)
141
+ # Removes a content block. Use this in subclasses if a content block defined in the parent should be removed from the child.
142
+ # @param [Symbol,String] name Name of the content block that should be removed
143
+ def remove_content(name)
144
+ name = name.to_sym
145
+ existing_index = @content_blocks.find_index { |el| el.name == name } || fail("Content block #{name.inspect} not found for removal in #{inspect}.")
146
+ @content_blocks.delete_at(existing_index)
145
147
  end
146
148
 
147
149
  # Renders the component using the controller passsed to it and returns it as a string.
148
150
  # @param [Boolean] standalone pass true iff `render` is called from `render_standalone`
149
151
  # Do not overwrite.
150
152
  def render(controller, standalone: false, **locals)
151
- # Call before_render hook if any and backfire instance variables back to the component
152
- # TODO: Can .request_context be removed from the next line? Test well!
153
- RequestContext.new(self, controller, locals:).request_context.evaluate_with_backfire(&@before_render_block) if @before_render_block
153
+ # Call before_render hooks (if any) and backfire instance variables back to the component
154
+ @before_render_blocks.each do |element|
155
+ RequestContext.new(self, controller, locals:).evaluate_with_backfire(&element.payload)
156
+ # Stop if a `before_render` block issued a body (e.g. through redirecting)
157
+ break unless controller.response_body.nil?
158
+ end
159
+
154
160
  # Render, unless before_render has already issued a body (e.g. through redirecting).
155
161
  if controller.response_body.nil?
156
162
  fail "#{self.class.inspect} must define `content` or set a response body in `before_render`" if @content_blocks.none?
@@ -161,9 +167,9 @@ module Compony
161
167
  if Compony.content_before_root_comp_block && standalone
162
168
  Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&Compony.content_before_root_comp_block)
163
169
  end
164
- content_blocks.each do |block|
170
+ content_blocks.reject{ |el| el.hidden }.each do |element|
165
171
  # Instanciate and evaluate a fresh RequestContext in order to use the buffer allocated by the ActionView (needed for `concat` calls)
166
- Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&block)
172
+ Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&element.payload)
167
173
  end
168
174
  if Compony.content_after_root_comp_block && standalone
169
175
  Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&Compony.content_after_root_comp_block)
@@ -179,25 +185,7 @@ module Compony
179
185
  # Adds or replaces an action (for action buttons)
180
186
  # If before: is specified, will insert the action before the named action. When replacing, an element keeps its position unless before: is specified.
181
187
  def action(action_name, before: nil, &block)
182
- action_name = action_name.to_sym
183
- before_name = before&.to_sym
184
- action = MethodAccessibleHash.new(name: action_name, block:)
185
-
186
- existing_index = @actions.find_index { |el| el.name == action_name }
187
- if existing_index.present? && before_name.present?
188
- @actions.delete_at(existing_index) # Replacing an existing element with a before: directive - must delete before calculating indices
189
- end
190
- if before_name.present?
191
- before_index = @actions.find_index { |el| el.name == before_name } || fail("Action #{before_name} for :before not found in #{inspect}.")
192
- end
193
-
194
- if before_index.present?
195
- @actions.insert(before_index, action)
196
- elsif existing_index.present?
197
- @actions[existing_index] = action
198
- else
199
- @actions << action
200
- end
188
+ @actions.natural_push(action_name, block, before:)
201
189
  end
202
190
 
203
191
  # DSL method
@@ -213,7 +201,7 @@ module Compony
213
201
  button_htmls = @actions.map do |action|
214
202
  next if @skipped_actions.include?(action.name)
215
203
  Compony.with_button_defaults(feasibility_action: action.name.to_sym) do
216
- action_button = action.block.call(controller)
204
+ action_button = action.payload.call(controller)
217
205
  next unless action_button
218
206
  button_html = action_button.render(controller)
219
207
  next if button_html.blank?
@@ -26,8 +26,11 @@ module Compony
26
26
  icon { :trash }
27
27
  color { :danger }
28
28
 
29
- content do
29
+ content :confirm_question, hidden: true do
30
30
  div I18n.t('compony.components.destroy.confirm_question', data_label: @data.label)
31
+ end
32
+
33
+ content :confirm_button, hidden: true do
31
34
  div do
32
35
  concat compony_button(comp_cst,
33
36
  @data,
@@ -36,6 +39,11 @@ module Compony
36
39
  end
37
40
  end
38
41
 
42
+ content do
43
+ content :confirm_question
44
+ content :confirm_button
45
+ end
46
+
39
47
  action :back_to_owner do
40
48
  next if data_class.owner_model_attr.blank?
41
49
  Compony.button(:show, @data.send(data_class.owner_model_attr), icon: :xmark, color: :secondary, label: I18n.t('compony.cancel'))
@@ -50,10 +50,8 @@ module Compony
50
50
  end
51
51
  hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
52
52
  end
53
- schema.validate!(controller.request.params)
54
-
55
- # TODO: Why are we not saving the validated params?
56
- attrs_to_assign = controller.request.params[form_comp.schema_wrapper_key_for(@data)]
53
+ validated_params = schema.validate!(controller.request.params)
54
+ attrs_to_assign = validated_params[form_comp.schema_wrapper_key_for(@data)]
57
55
  @data.assign_attributes(attrs_to_assign) if attrs_to_assign
58
56
  end
59
57
 
@@ -13,19 +13,30 @@ module Compony
13
13
  # Make sure the error message is going to be nice if form_fields were not implemented
14
14
  fail "#{component.inspect} requires config.form_fields do ..." if @form_fields.nil?
15
15
 
16
- # Must render the buttons now as the rendering within simple form breaks the form
17
- @submit_button = Compony.button_component_class.new(
18
- label: @submit_label || I18n.t('compony.components.form.submit'), icon: 'arrow-right', type: :submit
19
- ).render(controller)
16
+ # Calculate paths
20
17
  @submit_path = @comp_opts[:submit_path]
21
18
  @submit_path = @submit_path.call(controller) if @submit_path.respond_to?(:call)
22
19
  end
23
20
 
21
+ # Override this to provide a custom submit button
22
+ content :submit_button, hidden: true do
23
+ concat Compony.button_component_class.new(
24
+ label: @submit_label || I18n.t('compony.components.form.submit'), icon: 'arrow-right', type: :submit
25
+ ).render(controller)
26
+ end
27
+
28
+ # Override this to provide additional submit buttons.
29
+ content :buttons, hidden: true do
30
+ content(:submit_button)
31
+ end
32
+
24
33
  content do
25
34
  form_html = simple_form_for(data, method: @comp_opts[:submit_verb], url: @submit_path) do |f|
26
35
  component.with_simpleform(f) do
27
36
  instance_exec(&form_fields)
28
- div @submit_button, class: 'compony-form-buttons'
37
+ div class: 'compony-form-buttons' do
38
+ content(:buttons)
39
+ end
29
40
  end
30
41
  end
31
42
  concat form_html
@@ -32,10 +32,11 @@ module Compony
32
32
  label(:short) { I18n.t('compony.components.new.label.short') }
33
33
  icon { :plus }
34
34
 
35
- add_content do
35
+ content :label do
36
36
  h2 component.label
37
37
  end
38
- add_content do
38
+
39
+ content do
39
40
  concat form_comp.render(controller, data: @data)
40
41
  end
41
42
 
@@ -45,10 +46,8 @@ module Compony
45
46
  schema = Schemacop::Schema3.new :hash, additional_properties: true do
46
47
  hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
47
48
  end
48
- schema.validate!(controller.request.params)
49
-
50
- # TODO: Why are we not saving the validated params?
51
- attrs_to_assign = controller.request.params[form_comp.schema_wrapper_key_for(@data)]
49
+ validated_params = schema.validate!(controller.request.params)
50
+ attrs_to_assign = validated_params[form_comp.schema_wrapper_key_for(@data)]
52
51
  @data.assign_attributes(attrs_to_assign) if attrs_to_assign
53
52
  end
54
53
 
@@ -13,7 +13,7 @@ module Compony
13
13
  # Returns an instance of the form component responsible for rendering the form.
14
14
  # Feel free to override this in subclasses.
15
15
  def form_comp
16
- @form_comp ||= (form_comp_class || comp_class_for!(:form, family_cst)).new(
16
+ @form_comp ||= (form_comp_class || Compony.comp_class_for!(:form, family_cst)).new(
17
17
  self,
18
18
  submit_verb:,
19
19
  # If applicable, Rails adds the route keys automatically, thus, e.g. :id does not need to be passed here, as it comes from the request.
@@ -0,0 +1,60 @@
1
+ module Compony
2
+ # @api description
3
+ # This class provides an array-based data structure where elements have symbol names. New elements can be appended or placed at a location using `before:`.
4
+ # Important: do not mutate this class with any other method call than the natural_-prefixed methods defined below.
5
+ # Example:<br>
6
+ # ```ruby
7
+ # collection = Compony::NaturalOrdering.new
8
+ # collection.natural_push(:a, a_payload)
9
+ # collection.natural_push(:c, c_payload)
10
+ # collection.natural_push(:b, b_payload, before: :c)
11
+ # collection.natural_push(:d, d_payload, hidden: true)
12
+ # collection.natural_push(:a, a_new_payload) # overwrites :a
13
+ #
14
+ # collection.reject{|el| el.hidden}.map(&:name) # --> :a, :b, :c
15
+ # collection.map(&:payload) # --> a_new_payload, b_payload, c_payload, d_payload
16
+ # ```
17
+ class NaturalOrdering < Array
18
+ def natural_push(name, payload = :missing, before: nil, **kwargs)
19
+ name = name.to_sym
20
+ before_name = before&.to_sym
21
+ old_kwargs = {}
22
+ old_payload = nil
23
+
24
+ # Fetch existing element if any
25
+ existing_index = find_index { |el| el.name == name }
26
+ if existing_index.present?
27
+ # Copy all non-mentionned kwargs from the element we are about to overwrite
28
+ old_kwargs = self[existing_index].except(:name, :payload)
29
+ old_payload = self[existing_index].payload
30
+
31
+ # Replacing an existing element with a before: directive - must delete before calculating indices
32
+ if before_name.present?
33
+ delete_at(existing_index)
34
+ end
35
+ elsif payload == :missing
36
+ fail("Cannot insert new element #{name} without a payload (payload can only omitted if overriding another element) in #{inspect}.")
37
+ end
38
+
39
+ # Fetch before element
40
+ if before_name.present?
41
+ before_index = find_index { |el| el.name == before_name } || fail("Element #{before_name.inspect} for :before not found in #{inspect}.")
42
+ end
43
+
44
+ # Create the element to insert
45
+ element = MethodAccessibleHash.new(name:, payload: payload == :missing ? old_payload : payload, **old_kwargs.merge(kwargs))
46
+
47
+ # Insert new element
48
+ if before_index.present?
49
+ # Insert before another element
50
+ insert(before_index, element)
51
+ elsif existing_index.present?
52
+ # Override another element
53
+ self[existing_index] = element
54
+ else
55
+ # Append at the end
56
+ self << element
57
+ end
58
+ end
59
+ end
60
+ end
@@ -41,5 +41,19 @@ module Compony
41
41
  return true if @local_assigns.key?(method)
42
42
  return super
43
43
  end
44
+
45
+ # Renders a content block from the current component.
46
+ def content(name)
47
+ name = name.to_sym
48
+ content_block = component.content_blocks.find { |el| el.name == name } || fail("Content block #{name.inspect} not found in #{component.inspect}.")
49
+ # We have to clear Rails' output_buffer to prevent double rendering of blocks. To achieve this, a fresh render context is instanciated.
50
+ concat controller.render_to_string(
51
+ type: :dyny,
52
+ locals: { render_component: component, render_controller: controller, render_locals: local_assigns, render_block: content_block },
53
+ inline: <<~RUBY
54
+ Compony::RequestContext.new(render_component, render_controller, helpers: self, locals: local_assigns).evaluate_with_backfire(&render_block.payload)
55
+ RUBY
56
+ )
57
+ end
44
58
  end
45
59
  end
data/lib/compony.rb CHANGED
@@ -300,6 +300,7 @@ require 'compony/components/new'
300
300
  require 'compony/components/edit'
301
301
  require 'compony/components/destroy'
302
302
  require 'compony/method_accessible_hash'
303
+ require 'compony/natural_ordering'
303
304
  require 'compony/model_mixin'
304
305
  require 'compony/request_context'
305
306
  require 'compony/version'
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.2.3
4
+ version: 0.3.1
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: 2024-04-27 00:00:00.000000000 Z
12
+ date: 2024-05-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: yard
@@ -233,6 +233,7 @@ files:
233
233
  - doc/Compony/ModelFields/Time.html
234
234
  - doc/Compony/ModelFields/Url.html
235
235
  - doc/Compony/ModelMixin.html
236
+ - doc/Compony/NaturalOrdering.html
236
237
  - doc/Compony/RequestContext.html
237
238
  - doc/Compony/Version.html
238
239
  - doc/Compony/ViewHelpers.html
@@ -297,6 +298,7 @@ files:
297
298
  - lib/compony/model_fields/time.rb
298
299
  - lib/compony/model_fields/url.rb
299
300
  - lib/compony/model_mixin.rb
301
+ - lib/compony/natural_ordering.rb
300
302
  - lib/compony/request_context.rb
301
303
  - lib/compony/version.rb
302
304
  - lib/compony/view_helpers.rb