diff --git a/README.md b/README.md index 0434ed8..75ebd6d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ using stimulus reflex. ViewComponentReflex will maintain your component's instance variables between renders. You need to include `data-key=<%= key %>` on your root element, as well as any element that stimulates a reflex. ViewComponent is inherently state-less, so the key is used to reconcile state to its respective component. -### Example +### example ```ruby # counter_component.rb class CounterComponent < ViewComponentReflex::Component @@ -28,10 +28,8 @@ end ```erb # counter_component.html.erb -<%= component_controller do %> -

<%= @count %>

- <%= reflex_tag :increment, :button, "Click" %> -<% end %> +

<%= @count %>

+<%= reflex_tag :increment, :button, "Click" %> ``` ## Collections @@ -136,14 +134,15 @@ This is a key unique to a particular component. It's used to reconcile state bet ``` -### component_controller(options = {}, &blk) -This is a view helper to properly connect VCR to the component. It outputs `
` -You *must* wrap your component in this for everything to work properly. +### component_controller(opts_or_tag = :div, opts = {}, &blk) + +The rendered componentis automatically wrapped in a div to properly connect VCR to the component. If you want more control, you can use this helper in your view. ```erb -<%= component_controller do %> -

<%= @count %>

+ <%= @count %> <% end %> +

I won't be touched by updates

``` ## Common patterns @@ -170,14 +169,13 @@ end ``` ```erb -<%= component_controller do %> -
- <% if @loading %> -

Loading...

- <% end %> -
- - +
+ <% if @loading %> +

Loading...

+ <% end %> +
+ + <% end ``` diff --git a/app/components/view_component_reflex/component.rb b/app/components/view_component_reflex/component.rb index adcd6cf..af3d781 100644 --- a/app/components/view_component_reflex/component.rb +++ b/app/components/view_component_reflex/component.rb @@ -1,5 +1,6 @@ module ViewComponentReflex class Component < ViewComponent::Base + attr_reader :key class << self def init_stimulus_reflex klass = self @@ -94,18 +95,37 @@ def save_state end end + def render_in(view_context, &block) + @view_context = view_context + init_with_view_context # to get the key and the instance variables we require the view_context + rendered = super # we call render to see if component_controller helper is being used + if @component_controller_used + rendered + else + content_tag(:div, data: {controller: self.class.stimulus_controller, key: key}) { rendered } + end + end + + def init_with_view_context + self.class.init_stimulus_reflex + init_key + if !stimulus_reflex? || session[@key].nil? + store_instance_variables + else + load_instance_variables + end + end + def self.stimulus_controller name.chomp("Component").underscore.dasherize end def stimulus_reflex? - helpers.controller.instance_variable_get(:@stimulus_reflex) + view_context.instance_variable_get(:@stimulus_reflex) end def component_controller(opts_or_tag = :div, opts = {}, &blk) - self.class.init_stimulus_reflex - init_key - + @component_controller_used = true tag = :div if opts_or_tag.is_a? Hash options = opts_or_tag @@ -121,18 +141,6 @@ def component_controller(opts_or_tag = :div, opts = {}, &blk) content_tag tag, capture(&blk), options end - # key is required if you're using state - # We can't initialize the session state in the initial method - # because it doesn't have a view_context yet - # This is the next best place to do it - def init_key - # we want the erb file that renders the component. `caller` gives the file name, - # and line number, which should be unique. We hash it to make it a nice number - key = caller.select { |p| p.include? ".html.erb" }[1]&.hash.to_s - key += collection_key.to_s if collection_key - @key = key - end - def reflex_tag(reflex, name, content_or_options_with_block = nil, options = nil, escape = true, &block) action, method = reflex.to_s.split("->") if method.nil? @@ -163,35 +171,43 @@ def omitted_from_state [] end - def key - # initialize session state - if !stimulus_reflex? || session[@key].nil? - new_state = {} - # this will almost certainly break - blacklist = [ + + private + + def init_key + return @key if @key.present? + + key = caller.select { |p| p.include? ".html.erb" }[0]&.hash.to_s + key += collection_key.to_s if collection_key + @key = key + end + + def load_instance_variables + initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial") + ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v| + unless permit_parameter?(initial_state[k], instance_variable_get(k)) + instance_variable_set(k, v) + end + end + end + + def store_instance_variables + new_state = {} + + # this will almost certainly break + blacklist = [ :@view_context, :@lookup_context, :@view_renderer, :@view_flow, :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key, :@helpers, :@controller, :@request, :@content - ] - instance_variables.reject { |k| blacklist.include?(k) }.each do |k| - new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k) - end - ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state) - ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state) - else - initial_state = ViewComponentReflex::Engine.state_adapter.state(request, "#{@key}_initial") - ViewComponentReflex::Engine.state_adapter.state(request, @key).each do |k, v| - unless permit_parameter?(initial_state[k], instance_variable_get(k)) - instance_variable_set(k, v) - end - end + ] + instance_variables.reject { |k| blacklist.include?(k) }.each do |k| + new_state[k] = instance_variable_get(k) unless omitted_from_state.include?(k) end - @key + ViewComponentReflex::Engine.state_adapter.store_state(request, @key, new_state) + ViewComponentReflex::Engine.state_adapter.store_state(request, "#{@key}_initial", new_state) end - private - def merge_data_attributes(options, attributes) data = options[:data] if data.nil?