module Compony module ComponentMixins module Default # @api description # This contains all default component logic concerning standalone functionality. The module is included in every component. module Standalone extend ActiveSupport::Concern included do # Called in routes.rb # Returns the compiled standalone config for this component # If the components have an inheritance hierarchy, the configs are merged in the right order to perform proper overrides. attr_reader :standalone_configs end # Called by compony_controller when a request is issued. # This is the entrypoint where a request enters the Component world. # @param verb_config [Hash] The config generated by Compony::ComponentMixins::Default::Standalone::VerbDsl#to_conf # @param controller [ComponyController] The controller instance that calls this method. # @see ComponyController def on_standalone_access(verb_config, controller) # Register as root comp if parent_comp.nil? fail "#{inspect} is attempting to become root component, but #{root_comp.inspect} is already root." if Compony.root_comp.present? RequestStore.store[:compony_root_comp] = self end # Prepare the request context in which the innermost DSL calls will be executed request_context = RequestContext.new(self, controller) ###===--- # Dispatch request to component. Empty Dslblend base objects are used to provide multiple contexts to the authorize and respond blocks. # Lifecycle is (see also "doc/Resourceful Lifecycle.pdf"): # - load data (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful) # - after_load_data (optional, specified in Resourceful) # - assign_attributes (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful) # - after_assign_attributes (optional, specified in Resourceful) # - authorize # - store_data (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful) # - respond (typically either redirect or render standalone, specified in VerbDsl), which defaults to render_standalone, performing: # - before_render # - render (unless before_render already redirected) ###===--- if verb_config.load_data_block request_context.evaluate_with_backfire(&verb_config.load_data_block) if global_after_load_data_block request_context.evaluate_with_backfire(&global_after_load_data_block) end end if verb_config.assign_attributes_block request_context.evaluate_with_backfire(&verb_config.assign_attributes_block) if global_after_assign_attributes_block request_context.evaluate_with_backfire(&global_after_assign_attributes_block) end end # TODO: Make much prettier, providing message, action, subject and conditions fail CanCan::AccessDenied, [inspect, verb_config.authorize_block.inspect].join(', ') unless request_context.evaluate(&verb_config.authorize_block) if verb_config.store_data_block request_context.evaluate_with_backfire(&verb_config.store_data_block) end # Check if there is a specific respond block for the format. # If there isn't, fallback to the nil respond block, which defaults to `render_standalone`. respond_block = verb_config.respond_blocks[controller.request.format.symbol] || verb_config.respond_blocks[nil] request_context.evaluate(&respond_block) end # Call this on a standalone component to find out whether default GET access is permitted for the current user. # This is useful to hide/disable buttons leading to components a user may not press. # For resourceful components, before calling this, you must have loaded date beforehand, for instance in one of the following ways: # - when called standalone (via request to the component), the load data step must be completed # - 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` # By default, this checks the authorization to access the main standalone entrypoint (with name `nil`) and HTTP verb GET. def standalone_access_permitted_for?(controller, standalone_name: nil, verb: nil) verb ||= :get standalone_name = standalone_name&.to_sym verb = verb.to_sym standalone_config = standalone_configs[standalone_name] || fail("#{inspect} does not provide the standalone config #{standalone_config.inspect}.") verb = standalone_config.verbs[verb] || fail("#{inspect} standalone config #{standalone_config.inspect} does not provide verb #{verb.inspect}.") return RequestContext.new(self, controller).evaluate(&verb.authorize_block) end # Renders the component using the controller passed to it upon instanciation (calls the controller's render) # Do not overwrite def render_standalone(controller, status: nil, standalone_name: nil) # Start the render process. This produces a nil value if before_render has already produced a response, e.g. a redirect. rendered_html = render(controller, standalone: true) if rendered_html.present? # If nil, a response body was already produced in the controller and we take no action here (would have DoubleRenderError) opts = { html: rendered_html, layout: @standalone_configs[standalone_name].layout } opts[:status] = status if status.present? controller.respond_to do |format| # Form posts trigger format types turbo stream and then html, turbo stream wins. # For this reason, Rails prefers stream, in which case the layout is disabled, regardless of the option. # To mitigate this, we use respond_to to force a HTML-only response. format.html { controller.render(**opts) } end end end protected # DSL method # This can be called multiple times to make a component listen to multiple paths. Each standalone config (path) has a name, the default being `nil`. # To have a component listen to multiple paths, call standalone again and provide a name, e.g.: standalone(:autocomplete, path: 'foo/bar/autocomplete') # The kwarg parameter `path` is handled analog to the Rails route path # @param name [Symbol,nil] The name of the standalone config, defaults to nil. Only provide if you add additional configs. def standalone(name = nil, *, **nargs, &block) block = proc {} unless block_given? # If called without a block, must default to an empty block to provide a binding to the DSL. name = name&.to_sym # nil name is the most common case if @standalone_configs[name] @standalone_configs[name].deep_merge! StandaloneDsl.new(self, name, *, provide_defaults: false, **nargs).to_conf(&block) else @standalone_configs[name] = Compony::MethodAccessibleHash.new(StandaloneDsl.new(self, name, *, provide_defaults: true, **nargs).to_conf(&block)) end end # Undoes previous standalone calls def clear_standalone! @standalone_configs = {} end private def init_standalone @standalone_configs = {} end end end end end