{"id":29571558,"url":"https://github.com/moeki0/clapton","last_synced_at":"2025-10-28T22:12:40.361Z","repository":{"id":301042114,"uuid":"869256797","full_name":"moeki0/clapton","owner":"moeki0","description":"Clapton is a Ruby on Rails gem for building web apps with pure Ruby only (no JavaScript and no HTML templates).","archived":false,"fork":false,"pushed_at":"2024-10-19T14:12:04.000Z","size":703,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-06T17:54:53.714Z","etag":null,"topics":["framework","rails","ruby","ruby-on-rails"],"latest_commit_sha":null,"homepage":"https://claptonrb.org","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/moeki0.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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,"zenodo":null}},"created_at":"2024-10-08T01:53:26.000Z","updated_at":"2025-06-20T05:39:43.000Z","dependencies_parsed_at":"2025-06-24T22:04:01.092Z","dependency_job_id":"8e2b4b90-7693-42c7-808a-1a3bf69fae22","html_url":"https://github.com/moeki0/clapton","commit_stats":null,"previous_names":["moekiorg/clapton","kawakamimoeki/clapton"],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/moeki0/clapton","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeki0%2Fclapton","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeki0%2Fclapton/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeki0%2Fclapton/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeki0%2Fclapton/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/moeki0","download_url":"https://codeload.github.com/moeki0/clapton/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moeki0%2Fclapton/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281521342,"owners_count":26515807,"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","status":"online","status_checked_at":"2025-10-28T02:00:06.022Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["framework","rails","ruby","ruby-on-rails"],"created_at":"2025-07-19T04:05:50.292Z","updated_at":"2025-10-28T22:12:40.333Z","avatar_url":"https://github.com/moeki0.png","language":"Ruby","readme":"# Clapton\n\n![version](https://badgen.net/rubygems/v/clapton)\n![downloads](https://badgen.net/rubygems/dt/clapton)\n![license](https://badgen.net/github/license/kawakamimoeki/clapton)\n\nClapton is a Ruby on Rails gem for building web apps with pure Ruby only (no JavaScript and no HTML templates).\n\n## Stack\n\n- Ruby on Rails\n- Action Cable (WebSocket)\n- [Ruby2JS](https://www.ruby2js.com/) (for compiling Ruby to JavaScript)\n- [Morphdom](https://github.com/patrick-steele-idem/morphdom)\n- importmap\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'clapton'\n```\n\nAnd then execute:\n\n    $ bundle install\n\n## Usage\n\nTo use a Clapton component in your view:\n\n```ruby\n# app/components/task_list_component.rb\nclass TaskListComponent \u003c Clapton::Component\n  def render\n    div = c(:div)\n    @state.tasks.each do |task|\n      div.add(TaskItemComponent.new(id: task[:id], title: task[:title], due: task[:due], done: task[:done]))\n    end\n    btn = c(:button)\n    btn.add(c(:text, \"Add Task\"))\n    btn.add_action(:click, :TaskListState, :add_task)\n    div.add(btn)\n  end\nend\n\n```\n\n```ruby\n# app/components/task_item_component.rb\nclass TaskItemComponent \u003c Clapton::Component\n  def render\n    div = c(:div)\n    btn = c(:button)\n    btn.add(c(:text, @state.done ? \"✅\" : \"🟩\"))\n    btn.add_action(:click, :TaskListState, :toggle_done)\n\n    tf = c(:input, @state, :title)\n    tf.add_action(:input, :TaskListState, :update_title)\n\n    dt = c(:datetime, @state, :due)\n    dt.add_action(:input, :TaskListState, :update_due)\n\n    div.add(btn).add(tf).add(dt)\n  end\nend\n\n```\n\n```ruby\n# app/states/task_list_state.rb\nclass TaskListState \u003c Clapton::State\n  attribute :tasks\n\n  def add_task(params)\n    task = Task.create(title: \"New Task\", due: Date.today, done: false)\n    self.tasks \u003c\u003c { id: task.id, title: task.title, due: task.due, done: task.done }\n  end\n\n  def toggle_done(params)\n    task = Task.find(params[:id])\n    task.update(done: !params[:done])\n    self.tasks.find { |t| t[:id] == params[:id] }[:done] = task.done\n  end\n\n  def update_title(params)\n    task = Task.find(params[:id])\n    task.update(title: params[:title])\n    self.tasks.find { |t| t[:id] == params[:id] }[:title] = task.title\n  end\n\n  def update_due(params)\n    task = Task.find(params[:id])\n    task.update(due: params[:due])\n    self.tasks.find { |t| t[:id] == params[:id] }[:due] = task.due\n  end\nend\n```\n\n```ruby\n# app/states/task_item_state.rb\nclass TaskItemState \u003c Clapton::State\n  attribute :id\n  attribute :title\n  attribute :due\n  attribute :done\nend\n```\n\n```ruby\n# app/controllers/tasks_controller.rb\nclass TasksController \u003c ApplicationController\n  def index\n    @tasks = Task.all\n    @components = [\n      [:TaskListComponent, { tasks: @tasks.map { |task| { id: task.id, title: task.title, due: task.due, done: task.done } } }]\n    ]\n  end\nend\n```\n\n```html\n# app/views/layouts/application.html.erb\n\u003c%= clapton_javascript_tag %\u003e\n```\n\n```html\n# app/views/tasks/index.html.erb\n\u003c%= clapton_tag %\u003e\n```\n\nMake sure to include the necessary route in your `config/routes.rb`:\n\n```ruby\nmount Clapton::Engine =\u003e \"/clapton\"\n```\n\n![TODO APP DEMO](./docs/todo-app-demo.gif)\n\n### Component rendering\n\n```html\n\u003c%= clapton_component_tag(\n  :TaskListComponent,\n  {\n    tasks: @tasks.map { |task| { id: task.id, title: task.title, due: task.due, done: task.done } }\n  }\n) %\u003e\n```\n\n### Generate Component and State\n\n```bash\nrails generate clapton TaskList\n```\n\nAfter running the generator, you will see the following files:\n\n- `app/components/task_list_component.rb`\n- `app/states/task_list_state.rb`\n\n### Special Event\n\n#### render\n\nThe `render` event is a special event that is triggered when the component is rendered.\n\n```ruby\n# app/components/task_list_component.rb\nclass TaskListComponent \u003c Clapton::Component\n  def render\n    # ...\n    div = c(:div)\n    div.add_action(:render, :TaskListState, :add_empty_task, debounce: 500)\n  end\nend\n```\n\n### Effect\n\nThe `effect` method is a method that is triggered when the state is changed.\n\n```ruby\n# app/components/task_list_component.rb\nclass TaskListComponent \u003c Clapton::Component\n  effect [:tasks] do |state|\n    puts state[:tasks]\n  end\nend\n```\n\nIf dependencies are not specified, the effect will be triggered on the first render.\n\n```ruby\n# app/components/video_player_component.rb\nclass VideoPlayerComponent \u003c Clapton::Component\n  effect [] do\n    puts \"First render\"\n  end\nend\n```\n\n### Streaming\n\nClapton supports streaming.\n\n```ruby\n# app/states/chat_state.rb\nclass ChatState \u003c Clapton::State\n  attribute :messages\n\n  def send(params)\n    self.messages \u003c\u003c { role: \"user\", content: params[:content] }\n    yield continue: true # Continue the streaming\n\n    client = OpenAI::Client.new(\n      access_token: ENV.fetch(\"OPENAI_ACCESS_TOKEN\"),\n      log_errors: true\n    )\n    self.messages \u003c\u003c { role: \"assistant\", content: \"\" }\n    client.chat(\n      parameters: {\n        model: \"gpt-4o-mini\",\n        messages: messages,\n        stream: proc do |chunk, _bytesize|\n          if chunk.dig(\"choices\", 0, \"finish_reason\") == \"stop\"\n            yield continue: false # Stop the streaming\n          end\n\n          self.messages.last[:content] \u003c\u003c chunk.dig(\"choices\", 0, \"delta\", \"content\")\n          yield continue: true\n        end\n      }\n    )\n  end\nend\n```\n\n### Optional\n\n#### Action Cable\n\nClapton uses Action Cable to broadcast state changes to the client.\nIf you want to identify the user, you can set the `current_user` in the connection.\n\n```ruby\n# app/channels/application_cable/connection.rb\nmodule ApplicationCable\n  class Connection \u003c ActionCable::Connection::Base\n    identified_by :current_user\n\n    def connect\n      self.current_user = find_verified_user\n    end\n\n    private\n\n    def find_verified_user\n      if verified_user = User.find_by(id: cookies.signed[:user_id])\n        verified_user\n      else\n        reject_unauthorized_connection\n      end\n    end\n  end\nend\n```\n\n### Using with importmap-rails\n\nUse `clapton_javascript_tag` instead of `javascript_importmap_tags`.\n\n```diff\n- \u003c%= javascript_importmap_tags %\u003e\n+ \u003c%= clapton_javascript_tag %\u003e\n```\n\n### Events\n\n#### clapton:render\n\nThe `clapton:render` event is a custom event that is triggered when the component is rendered.\n\n```javascript\ndocument.addEventListener(\"clapton:render\", () =\u003e {\n  console.log(\"clapton:render\");\n});\n```\n\n### Testing\n\n#### RSpec\n\n```ruby\n# spec/spec_helper.rb\nrequire \"clapton/test_helper/rspec\"\n\nRSpec.configure do |config|\n  config.include Clapton::TestHelper::RSpec, type: :component\nend\n```\n\n```ruby\n# spec/components/task_list_component_spec.rb\n\ndescribe \"TaskListComponent\", type: :component do\n  it \"renders\" do\n    render_component(\"TaskListComponent\", tasks: [{ id: 1, title: \"Task 1\", done: false, due: Time.current }])\n    # You can use Capybara matchers here\n    expect(page).to have_selector(\"input[type='text']\")\n  end\nend\n```\n\n#### Minitest\n\n```ruby\n# test/test_helper.rb\nrequire \"clapton/test_helper/minitest\"\n\nclass ActiveSupport::TestCase\n  include Clapton::TestHelper::Minitest\nend\n```\n\n```ruby\n# test/components/task_list_component_test.rb\nclass TaskListComponentTest \u003c ActiveSupport::TestCase\n  test \"renders\" do\n    render_component(\"TaskListComponent\", tasks: [{ id: 1, title: \"Task 1\", done: false, due: Time.current }])\n    # You can use Capybara matchers here\n    assert_select \"input[type='text']\"\n  end\nend\n```\n\n## Deployment\n\nRun `bundle exec rake clapton:compile` to compile the components.\n\n`app/components` is codes that are compiled to JavaScript.\nSo, you need to ignore the directory from autoloading.\n\n```ruby\n# config/application.rb\n\nRails.autoloaders.main.ignore(Rails.root.join(\"app/components\"))\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/dev` to start the development server.\n\n### Testing\n\nRun `bundle exec rake test` to run the test suite.\n\nRun `cd test/dummy \u0026\u0026 bundle exec rake test` to run the test suite for the dummy app.\n\nRun `cd test/dummy \u0026\u0026 bundle exec rspec` to run the test suite for the dummy app with RSpec.\n\nRun `cd lib/clapton/javascripts \u0026\u0026 npm run test` to run the test suite for the JavaScript part.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/kawakamimoeki/clapton. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/kawakamimoeki/clapton/blob/main/CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoeki0%2Fclapton","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmoeki0%2Fclapton","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoeki0%2Fclapton/lists"}