π Welcome to render_async
Let's make your Rails pages fast again :racehorse:
render_async is here to make your pages show faster to users.
Pages become faster seamlessly by rendering partials to your views.
Partials render asynchronously and let users see your page faster than using regular rendering.
It works with Rails and its tools out of the box.
:sparkles: A quick overview of how render_async does its magic:
- user visits a page
render_asyncmakes an AJAX request on the controller action- controller renders a partial
- partial renders in the place where you put
render_asyncview helper
JavaScript is injected straight into <%= content_for :render_async %> so you choose
where to put it.
:mega: P.S. Join our Discord channel for help and discussion, and let's make render_async even better!
:package: Installation
Add this line to your application's Gemfile:
gem 'render_async'
And then execute:
$ bundle install
:hammer: Usage
Include
render_asyncview helper somewhere in your views (e.g.app/views/comments/show.html.erb):<%= render_async comment_stats_path %>Then create a route that will
config/routes.rb:get :comment_stats, controller: :commentsFill in the logic in your controller (e.g.
app/controllers/comments_controller.rb):def comment_stats @stats = Comment.get_stats render partial: "comment_stats" endCreate a partial that will render (e.g.
app/views/comments/_comment_stats.html.erb):<div class="col-md-6"> <%= @stats %> </div>Add
content_forin your base view file in the body part (e.g.app/views/layouts/application.html.erb):<%= content_for :render_async %>
:hammer_and_wrench: Advanced usage
Advanced usage includes information on different options, such as:
- Passing in a container ID
- Passing in a container class name
- Passing in HTML options
- Passing in an HTML element name
- Passing in a placeholder
- Passing in an event name
- Using default events
- Retry on failure
- Toggle event
- Polling
- Controlled polling
- Handling errors
- Caching
- Doing non-GET requests
- Using with Turbolinks
- Using with respond_to and JS format
- Nested Async Renders
- Customizing the content_for name
- Configuration
Passing in a container ID
render_async renders an element that gets replaced with the content
of your request response. In order to have more control over the element
that renders first (before the request), you can set the ID of that element.
To set ID of the container element, you can do the following:
<%= render_async users_path, container_id: 'users-container' %>
Rendered code in the view:
<div id="users-container">
</div>
...
Passing in a container class name
render_async renders an element that gets replaced with the content of your
request response. If you want to style that element, you can set the class name
on it.
<%= render_async users_path, container_class: 'users-container-class' %>
Rendered code in the view:
<div id="render_async_18b8a6cd161499117471" class="users-container-class">
</div>
...
Passing in HTML options
render_async can accept html_options as a hash.
html_options is an optional hash that gets passed to a Rails'
javascript_tag, to drop HTML tags into the script element.
Example of utilizing html_options with a nonce:
<%= render_async users_path, html_options: { nonce: true } %>
Rendered code in the view:
<script nonce="2x012CYGxKgM8qAApxRHxA==">
//<![CDATA[
...
//]]>
</script>
...
<div id="render_async_18b8a6cd161499117471" class="">
</div>
Passing in an HTML element name
render_async can take in an HTML element name, allowing you to control
what type of container gets rendered. This can be useful when you're using
render_async inside a table
and you need it to render a tr element before your request gets loaded, so
your content doesn't get pushed out of the table.
Example of using HTML element name:
<%= render_async users_path, html_element_name: 'tr' %>
Rendered code in the view:
<tr id="render_async_04229e7abe1507987376">
</tr>
...
Passing in a placeholder
render_async can be called with a block that will act as a placeholder before
your AJAX call finishes.
Example of passing in a block:
<%= render_async users_path do %>
<h1>Users are loading...</h1>
<% end %>
Rendered code in the view:
<div id="render_async_14d7ac165d1505993721">
<h1>Users are loading...</h1>
</div>
<script>
//<![CDATA[
...
//]]>
</script>
After AJAX is finished, placeholder will be replaced with the request's response.
Passing in an event name
render_async can receive :event_name option which will emit JavaScript
event after it's done with fetching and rendering request content to HTML.
This can be useful to have if you want to add some JavaScript functionality
after your partial is loaded through render_async.
You can also access the associated container (DOM node) in the event object that gets emitted.
Example of passing it to render_async:
<%= render_async users_path, event_name: "users-loaded" %>
Rendered code in view:
<div id="render_async_04229e7abe1507987376">
</div>
<script>
//<![CDATA[
...
document.dispatchEvent(new Event("users-loaded"));
...
//]]>
</script>
Then, in your JavaScript code, you could do something like this:
document.addEventListener("users-loaded", function(event) {
console.log("Users have loaded!", event.container); // Access the container which loaded the users
});
:bulb: Dispatching events is also supported for older browsers that don't support Event constructor.
Using default events
render_async will fire the event render_async_load when an async partial
has loaded and rendered on page.
In case there is an error, the event render_async_error will fire instead.
This event will fire for all render_async partials on the page. For every
event, the associated container (DOM node) will be passed along.
This can be useful to apply JavaScript to content loaded after the page is ready.
Example of using events:
// Vanilla javascript
document.addEventListener('render_async_load', function(event) {
console.log('Async partial loaded in this container:', event.container);
});
document.addEventListener('render_async_error', function(event) {
console.log('Async partial could not load in this container:', event.container);
});
// with jQuery
$(document).on('render_async_load', function(event) {
console.log('Async partial loaded in this container:', event.container);
});
$(document).on('render_async_error', function(event) {
console.log('Async partial could not load in this container:', event.container);
});
Retry on failure
render_async can retry your requests if they fail for some reason.
If you want render_async to retry a request for number of times, you can do
this:
<%= render_async users_path, retry_count: 5, error_message: "Couldn't fetch it" %>
Now render_async will retry users_path for 5 times. If it succeeds in
between, it will stop with dispatching requests. If it fails after 5 times,
it will show an error message which you need to specify.
This can show useful when you know your requests often fail, and you don't want to refresh the whole page just to retry them.
Retry after some time
If you want to retry requests but with some delay in between the calls, you can
pass a retry_delay option together with retry_count like so:
<%= render_async users_path,
retry_count: 5,
retry_delay: 2000 %>
This will make render_async wait for 2 seconds before retrying after each
failure. In the end, if the request is still failing after 5th time, it will
dispatch a default error event.
:candy: If you are catching an event after an error, you can get
retryCountfrom the event.retryCountwill have number of retries it took before the event was dispatched.
Here is an example on how to get retryCount:
<%= render_async users_path,
retry_count: 5,
retry_delay: 2000,
error_event_name: 'it-failed-badly' %>
<script>
document.addEventListener('it-failed-badly', function(event) {
console.log("Request failed after " + event.retryCount + " tries!")
});
</script>
Toggle event
You can trigger render_async loading by clicking or doing another event to a
certain HTML element. You can do this by passing in a selector and an event
name which will trigger render_async. If you don't specify an event name, the
default event that will trigger render_async will be 'click' event. You can
do this by doing the following:
<a href='#' id='comments-button'>Load comments</a>
<%= render_async comments_path, toggle: { selector: '#comments-button', event: :click } %>
This will trigger render_async to load the comments_path when you click the #comments-button element.
If you want to remove event once it's triggered, you can pass once: true in the toggle options.
The once option is false by default.
You can also pass in a placeholder before the render_async is triggered. That
way, the element that started render_async logic will be removed after the
request has been completed. You can achieve this behaviour with something like this:
<%= render_async comments_path, toggle: { selector: '#comments-button', event: :click } do %>
<a href='#' id='comments-button'>Load comments</a>
<% end %>
Control polling with a toggle
Also, you can mix interval and toggle features. This way, you can turn polling
on, and off by clicking the "Load comments" button. In order to do this, you need to
pass toggle and interval arguments to render_async call like this:
<a href='#' id='comments-button'>Load comments</a>
<%= render_async comments_path, toggle: { selector: '#comments-button', event: :click }, interval: 2000 %>
Polling
You can call render_async with interval argument. This will make render_async
call specified path at specified interval.
By doing this:
<%= render_async comments_path, interval: 5000 %>
You are telling render_async to fetch comments_path every 5 seconds.
This can be handy if you want to enable polling for a specific URL.
:warning: By passing interval to
render_async, initial container element will remain in HTML tree, it will not be replaced with request response. You can handle how that container element is rendered and its style by passing in an HTML element name and HTML element class.
Controlled polling
You can controller render_async polling in 2 manners.
First one is pretty simple, and it involves using the toggle
feature. To do this, you can follow instructions in the
control polling with a toggle section.
Second option is more advanced and it involves emitting events to the render_async's
container element. From your code, you can emit following events:
- 'async-stop' - this will stop polling
- 'async-start' - this will start polling.
:bulb: Please note that events need to be dispatched to a render_async container.
An example of how you can do this looks like this:
<%= render_async wave_render_async_path,
container_id: 'controllable-interval', # set container_id so we can get it later easily
interval: 3000 %>
<button id='stop-polling'>Stop polling</button>
<button id='start-polling'>Start polling</button>
<script>
var container = document.getElementById('controllable-interval')
var stopPolling = document.getElementById('stop-polling')
var startPolling = document.getElementById('start-polling')
var triggerEventOnContainer = function(eventName) {
var event = new Event(eventName);
container.dispatchEvent(event)
}
stopPolling.addEventListener('click', function() {
container.innerHTML = '<p>Polling stopped</p>'
triggerEventOnContainer('async-stop')
})
startPolling.addEventListener('click', function() {
triggerEventOnContainer('async-start')
})
</script>
We are rendering two buttons - "Stop polling" and "Start polling". Then, we attach event listener to catch any clicking on the buttons. When the buttons are clicked, we either stop the polling, or start the polling, depending which button a user clicks.
Handling errors
render_async let's you handle errors by allowing you to pass in error_message
and error_event_name.
error_message
passing an error_message will render a message if the AJAX requests fails for
some reason
<%= render_async users_path,
error_message: '<p>Sorry, users loading went wrong :(</p>' %>
error_event_name
calling render_async with error_event_name will dispatch event in the case
of an error with your AJAX call.
<%= render_asyc users_path, error_event_name: 'users-error-event' %>
You can then catch the event in your code with:
document.addEventListener('users-error-event', function() {
// I'm on it
})
Caching
render_async can utilize view fragment caching to avoid extra AJAX calls.
In your views (e.g. app/views/comments/show.html.erb):
# note 'render_async_cache' instead of standard 'render_async'
<%= render_async_cache comment_stats_path %>
Then, in the partial (e.g. app/views/comments/_comment_stats.html.erb):
<% cache render_async_cache_key(request.path), skip_digest: true do %>
<div class="col-md-6">
<%= @stats %>
</div>
<% end %>
- The first time the page renders, it will make the AJAX call.
- Any other times (until the cache expires), it will render from cache instantly, without making the AJAX call.
- You can expire cache simply by passing
:expires_inin your view where you cache the partial
Doing non-GET requests
By default, render_async creates AJAX GET requests for the path you provide.
If you want to change this behaviour, you can pass in a method argument to
render_async view helper.
<%= render_async users_path, method: 'POST' %>
You can also set body and headers of the request if you need them.
<%= render_async users_path,
method: 'POST',
data: { fresh: 'AF' },
headers: { 'Content-Type': 'text' } %>
Using with Turbolinks
On Turbolinks applications, you may experience caching issues when navigating
away from, and then back to, a page with a render_async call on it. This will
likely show up as an empty div.
If you're using Turbolinks 5 or higher, you can resolve this by setting Turbolinks
configuration of render_async to true:
RenderAsync.configure do |config|
config.turbolinks = true # Enable this option if you are using Turbolinks 5+
end
This way, you're not breaking Turbolinks flow of loading or reloading a page. It makes it more efficient that the next option that is suggested below.
Another option:
If you want, you can tell Turbolinks to reload your render_async call as follows:
<%= render_async events_path, 'data-turbolinks-track': 'reload' %>
This will reload the whole page with Turbolinks.
:bulb: Make sure to put
<%= content_for :render_async %>in your base view file in the<head>and not the<body>.
Using with respond_to and JS format
If you need to restrict the action to only respond to AJAX requests, you'll
likely wrap it inside respond_to/format.js blocks like this:
def comment_stats
respond_to do |format|
format.js do
@stats = Comment.get_stats
render partial: "comment_stats"
end
end
end
When you do this, Rails will sometime set the response's Content-Type header
to text/javascript. This causes the partial not to be rendered in the HTML.
This usually happens when there's browser caching.
You can get around it by specifying the content type to text/html in the
render call:
render partial: "comment_stats", content_type: 'text/html'
Nested Async Renders
It is possible to nest async templates within other async templates. When doing
so, another content_for is required to ensure the JavaScript needed to load
nested templates is included.
For example:
<%# app/views/comments/show.html.erb %>
<%= render_async comment_stats_path %>
<%# app/views/comments/_comment_stats.html.erb %>
<div class="col-md-6">
<%= @stats %>
</div>
<div class="col-md-6">
<%= render_async comment_advanced_stats_path %>
</div>
<%= content_for :render_async %>
Customizing the content_for name
The content_for name may be customized by passing the content_for_name
option to render_async. This option is especially useful when doing nested async
renders to better control the location of the injected JavaScript.
For example:
<%= render_async comment_stats_path, content_for_name: :render_async_comment_stats %>
<%= content_for :render_async_comment_stats %>
Configuration
render_async renders Vanilla JS (regular JavaScript, non-jQuery code)
by default in order to fetch the request from the server.
If you want render_async to use jQuery code, you need to configure it to do
so.
You can configure it by doing the following anywhere before you call
render_async:
RenderAsync.configure do |config|
config.jquery = true # This will render jQuery code, and skip Vanilla JS code
config.turbolinks = false # Enable this option if you are using Turbolinks 5+
end
Also, you can do it like this:
# This will render jQuery code, and skip Vanilla JS code
RenderAsync.configuration.jquery = true
:hammer_and_pick: Development
After checking out the repo, run bin/setup to install dependencies. Then, run
rake spec to run the tests. You can also run bin/console for an interactive
prompt that will allow you to experiment. To run integration tests, use
bin/integration-tests.
Got any questions or comments about development (or anything else)?
Join render_async's Discord channel
and let's make render_async even better!
:pray: Contributing
Thank you for considering or deciding to contribute, this is much appreciated! Any kind of bug reports and pull requests are encouraged and welcome on GitHub at https://github.com/renderedtext/render_async.
Got any issues or difficulties?
Join render_async's Discord channel
and let's make render_async even better!
:memo: License
The gem is available as open source under the terms of the MIT License.
Contributors
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!