compony 0.0.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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.yardopts +2 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +208 -0
  7. data/LICENSE +165 -0
  8. data/README.md +33 -0
  9. data/Rakefile +34 -0
  10. data/app/controllers/compony_controller.rb +31 -0
  11. data/compony.gemspec +32 -0
  12. data/config/locales/de.yml +29 -0
  13. data/config/locales/en.yml +29 -0
  14. data/config/routes.rb +18 -0
  15. data/doc/resourceful_lifecycle.graphml +819 -0
  16. data/doc/resourceful_lifecycle.pdf +1564 -0
  17. data/lib/compony/component.rb +225 -0
  18. data/lib/compony/component_mixins/default/labelling.rb +77 -0
  19. data/lib/compony/component_mixins/default/standalone/resourceful_verb_dsl.rb +55 -0
  20. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +56 -0
  21. data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +47 -0
  22. data/lib/compony/component_mixins/default/standalone.rb +117 -0
  23. data/lib/compony/component_mixins/resourceful.rb +92 -0
  24. data/lib/compony/components/button.rb +59 -0
  25. data/lib/compony/components/form.rb +138 -0
  26. data/lib/compony/components/resourceful/destroy.rb +77 -0
  27. data/lib/compony/components/resourceful/edit.rb +96 -0
  28. data/lib/compony/components/resourceful/new.rb +95 -0
  29. data/lib/compony/components/with_form.rb +37 -0
  30. data/lib/compony/controller_mixin.rb +12 -0
  31. data/lib/compony/engine.rb +19 -0
  32. data/lib/compony/method_accessible_hash.rb +43 -0
  33. data/lib/compony/model_fields/anchormodel.rb +28 -0
  34. data/lib/compony/model_fields/association.rb +53 -0
  35. data/lib/compony/model_fields/base.rb +63 -0
  36. data/lib/compony/model_fields/boolean.rb +9 -0
  37. data/lib/compony/model_fields/currency.rb +9 -0
  38. data/lib/compony/model_fields/date.rb +9 -0
  39. data/lib/compony/model_fields/datetime.rb +9 -0
  40. data/lib/compony/model_fields/decimal.rb +6 -0
  41. data/lib/compony/model_fields/float.rb +6 -0
  42. data/lib/compony/model_fields/integer.rb +6 -0
  43. data/lib/compony/model_fields/phone.rb +15 -0
  44. data/lib/compony/model_fields/rich_text.rb +9 -0
  45. data/lib/compony/model_fields/string.rb +6 -0
  46. data/lib/compony/model_fields/text.rb +6 -0
  47. data/lib/compony/model_fields/time.rb +6 -0
  48. data/lib/compony/model_mixin.rb +88 -0
  49. data/lib/compony/request_context.rb +45 -0
  50. data/lib/compony/version.rb +11 -0
  51. data/lib/compony/view_helpers.rb +36 -0
  52. data/lib/compony.rb +268 -0
  53. data/lib/generators/component/USAGE +8 -0
  54. data/lib/generators/component/component_generator.rb +14 -0
  55. data/lib/generators/component/templates/component.rb.erb +4 -0
  56. metadata +236 -0
