###* Server protocol =============== You rarely need to change server-side code in order to use Unpoly. There is no need to provide a JSON API, or add extra routes for AJAX requests. The server simply renders a series of full HTML pages, just like it would without Unpoly. That said, there is an **optional** protocol your server can use to exchange additional information when Unpoly is [updating fragments](/up.link). While the protocol can help you optimize performance and handle some edge cases, implementing it is entirely optional. For instance, `unpoly.com` itself is a static site that uses Unpoly on the frontend and doesn't even have a server component. If you have [installed Unpoly as a Rails gem](/install/rails), the protocol is already implemented and you will get some [Ruby bindings](https://github.com/unpoly/unpoly/blob/master/README_RAILS.md) in your controllers and views. If your server-side app uses another language or framework, you should be able to implement the protocol in a very short time. \#\#\# Redirect detection Unpoly requires an additional response header to detect redirects, which are otherwise undetectable for any AJAX client. After the form's action performs a redirect, the next response should include the new URL in the HTTP headers: ```http X-Up-Method: GET X-Up-Location: /current-url ``` The **simplest implementation** is to set these headers for every request. \#\#\# Optimizing responses When [updating a fragment](/up.link), Unpoly will send an additional HTTP header containing the CSS selector that is being replaced: ```http X-Up-Target: .user-list ``` Server-side code is free to **optimize its response** by only returning HTML that matches the selector. For example, you might prefer to not render an expensive sidebar if the sidebar is not targeted. \#\#\# Pushing a document title to the client When [updating a fragment](/up.link), Unpoly will by default extract the `` from the server response and update the document title accordingly. The server can also force Unpoly to set a document title by passing a HTTP header: ```http X-Up-Title: My server-pushed title ``` This is useful when you [optimize your response](#optimizing-responses) and not render the application layout unless it is targeted. Since your optimized response no longer includes a `<title>`, you can instead use the HTTP header to pass the document title. \#\#\# Signaling failed form submissions When [submitting a form via AJAX](/form-up-target) Unpoly needs to know whether the form submission has failed (to update the form with validation errors) or succeeded (to update the `up-target` selector). For Unpoly to be able to detect a failed form submission, the response must be return a non-200 HTTP status code. We recommend to use either 400 (bad request) or 422 (unprocessable entity). To do so in [Ruby on Rails](http://rubyonrails.org/), pass a [`:status` option to `render`](http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option): class UsersController < ApplicationController def create user_params = params[:user].permit(:email, :password) @user = User.new(user_params) if @user.save? sign_in @user else render 'form', status: :bad_request end end end \#\#\# Detecting live form validations When [validating a form](/up-validate), Unpoly will send an additional HTTP header containing a CSS selector for the form that is being updated: ```http X-Up-Validate: .user-form ``` When 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. Below you will an example for a writing route that is aware of Unpoly's live form validations. The code is for [Ruby on Rails](http://rubyonrails.org/), but you can adapt it for other languages: class UsersController < ApplicationController def create user_params = params[:user].permit(:email, :password) @user = User.new(user_params) if request.headers['X-Up-Validate'] @user.valid? # run validations, but don't save to the database render 'form' # render form with error messages elsif @user.save? sign_in @user else render 'form', status: :bad_request end end end \#\#\# Signaling the initial request method If the initial page was loaded with a non-`GET` HTTP method, Unpoly prefers to make a full page load when you try to update a fragment. Once the next page was loaded with a `GET` method, Unpoly will restore its standard behavior. This fixes two edge cases you might or might not care about: 1. Unpoly replaces the initial page state so it can later restore it when the user goes back to that initial URL. However, if the initial request was a POST, Unpoly will wrongly assume that it can restore the state by reloading with GET. 2. Some browsers have a bug where the initial request method is used for all subsequently pushed states. That means if the user reloads the page on a later GET state, the browser will wrongly attempt a POST request. Modern Firefoxes, Chromes and IE10+ don't seem to be affected by this. In order to allow Unpoly to detect the HTTP method of the initial page load, the server must set a cookie: ```http Set-Cookie: _up_method=POST ``` When Unpoly boots, it will look for this cookie and configure its behavior accordingly. The cookie is then deleted in order to not affect following requests. The **simplest implementation** is to set this cookie for every request that is neither `GET` nor contains an [`X-Up-Target` header](/#optimizing-responses). For all other requests an existing cookie should be deleted. @class up.protocol ### up.protocol = (($) -> u = up.util ###* @function up.protocol.locationFromXhr @internal ### locationFromXhr = (xhr) -> xhr.getResponseHeader(config.locationHeader) ###* @function up.protocol.titleFromXhr @internal ### titleFromXhr = (xhr) -> xhr.getResponseHeader(config.titleHeader) ###* @function up.protocol.methodFromXhr @internal ### methodFromXhr = (xhr) -> if method = xhr.getResponseHeader(config.methodHeader) u.normalizeMethod(method) ###* Server-side companion libraries like unpoly-rails set this cookie so we have a way to detect the request method of the initial page load. There is no JavaScript API for this. @function up.protocol.initialRequestMethod @internal ### initialRequestMethod = u.memoize -> methodFromServer = up.browser.popCookie(config.methodCookie) (methodFromServer || 'get').toLowerCase() ###* Configures strings used in the optional [server protocol](/up.protocol). @property up.protocol.config @param [config.targetHeader='X-Up-Target'] @param [config.locationHeader='X-Up-Location'] @param [config.titleHeader='X-Up-Title'] @param [config.validateHeader='X-Up-Validate'] @param [config.methodHeader='X-Up-Method'] @param [config.methodCookie='_up_method'] @param [config.methodParam='_method'] The name of the POST parameter when [wrapping HTTP methods](/up.form.config#config.wrapMethods) in a `POST` request. @experimental ### config = u.config targetHeader: 'X-Up-Target' locationHeader: 'X-Up-Location' validateHeader: 'X-Up-Validate' titleHeader: 'X-Up-Title' methodHeader: 'X-Up-Method' methodCookie: '_up_method' methodParam: '_method' ## Unfortunately we cannot offer reset without introducing cycles ## in the asset load order # # reset = -> # config.reset() # # up.on 'up:framework:reset', reset config: config locationFromXhr: locationFromXhr titleFromXhr: titleFromXhr methodFromXhr: methodFromXhr initialRequestMethod: initialRequestMethod )(jQuery)