{"id":13416347,"url":"https://github.com/joshleblanc/view_component_reflex","last_synced_at":"2025-03-14T23:31:39.582Z","repository":{"id":37029992,"uuid":"268794810","full_name":"joshleblanc/view_component_reflex","owner":"joshleblanc","description":"Call component methods right from your markup","archived":false,"fork":false,"pushed_at":"2024-06-12T18:30:14.000Z","size":372,"stargazers_count":289,"open_issues_count":10,"forks_count":27,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-09-25T12:23:16.944Z","etag":null,"topics":["hacktoberfest"],"latest_commit_sha":null,"homepage":"http://view-component-reflex-expo.grep.sh/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshleblanc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-02T12:27:51.000Z","updated_at":"2024-09-06T00:37:54.000Z","dependencies_parsed_at":"2024-03-02T14:12:59.334Z","dependency_job_id":"c8d8de1d-55b4-47ae-9594-bc4eb5c48b67","html_url":"https://github.com/joshleblanc/view_component_reflex","commit_stats":{"total_commits":477,"total_committers":21,"mean_commits":"22.714285714285715","dds":0.3249475890985325,"last_synced_commit":"d13da61d0a6d150840de19ee651588b1a1b205f0"},"previous_names":[],"tags_count":99,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshleblanc%2Fview_component_reflex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshleblanc%2Fview_component_reflex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshleblanc%2Fview_component_reflex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshleblanc%2Fview_component_reflex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshleblanc","download_url":"https://codeload.github.com/joshleblanc/view_component_reflex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243663515,"owners_count":20327300,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest"],"created_at":"2024-07-30T21:00:57.459Z","updated_at":"2025-03-14T23:31:37.160Z","avatar_url":"https://github.com/joshleblanc.png","language":"Ruby","funding_links":["https://www.buymeacoffee.com/jleblanc"],"categories":["Ruby"],"sub_categories":[],"readme":"# ViewComponentReflex\n\nViewComponentReflex allows you to write reflexes right in your view component code.\n\nIt builds upon [stimulus_reflex](https://github.com/hopsoft/stimulus_reflex) and [view_component](https://github.com/github/view_component)\n\n## Usage\n\nYou can add reflexes to your component by inheriting from `ViewComponentReflex::Component`.\n\nThis will act as if you created a reflex with the method `my_cool_stuff`. To call this reflex, add `data-reflex=\"click-\u003eMyComponentReflex#my_cool_reflex\"`, just like you're\nusing stimulus reflex.\n\nViewComponentReflex will maintain your component's instance variables between renders. You need to include `data-key=\u003c%= key %\u003e` on your root element, as well \nas any element that stimulates a reflex. ViewComponent is inherently state-less, so the key is used to reconcile state to its respective component.\n\n### Example\n```ruby\n# counter_component.rb\nclass CounterComponent \u003c ViewComponentReflex::Component\n  def initialize\n    @count = 0\n  end\n\n  def increment\n    @count += 1\n  end\nend\n```\n\n```erb\n# counter_component.html.erb\n\u003c%= component_controller do %\u003e\n    \u003cp\u003e\u003c%= @count %\u003e\u003c/p\u003e\n    \u003c%= reflex_tag :increment, :button, \"Click\" %\u003e\n\u003c% end %\u003e\n```\n\n## Collections\n\nIn order to reconcile state to components in collections, you can specify a `collection_key` method that returns some\nvalue unique to that component.\n\n```ruby\nclass TodoComponent \u003c ViewComponentReflex::Component\n  def initialize(todo:)\n    @todo = todo\n  end\n\n  def collection_key\n    @todo.id\n  end\nend\n#\n\u003c%= render(TodoComponent.with_collection(Todo.all)) %\u003e\n```\n\nIn case you're rendering a collection of empty models, use a UUID of some sort to address the correct component instance on your page:\n\n```ruby\nclass TodoComponent \u003c ViewComponentReflex::Component\n  def initialize(todo:)\n    @todo = todo\n  end\n\n  def collection_key\n    @todo.id || SecureRandom.hex(16)\n  end\nend\n#\n\u003c%= render(TodoComponent.with_collection((0..5).map { Todo.new })) %\u003e\n```\n\n## API\n\n### permit_parameter?(initial_param, new_params)\nIf a new parameter is passed to the component during rendering, it is used instead of what's in state.\nIf you're storing instances in state, you can use this to properly compare them.\n\n```ruby\ndef permit_parameter?(initial_param, new_param)\n  if new_param.instance_of? MyModel \n    new_param.id == @my_model.id\n  else\n    super\n  end\nend\n```\n\n### omitted_from_state\nReturn an array of instance variables you want to omit from state. Only really useful if you're using the session state\nadapter, and you have an instance variable that can't be serialized.\n\n```ruby\ndef omitted_from_state\n  [:@form]\nend\n```\n\n### reflex_tag(reflex, name, content_or_options_with_block = nil, options = nil, escape = true, \u0026block)\nThis shares the same definition as `content_tag`, except it accepts a reflex as the first parameter.\n\n```erb\n\u003c%= reflex_tag :increment, :button, \"Click me!\" %\u003e\n```\n\nWould add a click handler to the `increment` method on your component.\n\nTo use a non-click event, specific that with `-\u003e` notation\n\n```erb\n\u003c%= reflex_tag \"mouseenter-\u003eincrement\", :button, \"Click me!\" %\u003e\n```\n\n### reflex_data_attributes(reflex)\n\nThis helper will give you the data attributes used in the reflex_tag above if you want to build your own elements.\n\nBuild your own tag:\n\n```erb\n\u003c%= link_to (image_tag photo.image.url(:medium)), data: reflex_data_attributes(:increment) %\u003e\n```\n\nRender a ViewComponent\n\n```erb\n\u003c%= render ButtonComponent.new(data: reflex_data_attributes(\"mouseenter-\u003eincrement\")) %\u003e\n```\n\nMake sure that you assign the reflex_data_attributes to the correct element in your component.\n\n### collection_key\nIf you're rendering a component as a collection with `MyComponent.with_collection(SomeCollection)`, you must define this method to return some unique value for the component.\nThis is used to reconcile state in the background.\n\n```ruby\ndef initialize\n  @my_model = MyModel.new\nend\n\ndef collection_key\n  @my_model.id\nend\n```\n\n### stimulate(target, data)\nStimulate another reflex from within your component. This typically requires the key of the component you're stimulating\nwhich can be passed in via parameters.\n\n```ruby\ndef initialize(parent_key)\n  @parent_key = parent_key\nend\n\ndef stimulate_other\n  stimulate(\"OtherComponent#method\", { key: @parent_key })\nend\n```\n\n### refresh!(selectors)\nRefresh a specific element on the page. Using this will implicitly run `prevent_render!`.\nIf you want to render a specific element, as well as the component, a common pattern would be to pass `selector` as one of the parameters\n\n```\ndef my_method\n  refresh! '#my-special-element', selector\nend\n```\n\n### selector\nReturns the unique selector for this component. Useful to pass to `refresh!` when refreshing custom elements.\n\n### prevent_refresh!\nBy default, VCR will re-render your component after it executes your method. `prevent_refresh!` prevents this from happening.\n\n```ruby\ndef my_method\n  prevent_refresh!\n  @foo = :bar\nend # the rendered page will not reflect this change\n```\n\n### refresh_all!\nRefresh the entire body of the page\n\n```ruby\ndef do_some_global_action\n  prevent_refresh!\n  session[:model] = MyModel.new\n  refresh_all!\nend\n```\n\n### stream_to(channel)\nStream to a custom channel, rather than the default stimulus reflex one\n\n```ruby\ndef do_something\n  stream_to MyChannel\n  \n  @foo = :bar\nend\n```\n\n### key\nThis is a key unique to a particular component. It's used to reconcile state between renders, and should be passed as a data attribute whenever a reflex is called\n\n```erb\n\u003cbutton type=\"button\" data-reflex=\"click-\u003eMyComponent#do_something\" data-key=\"\u003c%= key %\u003e\"\u003eClick me!\u003c/button\u003e\n```\n\n### component_controller(options = {}, \u0026blk)\nThis is a view helper to properly connect VCR to the component. It outputs `\u003cdiv data-controller=\"my-controller\" key=\u003c%= key %\u003e\u003c/div\u003e`\nYou *must* wrap your component in this for everything to work properly.\n\n```erb\n\u003c%= component_controller do %\u003e\n  \u003cp\u003e\u003c%= @count %\u003e\u003c/p\n\u003c% end %\u003e\n```\n\n### after_state_initialized(parameters_changed)\n\nThis is called after the state has been inserted in the component. You can use this to run conditional functions after \nsome parameter has superseeded whatever's in state\n\n```\ndef after_state_initialized(parameters_changed)\n  if parameters_changed.include?(:@filter)\n    calculate_visible_rows\n  end\nend\n```\n\n## Custom reflex base class\nReflexes typically inherit from a base ApplicationReflex. You can define the base class for a view_component_reflex by using the `reflex_base_class` accessor.\nThe parent class must inherit ViewComponentReflex::Reflex, and will throw an error if it does not.\n\n```ruby\nclass ApplicationReflex \u003c ViewComponentReflex::Reflex\n\nend\n\n\nclass MyComponent \u003c ViewComponentReflex::Component\n  MyComponent.reflex_base_class = ApplicationReflex\nend\n```\n\n## Per-component state adapters\nYou can override the default state adapter for a component by using the `state_adapter` helper.\n\n```ruby\nclass MyComponent \u003c ViewComponentReflex::Component\n  state_adapter :dom # or :session, or :memory\nend\n```\n\nThis will also accept a fully qualified constant \n\n```ruby\nclass MyComponent \u003c ViewComponentReflex::Component\n  state_adapter ViewComponentReflex::StateAdapter::Redis.new(\n    redis_opts: {\n      url: \"redis://localhost:6379/1\", driver: :hiredis\n    },\n    ttl: 3600)\nend\n```\n\n## Common patterns\nA lot of the time, you only need to update specific components when changing instance variables. For example, changing `@loading` might only need\nto display a spinner somewhere on the page. You can define setters to implicitly render the appropriate pieces of dom whenever that variable is set\n\n```ruby\ndef initialize\n  @loading = false\nend\n\ndef loading=(new_value)\n  @loading = new_value\n  refresh! '#loader'\nend\n\ndef do_expensive_action\n  prevent_refresh! \n\n  self.loading = true\n  execute_it\n  self.loading = false\nend\n```\n\n```erb\n\u003c%= component_controller do %\u003e\n  \u003cdiv id=\"loader\"\u003e \n    \u003c% if @loading %\u003e\n      \u003cp\u003eLoading...\u003c/p\u003e\n    \u003c% end %\u003e\n  \u003c/div\u003e\n\n  \u003cbutton type=\"button\" data-reflex=\"click-\u003eMyComponent#do_expensive_action\" data-key=\"\u003c%= key %\u003e\"\u003eClick me!\u003c/button\u003e\n\u003c% end\n```\n\n## State\n\nBy default (since version `2.3.2`), view_component_reflex stores component state in session. You can optionally set the state adapter\nto use the memory by changing `config.state_adapter` to `ViewComponentReflex::StateAdapter::Memory`.\n\nOptionally, you can also store state right in the dom with `ViewComponentReflex::StateAdapter::Dom`.\nNote that the DOM adapter requires the `data-reflex-dataset=\"children\"` property to be set on anything firing the reflex. You can mix it with other options like `data-reflex-dataset=\"ancestors children\"`.\nOptionally you can include all datasets in the reflex `data-reflex-dataset=\"*\"` but in large applications you may want to limit the amount of data being transferred only to what is necessary.\n\nWhen using the `ViewComponentReflex::StateAdapter::Dom` make sure your key does not contain underscores and is limited to max. 80 characters. That's important when you'd be using customized [collection_key](#collections) which increases the length of the key.\n\n## Custom State Adapters\n\nViewComponentReflex uses session for its state by default. To change this, add\nan initializer to `config/initializers/view_component_reflex.rb`.\n\n```ruby\nViewComponentReflex::Engine.configure do |config|\n  config.state_adapter = YourAdapter\nend\n```\n\n## Existing Fast Redis based State Adapter\n\nThis adapter uses hmset and hgetall to reduce the number of operations. \nThis is the recommended adapter if you are using AnyCable.\n\n```ruby\nViewComponentReflex::Engine.configure do |config|\n  config.state_adapter = ViewComponentReflex::StateAdapter::Redis.new(\n      redis_opts: {\n          url: \"redis://localhost:6379/1\", driver: :hiredis\n      },\n      ttl: 3600)\nend\n```\n\n`YourAdapter` should implement \n\n```ruby\nclass YourAdapter\n  ##\n  # request - a rails request object\n  # key - a unique string that identifies the component instance\n  def self.state(request, key)\n    # Return state for a given key\n  end\n\n  ##\n  # set_state is used to modify the state.\n  #\n  # request - a rails request object\n  # controller - the current controller\n  # key - a unique string that identifies the component\n  # new_state - the new state to set\n  def self.set_state(request, controller, key, new_state)\n    # update the state\n  end\n\n\n  ##\n  # store_state is used to replace the state entirely. It only accepts\n  # a request object, rather than a reflex because it's called from the component's \n  # side with the component's instance variables.\n  #\n  # request - a rails request object\n  # key - a unique string that identifies the component instance\n  # new_state - a hash containing the component state\n  def self.store_state(request, key, new_state = {})\n    # replace the state\n  end\nend\n```\n\n\n## Installation\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'view_component_reflex'\n```\n\nAnd then execute:\n```bash\n$ bundle\n```\n\nOr install it yourself as:\n```bash\n$ gem install view_component_reflex\n```\n\n# Common problems\n\n## Uninitialized constants \\\u003ccomponent\\\u003eReflex\nA component needs to be wrapped in `\u003c%= component_controller do %\u003e` in order to properly initialize, otherwise the Reflex class won't get created.\n\n## Session is an empty hash\nStimulusReflex 3.3 introduced _selector morphs_, allowing you to render arbitrary strings via `ApplicationController.render`, for example:\n\n```rb\ndef test_selector\n  morph '#some-container', ApplicationController.render(MyComponent.new(some: :param))\nend\n```\n\nStimulusReflex 3.4 introduced a fix that merges the current `request.env` and provides the CSRF token to fetch the session.\n\n## Help, my instance variables do not persist into the session\n\nThese instance variable names are not working and unsafe:\n\n```rb\ndef unsafe_instance_variables\n  [\n    :@view_context, :@lookup_context, :@view_renderer, :@view_flow,\n    :@virtual_path, :@variant, :@current_template, :@output_buffer, :@key,\n    :@helpers, :@controller, :@request, :@tag_builder, :@initialized_state\n  ]\nend\n```\nPlease use a different name to be able to save them to the session.\n\n## Foo Can't Be Dumped\n\nIf you are getting errors that e.g. MatchData, Singleton etc. can't be dumped, ensure that you do not set any instance variables in your components (or any class you inject into them, for that matter) that cannot be marshaled.\n\nThis can be easily remedied though, by providing a list of unmarshalable instance variables and overwriting `marshal_dump` and `marshal_load` (from [https://stackoverflow.com/a/32877159/4341756](https://stackoverflow.com/a/32877159/4341756)):\n\n```rb\nclass MarshalTest\n  UNMARSHALED_VARIABLES = [:@foo, :@bar]\n\n  def marshal_dump\n    instance_variables.reject{|m| UNMARSHALED_VARIABLES.include? m}.inject({}) do |vars, attr|\n      vars[attr] = instance_variable_get(attr)\n      vars\n    end\n  end\n\n  def marshal_load(vars)\n    vars.each do |attr, value|\n      instance_variable_set(attr, value) unless UNMARSHALED_VARIABLES.include?(attr)\n    end\n  end\nend\n```\n\n## Anycable\n\n@sebyx07 provided a solution to use anycable (https://github.com/joshleblanc/view_component_reflex/issues/23#issue-721786338)\n\nLeaving this, might help others:\n\nI tried this with any cable and I had to add this to development.rb\nOtherwise @instance_variables were nil after a reflex\n\n```ruby\n  config.cache_store = :redis_cache_store, { url: \"redis://localhost:6379/1\", driver: :hiredis }\n  config.session_store :cache_store\n```\n\n## Sidecar assets\n\nWhen using ViewComponent generated sidecar assets, the stimulus controller won't resolve properly.\nTo resolve this, override the `self.stimulus_controller` method in your component to the correct method. \n\nFor example, if you have a structure of \n\n```\ncomponents/\n  example_component/\n    example_component_controller.js\n    example_component.html.erb\n  example_component.rb\n```\n\nYou would update your `self.stimulus_controller` method to \n\n```ruby\ndef self.stimulus_controller\n  \"example-component--example-component\"\nend\n```\n\nThanks @omairrazam for this solution\n\n## License\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Caveats\n\nState uses session to maintain state as of right now. It also assumes your component view is written with a file extension of either `.html.erb`, `.html.haml` or `.html.slim`.\n\n## Support me\n\n\u003ca href=\"https://www.buymeacoffee.com/jleblanc\" target=\"_blank\"\u003e\u003cimg src=\"https://cdn.buymeacoffee.com/buttons/default-orange.png\" alt=\"Buy Me A Coffee\" height=\"40\" \u003e\u003c/a\u003e\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshleblanc%2Fview_component_reflex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshleblanc%2Fview_component_reflex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshleblanc%2Fview_component_reflex/lists"}