compony 0.9.0 → 0.10.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  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 +2 -2
  11. data/doc/Compony/ComponentMixins/Default/Labelling.html +120 -39
  12. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
  13. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +1 -1
  14. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
  15. data/doc/Compony/ComponentMixins/Default/Standalone.html +1 -1
  16. data/doc/Compony/ComponentMixins/Default.html +1 -1
  17. data/doc/Compony/ComponentMixins/Resourceful.html +1 -1
  18. data/doc/Compony/ComponentMixins.html +1 -1
  19. data/doc/Compony/Components/Buttons/CssButton.html +1 -1
  20. data/doc/Compony/Components/Buttons/Link.html +1 -1
  21. data/doc/Compony/Components/Buttons.html +1 -1
  22. data/doc/Compony/Components/Destroy.html +13 -13
  23. data/doc/Compony/Components/Edit.html +17 -17
  24. data/doc/Compony/Components/Form.html +49 -49
  25. data/doc/Compony/Components/Index.html +1 -1
  26. data/doc/Compony/Components/List.html +346 -473
  27. data/doc/Compony/Components/New.html +13 -13
  28. data/doc/Compony/Components/Show.html +28 -28
  29. data/doc/Compony/Components/WithForm.html +1 -1
  30. data/doc/Compony/Components.html +1 -1
  31. data/doc/Compony/ControllerMixin.html +1 -1
  32. data/doc/Compony/Engine.html +1 -1
  33. data/doc/Compony/ExposedIntentsDsl.html +1 -1
  34. data/doc/Compony/Intent.html +79 -65
  35. data/doc/Compony/MethodAccessibleHash.html +1 -1
  36. data/doc/Compony/ModelFields/Anchormodel.html +1 -1
  37. data/doc/Compony/ModelFields/Association.html +1 -1
  38. data/doc/Compony/ModelFields/Attachment.html +1 -1
  39. data/doc/Compony/ModelFields/Base.html +1 -1
  40. data/doc/Compony/ModelFields/Boolean.html +1 -1
  41. data/doc/Compony/ModelFields/Color.html +1 -1
  42. data/doc/Compony/ModelFields/Currency.html +1 -1
  43. data/doc/Compony/ModelFields/Date.html +1 -1
  44. data/doc/Compony/ModelFields/Datetime.html +1 -1
  45. data/doc/Compony/ModelFields/Decimal.html +1 -1
  46. data/doc/Compony/ModelFields/Email.html +1 -1
  47. data/doc/Compony/ModelFields/Float.html +1 -1
  48. data/doc/Compony/ModelFields/Integer.html +1 -1
  49. data/doc/Compony/ModelFields/Percentage.html +1 -1
  50. data/doc/Compony/ModelFields/Phone.html +1 -1
  51. data/doc/Compony/ModelFields/RichText.html +1 -1
  52. data/doc/Compony/ModelFields/String.html +1 -1
  53. data/doc/Compony/ModelFields/Text.html +1 -1
  54. data/doc/Compony/ModelFields/Time.html +1 -1
  55. data/doc/Compony/ModelFields/Url.html +1 -1
  56. data/doc/Compony/ModelFields.html +1 -1
  57. data/doc/Compony/ModelMixin.html +1 -1
  58. data/doc/Compony/NaturalOrdering.html +1 -1
  59. data/doc/Compony/RequestContext.html +22 -10
  60. data/doc/Compony/Version.html +1 -1
  61. data/doc/Compony/ViewHelpers.html +1 -1
  62. data/doc/Compony/VirtualModel.html +1 -1
  63. data/doc/Compony.html +8 -8
  64. data/doc/ComponyController.html +1 -1
  65. data/doc/_index.html +8 -8
  66. data/doc/class_list.html +1 -1
  67. data/doc/file.README.html +2 -2
  68. data/doc/guide/nesting.md +26 -4
  69. data/doc/index.html +2 -2
  70. data/doc/method_list.html +174 -182
  71. data/doc/top-level-namespace.html +1 -1
  72. data/lib/compony/component.rb +1 -1
  73. data/lib/compony/component_mixins/default/labelling.rb +28 -17
  74. data/lib/compony/components/destroy.rb +2 -5
  75. data/lib/compony/components/edit.rb +2 -4
  76. data/lib/compony/components/form.rb +0 -1
  77. data/lib/compony/components/index.rb +1 -1
  78. data/lib/compony/components/list.rb +57 -62
  79. data/lib/compony/components/new.rb +0 -1
  80. data/lib/compony/components/show.rb +4 -11
  81. data/lib/compony/intent.rb +10 -4
  82. data/lib/compony/{exposed_intents_dsl.rb → manage_intents_dsl.rb} +6 -3
  83. data/lib/compony/model_mixin.rb +1 -1
  84. data/lib/compony/request_context.rb +10 -3
  85. data/lib/compony.rb +4 -4
  86. metadata +3 -3
