igor-alexandrov / wiselinks

If Turbolinks are not enough for you. Wiselinks makes your application work faster.
MIT License
723 stars 89 forks source link

Build Status Code Climate Coverage Status Dependency Status Gem Version

Wiselinks

Wiselinks makes following links and submitting forms in your web application faster.

You may find Wiselinks similar to Turbolinks or Pjax, but Wiselinks works as a whitelist rather than blacklist. We tried to make Wiselinks as easy to use as Turbolinks are but also as configurable as Pjax.

Try Wiselinks online in our demo application:

Compatibility

Please be advised that Javascript events in wiselinks-0.5.0 are not backward compatible.

Wiselinks uses History.js library to perform requests.

Wiselinks works in all major browsers including browsers that do not support HTML History API out of the box.

In Comparison to Turbolinks and PJAX

Wiselinks Turbolinks PJAX
Work in HTML5 browsers Yes Yes Yes
Work in browsers without History API Yes, with hashbang (can be switched off) No, degrades to normal request processing No, degrades to normal request processing
Work without JavaScript No, degrades to normal request processing No, degrades to normal request processing No, degrades to normal request processing
Form processing Yes No Yes (experimental feature)
Form blank values exclusion Yes (optional) No No
Form values optimization Yes No No
Assets change detection Yes, by calculating assets MD5 hash on boot Yes, by parsing document head on every request Yes, manually
30x HTTP redirects processing Yes No Yes

Installation

Rails

Add this to your Gemfile:

gem 'wiselinks'

Then do:

bundle install

Restart your server and you're now using wiselinks!

All others

Copy wiselinks-x.y.z.js or wiselinks-x.y.z.min.js from build folder in this project to your project.

How does it work?

CoffeeScript

Create Wiselinks object in your application.js.coffee:

#= require jquery
#= require wiselinks

$(document).ready ->
    window.wiselinks = new Wiselinks()

You can disable HTML4 browsers support easily:

#= require jquery
#= require wiselinks

$(document).ready ->
    window.wiselinks = new Wiselinks($('body'), html4: false )

Or you can add some more options, if you want:

#= require jquery
#= require jquery.role
#= require wiselinks

$(document).ready ->
    # DOM element with id = "content" will be replaced after data load.
    window.wiselinks = new Wiselinks($('#content'))

    $(document).off('page:loading').on(
        'page:loading'
        (event, $target, render, url) ->
            console.log("Loading: #{url} to #{$target.selector} within '#{render}'")
            # code to start loading animation
    )

    $(document).off('page:redirected').on(
        'page:redirected'
        (event, $target, render, url) ->
            console.log("Redirected to: #{url}")
            # code to start loading animation
    )

    $(document).off('page:always').on(
        'page:always'
        (event, xhr, settings) ->
            console.log("Wiselinks page loading completed")
            # code to stop loading animation
    )

    $(document).off('page:done').on(
        'page:done'
        (event, $target, status, url, data) ->
            console.log("Wiselinks status: '#{status}'")
    )

    $(document).off('page:fail').on(
        'page:fail'
        (event, $target, status, url, error, code) ->
            console.log("Wiselinks status: '#{status}'")
            # code to show error message
    )

HTML

Click on links with data-push attribute will fire History.pushState() event. Data from the request will replace content of the container that was passed to Wiselinks (default is $('body'))

<ul class="menu">
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/" data-push="true">Home</a>
    </li>
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/contacts" data-push="true">Contacts</a>
    </li>
</ul>

Click on links with data-replace will fire History.replaceState() event. Data from the request will replace content of the container that was passed to Wiselinks (default is "body")

<div class="dialog">
    <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/step2" data-replace="true">Proceed to the next step</a>
</div>

Click on following links will fire History.pushState() event. Data from the request will be pasted into <div id="catalog">. This configuration is widely when you have list of items that are paginated, sorted or maybe grouped by some attributes and you want to update only these items and nothing more on page.

<ul class="pagination">
    <li>
        <span>1</span>
    </li>
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/?page=2" data-push="true" data-target="#catalog">2</a>
    </li>
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/?page=3" data-push="true" data-target="#catalog">3</a>
    </li>
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/?page=4" data-push="true" data-target="#catalog">4</a>
    </li>
</ul>

<ul class="sort">
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/?sort=title" data-push="true" data-target="#catalog">Sort by Title</a>
    </li>
    <li>
        <a href="https://github.com/igor-alexandrov/wiselinks/blob/master/?sort=price" data-push="true" data-target="#catalog">Sort by Price</a>
    </li>
</ul>

<div id="catalog">
    <!-- the list of your items -->
    ...
</div>

Form processing

Wiselinks can process forms. After submit button is clicked, Wiselinks will perform a request to form url with form attributes serialized to a string. Wiselinks always performs a HTTP GET request.

