compony 0.2.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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