@@ -0,0 +1,225 @@
1
+ module Compony
2
+ class Component
3
+ # Include all functionality that was moved to default mixins for better overview
4
+ Compony::ComponentMixins::Default.constants.each { |cst| include Compony::ComponentMixins::Default.const_get(cst) }
5
+
6
+ class_attribute :setup_blocks
7
+
8
+ attr_reader :parent_comp
9
+ attr_reader :comp_opts
10
+
11
+ # root comp: component that is registered to be root of the application.
12
+ # parent comp: component that is registered to be the parent of this comp. If there is none, this is the root comp.
13
+
14
+ # DSL method
15
+ def self.setup(&block)
16
+ fail("`setup` expects a block in #{inspect}.") unless block_given?
17
+ self.setup_blocks ||= []
18
+ self.setup_blocks = setup_blocks.dup # This is required to prevent the parent class to see children's setup blocks.
19
+ setup_blocks << block
20
+ end
21
+
22
+ def initialize(parent_comp = nil, index: 0, **comp_opts)
23
+ @parent_comp = parent_comp
24
+ @sub_comps = []
25
+ @index = index
26
+ @comp_opts = comp_opts
27
+ @before_render_block = nil
28
+ @content_blocks = []
29
+ @actions = []
30
+ @skipped_actions = Set.new
31
+
32
+ init_standalone
33
+ init_labelling
34
+
35
+ fail "#{inspect} is missing a call to `setup`." unless setup_blocks&.any?
36
+
37
+ setup_blocks.each do |setup_block|
38
+ instance_exec(&setup_block)
39
+ end
40
+ check_config!
41
+ end
42
+
43
+ def inspect
44
+ "#<#{self.class.name}:#{hash}>"
45
+ end
46
+
47
+ # Returns the current root comp.
48
+ # Do not overwrite.
49
+ def root_comp
50
+ return self unless parent_comp
51
+ return parent_comp.root_comp
52
+ end
53
+
54
+ # Returns whether or not this is the root comp.
55
+ # Do not overwrite.
56
+ def root_comp?
57
+ parent_comp.nil?
58
+ end
59
+
60
+ # Returns an identifier describing this component. Must be unique among simplings under the same parent_comp.
61
+ # Do not override.
62
+ def id
63
+ "#{family_name}_#{comp_name}_#{@index}"
64
+ end
65
+
66
+ # Returns the id path from the root_comp.
67
+ # Do not overwrite.
68
+ def path
69
+ if root_comp?
70
+ id
71
+ else
72
+ "#{parent_comp.path}/#{id}"
73
+ end
74
+ end
75
+
76
+ # Returns a hash for the path. Used for params prefixing.
77
+ # Do not overwrite.
78
+ def path_hash
79
+ Digest::SHA1.hexdigest(path)[..4]
80
+ end
81
+
82
+ # Given an unprefixed name of a param, adds the path hash
83
+ # Do not overwrite.
84
+ def param_name(unprefixed_param_name)
85
+ "#{path_hash}_#{unprefixed_param_name}"
86
+ end
87
+
88
+ # Instanciate a component with `self` as a parent
89
+ def sub_comp(component_class, **comp_opts)
90
+ sub = component_class.new(self, index: @sub_comps.count, **comp_opts)
91
+ @sub_comps << sub
92
+ return sub
93
+ end
94
+
95
+ # Returns the name of the module constant (=family) of this component. Do not override.
96
+ def family_cst
97
+ self.class.module_parent.to_s.demodulize.to_sym
98
+ end
99
+
100
+ # Returns the family name
101
+ def family_name
102
+ family_cst.to_s.underscore
103
+ end
104
+
105
+ # Returns the name of the class constant of this component. Do not override.
106
+ def comp_cst
107
+ self.class.name.demodulize.to_sym
108
+ end
109
+
110
+ # Returns the component name
111
+ def comp_name
112
+ comp_cst.to_s.underscore
113
+ end
114
+
115
+ # @todo deprecate (check for usages beforehand)
116
+ def comp_class_for(...)
117
+ Compony.comp_class_for(...)
118
+ end
119
+
120
+ # @todo deprecate (check for usages beforehand)
121
+ def comp_class_for!(...)
122
+ Compony.comp_class_for!(...)
123
+ end
124
+
125
+ # DSL method
126
+ def before_render(&block)
127
+ @before_render_block = block
128
+ end
129
+
130
+ # DSL method
131
+ # Overrides previous content (also from superclasses). Will be the first content block to run.
132
+ # You can use dyny here.
133
+ def content(&block)
134
+ fail("`content` expects a block in #{inspect}.") unless block_given?
135
+ @content_blocks = [block]
136
+ end
137
+
138
+ # DSL method
139
+ # Adds a content block that will be executed after all previous ones.
140
+ # It is safe to use this method even if `content` has never been called
141
+ # You can use dyny here.
142
+ def add_content(index = -1, &block)
143
+ fail("`content` expects a block in #{inspect}.") unless block_given?
144
+ @content_blocks ||= []
145
+ @content_blocks.insert(index, block)
146
+ end
147
+
148
+ # Renders the component using the controller passsed to it and returns it as a string.
149
+ # Do not overwrite.
150
+ def render(controller, **locals)
151
+ # Call before_render hook if any and backfire instance variables back to the component
152
+ RequestContext.new(self, controller, locals:).request_context.evaluate_with_backfire(&@before_render_block) if @before_render_block
153
+ # Render, unless before_render has already issued a body (e.g. through redirecting).
154
+ if controller.response.body.blank?
155
+ fail "#{self.class.inspect} must define `content` or set a response body in `before_render`" if @content_blocks.none?
156
+ return controller.render_to_string(
157
+ type: :dyny,
158
+ locals: { content_blocks: @content_blocks, component: self, render_locals: locals },
159
+ inline: <<~RUBY
160
+ content_blocks.each do |block|
161
+ # Instanciate and evaluate a fresh RequestContext in order to use the buffer allocated by the ActionView (needed for `concat` calls)
162
+ Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&block)
163
+ end
164
+ RUBY
165
+ )
166
+ else
167
+ return nil # Prevent double render errors
168
+ end
169
+ end
170
+
171
+ # DSL method
172
+ # Adds or replaces an action (for action buttons)
173
+ # If before: is specified, will insert the action before the named action. When replacing, an element keeps its position unless before: is specified.
174
+ def action(action_name, before: nil, &block)
175
+ action_name = action_name.to_sym
176
+ before_name = before&.to_sym
177
+ action = MethodAccessibleHash.new(name: action_name, block:)
178
+
179
+ existing_index = @actions.find_index { |el| el.name == action_name }
180
+ if existing_index.present? && before_name.present?
181
+ @actions.delete_at(existing_index) # Replacing an existing element with a before: directive - must delete before calculating indices
182
+ end
183
+ if before_name.present?
184
+ before_index = @actions.find_index { |el| el.name == before_name } || fail("Action #{before_name} for :before not found in #{inspect}.")
185
+ end
186
+
187
+ if before_index.present?
188
+ @actions.insert(before_index, action)
189
+ elsif existing_index.present?
190
+ @actions[existing_index] = action
191
+ else
192
+ @actions << action
193
+ end
194
+ end
195
+
196
+ # DSL method
197
+ # Marks an action for skip
198
+ def skip_action(action_name)
199
+ @skipped_actions << action_name.to_sym
200
+ end
201
+
202
+ # Used to render all actions of this component, each button wrapped in a div with the specified class
203
+ def render_actions(controller, wrapper_class: '', action_class: '')
204
+ h = controller.helpers
205
+ h.content_tag(:div, class: wrapper_class) do
206
+ button_htmls = @actions.map do |action|
207
+ next if @skipped_actions.include?(action.name)
208
+ Compony.with_button_defaults(feasibility_action: action.name.to_sym) do
209
+ h.content_tag(:div, action.block.call.render(controller), class: action_class)
210
+ end
211
+ end
212
+ next h.safe_join button_htmls
213
+ end
214
+ end
215
+
216
+ # Is true for resourceful components
217
+ def resourceful?
218
+ return false
219
+ end
220
+
221
+ protected
222
+
223
+ def check_config!; end
224
+ end
225
+ end
@@ -0,0 +1,77 @@
1
+ require 'active_support/concern'
2
+
3
+ module Compony
4
+ module ComponentMixins
5
+ module Default
6
+ # This module contains all methods for Component that concern labelling and look
7
+ module Labelling
8
+ extend ActiveSupport::Concern
9
+
10
+ # DSL method and accessor
11
+ # When assigning via DSL, pass format as first parameter.
12
+ # When accessing the value, pass foramt as named parameter
13
+ def label(data_or_format = nil, format: :long, &block)
14
+ format = data_or_format if block_given?
15
+ format ||= :long
16
+ format = format.to_sym
17
+
18
+ if block_given?
19
+ # Assignment via DSL
20
+ if format == :all
21
+ @label_blocks[:short] = block
22
+ @label_blocks[:long] = block
23
+ else
24
+ @label_blocks[format] = block
25
+ end
26
+ else
27
+ # Retrieval of the actual label
28
+ fail('Label format :all may only be used for setting a label (with a block), not for retrieving it.') if format == :all
29
+ label_block = @label_blocks[format] || fail("Format #{format} was not found for #{inspect}.")
30
+ case label_block.arity
31
+ when 0
32
+ label_block.call
33
+ when 1
34
+ data_or_format ||= data
35
+ if data_or_format.blank?
36
+ fail "Label block of #{inspect} takes an argument, but no data was provided and a call to `data` did not return any data either."
37
+ end
38
+ label_block.call(data_or_format)
39
+ else
40
+ fail "#{inspect} has a label block that takes 2 or more arguments, which is unsupported."
41
+ end
42
+ end
43
+ end
44
+
45
+ # DSL method and accessor
46
+ def icon(&block)
47
+ if block_given?
48
+ @icon_block = block
49
+ else
50
+ @icon_block.call
51
+ end
52
+ end
53
+
54
+ # DSL method and accessor
55
+ def color(&block)
56
+ if block_given?
57
+ @color_block = block
58
+ else
59
+ @color_block.call
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def init_labelling
66
+ # Provide defaults
67
+ @label_blocks = {
68
+ long: -> { "#{I18n.t(family_name.humanize)}: #{I18n.t(comp_name.humanize)}" },
69
+ short: -> { I18n.t(comp_name.humanize) }
70
+ }
71
+ @icon_block = -> { :'arrow-right' }
72
+ @color_block = -> { :primary }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,55 @@
1
+ module Compony
2
+ module ComponentMixins
3
+ module Default
4
+ module Standalone
5
+ class ResourcefulVerbDsl < VerbDsl
6
+ def initialize(...)
7
+ # All resourceful components have a load_data_block, which defaults to the one defined in Resource, defaulting to finding the record.
8
+ @load_data_block = proc { evaluate_with_backfire(&@global_load_data_block) }
9
+ super
10
+ end
11
+
12
+ def to_conf(&)
13
+ return super.deep_merge({
14
+ load_data_block: @load_data_block,
15
+ assign_attributes_block: @assign_attributes_block,
16
+ store_data_block: @store_data_block
17
+ }).compact
18
+ end
19
+
20
+ protected
21
+
22
+ # DSL
23
+ # This is the first step in the life cycle. The block is expected to assign something to `@data`.
24
+ def load_data(&block)
25
+ @load_data_block = block
26
+ end
27
+
28
+ # DSL
29
+ # This is called after `load_data`. The block is expected to assign data from `params` as attributes of `@data`.
30
+ # If this method gets never called, the verb config will not contain a assign_attributes block.
31
+ # If called without a block, the verb config will call the global_assign_attributes block defined in Resource.
32
+ def assign_attributes(&block)
33
+ if block_given?
34
+ @assign_attributes_block = block
35
+ else
36
+ @assign_attributes_block = proc { evaluate_with_backfire(&@global_assign_attributes_block) }
37
+ end
38
+ end
39
+
40
+ # DSL
41
+ # This is called after authorization. The block is expected to write back to the database.
42
+ # If this method gets never called, the verb config will not contain a store_data block.
43
+ # If called without a block, the verb config will call the global_store_data block defined in Resource.
44
+ def store_data(&block)
45
+ if block_given?
46
+ @store_data_block = block
47
+ else
48
+ @store_data_block = proc { evaluate_with_backfire(&@global_store_data_block) }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,56 @@
1
+ module Compony
2
+ module ComponentMixins
3
+ module Default
4
+ module Standalone
5
+ # @api description
6
+ # Wrapper and DSL helper for component's standalone config
7
+ class StandaloneDsl < Dslblend::Base
8
+ def initialize(component, name = nil, path: nil)
9
+ super()
10
+ @component = component
11
+ @name = name&.to_sym
12
+ @path = path
13
+ @verbs = {}
14
+ @skip_authentication = false
15
+ @layout = true # can be overriden by false or a string
16
+ end
17
+
18
+ def to_conf(&block)
19
+ evaluate(&block)
20
+ @component = block.binding.eval('self') # Fetches the component holding this DSL call (via the block)
21
+ return {
22
+ name: @name,
23
+ path: @path,
24
+ verbs: @verbs,
25
+ rails_action_name: Compony.rails_action_name(comp_name, family_name, @name),
26
+ path_helper_name: Compony.path_helper_name(comp_name, family_name, @name),
27
+ skip_authentication: @skip_authentication,
28
+ layout: @layout
29
+ }.compact
30
+ end
31
+
32
+ protected
33
+
34
+ # DSL
35
+ def verb(verb, *args, **nargs, &)
36
+ verb = verb.to_sym
37
+ verb_dsl_class = @component.resourceful? ? ResourcefulVerbDsl : VerbDsl
38
+ @verbs[verb] ||= Compony::MethodAccessibleHash.new
39
+ @verbs[verb].deep_merge! verb_dsl_class.new(@component, verb, *args, **nargs).to_conf(&)
40
+ end
41
+
42
+ # DSL
43
+ def skip_authentication!
44
+ @skip_authentication = true
45
+ end
46
+
47
+ # DSL
48
+ # Defaults to Rails' default (layouts/application)
49
+ def layout(layout)
50
+ @layout = layout.to_s
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ module Compony
2
+ module ComponentMixins
3
+ module Default
4
+ module Standalone
5
+ class VerbDsl < Dslblend::Base
6
+ AVAILABLE_VERBS = %i[get head post put delete connect options trace patch].freeze
7
+
8
+ def initialize(component, verb)
9
+ super()
10
+
11
+ verb = verb.to_sym
12
+ fail "Unknown HTTP verb #{verb.inspect}, use one of #{AVAILABLE_VERBS.inspect}" unless AVAILABLE_VERBS.include?(verb)
13
+
14
+ @component = component
15
+ @verb = verb
16
+ @respond_blocks = { nil => proc { render_standalone(controller) } } # default format
17
+ @authorize_block = nil
18
+ end
19
+
20
+ def to_conf(&)
21
+ evaluate(&) if block_given?
22
+ return {
23
+ verb: @verb,
24
+ authorize_block: @authorize_block || proc { can?(comp_name.to_sym, family_name.to_sym) },
25
+ respond_blocks: @respond_blocks
26
+ }.compact
27
+ end
28
+
29
+ protected
30
+
31
+ # DSL
32
+ # This block is expected to return true if and only if current_ability has the right to access the component over the given verb.
33
+ def authorize(&block)
34
+ @authorize_block = block
35
+ end
36
+
37
+ # DSL
38
+ # This is the last step in the life cycle. It may redirect or render. If omitted, the default is standalone_render.
39
+ # @param format [String, Symbol] Format this block should respond to, defaults to `nil` which means "all other formats".
40
+ def respond(format = nil, &block)
41
+ @respond_blocks[format&.to_sym] = block
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,117 @@
1
+ module Compony
2
+ module ComponentMixins
3
+ module Default
4
+ # This contains all default component logic concerning standalone functionality
5
+ module Standalone
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Called in routes.rb
10
+ # Returns the compiled standalone config for this component
11
+ # If the components have an inheritance hierarchy, the configs are merged in the right order to perform proper overrides.
12
+ attr_reader :standalone_configs
13
+ end
14
+
15
+ # Called by fab_controller when a request is issued.
16
+ # This is the entrypoint where a request enters the Component world.
17
+ def on_standalone_access(verb_config, controller)
18
+ # Register as root comp
19
+ if parent_comp.nil?
20
+ fail "#{inspect} is attempting to become root component, but #{root_comp.inspect} is already root." if Compony.root_comp.present?
21
+ RequestStore.store[:compony_root_comp] = self
22
+ end
23
+
24
+ # Prepare the request context in which the innermost DSL calls will be executed
25
+ request_context = RequestContext.new(self, controller)
26
+
27
+ ###===---
28
+ # Dispatch request to component. Empty Dslblend base objects are used to provide multiple contexts to the authorize and respond blocks.
29
+ # Lifecycle is (see also "doc/Resourceful Lifecycle.pdf"):
30
+ # - load data (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful)
31
+ # - after_load_data (optional, specified in Resourceful)
32
+ # - assign_attributes (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful)
33
+ # - after_assign_attributes (optional, specified in Resourceful)
34
+ # - authorize
35
+ # - store_data (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful)
36
+ # - respond (typically either redirect or render standalone, specified in VerbDsl), which defaults to render_standalone, performing:
37
+ # - before_render
38
+ # - render (unless before_render already redirected)
39
+ ###===---
40
+
41
+ if verb_config.load_data_block
42
+ request_context.evaluate_with_backfire(&verb_config.load_data_block)
43
+ if global_after_load_data_block
44
+ request_context.evaluate_with_backfire(&global_after_load_data_block)
45
+ end
46
+ end
47
+
48
+ if verb_config.assign_attributes_block
49
+ request_context.evaluate_with_backfire(&verb_config.assign_attributes_block)
50
+ if global_after_assign_attributes_block
51
+ request_context.evaluate_with_backfire(&global_after_assign_attributes_block)
52
+ end
53
+ end
54
+
55
+ # TODO: Make much prettier, providing message, action, subject and conditions
56
+ fail CanCan::AccessDenied, inspect unless request_context.evaluate(&verb_config.authorize_block)
57
+
58
+ if verb_config.store_data_block
59
+ request_context.evaluate_with_backfire(&verb_config.store_data_block)
60
+ end
61
+
62
+ # Check if there is a specific respond block for the format.
63
+ # If there isn't, fallback to the nil respond block, which defaults to `render_standalone`.
64
+ respond_block = verb_config.respond_blocks[controller.request.format.symbol] || verb_config.respond_blocks[nil]
65
+ request_context.evaluate(&respond_block)
66
+ end
67
+
68
+ # Call this on a standalone component to find out whether default GET access is permitted for the current user.
69
+ # This is useful to hide/disable buttons leading to components a user may not press.
70
+ # For resourceful components, before calling this, you must have loaded date beforehand, for instance in one of the following ways:
71
+ # - when called standalone (via request to the component), the load data step must be completed
72
+ # - when called to check for permission only, e.g. to display a button to it, initialize the component by passing the :data keyword to `new`
73
+ # By default, this checks the authorization to access the main standalone entrypoint (with name `nil`) and HTTP verb GET.
74
+ def standalone_access_permitted_for?(controller, standalone_name: nil, verb: :get)
75
+ standalone_name = standalone_name&.to_sym
76
+ verb = verb.to_sym
77
+ standalone_config = standalone_configs[standalone_name] || fail("#{inspect} does not provide the standalone config #{standalone_config.inspect}.")
78
+ verb = standalone_config.verbs[verb] || fail("#{inspect} standalone config #{standalone_config.inspect} does not provide verb #{verb.inspect}.")
79
+ return RequestContext.new(self, controller).evaluate(&verb.authorize_block)
80
+ end
81
+
82
+ # Renders the component using the controller passed to it upon instanciation (calls the controller's render)
83
+ # Do not overwrite
84
+ def render_standalone(controller, status: nil, standalone_name: nil)
85
+ # Start the render process. This produces a nil value if before_render has already produced a response, e.g. a redirect.
86
+ rendered_html = render(controller)
87
+ if rendered_html.present? # If nil, a response body was already produced in the controller and we take no action here (would have DoubleRenderError)
88
+ opts = { html: rendered_html, layout: @standalone_configs[standalone_name].layout }
89
+ opts[:status] = status if status.present?
90
+ controller.respond_to do |format|
91
+ # Form posts trigger format types turbo stream and then html, turbo stream wins.
92
+ # For this reason, Rails prefers stream, in which case the layout is disabled, regardless of the option.
93
+ # To mitigate this, we use respond_to to force a HTML-only response.
94
+ format.html { controller.render(**opts) }
95
+ end
96
+ end
97
+ end
98
+
99
+ protected
100
+
101
+ # DSL method
102
+ def standalone(name = nil, *args, **nargs, &block)
103
+ block = proc {} unless block_given? # If called without a block, must default to an empty block to provide a binding to the DSL.
104
+ name = name&.to_sym # nil name is the most common case
105
+ @standalone_configs[name] ||= Compony::MethodAccessibleHash.new
106
+ @standalone_configs[name].deep_merge! StandaloneDsl.new(self, name, *args, **nargs).to_conf(&block)
107
+ end
108
+
109
+ private
110
+
111
+ def init_standalone
112
+ @standalone_configs = {}
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,92 @@
1
+ module Compony
2
+ module ComponentMixins
3
+ # Include this when your component's family name corresponds to the pluralized Rails model name the component's family is responsible for.
4
+ # When including this, the component gets an attribute @data which contains a record or a collection of records.
5
+ # Resourceful components are always aware of a data_class, corresponding to the expected @data.class and used e.g. to render lists or for `.new`.
6
+ module Resourceful
7
+ extend ActiveSupport::Concern
8
+
9
+ attr_reader :data
10
+
11
+ # Must prefix the following instance variables with global_ in order to avoid overwriting VerbDsl inst vars due to Dslblend.
12
+ attr_reader :global_load_data_block
13
+ attr_reader :global_after_load_data_block
14
+ attr_reader :global_assign_attributes_block
15
+ attr_reader :global_after_assign_attributes_block
16
+ attr_reader :global_store_data_block
17
+
18
+ def initialize(*args, data: nil, data_class: nil, **nargs, &block)
19
+ @data = data
20
+ @data_class = data_class
21
+
22
+ # Provide defaults for hook blocks
23
+ @global_load_data_block ||= proc { @data = self.data_class.find(controller.params[:id]) }
24
+
25
+ super(*args, **nargs, &block)
26
+ end
27
+
28
+ # DSL method
29
+ # Sets or calculates the model class based on the component's family name
30
+ def data_class(new_data_class = nil)
31
+ @data_class ||= new_data_class || family_cst.to_s.singularize.constantize
32
+ end
33
+
34
+ # Instanciate a component with `self` as a parent and render it, having it inherit the resource
35
+ def resourceful_sub_comp(component_class, **comp_opts)
36
+ comp_opts[:data] ||= data # Inject additional param before forwarding all of them to super
37
+ comp_opts[:data_class] ||= data_class # Inject additional param before forwarding all of them to super
38
+ sub_comp(component_class, **comp_opts)
39
+ end
40
+
41
+ def resourceful?
42
+ return true
43
+ end
44
+
45
+ protected
46
+
47
+ # DSL method
48
+ # Sets a default load_data block for all standalone paths and verbs.
49
+ # Can be overwritten for a specific path and verb in the
50
+ # {Compony::ComponentMixins::Default::Standalone::VerbDsl}.
51
+ # The block is expected to assign `@data`.
52
+ # @see Compony::ComponentMixins::Default::Standalone::VerbDsl#load_data
53
+ def load_data(&block)
54
+ @global_load_data_block = block
55
+ end
56
+
57
+ # DSL method
58
+ # Runs after loading data and before authorization for all standalone paths and verbs.
59
+ # Example use case: if `load_data` produced an AR collection proxy, can still refine result here before `to_sql` is called.
60
+ def after_load_data(&block)
61
+ @global_after_load_data_block = block
62
+ end
63
+
64
+ # DSL method
65
+ # Sets a default default assign_attributes block for all standalone paths and verbs.
66
+ # Can be overwritten for a specific path and verb in the
67
+ # {Compony::ComponentMixins::Default::Standalone::VerbDsl}.
68
+ # The block is expected to assign suitable `params` to attributes of `@data`.
69
+ # @see Compony::ComponentMixins::Default::Standalone::VerbDsl#assign_attributes
70
+ def assign_attributes(&block)
71
+ @global_assign_attributes_block = block
72
+ end
73
+
74
+ # DSL method
75
+ # Runs after `assign_attributes` and before `store_data` for all standalone paths and verbs.
76
+ # Example use case: prefilling some fields for a form
77
+ def after_assign_attributes(&block)
78
+ @global_after_assign_attributes_block = block
79
+ end
80
+
81
+ # DSL method
82
+ # Sets a default store_data block for all standalone paths and verbs.
83
+ # Can be overwritten for a specific path and verb in the
84
+ # {Compony::ComponentMixins::Default::Standalone::VerbDsl}.
85
+ # The block is expected save `@data` to the database.
86
+ # @see Compony::ComponentMixins::Default::Standalone::VerbDsl#store_data
87
+ def store_data(&block)
88
+ @global_store_data_block = block
89
+ end
90
+ end
91
+ end
92
+ end