<div class="filter">
    <form action="/" method="get" data-push="true" data-target="@catalog">
        <input type="text" size="30" name="title" id="title">

        <select name="scope" id="scope">
            <option value="">All Tax Liens</option>
            <option value="accruing">Accruing Interest</option>
            <option value="awaiting_payment">Awaiting Payment</option>
            <option value="closed">Closed</option>
            <option value="trashed">Trash</option>
        </select>

        <input type="submit" value="Find" name="commit">
    </form>
</div>

<div role="catalog">
    <!-- the list of your items -->
    ...
</div>

data-include-blank-url-params

During form submit Wiselinks excludes blank parameters to make your URLs cleaner. You can disable this behaviour with data-include-blank-url-params attribute.

<div class="filter">
    <form action="/" method="get" data-push="true" data-target="@catalog" data-include-blank-url-params="true">
        <input type="text" size="30" name="title" id="title">
        <input type="submit" value="Find" name="commit">
    </form>
</div>

data-optimize-url-params

Array parameters category_ids[]=1&category_ids[]=2&category_ids[]=3 are optimized to more human readable category_ids=1,2,3. To changed this behaviour use data-optimize-url-params attribute.

<div class="filter">
    <form action="/" method="get" data-push="true" data-target="@catalog" data-optimize-url-params="false">
        <input type="text" size="30" name="title" id="title">
        <input type="submit" value="Find" name="commit">
    </form>
</div>

Server processing

The idea of Wiselinks is that you should render only content that you need in current request. Usually you don't need to reload your stylesheets and javascripts on every request.

X-Wiselinks header is passed with every Wiselinks request. Server should respond with content that should be inserted into $target.

In Rails after installing Wiselinks gem, all requests that have X-Wiselinks header will be automatically processed within 'app/views/layouts/wiselinks' layout, that basically has only yield operator. Of course you can override layout name by redefining wiselinks_layout method in your controller.

Javascript Events

While using Wiselinks you can rely on DOMContentLoaded or jQuery.ready() to trigger your JavaScript code, but Wiselinks gives you some additional useful event to deal with the lifecycle of the page:

page:loading ($target, render, url)

Event is triggered before the XMLHttpRequest is initialised and performed.

page:redirected ($target, render, url)

Event is triggered when you were redirected during XMLHttpRequest (with HTTP 30x status).

page:done ($target, status, url, data)

Event is triggered if the request succeeds.

page:fail ($target, status, url, error, code)

Event is triggered if the request fails.

page:always ($target, status, url)

Event is triggered after each request.

So if you want to show a client-side loading spinner, you could listen for page:loading to start it and page:always to stop it.

ActionDispatch::Request extensions

Wiselinks adds a couple of methods to ActionDispatch::Request. These methods are mostly syntax sugar and don't have any complex logic, so you can use them or not.

wiselinks?

Method returns true if current request is initiated by Wiselinks (has X-Wiselinks header), false otherwise.

wiselinks_template?

Method returns true if current request is initiated by Wiselinks and client want to render template (X-Wiselinks != 'partial'), false otherwise.

wiselinks_partial?

Method returns true if current request is initiated by Wiselinks and client want to render partial (X-Wiselinks == 'partial'), false otherwise.

Assets change detection

You can enable assets change detection with Wiselinks. To do this you have to enable assets digests by adding this to you environment file:

config.assets.digest = true

Then you should append your layout by adding this to head section:

<%= wiselinks_meta_tag %>

Now Wiselinks will track changes of your assets and if anything will change, your page will be reloaded completely.

Title handling

Wiselinks handles page titles by passing X-Wiselinks-Title header with response. To set this header you can use wiselinks_title helper (in Rails).

<% wiselinks_title("Wiselinks is awesome") %>

<div>
    <!-- your content -->
    ...
</div>

Of course you can use wiselinks_title helper in your own helpers too.

Redirect handling

Wiselinks follows 30x HTTP redirects. Location is updated in browser with X-Wiselinks-Url header that is setting up automatically (in Rails) on every wiselinks request.

Target missing handling

By default, if Wiselinks cannot find target that you specified during initialization, it will fail silently. But you can override this behaviour:

#= require jquery
#= require wiselinks

$(document).ready ->
    window.wiselinks = new Wiselinks(
      $('something that does not exist'),
      target_missing: 'exception'
    )

[Wiselinks] Target missing exception will be thrown. This also works for data-target attributes.

Google Analytics and Yandex Metrika

If you want to handle these analytics tools, then you should add handler to page:done event.

Let's say, that you have two objects, first is _gaq – instance of Google Analytics, second is _metrika – instance of Yandex Metrika. Then you have to add the following code somewhere in your application.js.coffee.

$(document).off('page:done').on(
  'page:done'
  (event, $target, status, url, data) ->
    _gaq.push(['_trackPageview', url])
    _metrika.hit(url)
)

After this, you will have correct page view statistics in your analytics tools.

Example

We crafted example application that uses nearly all features of Wiselinks so you can see it in action.

Note on Patches / Pull Requests

Credits

JetRockets

Wiselinks is maintained by JetRockets.

Contributors:

License

It is free software, and may be redistributed under the terms specified in the LICENSE file.