@@ -102,7 +102,7 @@
102
102
  </div>
103
103
 
104
104
  <div id="footer">
105
- Generated on Fri Nov 28 12:38:44 2025 by
105
+ Generated on Mon Dec 8 15:19:19 2025 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.3.5).
108
108
  </div>
@@ -223,7 +223,7 @@ module Compony
223
223
  # Build the declared intents
224
224
  return @exposed_intents if @exposed_intents
225
225
  @exposed_intents = NaturalOrdering.new
226
- @exposed_intent_blocks.each { |block| ExposedIntentsDsl.new(@exposed_intents).evaluate(&block) } # alters @exposed_intents
226
+ @exposed_intent_blocks.each { |block| ManageIntentsDsl.new(@exposed_intents).evaluate(&block) } # alters @exposed_intents
227
227
  return @exposed_intents.map!(&:payload)
228
228
  end
229
229
  end
@@ -53,23 +53,35 @@ module Compony
53
53
  end
54
54
  end
55
55
 
56
- # DSL method and accessor for an icon.
57
- # While this is not used in Compony directly, this is useful if your front-end uses an icon library such as fontawesome.
58
- def icon(&block)
59
- if block_given?
60
- @icon_block = block
61
- else
62
- @icon_block.call
63
- end
56
+ # DSL method
57
+ # Defines defaults for intents when rendering buttons. Just like in {label}, the block may be given a resource.
58
+ # @param [Symbol] keyword The name of the keyword that should be given to the button by the intent if not overwritten
59
+ # @param [Proc] block The block that, when called in the context of the component while rendering, returns the value for the arg given to the button.
60
+ # @see {Compony::Component#button_defaults}
61
+ def button(keyword, &block)
62
+ fail("Please pass a block to `button` in #{inspect}.") unless block_given?
63
+ @button_blocks ||= {}
64
+ @button_blocks[keyword.to_sym] = block
64
65
  end
65
66
 
66
- # DSL method and accessor
67
- # While this is not used in Compony directly, this is useful if you use a custom button component class that supports colors.
68
- def color(&block)
69
- if block_given?
70
- @color_block = block
71
- else
72
- @color_block.call
67
+ # Executes and retrieves the button blocks
68
+ # If this component is resourceful, give the block the resource. Expect the arity to match.
69
+ # @param resource Pass the resource if and only if the component is resourceful.
70
+ def button_defaults(resource = nil)
71
+ return @button_blocks.to_h do |keyword, block|
72
+ value = case block.arity
73
+ when 0
74
+ block.call
75
+ when 1
76
+ resource ||= data
77
+ if resource.blank?
78
+ fail("Button block #{keyword.inspect} of #{inspect} takes a resource, but none was provided and a call to `data` did not return any.")
79
+ end
80
+ block.call(resource)
81
+ else
82
+ fail "#{inspect} has a button block #{keyword.inspect} that takes 2 or more arguments, which is unsupported."
83
+ end
84
+ next [keyword, value]
73
85
  end
74
86
  end
75
87
 
@@ -81,8 +93,7 @@ module Compony
81
93
  long: -> { "#{I18n.t(family_name.humanize)}: #{I18n.t(comp_name.humanize)}" },
82
94
  short: -> { I18n.t(comp_name.humanize) }
83
95
  }
84
- @icon_block = -> { :'arrow-right' }
85
- @color_block = -> { :primary }
96
+ @button_blocks = {}
86
97
  end
87
98
  end
88
99
  end
@@ -23,8 +23,6 @@ module Compony
23
23
 
24
24
  label(:long) { |data| I18n.t('compony.components.destroy.label.long', data_label: data.label) }
25
25
  label(:short) { |_| I18n.t('compony.components.destroy.label.short') }
26
- icon { :trash }
27
- color { :danger }
28
26
 
29
27
  content :confirm_question, hidden: true do
