Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,10 +28,8 @@ end

```erb
# counter_component.html.erb
<%= component_controller do %>
<p><%= @count %></p>
<%= reflex_tag :increment, :button, "Click" %>
<% end %>
<p><%= @count %></p>
<%= reflex_tag :increment, :button, "Click" %>
```

## Collections
Expand Down Expand Up @@ -136,14 +134,15 @@ This is a key unique to a particular component. It's used to reconcile state bet
<button type="button" data-reflex="click->MyComponent#do_something" data-key="<%= key %>">Click me!</button>
```

### component_controller(options = {}, &blk)
This is a view helper to properly connect VCR to the component. It outputs `<div data-controller="my-controller" key=<%= key %></div>`
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 %>
<p><%= @count %></p
<%= component_controller :p, class: "fancy_count" do %>
<%= @count %>
<% end %>
<p>I won't be touched by updates</p>
```

## Common patterns
Expand All @@ -170,14 +169,13 @@ end
```

```erb
<%= component_controller do %>
<div id="loader">
<% if @loading %>
<p>Loading...</p>
<% end %>
</div>

<button type="button" data-reflex="click->MyComponent#do_expensive_action" data-key="<%= key %>">Click me!</button>
<div id="loader">
<% if @loading %>
<p>Loading...</p>
<% end %>
</div>

<button type="button" data-reflex="click->MyComponent#do_expensive_action" data-key="<%= key %>">Click me!</button>
<% end
```

Expand Down
92 changes: 54 additions & 38 deletions app/components/view_component_reflex/component.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module ViewComponentReflex
class Component < ViewComponent::Base
attr_reader :key
class << self
def init_stimulus_reflex
klass = self
Expand Down Expand Up @@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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?
Expand Down