{"id":13878791,"url":"https://github.com/unpoly/unpoly-rails","last_synced_at":"2025-04-07T19:16:09.061Z","repository":{"id":50748486,"uuid":"386622760","full_name":"unpoly/unpoly-rails","owner":"unpoly","description":"Ruby on Rails bindings for Unpoly","archived":false,"fork":false,"pushed_at":"2025-02-19T07:13:33.000Z","size":141,"stargazers_count":18,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-31T16:20:30.246Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/unpoly.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"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":"2021-07-16T12:04:11.000Z","updated_at":"2025-02-19T07:13:37.000Z","dependencies_parsed_at":"2023-01-22T19:15:40.889Z","dependency_job_id":"3a242a52-fe09-430e-8482-e7edee30839e","html_url":"https://github.com/unpoly/unpoly-rails","commit_stats":{"total_commits":44,"total_committers":2,"mean_commits":22.0,"dds":"0.022727272727272707","last_synced_commit":"ba8a9588c4825137f07939564a75b912ad36b9f7"},"previous_names":[],"tags_count":56,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unpoly%2Funpoly-rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unpoly%2Funpoly-rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unpoly%2Funpoly-rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unpoly%2Funpoly-rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/unpoly","download_url":"https://codeload.github.com/unpoly/unpoly-rails/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247713258,"owners_count":20983683,"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":[],"created_at":"2024-08-06T08:02:00.118Z","updated_at":"2025-04-07T19:16:09.027Z","avatar_url":"https://github.com/unpoly.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"unpoly-rails: Ruby on Rails bindings for Unpoly\n===============================================\n\n[Unpoly](https://unpoly.com) is an [unobtrusive JavaScript](https://en.wikipedia.org/wiki/Unobtrusive_JavaScript) framework for server-side web applications.\n\nThe `unpoly-rails` gem helps integrating Unpoly with [Ruby on Rails](https://rubyonrails.org/) applications.\n\nThis branch tracks the next major version, Unpoly **3.x**.\\\nIf you're using Unpoly **2.x**, use the [`2.x-stable`](https://github.com/unpoly/unpoly-rails/tree/2.x-stable) branch.\\\nIf you're using Unpoly **1.x** or **0.x**, use the [`1.x-stable`](https://github.com/unpoly/unpoly/tree/1.x-stable/lib/unpoly/rails) branch in the [`unpoly`](https://github.com/unpoly/unpoly-rails/tree/2.x-stable) repository.\n\n\nInstalling the gem\n------------------\n\nAdd the following line to your `Gemfile`:\n\n```ruby\ngem 'unpoly-rails'\n```\n\nNow run `bundle install` and restart your development server.\n\n\nInstalling frontend assets\n--------------------------\n\n### With npm\n\nIf you're using a build tool like [esbuild](https://esbuild.github.io/) or [Webpacker](https://edgeguides.rubyonrails.org/webpacker.html), install the [`unpoly` npm package](https://unpoly.com/install/npm) to get Unpoly's frontend files.\n\nNow `import` Unpoly from your `application.js` pack:\n\n```js\nimport 'unpoly/unpoly.js'\nimport 'unpoly/unpoly.css'\n```\n\nYou may need to import [additional files](https://unpoly.com/install), e.g. when migrating from an old Unpoly version.\n\n\n### With the Asset Pipeline\n\nIf you're using the [Asset Pipeline](https://guides.rubyonrails.org/asset_pipeline.html), this `unpoly-rails` gem also contains Unpoly's frontend files.  The files are automatically added to the Asset Pipeline's \u003ca href=\"https://guides.rubyonrails.org/asset_pipeline.html#search-paths\"\u003esearch path\u003c/a\u003e.\n\nAdd the following line to your `application.js` manifest:\n\n```js\n//= require unpoly\n```\n\nAlso add the following line to your `application.css` manifest:\n\n```css\n/*\n *= require unpoly\n */\n```\n\nYou may need to require [additional files](https://unpoly.com/install), e.g. when migrating from an old Unpoly version.\n\n\nServer protocol helpers\n-----------------------\n\nThis `unpoly-rails` gem implements the \u003ca href=\"https://unpoly.com/up.protocol\"\u003eoptional server protocol\u003c/a\u003e by providing the following helper methods to your controllers, views and helpers.\n\n\n### Detecting a fragment update\n\nUse `up?` to test whether the current request is a [fragment update](https://unpoly.com/up.link):\n\n```ruby\nup? # =\u003e true or false\n```\n\nTo retrieve the CSS selector that is being [updated](https://unpoly.com/up.link), use `up.target`:\n\n```ruby\nup.target # =\u003e '.content'\n```\n\nThe Unpoly frontend will expect an HTML response containing an element that matches this selector. Your Rails app is free to render a smaller response that only contains HTML matching the targeted selector. You may call `up.target?` to test whether a given CSS selector has been targeted:\n\n```ruby\nif up.target?('.sidebar')\n  render('expensive_sidebar_partial')\nend\n```\n\nFragment updates may target different selectors for successful (HTTP status `200 OK`) and failed (status `4xx` or `5xx`) responses.\nUse these methods to inspect the target for failed responses:\n\n- `up.fail_target`: The CSS selector targeted for a failed response\n- `up.fail_target?(selector)`: Whether the given selector is targeted for a failed response\n- `up.any_target?(selector)`: Whether the given selector is targeted for either a successful or a failed response\n\n### Changing the render target\n\nThe server may instruct the frontend to render a different target by assigning a new CSS selector to the `up.target` property:\n\n```ruby\nunless signed_in?\n  up.target = 'body'\n  render 'sign_in'\nend\n```\n\nThe frontend will use the server-provided target for both successful (HTTP status `200 OK`) and failed (status `4xx` or `5xx`) responses.\n\n\n### Rendering nothing\n\nSometimes it's OK to render nothing, e.g. when you know that the current layer is to be closed.\n\nIn this case use `head(:no_content)`:\n\n```ruby\nclass NotesController \u003c ApplicationController\n  def create\n    @note = Note.new(note_params)\n    if @note.save\n      if up.layer.overlay?\n        up.layer.accept(@note.id)\n        head :no_content\n      else\n        redirect_to @note\n      end\n    end\n  end\nend\n```\n\n\n### Pushing a document title to the client\n\nTo force Unpoly to set a document title when processing the response:\n\n```ruby\nup.title = 'Title from server'\n```\n\nThis is useful when you skip rendering the `\u003chead\u003e` in an Unpoly request.\n\n### Emitting events on the frontend\n\nYou may use `up.emit` to emit an event on the `document` after the\nfragment was updated:\n\n```ruby\nclass UsersController \u003c ApplicationController\n\n  def show\n    @user = User.find(params[:id])\n    up.emit('user:selected', id: @user.id)\n  end\n\nend\n```\n\nIf you wish to emit an event on the current [layer](https://unpoly.com/up.layer)\ninstead of the `document`, use `up.layer.emit`:\n\n```ruby\nclass UsersController \u003c ApplicationController\n\n  def show\n    @user = User.find(params[:id])\n    up.layer.emit('user:selected', id: @user.id)\n  end\n\nend\n```\n\n### Detecting an Unpoly form validation\n\nTo test whether the current request is a [form validation](https://unpoly.com/input-up-validate):\n\n```ruby\nup.validate?\n```\n\nWhen detecting a validation request, the server is expected to validate (but not save) the form submission and render a new copy of the form with validation errors. A typical saving action should behave like this:\n\n```ruby\nclass UsersController \u003c ApplicationController\n\n  def create\n    user_params = params[:user].permit(:email, :password)\n    @user = User.new(user_params)\n    if up.validate?\n      @user.valid?  # run validations, but don't save to the database\n      render 'form' # render form with error messages\n    elsif @user.save?\n      sign_in @user\n    else\n      render 'form', status: :bad_request\n    end\n  end\n\nend\n```\n\nYou may also access the [names of the fields that triggered the validation request](https://unpoly.com/X-Up-Validate):\n\n```ruby\nup.validate_names # =\u003e ['email', 'password']\n```\n\nYou may also test if a given field name is being validated:\n\n```ruby\nup.validate_name?('email') # =\u003e true\n```\n\n\n### Detecting a fragment reload\n\nWhen Unpoly [reloads](https://unpoly.com/up.reload) or [polls](https://unpoly.com/up-poll) a fragment, the server will often render the same HTML. You can configure your controller actions to only render HTML if the underlying content changed since an earlier request.\n\nOnly rendering when needed saves \u003cb\u003eCPU time\u003c/b\u003e on your server, which spends most of its response time rendering HTML. This also reduces the \u003cb\u003ebandwidth cost\u003c/b\u003e for a request/response exchange to **~1 KB**.\n\nWhen a fragment is reloaded, Unpoly sends an [`If-Modified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since) request header with the fragment's earlier [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) time. It also sends an [`If-None-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) header with the fragment's earlier [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag).\n\nRails' [conditional GET support](https://guides.rubyonrails.org/caching_with_rails.html#conditional-get-support) lets you compare and set modification times and ETags with methods like `#fresh_when` or `#stale?`:\n\n```ruby\nclass MessagesController \u003c ApplicationController\n\n  def index\n    @messages = current_user.messages.order(time: :desc)\n\n    # If the request's ETag and last modification time matches the given `@messages`,\n    # does not render and send a a `304 Not Modified` response.\n    # If the request's ETag or last modification time does not match, we will render\n    # the `index` view with fresh `ETag` and `Last-Modified` headers.\n    fresh_when(@messages)\n  end\n\nend\n```\n\n\n\n### Allowing callbacks with a strict CSP\n\nWhen your Content Security Policy disallows `eval()`, Unpoly cannot directly run callbacks HTML attributes. This affects `[up-]` attributes like `[up-on-loaded]` or `[up-on-accepted]`. See [Unpoly's CSP guide](https://unpoly.com/csp) for details.\n\nThe following callback would crash the fragment update with an error like `Uncaught EvalError: call to Function() blocked by CSP`:\n\n```ruby\nlink_to 'Click me', '/path, 'up-follow': true, 'up-on-loaded': \"alert()\"\n```\n\nUnpoly lets your work around this by prefixing your callback with your response's [CSP nonce](https://content-security-policy.com/nonce/):\n\n```ruby\nlink_to 'Click me', '/path', 'up-follow': true, 'up-on-loaded': 'nonce-kO52Iphm8BAVrcdGcNYjIA== alert()')\n```\n\nTo keep your callbacks compact, you may use the `up.safe_callback` helper for this:\n\n```ruby\nlink_to 'Click me', '/path, 'up-follow': true, 'up-on-loaded': up.safe_callback(\"alert()\")\n```\n\nFor this to work you must also include the `\u003cmeta name=\"csp-nonce\"\u003e` tag in the `\u003chead\u003e` of your initial page. Rails has a [`csp_meta_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag) helper for that purpose.\n\n\u003e [!NOTE]\n\u003e Prefixing nonces only works for `[up-on...]` attributes. You cannot use it for native HTML attributes like `[onclick]`.\n\n\n### Accessing the targeted layer\n\nUse the methods below to interact with the [layer](/up.layer) of the fragment being targeted.\n\nNote that fragment updates may target different layers for successful (HTTP status `200 OK`) and failed (status `4xx` or `5xx`) responses.\n\n#### `up.layer.mode`\n\nReturns the [mode](https://unpoly.com/up.layer.mode) of the targeted layer (e.g. `\"root\"` or `\"modal\"`).\n\n#### `up.layer.root?`\n\nReturns whether the targeted layer is the root layer.\n\n#### `up.layer.overlay?`\n\nReturns whether the targeted layer is an overlay (not the root layer).\n\n#### `up.layer.context`\n\nReturns the [context](https://unpoly.com/up.context) object of the targeted layer.\nSee documentation for `up.context`, which is an alias for `up.layer.context`.\n\n#### `up.layer.accept(value)`\n\n[Accepts](https://unpoly.com/up.layer.accept) the current overlay.\n\nDoes nothing if the root layer is targeted.\n\nNote that Rails expects every controller action to render or redirect.\nYour action should either call `up.render_nothing` or respond with `text/html` content matching the requested target.\n\n#### `up.layer.dismiss(value)`\n\n[Dismisses](https://unpoly.com/up.layer.dismisses) the current overlay.\n\nDoes nothing if the root layer is targeted.\n\nNote that Rails expects every controller action to render or redirect.\nYour action should either call `up.render_nothing` or respond with `text/html` content matching the requested target.\n\n#### `up.layer.emit(type, options)`\n\n[Emits an event](https://unpoly.com/up.layer.emit) on the targeted layer.\n\n#### `up.fail_layer.mode`\n\nReturns the [mode](https://unpoly.com/up.layer.mode) of the layer targeted for a failed response.\n\n#### `up.fail_layer.root?`\n\nReturns whether the layer targeted for a failed response is the root layer.\n\n#### `up.fail_layer.overlay?`\n\nReturns whether the layer targeted for a failed response is an overlay.\n\n#### `up.fail_layer.context`\n\nReturns the [context](https://unpoly.com/up.context) object of the layer targeted for a failed response.\n\n\n### Opening a new overlay\n\nYou can request the frontend to [render your response in a new overlay](https://unpoly.com/X-Up-Open-Layer):\n\n```ruby\nup.layer.open\n```\n\nThis will discard the [initial request target](#detecting-a-fragment-update)\nand render your response into [`:main`](https://unpoly.com/main). You can pass an explicit\ntarget as a `:target` option:\n\n```ruby\nup.layer.open(target: '#overlay')\n```\n\nAny [visual layer options](https://unpoly.com/customizing-overlays) can be also be passed as keyword arguments:\n\n\n```ruby\nup.layer.open(mode: 'drawer', position: 'right')\n```\n\n\n\n### Expiring the client-side cache\n\nThe Unpoly frontend [caches server responses](https://unpoly.com/caching) for a few minutes, making requests to these URLs return instantly.\nOnly `GET` requests are cached. The entire cache is expired after every non-`GET` request (like `POST` or `PUT`).\n\nThe server may override these defaults. For instance, the server can expire Unpoly's client-side response cache, even for `GET` requests:\n\n```ruby\nup.cache.expire\n```\n\nYou may also expire a single URL or [URL pattern](https://unpoly.com/url-patterns):\n\n```ruby\nup.cache.expire('/notes/*')\n```\n\nYou may also prevent cache expiration for an unsafe request:\n\n```ruby\nup.cache.expire(false)\n```\n\nHere is an longer example where the server uses careful cache management to avoid expiring too much of the client-side cache:\n\n```ruby\ndef NotesController \u003c ApplicationController\n\n  def create\n    @note = Note.create!(params[:note].permit(...))\n    if @note.save\n      up.cache.expire('/notes/*') # Only expire affected entries\n      redirect_to(@note)\n    else\n      up.cache.expire(false) # Keep the cache fresh because we haven't saved\n      render 'new'\n    end\n  end\n  ...\nend\n```\n\n### Evicting pages from the client-side cache\n\nInstead of *expiring* pages from the cache you may also *evict*. The difference is that expired pages can still be rendered instantly and are then [revalidated](/caching#revalidation) with the server. Evicted pages are erased from the cache.\n\nYou may also expire all entries matching an [URL pattern](https://unpoly.com/url-patterns):\n\nTo evict the entire client-side cache:\n\n```ruby\nup.cache.evict\n```\n\nYou may also evict a single URL or [URL pattern](https://unpoly.com/url-patterns):\n\n```ruby\nup.cache.evict('/notes/*')\n```\n\n\n### Unpoly headers are preserved through redirects\n\n`unpoly-rails` patches [`redirect_to`](https://api.rubyonrails.org/classes/ActionController/Redirecting.html#method-i-redirect_to)\nso [Unpoly-related request and response headers](https://unpoly.com/up.protocol) are preserved for the action you redirect to.\n\n\n### Accessing Unpoly request headers automatically sets a `Vary` response header\n\nAccessing [Unpoly-related request headers](https://unpoly.com/up.protocol) through helper methods like `up.target` will automatically add a [`Vary`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) response header. This is to indicate that the request header influenced the response and the response should be cached separately for each request header value.\n\nFor example, a controller may access the request's `X-Up-Mode` through the `up.layer.mode` helper:\n\n```ruby\ndef create\n  # ...\n\n  if up.layer.mode == 'modal' # Sets Vary header\n    up.layer.accept\n  else\n    redirect_to :show\n  end\nend\n```\n\n`unpoly-rails` will automatically add a `Vary` header to the response:\n\n```http\nVary: X-Up-Mode\n```\n\nThere are cases when reading an Unpoly request header does not necessarily influence the response, e.g. for logging. In that cases no `Vary` header should be set. To do so, call the helper method inside an `up.no_vary` block:\n\n```ruby\nup.no_vary do\n  Rails.logger.info(\"Unpoly mode is \" + up.layer.mode.inspect) # No Vary header is set\nend\n````\n\nNote that accessing `response.headers[]` directly never sets a `Vary` header:\n\n```ruby\nRails.logger.info(\"Unpoly mode is \" + response.headers['X-Up-Mode']) # No Vary header is set\n```\n\n\n### Automatic redirect detection\n\n`unpoly-rails` installs a [`before_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-before_action) into all controllers which echoes the request's URL as a response header `X-Up-Location` and the request's\nHTTP method as `X-Up-Method`.\n\n\n### Automatic method detection for initial page load\n\n`unpoly-rails` sets an `_up_method` cookie that Unpoly needs to detect the request method for the initial page load.\n\nIf the initial page was loaded with a non-`GET` HTTP method, Unpoly will fall back to full page loads for all actions that require `pushState`.\n\nThe reason for this is that some browsers remember the method of the initial page load and don't let the application change it, even with `pushState`. Thus, when the user reloads the page much later, an affected browser might request a `POST`, `PUT`, etc. instead of the correct method.\n\n\n### Working with context\n\nCalling `up.context` will return the [context](https://unpoly.com/up.context) object of the targeted layer.\n\nThe context is a JSON object shared between the frontend and the server.\nIt persists for a series of Unpoly navigation, but is cleared when the user makes a full page load.\nDifferent Unpoly [layers](https://unpoly.com/up.layer) will usually have separate context objects,\nalthough layers may choose to share their context scope.\n\nYou may read and change the context object. Changes will be sent to the frontend with your response.\n\n```ruby\nclass GamesController \u003c ApplicationController\n\n  def restart\n    up.context[:lives] = 3\n    render 'stage1'\n  end\n\nend\n```\n\nKeys can be accessed as either strings or symbols:\n\n```ruby\nputs \"You have \" + up.layer.context[:lives] + \" lives left\"\nputs \"You have \" + up.layer.context['lives'] + \" lives left\"\n````\n\nYou may delete a key from the frontend by calling `up.context.delete`:\n\n```ruby\nup.context.delete(:foo)\n````\n\nYou may replace the entire context by calling `up.context.replace`:\n\n```ruby\ncontext_from_file = JSON.parse(File.read('context.json))\nup.context.replace(context_from_file)\n```\n\n`up.context` is an alias for `up.layer.context`.\n\n\n\nWhat you still need to do manually\n----------------------------------\n\n### Failed form submissions must return a non-200 status code\n\nUnpoly lets you submit forms via AJAX by using the [`form[up-submit]`](https://unpoly.com/form-up-submit) selector or [`up.submit()`](https://unpoly.com/up.submit) function.\n\nFor Unpoly to be able to detect a failed form submission,\nthe form must be re-rendered with a non-200 HTTP status code.\nWe recommend to use either 400 (bad request) or 422 (unprocessable entity).\n\nTo do so in Rails, pass a [`:status` option to `render`](http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option):\n\n```ruby\nclass UsersController \u003c ApplicationController\n\n  def create\n    user_params = params[:user].permit(:email, :password)\n    @user = User.new(user_params)\n    if @user.save?\n      sign_in @user\n    else\n      render 'form', status: :bad_request\n    end\n  end\n\nend\n```\n\nDevelopment\n-----------\n\n### Before you make a PR\n\nBefore you create a pull request, please have some discussion about the proposed change by [opening an issue on GitHub](https://github.com/unpoly/unpoly/issues/new).\n\n### Running tests\n\n- Install the Ruby version from `.ruby-version` (currently 2.3.8)\n- Install Bundler by running `gem install bundler`\n- Install dependencies by running `bundle install`\n- Run `bundle exec rspec`\n\nThe tests run against a minimal Rails app that lives in `spec/dummy`.\n\n### Making a new release\n\nInstall the `unpoly-rails` and [`unpoly`](https://github.com/unpoly/unpoly) repositories into the same parent folder:\n\n```\nprojects/\n  unpoly/\n  unpoly-rails/\n```\n\nDuring development `unpoly-rails` will use assets from the folder `assets/unpoly-dev`, which is symlinked against the `dist` folder of the ``unpoly` repo.\n\nBefore packaging the gem, a rake task will copy symlinked files `assets/unpoly-dev/*` to `assets/unpoly/*`. The latter is packaged into the gem and distributed.\n\n```\nprojects/\n  unpoly/\n    dist/\n      unpoly.js\n      unpoly.css\n  unpoly-rails\n    assets/\n      unpoly-dev   -\u003e ../../unpoly/dist\n        unpoly.js  -\u003e ../../unpoly/dist/unpoly.js\n        unpoly.css -\u003e ../../unpoly/dist/unpoly.css\n      unpoly\n        unpoly.js\n        unpoly.css\n```\n\nMaking a new release of `unpoly-rails` involves the following steps:\n\n- Make a new build of unpoly (`npm run build`)\n- Make a new release of the unpoly npm package\n- Bump the version in `lib/unpoly/rails/version.rb` to match that in Unpoly's `package.json`\n- Commit and push the changes\n- Run `rake gem:release`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funpoly%2Funpoly-rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Funpoly%2Funpoly-rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funpoly%2Funpoly-rails/lists"}