30
28
  div I18n.t('compony.components.destroy.confirm_question', data_label: @data.label)
@@ -47,9 +45,8 @@ module Compony
47
45
  exposed_intents do
48
46
  if data_class.owner_model_attr
49
47
  add :show, @data.send(data_class.owner_model_attr),
50
- button: { icon: :xmark, color: :secondary },
51
- label: I18n.t('compony.cancel'),
52
- name: :back_to_owner
48
+ label: I18n.t('compony.cancel'),
49
+ name: :back_to_owner
53
50
  end
54
51
  end
55
52
 
@@ -29,16 +29,14 @@ module Compony
29
29
 
30
30
  label(:long) { |data| I18n.t('compony.components.edit.label.long', data_label: data.label) }
31
31
  label(:short) { |_| I18n.t('compony.components.edit.label.short') }
32
- icon { :pencil }
33
32
 
34
33
  form_cancancan_action :edit
35
34
 
36
35
  exposed_intents do
37
36
  if data_class.owner_model_attr
38
37
  add :show, @data.send(data_class.owner_model_attr),
39
- button: { icon: :xmark, color: :secondary },
40
- label: I18n.t('compony.cancel'),
41
- name: :back_to_owner
38
+ label: I18n.t('compony.cancel'),
39
+ name: :back_to_owner
42
40
  end
43
41
  end
44
42
 
@@ -28,7 +28,6 @@ module Compony
28
28
  # Fake submit button rendered by a button component and submitting the form via JS:
29
29
  concat Compony.button_component_class.new(
30
30
  label: @submit_label || I18n.t('compony.components.form.submit'),
31
- icon: 'arrow-right',
32
31
  href: '#',
33
32
  onclick: "this.closest('form').requestSubmit(); return false;"
34
33
  ).render(controller)
@@ -19,7 +19,7 @@ module Compony
19
19
  end
20
20
 
21
21
  exposed_intents do
22
- if Compony.comp_class_for(:new, data_class)
22
+ unless data_class.owner_model_attr
23
23
  add :new, data_class.model_name.plural, name: :new
24
24
  end
25
25
  end
@@ -25,19 +25,25 @@ module Compony
25
25
  skip_sorting_in_filter: false,
26
26
  skip_sorting_links: false,
27
27
  skip_columns: [],
28
- skip_row_actions: [],
28
+ skip_row_intents: [],
29
29
  skip_filters: [],
30
30
  default_sorting: 'id asc',
31
31
  **)
32
- @pagination = !skip_pagination
32
+ @pagination = true
33
+ @skip_pagination = !!skip_pagination
33
34
  @results_per_page = results_per_page
34
- @filtering = !skip_filtering
35
- @sorting_in_filter = !skip_sorting && !skip_sorting_in_filter
36
- @sorting_links = !skip_sorting && !skip_sorting_links
35
+ @filtering = true
36
+ @skip_filtering = !!skip_filtering
37
+ @sorting = true
38
+ @sorting_in_filter = true
39
+ @sorting_links = true
40
+ @skip_sorting_in_filter = !!skip_sorting || !!skip_sorting_in_filter
41
+ @skip_sorting_links = !!skip_sorting || !!skip_sorting_links
37
42
  @columns = Compony::NaturalOrdering.new
38
- @row_actions = Compony::NaturalOrdering.new
43
+ @row_intent_blocks = []
39
44
  @skipped_columns = skip_columns.map(&:to_sym)
40
- @skipped_row_actions = skip_row_actions.map(&:to_sym)
45
+ @skipped_row_intents = skip_row_intents.is_a?(Enumerable) ? skip_row_intents.map(&:to_sym) : []
46
+ @skip_row_intents = skip_row_intents.is_a?(TrueClass)
41
47
  @filters = Compony::NaturalOrdering.new
42
48
  @sorts = Compony::NaturalOrdering.new
43
49
  @skipped_filters = skip_filters.map(&:to_sym)
@@ -50,9 +56,9 @@ module Compony
50
56
  end
51
57
 
52
58
  # DSL method
53
- # Disables pagination (caution: all records will be loaded).
54
- def skip_pagination!
55
- @pagination = false
59
+ # Enables or disables pagination (caution: all records will be loaded).
60
+ def pagination(new_pagination)
61
+ @pagination = !!new_pagination
56
62
  end
57
63
 
58
64
  # DSL method
@@ -62,28 +68,28 @@ module Compony
62
68
  end
63
69
 
64
70
  # DSL method
65
- # Disables filtering entirely (sorting is independent of this setting).
66
- def skip_filtering!
67
- @filtering = false
71
+ # Enables or disables filtering entirely (sorting is independent of this setting).
72
+ def filtering(new_filtering)
73
+ @filtering = !!new_filtering
68
74
  end
69
75
 
70
76
  # DSL method
71
- # Disables sorting entirely (both links and sorting input in filter).
72
- def skip_sorting!
73
- @sorting_in_filter = false
74
- @sorting_links = false
77
+ # Enables or disables sorting entirely (both links and sorting input in filter).
78
+ def sorting(new_sorting)
79
+ @sorting_in_filter = !!new_sorting
80
+ @sorting_links = !!new_sorting
75
81
  end
76
82
 
77
83
  # DSL method
78
- # Disables sorting in filter.
79
- def skip_sorting_in_filter!
80
- @sorting_in_filter = false
84
+ # Enables or disables sorting in filter.
85
+ def sorting_in_filter(new_sorting_in_filter)
86
+ @sorting_in_filter = !!new_sorting_in_filter
81
87
  end
82
88
 
83
89
  # DSL method
84
- # Disables sorting links.
85
- def skip_sorting_links!
86
- @sorting_links = false
90
+ # Enables or disables sorting links.
91
+ def sorting_links(new_sorting_links)
92
+ @sorting_links = !!new_sorting_links
87
93
  end
88
94
 
89
95
  # DSL method
@@ -157,28 +163,18 @@ module Compony
157
163
  end
158
164
 
159
165
  # DSL method
160
- # Adds a row action. The very last col provides actions such as :show, :edit or :destroy. Use this method to add your own.
161
- # In case the action exists as a component in the family of `data_class`, it is enough to pass the action's name, and the rest is auto-generated.
162
- # In order to create a custom row action, pass a block that will be given the current record and instance-execed once per row, for every record.
163
- # @param name [Symbol, String] The name of the action (e.g. :edit).
164
- # @param button_opts [Hash] Only relevant in case of an auto-generated row action, this allows to configure the generated button.
165
- # @param block [Block] To create a custom row action; block will be given the current record and instance-execed once per row, for every record.
166
- def row_action(name, button_opts: {}, **, &block)
167
- name = name.to_sym
168
- unless block_given?
169
- block = proc do |record|
170
- next if Compony.comp_class_for(name, record).nil?
171
- render_intent(name, record, **{ label: { format: :short } }.deep_merge(button_opts))
172
- end
166
+ # If a block is given: Enters the DSL where row intents can be added or removed (use from {Component#setup} within the component).
167
+ # If no block is given: Builds the declared intents for the given record and returns them (use in `content` or `before_render`, pass kwarg `:data`).
168
+ def row_intents(**intent_opts, &block)
169
+ if block_given?
170
+ # Enter DSL
171
+ @row_intent_blocks << block
172
+ else
173
+ # Build the declared intents
174
+ intents_ordering = NaturalOrdering.new
175
+ @row_intent_blocks.each { |block| ManageIntentsDsl.new(intents_ordering, **intent_opts).evaluate(&block) } # this populates intents_ordering
176
+ return intents_ordering.map!(&:payload)
173
177
  end
174
- @row_actions.natural_push(name, block, **)
175
- end
176
-
177
- # DSL method
178
- # Marks a single row action as skipped. It will not be displayed, even if it is defined.
179
- # @param name [Symbol,String] Name of the row action to be skipped.
180
- def skip_row_action(name)
181
- @skipped_row_actions << name.to_sym
182
178
  end
183
179
 
184
180
  # DSL method
@@ -237,7 +233,7 @@ module Compony
237
233
  def process_data!(controller)
238
234
  fail('Data was already processed!') if @processed_data
239
235
  # Filtering
240
- if filtering_enabled?
236
+ if filtering_enabled? || sorting_enabled?
241
237
  @q = @data.ransack(controller.params[param_name(:q)], auth_object: controller.current_ability, search_key: param_name(:q))
242
238
  @q.sorts = @default_sorting if @q.sorts.empty?
243
239
  filtered_data = @q.result.accessible_by(controller.current_ability)
@@ -274,10 +270,12 @@ module Compony
274
270
  @data = data_class.accessible_by(controller.current_ability)
275
271
  end
276
272
 
277
- # Default row actions (use override or skip_row_action to prevent)
278
- row_action(:show)
279
- row_action(:edit)
280
- row_action(:destroy)
273
+ # Default row intents
274
+ row_intents do
275
+ add :show, name: :show
276
+ add :edit, name: :edit
277
+ add :destroy, name: :destroy
278
+ end
281
279
 
282
280
  before_render do
283
281
  process_data!(controller)
@@ -336,7 +334,7 @@ module Compony
336
334
  @columns.each do |column|
337
335
  th column[:label], class: 'list-data-label'
338
336
  end
339
- if @row_actions.any? { |row_action| @skipped_row_actions.exclude?(row_action[:name]) }
337
+ unless @skip_row_intents
340
338
  th I18n.t('compony.components.index.actions'), class: 'list-actions-label'
341
339
  end
342
340
  end
@@ -349,14 +347,11 @@ module Compony
349
347
  instance_exec(record, &column[:payload])
350
348
  end
351
349
  end
352
- rendered_row_actions = @row_actions.map do |row_action|
353
- next if @skipped_row_actions.include?(row_action[:name])
354
- next instance_exec(record, &row_action[:payload])
355
- end.compact
356
- if rendered_row_actions.any?
350
+ unless @skip_row_intents
357
351
  td do
358
- rendered_row_actions.each do |row_action_html|
359
- concat row_action_html if row_action_html
352
+ row_intents(data: record, label: { format: :short }, button: { data: { 'turbo-frame': :_top } }).each do |row_intent|
353
+ next if @skipped_row_intents.include?(row_intent.name)
354
+ concat row_intent.render(controller)
360
355
  end
361
356
  end
362
357
  end
@@ -390,27 +385,27 @@ module Compony
390
385
 
391
386
  # Returns whether filtering is possible and wanted in general (regardless of whether there are any filters defined)
392
387
  def filtering_enabled?
393
- @filtering && defined?(Ransack)
388
+ @filtering && defined?(Ransack) && !@skip_filtering
394
389
  end
395
390
 
396
391
  # Returns whether sorting is possible and wanted in general (regardless of whether there are any sorts defined)
397
392
  def sorting_enabled?
398
- (@sorting_in_filter || @sorting_links) && defined?(Ransack)
393
+ ((@sorting_in_filter && !@skip_sorting_in_filter) || (@sorting_links && !@skip_sorting_links)) && defined?(Ransack)
399
394
  end
400
395
 
401
396
  # Returns whether sorting in filter is possible and wanted in general (regardless of whether there are any sorts defined)
402
397
  def sorting_in_filter_enabled?
403
- sorting_enabled? && @sorting_in_filter
398
+ sorting_enabled? && @sorting_in_filter && !@skip_sorting_in_filter
404
399
  end
405
400
 
406
401
  # Returns whether generating sorting links is possible and wanted in general (regardless of whether there are any sorts defined)
407
402
  def sorting_links_enabled?
408
- sorting_enabled? && @sorting_links
403
+ sorting_enabled? && @sorting_links && !@skip_sorting_links
409
404
  end
410
405
 
411
406
  # Returns whether pagination is enabled (regardless of whether there is more than one page)
412
407
  def pagination_enabled?
413
- @pagination
408
+ @pagination && !@skip_pagination
414
409
  end
415
410
 
416
411
  # Returns the select options for sorting suitable for passing in a `f.select`. Used in sorting-in-filter feature. Useful for custom subclasses of List.
@@ -30,7 +30,6 @@ module Compony
30
30
 
31
31
  label(:long) { I18n.t('compony.components.new.label.long', data_class: data_class.model_name.human) }
32
32
  label(:short) { I18n.t('compony.components.new.label.short') }
33
- icon { :plus }
34
33
 
35
34
  form_cancancan_action :new
36
35
 
@@ -14,23 +14,16 @@ module Compony
14
14
 
15
15
  label(:long) { |data| data.label } # rubocop:disable Style/SymbolProc
16
16
  label(:short) { |_| I18n.t('compony.components.show.label.short') }
17
- icon { :eye }
18
17
 
19
18
  exposed_intents do
20
19
  if data_class.owner_model_attr
21
20
  add :show, @data.send(data_class.owner_model_attr),
22
- button: { icon: :xmark, color: :secondary },
23
- label: I18n.t('compony.back'),
24
- name: :back_to_owner
21
+ label: I18n.t('compony.back'),
22
+ name: :back_to_owner
25
23
  end
26
24
 
27
- if Compony.comp_class_for(:edit, family_name)
28
- add :edit, @data, label: { format: :short }, name: :edit
29
- end
30
-
31
- if Compony.comp_class_for(:destroy, family_name)
32
- add :destroy, @data, label: { format: :short }, name: :destroy
33
- end
25
+ add :edit, @data, label: { format: :short }, name: :edit
26
+ add :destroy, @data, label: { format: :short }, name: :destroy
34
27
  end
35
28
 
36
29
  content :label do
@@ -23,7 +23,7 @@ module Compony
23
23
  # @param data_class [Class] If given, the target component will be instanciated with this argument.
24
24
  # @param feasibility_target [ApplicationRecord] If given, will override the feasibility target (prevention framework)
25
25
  # @param feasibility_action [ApplicationRecord] If given, will override the feasibility action (prevention framework)
26
- def initialize(comp_name_or_cst_or_class,
26
+ def initialize(comp_name_or_cst_or_class = nil,
27
27
  model_or_family_name_or_cst = nil,
28
28
  standalone_name: nil,
29
29
  name: nil,
@@ -42,7 +42,11 @@ module Compony
42
42
  @data_class = data_class
43
43
 
44
44
  # Figure out comp_class
45
- if comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
45
+ if comp_name_or_cst_or_class.nil?
46
+ if name.blank? || !label.is_a?(String)
47
+ fail('An intent created without positional arguments must be given the kwargs `name:`, `label` (String).')
48
+ end
49
+ elsif comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
46
50
  # A class was given as the first argument
47
51
  @comp_class = comp_name_or_cst_or_class
48
52
  else
@@ -75,6 +79,7 @@ module Compony
75
79
  # Instanciates the component and returns the instance. If `data` and/or `data_class` were specified when instantiating this intent, they are passed.
76
80
  # All given arguments will be given to the component's initializer, also overriding `data` and `data_class` if present.
77
81
  def comp(*, **)
82
+ return nil if @comp_class.nil?
78
83
  return @comp ||= @comp_class.new(*, data: @data, data_class: @data_class, **)
79
84
  end
80
85
 
@@ -83,6 +88,7 @@ module Compony
83
88
  # @param model [ApplicationRecord] If given and non-nil, will override the model passed to the component's path block
84
89
  # @param standalone_name [Symbol] If given and non-nil, will override the `standalone_name` passed to the component's path block
85
90
  def path(model = nil, *, standalone_name: nil, **path_opt_overrides)
91
+ return nil if @comp_class.nil?
86
92
  path_opts = @path_opts.deep_merge(path_opt_overrides)
87
93
  comp.path(model || (model? ? @data : nil), standalone_name: standalone_name || @standalone_name, **path_opts)
88
94
  end
@@ -135,10 +141,10 @@ module Compony
135
141
  # @param button_arg_overrides [Hash] Any further kwargs are passed to the button component's initializer.
136
142
  def render(controller, parent_comp = nil, style: nil, **button_arg_overrides)
137
143
  # Abort if not authorized
138
- return nil unless comp.standalone_access_permitted_for?(controller, standalone_name: @standalone_name, verb: method)
144
+ return nil if comp && !comp.standalone_access_permitted_for?(controller, standalone_name: @standalone_name, verb: method)
139
145
  # Prepare opts
140
146
  button_comp_class ||= Compony.button_component_class(*[style || @style].compact)
141
- button_opts = button_comp_opts.merge(button_arg_overrides)
147
+ button_opts = (comp&.button_defaults || {}).merge(button_comp_opts).merge(button_arg_overrides) # overrides go right to left
142
148
  # Perform render
143
149
  if parent_comp
144
150
  return parent_comp.sub_comp(button_comp_class, **button_opts).render(controller)
@@ -1,8 +1,9 @@
1
1
  module Compony
2
- class ExposedIntentsDsl < Dslblend::Base
3
- def initialize(previously_exposed_intents)
2
+ class ManageIntentsDsl < Dslblend::Base
3
+ def initialize(previously_exposed_intents, **intent_opts)
4
4
  super()
5
5
  @exposed_intents = previously_exposed_intents
6
+ @intent_opts = intent_opts
6
7
  end
7
8
 
8
9
  protected
@@ -12,8 +13,10 @@ module Compony
12
13
  # Intents specified this way can be retrieved and rendered by the parent component or by calling `root_intents` in case of standalone access.
13
14
  # @param [Symbol] before If specified, will insert the intent before the other. When replacing, an element keeps its position unless `before:`` is passed.
14
15
  def add(*, before: nil, **)
15
- intent = Compony.intent(*, **)
16
+ intent = Compony.intent(*, **@intent_opts, **)
16
17
  @exposed_intents.natural_push(intent.name, intent, before:)
18
+ rescue NameError # Ignore if the component is not actually defined
19
+ return nil
17
20
  end
18
21
 
19
22
  # DSL method
@@ -71,7 +71,7 @@ module Compony
71
71
  return if autodetect_feasibilities_completed
72
72
  # Add a prevention that reflects the `has_many` `dependent' properties. Avoids that users can press buttons that will result in a failed destroy.
73
73
  reflect_on_all_associations.select { |assoc| %i[restrict_with_exception restrict_with_error].include? assoc.options[:dependent] }.each do |assoc|
74
- prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: I18n.t(assoc.klass.humanize_class_name.pluralize))) do
74
+ prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: assoc.klass.model_name.human(count: 2))) do
75
75
  if assoc.is_a? ActiveRecord::Reflection::HasOneReflection
76
76
  !public_send(assoc.name).nil?
77
77
  else
@@ -78,9 +78,16 @@ module Compony
78
78
  end
79
79
 
80
80
  # View helper that instanciates a sub comp and renders it.
81
- # Example usage: `concat render_sub_comp(Components::Something::Nested)`
82
- def render_sub_comp(...)
83
- sub_comp(...).render(controller)
81
+ # Example usage: `concat render_sub_comp(:list, @data.belongings)`
82
+ # If the parameter `turbo_frame` is given, the sub comp is rendered inside a hotwire turbo frame. This is useful when having forms in multiple nested comps.
83
+ def render_sub_comp(*, turbo_frame: nil, **)
84
+ if turbo_frame
85
+ turbo_frame_tag(turbo_frame.to_sym) do
86
+ concat sub_comp(*, **).render(controller)
87
+ end
88
+ else
89
+ sub_comp(*, **).render(controller)
90
+ end
84
91
  end
85
92
  end
86
93
  end
data/lib/compony.rb CHANGED
@@ -147,9 +147,9 @@ module Compony
147
147
  # Same as Compony#comp_class_for but fails if none found
148
148
  # @see Intent for allowed parameters.
149
149
  # @see Compony#comp_class_for
150
- def self.comp_class_for!(...)
151
- comp_class_for(...) || fail(
152
- "No component found for [#{comp_name_or_cst.inspect}, #{model_or_family_name_or_cst.inspect}]"
150
+ def self.comp_class_for!(*args, **kwargs)
151
+ comp_class_for(*args, **kwargs) || fail(
152
+ "No component found for #{args.inspect}, #{kwargs.inspect}"
153
153
  )
154
154
  end
155
155
 
@@ -218,7 +218,7 @@ require 'compony/component_mixins/default/standalone/verb_dsl'
218
218
  require 'compony/component_mixins/default/standalone/resourceful_verb_dsl'
219
219
  require 'compony/component_mixins/default/labelling'
220
220
  require 'compony/component_mixins/resourceful'
221
- require 'compony/exposed_intents_dsl'
221
+ require 'compony/manage_intents_dsl'
222
222
  require 'compony/component'
223
223
  require 'compony/components/buttons/link'
224
224
  require 'compony/components/buttons/css_button'
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.9.0
4
+ version: 0.10.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-28 00:00:00.000000000 Z
12
+ date: 2025-12-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: yard
@@ -312,8 +312,8 @@ files:
312
312
  - lib/compony/components/with_form.rb
313
313
  - lib/compony/controller_mixin.rb
314
314
  - lib/compony/engine.rb
315
- - lib/compony/exposed_intents_dsl.rb
316
315
  - lib/compony/intent.rb
316
+ - lib/compony/manage_intents_dsl.rb
317
317
  - lib/compony/method_accessible_hash.rb
318
318
  - lib/compony/model_fields/anchormodel.rb
319
319
  - lib/compony/model_fields/association.rb