├── .rspec ├── cover.png ├── lib ├── render_async │ ├── version.rb │ ├── engine.rb │ ├── configuration.rb │ └── view_helper.rb └── render_async.rb ├── spec ├── spec_helper.rb ├── render_async │ ├── version_spec.rb │ ├── configuration_spec.rb │ └── view_helper_spec.rb └── render_async_spec.rb ├── Gemfile ├── .github ├── FUNDING.yml ├── workflows │ └── test.yml └── CONTRIBUTING.md ├── Rakefile ├── bin ├── setup ├── console └── integration-tests ├── .gitignore ├── .gitmodules ├── render_async.gemspec ├── LICENSE ├── app └── views │ └── render_async │ ├── _render_async.html.erb │ ├── _request_jquery.js.erb │ └── _request_vanilla.js.erb ├── .all-contributorsrc ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolalsvk/render_async/HEAD/cover.png -------------------------------------------------------------------------------- /lib/render_async/version.rb: -------------------------------------------------------------------------------- 1 | module RenderAsync 2 | VERSION = "2.1.11".freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "render_async" 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in render_async.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nikolalsvk 2 | tidelift: rubygems/render_async 3 | custom: "https://www.paypal.me/nikolalsvk/10" 4 | -------------------------------------------------------------------------------- /lib/render_async/engine.rb: -------------------------------------------------------------------------------- 1 | module RenderAsync 2 | class Engine < Rails::Engine 3 | # Make it a Rails Engine 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /spec/render_async/version_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe RenderAsync do 4 | it "has a version number" do 5 | expect(RenderAsync::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/render_async/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe RenderAsync::Configuration do 4 | it 'initializes jquery setting to false' do 5 | expect(RenderAsync::Configuration.new.jquery).to eq false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/fixtures/rails-5-base-app"] 2 | path = spec/fixtures/rails-5-base-app 3 | url = git@github.com:nikolalsvk/rails-5-base-app.git 4 | branch = render-async 5 | [submodule "spec/fixtures/rails-6-base-app"] 6 | path = spec/fixtures/rails-6-base-app 7 | url = git@github.com:nikolalsvk/rails-6-base-app.git 8 | branch = render-async 9 | -------------------------------------------------------------------------------- /lib/render_async/configuration.rb: -------------------------------------------------------------------------------- 1 | module RenderAsync 2 | class Configuration 3 | attr_accessor :jquery, :turbolinks, :turbo, :replace_container, :nonces 4 | 5 | def initialize 6 | @jquery = false 7 | @turbolinks = false 8 | @turbo = false 9 | @replace_container = true 10 | @nonces = false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "render_async" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/integration-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | git submodule init 5 | git submodule update 6 | 7 | gem build render_async.gemspec 8 | gem install render_async*.gem 9 | 10 | cd spec/fixtures/rails-5-base-app 11 | ls 12 | bundle install 13 | bundle exec cucumber 14 | 15 | cd ../../../spec/fixtures/rails-6-base-app 16 | ls 17 | bundle install 18 | yarn install 19 | RAILS_ENV=test bundle exec rails webpacker:compile 20 | bundle exec cucumber 21 | -------------------------------------------------------------------------------- /lib/render_async.rb: -------------------------------------------------------------------------------- 1 | require "render_async/version" 2 | require "render_async/view_helper" 3 | require "render_async/engine" if defined? Rails 4 | require "render_async/configuration" 5 | 6 | ActionView::Base.send :include, RenderAsync::ViewHelper if defined? ActionView::Base 7 | 8 | module RenderAsync 9 | class << self 10 | attr_accessor :configuration 11 | end 12 | 13 | def self.configuration 14 | @configuration ||= RenderAsync::Configuration.new 15 | end 16 | 17 | def self.reset 18 | @configuration = RenderAsync::Configuration.new 19 | end 20 | 21 | def self.configure 22 | yield(configuration) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | rspec: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby-version: ["3.1"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Ruby ${{ matrix.ruby-version }} 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true 24 | 25 | - name: Install dependencies 26 | run: | 27 | gem install bundler 28 | bin/setup 29 | 30 | - name: Run RSpec tests 31 | run: bundle exec rspec 32 | -------------------------------------------------------------------------------- /spec/render_async_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe RenderAsync do 4 | describe '.configuration' do 5 | it 'initializes jquery setting to false' do 6 | expect(RenderAsync.configuration.jquery).to eq false 7 | end 8 | 9 | it 'initializes turbolinks setting to false' do 10 | expect(RenderAsync.configuration.turbolinks).to eq false 11 | end 12 | 13 | it 'initializes turbo setting to false' do 14 | expect(RenderAsync.configuration.turbo).to eq false 15 | end 16 | end 17 | 18 | describe '.reset' do 19 | it 'resets jquery setting to false' do 20 | RenderAsync.configuration.jquery = true 21 | 22 | RenderAsync.reset 23 | 24 | expect(RenderAsync.configuration.jquery).to eq false 25 | end 26 | end 27 | 28 | describe '.configure' do 29 | it 'sets jquery value' do 30 | RenderAsync.configure do |config| 31 | config.jquery = true 32 | end 33 | 34 | expect(RenderAsync.configuration.jquery).to eq true 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /render_async.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'render_async/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "render_async" 8 | spec.version = RenderAsync::VERSION 9 | spec.authors = ["Kasper Grubbe", "nikolalsvk"] 10 | spec.email = ["nikolaseap@gmail.com"] 11 | 12 | spec.summary = "Render parts of the page asynchronously with AJAX" 13 | spec.description = "Load parts of your page through simple JavaScript and Rails pipeline" 14 | spec.homepage = "https://github.com/renderedtext/render_async" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec", "~> 3.2" 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Rendered Text 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/views/render_async/_render_async.html.erb: -------------------------------------------------------------------------------- 1 | <<%= html_element_name %> 2 | id="<%= container_id %>" 3 | class="<%= container_class %>" 4 | > 5 | <%= placeholder %> 6 | > 7 | 8 | <% content_for content_for_name do %> 9 | <%= javascript_tag html_options do %> 10 | <% locals = { container_id: container_id, 11 | replace_container: replace_container, 12 | path: path, 13 | method: method, 14 | data: data, 15 | event_name: event_name, 16 | toggle: toggle, 17 | headers: headers, 18 | error_message: error_message, 19 | error_event_name: error_event_name, 20 | retry_count: retry_count, 21 | retry_count_header: retry_count_header, 22 | retry_delay: retry_delay, 23 | interval: interval, 24 | turbolinks: RenderAsync.configuration.turbolinks, 25 | turbo: RenderAsync.configuration.turbo} %> 26 | 27 | <% if RenderAsync.configuration.jquery %> 28 | <%= render partial: 'render_async/request_jquery', 29 | formats: [:js], 30 | locals: locals %> 31 | <% else %> 32 | <%= render partial: 'render_async/request_vanilla', 33 | formats: [:js], 34 | locals: locals %> 35 | <% end %> 36 | <% end %> 37 | <% end %> 38 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # :pray: Contributing 2 | 3 | Thank you for considering or deciding to contribute, this is much appreciated! 4 | Any kind of bug reports and pull requests are encouraged and welcome on GitHub at 5 | https://github.com/renderedtext/render_async. 6 | 7 | ## :inbox_tray: Installing dependencies 8 | 9 | You can install all needed dependencies by running `bin/setup`. 10 | 11 | ## :runner: Running RSpec tests 12 | 13 | You can run either `rake spec` or `bundle exec rspec` to run all the RSpec tests 14 | in the project. 15 | 16 | ## :running_woman: Running integration tests 17 | 18 | There is a simple command `bin/integration-tests` which sets up 2 submodules, 19 | and runs Cucumber features in them. 20 | 21 | There are 2 submodules for render_async. The submodules are Rails 5 and Rails 6 22 | projects which are located in: 23 | 24 | - `spec/fixtures/rails-5-base-app`, and 25 | - `spec/fixtures/rails-6-base-app`. 26 | 27 | You can find [Rails 5 base app here](https://github.com/nikolalsvk/rails-5-base-app/tree/render-async), 28 | and the [Rails 6 base app here](https://github.com/nikolalsvk/rails-6-base-app/tree/render-async). 29 | 30 | Each of them have different use cases of render_async defined in `app/views/render_asyncs/_use_cases.html.erb` in their repos. 31 | All the feature tests are inside `features/render_async.feature` and `features/render_async_jquery.feature` files. 32 | 33 | If you are adding one or more feature tests or use cases, make sure to make a 34 | PR on those repos as well and include them in the PR on the render_async repo. 35 | 36 | ## :sos: Need help? 37 | 38 | Got any issues or difficulties? 39 | Join [render_async's Discord channel](https://discord.gg/SPfbeRm) 40 | and ask questions there. We will try to respond to you as quickly as possible. 41 | -------------------------------------------------------------------------------- /lib/render_async/view_helper.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module RenderAsync 4 | module ViewHelper 5 | def render_async_cache_key(path) 6 | "render_async_#{path}" 7 | end 8 | 9 | def render_async_cache(path, options = {}, &placeholder) 10 | cached_view = Rails.cache.read("views/#{render_async_cache_key(path)}") 11 | 12 | if cached_view.present? 13 | render :html => cached_view.html_safe 14 | else 15 | render_async(path, options, &placeholder) 16 | end 17 | end 18 | 19 | def render_async(path, options = {}, &placeholder) 20 | event_name = options.delete(:event_name) 21 | placeholder = capture(&placeholder) if block_given? 22 | 23 | render 'render_async/render_async', **container_element_options(options), 24 | path: path, 25 | html_options: html_options(options), 26 | event_name: event_name, 27 | placeholder: placeholder, 28 | **request_options(options), 29 | **error_handling_options(options), 30 | **retry_options(options), 31 | **polling_options(options), 32 | **content_for_options(options) 33 | end 34 | 35 | private 36 | 37 | def container_element_options(options) 38 | { html_element_name: options[:html_element_name] || 'div', 39 | container_id: options[:container_id] || generate_container_id, 40 | container_class: options[:container_class], 41 | replace_container: replace_container(options) } 42 | end 43 | 44 | def html_options(options) 45 | set_options = options.delete(:html_options) || {} 46 | 47 | set_options[:nonce] = configuration.nonces if set_options[:nonce].nil? 48 | 49 | set_options 50 | end 51 | 52 | def request_options(options) 53 | { method: options[:method] || 'GET', 54 | data: options[:data], 55 | headers: options[:headers] || {} } 56 | end 57 | 58 | def error_handling_options(options) 59 | { error_message: options[:error_message], 60 | error_event_name: options[:error_event_name] } 61 | end 62 | 63 | def retry_options(options) 64 | { 65 | retry_count: options.delete(:retry_count) || 0, 66 | retry_count_header: options.delete(:retry_count_header), 67 | retry_delay: options.delete(:retry_delay) 68 | } 69 | end 70 | 71 | def polling_options(options) 72 | { interval: options[:interval], 73 | toggle: options[:toggle] } 74 | end 75 | 76 | def content_for_options(options) 77 | { 78 | content_for_name: options[:content_for_name] || :render_async 79 | } 80 | end 81 | 82 | def generate_container_id 83 | "render_async_#{SecureRandom.hex(5)}#{Time.now.to_i}" 84 | end 85 | 86 | def replace_container(options) 87 | return options[:replace_container] unless options[:replace_container].nil? 88 | 89 | configuration.replace_container 90 | end 91 | 92 | def configuration 93 | RenderAsync.configuration 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /app/views/render_async/_request_jquery.js.erb: -------------------------------------------------------------------------------- 1 | if (window.jQuery) { 2 | (function($) { 3 | <% if turbolinks %> 4 | if (document.documentElement.hasAttribute("data-turbolinks-preview")) { 5 | return; 6 | } 7 | <% end %> 8 | <% if turbo %> 9 | if (document.documentElement.hasAttribute("data-turbo-preview")) { 10 | return; 11 | } 12 | <% end %> 13 | function createEvent(name, container) { 14 | var event = undefined; 15 | if (typeof(Event) === 'function') { 16 | event = new Event(name); 17 | } else { 18 | event = document.createEvent('Event'); 19 | event.initEvent(name, true, true); 20 | } 21 | event.container = container 22 | return event; 23 | } 24 | 25 | function _runAfterDocumentLoaded(callback) { 26 | if (document.readyState === 'complete' || document.readyState === 'interactive') { 27 | // Handle a case where nested partials get loaded after the document loads 28 | callback(); 29 | } else { 30 | <% if turbolinks %> 31 | $(document).one('turbolinks:load', callback); 32 | <% elsif turbo %> 33 | $(document).one('turbo:load', callback); 34 | <% else %> 35 | $(document).ready(callback); 36 | <% end %> 37 | } 38 | } 39 | 40 | function _makeRequest(currentRetryCount) { 41 | var headers = <%= headers.to_json.html_safe %>; 42 | var csrfTokenElement = document.querySelector('meta[name="csrf-token"]'); 43 | if (csrfTokenElement) 44 | headers['X-CSRF-Token'] = csrfTokenElement.content; 45 | 46 | <% if retry_count_header %> 47 | if (typeof(currentRetryCount) === 'number') { 48 | headers['<%= retry_count_header.html_safe %>'] = currentRetryCount; 49 | } 50 | <% end %> 51 | 52 | $.ajax({ 53 | url: '<%= path.html_safe %>', 54 | method: '<%= method %>', 55 | data: "<%= escape_javascript(data.to_s.html_safe) %>", 56 | headers: headers 57 | }).done(function(response) { 58 | var container = $("#<%= container_id %>"); 59 | 60 | // If user navigated away before the request completed 61 | if (!container.length) return; 62 | 63 | <% if !interval && replace_container %> 64 | container.replaceWith(response); 65 | <% else %> 66 | container.empty(); 67 | container.append(response); 68 | <% end %> 69 | 70 | var loadEvent = createEvent('render_async_load', container); 71 | document.dispatchEvent(loadEvent); 72 | 73 | <% if event_name.present? %> 74 | var event = createEvent("<%= event_name %>", container) 75 | document.dispatchEvent(event); 76 | <% end %> 77 | }).fail(function(response) { 78 | var skipErrorMessage = false; 79 | <% if retry_count > 0 %> 80 | skipErrorMessage = retry(currentRetryCount) 81 | <% end %> 82 | 83 | if (skipErrorMessage) return; 84 | 85 | var container = $("#<%= container_id %>"); 86 | if (!container.length) return; 87 | 88 | container.replaceWith("<%= error_message.try(:html_safe) %>"); 89 | 90 | var errorEvent = createEvent( 91 | "<%= error_event_name || 'render_async_error' %>", 92 | container 93 | ) 94 | errorEvent.retryCount = currentRetryCount 95 | 96 | document.dispatchEvent(errorEvent); 97 | }); 98 | }; 99 | 100 | <% if retry_count > 0 %> 101 | var _retryMakeRequest = _makeRequest 102 | 103 | <% if retry_delay %> 104 | _retryMakeRequest = function(currentRetryCount) { 105 | setTimeout(function() { 106 | _makeRequest(currentRetryCount) 107 | }, <%= retry_delay %>) 108 | } 109 | <% else %> 110 | _retryMakeRequest = _makeRequest 111 | <% end %> 112 | 113 | function retry(currentRetryCount) { 114 | if (typeof(currentRetryCount) === 'number') { 115 | if (currentRetryCount >= <%= retry_count %>) 116 | return false; 117 | 118 | _retryMakeRequest(currentRetryCount + 1); 119 | return true; 120 | } 121 | 122 | _retryMakeRequest(1); 123 | return true; 124 | } 125 | <% end %> 126 | 127 | var _renderAsyncFunction = _makeRequest; 128 | 129 | var _interval; 130 | <% if interval %> 131 | var _renderAsyncFunction = function() { 132 | // If interval is already set, return 133 | if (typeof(_interval) === 'number') return 134 | 135 | _makeRequest(); 136 | _interval = setInterval(_makeRequest, <%= interval %>); 137 | } 138 | 139 | var _clearRenderAsyncInterval = function() { 140 | if (typeof(_interval) === 'number'){ 141 | clearInterval(_interval) 142 | _interval = undefined; 143 | } 144 | } 145 | 146 | function _setUpControlEvents() { 147 | var container = $("#<%= container_id %>"); 148 | 149 | // Register a stop polling event on the container 150 | $(container).on('async-stop', _clearRenderAsyncInterval) 151 | 152 | // Register a start polling event on the container 153 | $(container).on('async-start', _renderAsyncFunction) 154 | } 155 | 156 | _runAfterDocumentLoaded(_setUpControlEvents) 157 | 158 | <% if turbolinks %> 159 | $(document).one('turbolinks:visit', _clearRenderAsyncInterval); 160 | <% end %> 161 | <% if turbo %> 162 | $(document).one('turbo:visit', _clearRenderAsyncInterval); 163 | <% end %> 164 | <% end %> 165 | 166 | <% if !replace_container %> 167 | function _setUpRefreshEvent() { 168 | var container = $("#<%= container_id %>"); 169 | 170 | $(container).on('refresh', _renderAsyncFunction) 171 | } 172 | 173 | _runAfterDocumentLoaded(_setUpRefreshEvent) 174 | <% end %> 175 | 176 | <% if toggle %> 177 | function _setUpToggle() { 178 | $(document).<%= toggle[:once] ? 'one' : 'on' %>('<%= toggle[:event] || 'click' %>', '<%= toggle[:selector] %>', function(event) { 179 | if (typeof(_interval) === 'number') { 180 | clearInterval(_interval); 181 | _interval = undefined; 182 | } else { 183 | _renderAsyncFunction(); 184 | } 185 | }); 186 | 187 | <% if toggle[:start] %> 188 | _renderAsyncFunction() 189 | <% end %> 190 | } 191 | 192 | _runAfterDocumentLoaded(_setUpToggle); 193 | <% elsif !toggle %> 194 | _runAfterDocumentLoaded(_renderAsyncFunction) 195 | <% end %> 196 | }(jQuery)); 197 | } else { 198 | console.warn("Looks like you've enabled jQuery for render_async, but jQuery is not defined on the window object"); 199 | }; 200 | -------------------------------------------------------------------------------- /app/views/render_async/_request_vanilla.js.erb: -------------------------------------------------------------------------------- 1 | (function() { 2 | <% if turbolinks %> 3 | if (document.documentElement.hasAttribute("data-turbolinks-preview")) { 4 | return; 5 | } 6 | <% end %> 7 | <% if turbo %> 8 | if (document.documentElement.hasAttribute("data-turbo-preview")) { 9 | return; 10 | } 11 | <% end %> 12 | function createEvent(name, container) { 13 | var event = undefined; 14 | if (typeof(Event) === 'function') { 15 | event = new Event(name); 16 | } else { 17 | event = document.createEvent('Event'); 18 | event.initEvent(name, true, true); 19 | } 20 | event.container = container 21 | return event; 22 | } 23 | 24 | function _runAfterDocumentLoaded(callback) { 25 | <% if turbolinks %> 26 | document.addEventListener("turbolinks:load", function(e) { 27 | e.target.removeEventListener(e.type, arguments.callee); 28 | callback(); 29 | }); 30 | <% elsif turbo %> 31 | document.addEventListener("turbo:load", function(e) { 32 | e.target.removeEventListener(e.type, arguments.callee); 33 | callback(); 34 | }); 35 | <% else %> 36 | document.addEventListener("DOMContentLoaded", callback); 37 | <% end %> 38 | } 39 | 40 | function _makeRequest(currentRetryCount) { 41 | var request = new XMLHttpRequest(); 42 | var asyncRequest = true; 43 | var SUCCESS = 200; 44 | var ERROR = 400; 45 | 46 | request.open('<%= method %>', '<%= path.html_safe %>', asyncRequest); 47 | 48 | var headers = <%= headers.to_json.html_safe %>; 49 | var csrfTokenElement = document.querySelector('meta[name="csrf-token"]'); 50 | if (csrfTokenElement) 51 | headers['X-CSRF-Token'] = csrfTokenElement.content; 52 | 53 | <% if retry_count_header %> 54 | if (typeof(currentRetryCount) === 'number') { 55 | headers['<%= retry_count_header.html_safe %>'] = currentRetryCount; 56 | } 57 | <% end %> 58 | 59 | Object.keys(headers).map(function(key) { 60 | request.setRequestHeader(key, headers[key]); 61 | }); 62 | 63 | request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 64 | 65 | request.onreadystatechange = function() { 66 | if (request.readyState === 4) { 67 | if (request.status >= SUCCESS && request.status < ERROR) { 68 | var container = document.getElementById('<%= container_id %>'); 69 | 70 | // If user navigated away before the request completed 71 | if (!container) return; 72 | 73 | <% if !interval && replace_container %> 74 | container.outerHTML = request.response; 75 | <% else %> 76 | container.innerHTML = request.response; 77 | <% end %> 78 | 79 | var loadEvent = createEvent('render_async_load', container); 80 | document.dispatchEvent(loadEvent); 81 | 82 | <% if event_name.present? %> 83 | var event = createEvent('<%= event_name %>', container); 84 | document.dispatchEvent(event); 85 | <% end %> 86 | } else { 87 | var skipErrorMessage = false; 88 | <% if retry_count > 0 %> 89 | skipErrorMessage = retry(currentRetryCount) 90 | <% end %> 91 | 92 | if (skipErrorMessage) return; 93 | 94 | var container = document.getElementById('<%= container_id %>'); 95 | if (!container) return; 96 | 97 | container.outerHTML = '<%= error_message.try(:html_safe) %>'; 98 | 99 | var errorEvent = createEvent( 100 | "<%= error_event_name || 'render_async_error' %>", 101 | container 102 | ); 103 | errorEvent.retryCount = currentRetryCount 104 | 105 | document.dispatchEvent(errorEvent); 106 | } 107 | } 108 | }; 109 | 110 | var body = "<%= escape_javascript(data.to_s.html_safe) %>"; 111 | request.send(body); 112 | }; 113 | 114 | <% if retry_count > 0 %> 115 | 116 | <% if retry_delay %> 117 | var _retryMakeRequest = function(currentRetryCount) { 118 | setTimeout(function() { 119 | _makeRequest(currentRetryCount) 120 | }, <%= retry_delay %>) 121 | } 122 | <% else %> 123 | var _retryMakeRequest = _makeRequest 124 | <% end %> 125 | 126 | function retry(currentRetryCount) { 127 | if (typeof(currentRetryCount) === 'number') { 128 | if (currentRetryCount >= <%= retry_count %>) 129 | return false; 130 | 131 | _retryMakeRequest(currentRetryCount + 1); 132 | return true; 133 | } 134 | 135 | _retryMakeRequest(1); 136 | return true; 137 | } 138 | <% end %> 139 | 140 | var _renderAsyncFunction = _makeRequest; 141 | 142 | var _interval; 143 | <% if interval %> 144 | var _renderAsyncFunction = function() { 145 | // If interval is already set, return 146 | if (typeof(_interval) === 'number') return 147 | 148 | _makeRequest(); 149 | _interval = setInterval(_makeRequest, <%= interval %>); 150 | } 151 | 152 | var _clearRenderAsyncInterval = function() { 153 | if (typeof(_interval) === 'number'){ 154 | clearInterval(_interval) 155 | _interval = undefined; 156 | } 157 | } 158 | 159 | function _setUpControlEvents() { 160 | var container = document.getElementById('<%= container_id %>'); 161 | 162 | // Register a polling stop event on the container 163 | container.addEventListener("async-stop", _clearRenderAsyncInterval) 164 | 165 | // Register a start polling event on the container 166 | container.addEventListener("async-start", _renderAsyncFunction) 167 | } 168 | 169 | _runAfterDocumentLoaded(_setUpControlEvents) 170 | 171 | <% if turbolinks %> 172 | document.addEventListener("turbolinks:visit", _clearRenderAsyncInterval) 173 | <% end %> 174 | <% if turbo %> 175 | document.addEventListener("turbo:visit", _clearRenderAsyncInterval) 176 | <% end %> 177 | <% end %> 178 | 179 | <% if !replace_container %> 180 | function _setUpRefreshEvent() { 181 | var container = document.getElementById('<%= container_id %>'); 182 | 183 | container.addEventListener('refresh', _renderAsyncFunction) 184 | } 185 | 186 | _runAfterDocumentLoaded(_setUpRefreshEvent) 187 | <% end %> 188 | 189 | <% if toggle %> 190 | function _setUpToggle() { 191 | var selectors = document.querySelectorAll('<%= toggle[:selector] %>'); 192 | var handler = function(event) { 193 | if (typeof(_interval) === 'number') { 194 | clearInterval(_interval); 195 | _interval = undefined; 196 | } else { 197 | _renderAsyncFunction(); 198 | } 199 | <% if toggle[:once] %> 200 | this.removeEventListener(event.type, handler); 201 | <% end %> 202 | }; 203 | 204 | <% if toggle[:start] %> 205 | _renderAsyncFunction() 206 | <% end %> 207 | 208 | for (var i = 0; i < selectors.length; ++i) { 209 | selectors[i].addEventListener('<%= toggle[:event] || 'click' %>', handler) 210 | } 211 | } 212 | 213 | _runAfterDocumentLoaded(_setUpToggle); 214 | <% elsif !toggle %> 215 | _runAfterDocumentLoaded(_renderAsyncFunction); 216 | <% end %> 217 | })(); 218 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "render_async", 3 | "projectOwner": "renderedtext", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "nikolalsvk", 12 | "name": "Nikola Đuza", 13 | "avatar_url": "https://avatars2.githubusercontent.com/u/3028124?v=4", 14 | "profile": "https://nikolalsvk.github.io", 15 | "contributions": [ 16 | "question", 17 | "code", 18 | "doc", 19 | "review" 20 | ] 21 | }, 22 | { 23 | "login": "colinxfleming", 24 | "name": "Colin", 25 | "avatar_url": "https://avatars0.githubusercontent.com/u/3866868?v=4", 26 | "profile": "http://www.colinxfleming.com", 27 | "contributions": [ 28 | "code", 29 | "doc", 30 | "example" 31 | ] 32 | }, 33 | { 34 | "login": "kaspergrubbe", 35 | "name": "Kasper Grubbe", 36 | "avatar_url": "https://avatars2.githubusercontent.com/u/334273?v=4", 37 | "profile": "http://kaspergrubbe.com", 38 | "contributions": [ 39 | "code" 40 | ] 41 | }, 42 | { 43 | "login": "sairam", 44 | "name": "Sai Ram Kunala", 45 | "avatar_url": "https://avatars2.githubusercontent.com/u/163584?v=4", 46 | "profile": "https://sairam.xyz/", 47 | "contributions": [ 48 | "doc" 49 | ] 50 | }, 51 | { 52 | "login": "nightsurge", 53 | "name": "Josh Arnold", 54 | "avatar_url": "https://avatars2.githubusercontent.com/u/3065882?v=4", 55 | "profile": "https://github.com/nightsurge", 56 | "contributions": [ 57 | "code", 58 | "doc" 59 | ] 60 | }, 61 | { 62 | "login": "SaladFork", 63 | "name": "Elad Shahar", 64 | "avatar_url": "https://avatars3.githubusercontent.com/u/107798?v=4", 65 | "profile": "https://eladshahar.com", 66 | "contributions": [ 67 | "code", 68 | "example" 69 | ] 70 | }, 71 | { 72 | "login": "sasharevzin", 73 | "name": "Sasha", 74 | "avatar_url": "https://avatars3.githubusercontent.com/u/232392?v=4", 75 | "profile": "http://www.revzin.co.il", 76 | "contributions": [ 77 | "code", 78 | "doc" 79 | ] 80 | }, 81 | { 82 | "login": "elsurudo", 83 | "name": "Ernest Surudo", 84 | "avatar_url": "https://avatars3.githubusercontent.com/u/50223?v=4", 85 | "profile": "http://elsurudo.com", 86 | "contributions": [ 87 | "code" 88 | ] 89 | }, 90 | { 91 | "login": "krainboltgreene", 92 | "name": "Kurtis Rainbolt-Greene", 93 | "avatar_url": "https://avatars1.githubusercontent.com/u/334809?v=4", 94 | "profile": "https://kurtis.rainbolt-greene.online", 95 | "contributions": [ 96 | "code" 97 | ] 98 | }, 99 | { 100 | "login": "schneems", 101 | "name": "Richard Schneeman", 102 | "avatar_url": "https://avatars2.githubusercontent.com/u/59744?v=4", 103 | "profile": "https://www.schneems.com", 104 | "contributions": [ 105 | "doc" 106 | ] 107 | }, 108 | { 109 | "login": "richardvenneman", 110 | "name": "Richard Venneman", 111 | "avatar_url": "https://avatars1.githubusercontent.com/u/75705?v=4", 112 | "profile": "https://www.cityspotters.com", 113 | "contributions": [ 114 | "doc" 115 | ] 116 | }, 117 | { 118 | "login": "filipewl", 119 | "name": "Filipe W. Lima", 120 | "avatar_url": "https://avatars3.githubusercontent.com/u/381395?v=4", 121 | "profile": "https://github.com/filipewl", 122 | "contributions": [ 123 | "doc" 124 | ] 125 | }, 126 | { 127 | "login": "eclemens", 128 | "name": "Jesús Eduardo Clemens Chong", 129 | "avatar_url": "https://avatars0.githubusercontent.com/u/3135638?v=4", 130 | "profile": "https://github.com/eclemens", 131 | "contributions": [ 132 | "code" 133 | ] 134 | }, 135 | { 136 | "login": "reneklacan", 137 | "name": "René Klačan", 138 | "avatar_url": "https://avatars3.githubusercontent.com/u/1935686?v=4", 139 | "profile": "https://github.com/reneklacan", 140 | "contributions": [ 141 | "code", 142 | "doc" 143 | ] 144 | }, 145 | { 146 | "login": "gil27", 147 | "name": "Gil Gomes", 148 | "avatar_url": "https://avatars1.githubusercontent.com/u/1313442?v=4", 149 | "profile": "http://gilgomes.com.br", 150 | "contributions": [ 151 | "doc" 152 | ] 153 | }, 154 | { 155 | "login": "ThanhKhoaIT", 156 | "name": "Khoa Nguyen", 157 | "avatar_url": "https://avatars0.githubusercontent.com/u/6081795?v=4", 158 | "profile": "https://github.com/ThanhKhoaIT", 159 | "contributions": [ 160 | "code", 161 | "doc" 162 | ] 163 | }, 164 | { 165 | "login": "preetsethi", 166 | "name": "Preet Sethi", 167 | "avatar_url": "https://avatars2.githubusercontent.com/u/8645918?v=4", 168 | "profile": "https://www.linkedin.com/in/preetsethila/", 169 | "contributions": [ 170 | "code" 171 | ] 172 | }, 173 | { 174 | "login": "fffx", 175 | "name": "fangxing", 176 | "avatar_url": "https://avatars3.githubusercontent.com/u/11586335?v=4", 177 | "profile": "https://github.com/fffx", 178 | "contributions": [ 179 | "code" 180 | ] 181 | }, 182 | { 183 | "login": "lipsumar", 184 | "name": "Emmanuel Pire", 185 | "avatar_url": "https://avatars3.githubusercontent.com/u/1191418?v=4", 186 | "profile": "http://blog.lipsumarium.com", 187 | "contributions": [ 188 | "code", 189 | "doc" 190 | ] 191 | }, 192 | { 193 | "login": "maximgeerinck", 194 | "name": "Maxim Geerinck", 195 | "avatar_url": "https://avatars1.githubusercontent.com/u/615509?v=4", 196 | "profile": "https://github.com/maximgeerinck", 197 | "contributions": [ 198 | "code" 199 | ] 200 | }, 201 | { 202 | "login": "vanboom", 203 | "name": "Don", 204 | "avatar_url": "https://avatars1.githubusercontent.com/u/251706?v=4", 205 | "profile": "https://github.com/vanboom", 206 | "contributions": [ 207 | "code" 208 | ] 209 | }, 210 | { 211 | "login": "villu164", 212 | "name": "villu164", 213 | "avatar_url": "https://avatars0.githubusercontent.com/u/998682?v=4", 214 | "profile": "https://github.com/villu164", 215 | "contributions": [ 216 | "doc" 217 | ] 218 | }, 219 | { 220 | "login": "Mbuckley0", 221 | "name": "Mitchell Buckley", 222 | "avatar_url": "https://avatars.githubusercontent.com/u/11203679?v=4", 223 | "profile": "https://github.com/Mbuckley0", 224 | "contributions": [ 225 | "code", 226 | "doc" 227 | ] 228 | }, 229 | { 230 | "login": "yhirano55", 231 | "name": "yhirano55", 232 | "avatar_url": "https://avatars.githubusercontent.com/u/15371677?v=4", 233 | "profile": "https://github.com/yhirano55", 234 | "contributions": [ 235 | "code", 236 | "doc" 237 | ] 238 | } 239 | ], 240 | "repoType": "github" 241 | } 242 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.1.11 (2021/10/11) 2 | 3 | * [#148](https://github.com/renderedtext/render_async/pull/148): Polish retry count header feature - [@nikolalsvk](https://github.com/nikolalsvk). 4 | * [#147](https://github.com/renderedtext/render_async/pull/147): Add support for retry count header - [@reneklacan](https://github.com/reneklacan). 5 | 6 | ### 2.1.10 (2021/03/21) 7 | 8 | * [#146](https://github.com/renderedtext/render_async/pull/146): Add comment when we check if container is present - [@nikolalsvk](https://github.com/nikolalsvk). 9 | * [#145](https://github.com/renderedtext/render_async/pull/145): Wrap html_options for turbolinks - [@yhirano55](https://github.com/yhirano55). 10 | * [#144](https://github.com/renderedtext/render_async/pull/144): Avoid TypeError if container has already disappear - [@yhirano55](https://github.com/yhirano55). 11 | 12 | ### 2.1.9 (2021/02/23) 13 | 14 | * [#142](https://github.com/renderedtext/render_async/pull/142): Update Turbolinks docs - [@nikolalsvk](https://github.com/nikolalsvk). 15 | * [#141](https://github.com/renderedtext/render_async/pull/141): Add Support for Turbo - [@MBuckley0](https://github.com/Mbuckley0). 16 | * [#139](https://github.com/renderedtext/render_async/pull/139): Fix readme configuration-options anchor - [@richardvenneman](https://github.com/richardvenneman). 17 | * [#138](https://github.com/renderedtext/render_async/pull/138): Add Rails 6 base app as a fixture - [@nikolalsvk](https://github.com/nikolalsvk). 18 | * [#137](https://github.com/renderedtext/render_async/pull/137): Rename config - [@nikolalsvk](https://github.com/nikolalsvk). 19 | 20 | ### 2.1.8 (2020/10/24) 21 | 22 | * [#134](https://github.com/renderedtext/render_async/pull/134): Add config option for setting nonce - [@nikolalsvk](https://github.com/nikolalsvk). 23 | * [#132](https://github.com/renderedtext/render_async/pull/132): Refresh render_async with an event - [@nikolalsvk](https://github.com/nikolalsvk). 24 | * [#131](https://github.com/renderedtext/render_async/pull/131): Start to poll on page load with toggle - [@nikolalsvk](https://github.com/nikolalsvk). 25 | * [#130](https://github.com/renderedtext/render_async/pull/130): Set up control events after document loaded - [@nikolalsvk](https://github.com/nikolalsvk). 26 | * [#127](https://github.com/renderedtext/render_async/pull/127): Update README.md, to reflect correct turbolinks configuration value - [@villu164](https://github.com/villu164). 27 | 28 | ### 2.1.7 (2020/8/1) 29 | 30 | * [#125](https://github.com/renderedtext/render_async/pull/125): Implement retry after some time feature - [@nikolalsvk](https://github.com/nikolalsvk). 31 | * [#124](https://github.com/renderedtext/render_async/pull/124): Add more info on how to control polling - [@nikolalsvk](https://github.com/nikolalsvk). 32 | * [#123](https://github.com/renderedtext/render_async/pull/123): Simplify calling of start and stop event when interval is defined - [@nikolalsvk](https://github.com/nikolalsvk). 33 | * [#119](https://github.com/renderedtext/render_async/pull/119): Add polling control start/stop events - [@vanboom](https://github.com/vanboom). 34 | * [#120](https://github.com/renderedtext/render_async/pull/120): Fine tune custom content_for feature - [@nikolalsvk](https://github.com/nikolalsvk). 35 | * [#117](https://github.com/renderedtext/render_async/pull/117): Allow a custom content_for name - [@vanboom](https://github.com/vanboom). 36 | 37 | ### 2.1.6 (2020/5/9) 38 | 39 | * [#114](https://github.com/renderedtext/render_async/pull/114): Call render_async logic if document state is ready or interactive - [@nikolalsvk](https://github.com/nikolalsvk). 40 | * [#113](https://github.com/renderedtext/render_async/pull/113): Remove interval after Turbolinks visit event - [@nikolalsvk](https://github.com/nikolalsvk). 41 | * [#112](https://github.com/renderedtext/render_async/pull/112): Add X-Requested-With header in Vanilla JS - [@nikolalsvk](https://github.com/nikolalsvk). 42 | * [#110](https://github.com/renderedtext/render_async/pull/110): Remove preventDefault, and load toggle in stream - [@vanboom](https://github.com/vanboom). 43 | 44 | ### 2.1.5 (2020/3/22) 45 | 46 | * [#105](https://github.com/renderedtext/render_async/pull/105): Load toggle listeners after page loads - [@nikolalsvk](https://github.com/nikolalsvk). 47 | * [#104](https://github.com/renderedtext/render_async/pull/104): Attach container to dispatched events - [@nikolalsvk](https://github.com/nikolalsvk). 48 | * [#103](https://github.com/renderedtext/render_async/pull/103): Add generic "load" and "error" events - [@lipsumar](https://github.com/lipsumar). 49 | * [#98](https://github.com/renderedtext/render_async/pull/98): Bump nonce pattern in the README to follow Rails CSP standard - [@colinxfleming](https://github.com/colinxfleming). 50 | 51 | ### 2.1.4 (2019/11/11) 52 | 53 | * [#96](https://github.com/renderedtext/render_async/pull/96): Add once option to remove event once it's triggered #94 - [@fffx](https://github.com/fffx). 54 | 55 | ### 2.1.3 (2019/9/24) 56 | 57 | * [#95](https://github.com/renderedtext/render_async/pull/95): Use double quotes for displaying error message - [@nikolalsvk](https://github.com/nikolalsvk). 58 | * [#93](https://github.com/renderedtext/render_async/pull/93): Fix: "Uncaught ReferenceError: \_interval" #92 - [@fffx](https://github.com/fffx). 59 | 60 | ### 2.1.2 (2019/8/17) 61 | 62 | * [#89](https://github.com/renderedtext/render_async/pull/89): Bump version to 2.1.2 - [@nikolalsvk](https://github.com/nikolalsvk). 63 | * [#82](https://github.com/renderedtext/render_async/pull/88): When toggle true, do not fire `_renderAsyncFunc` on `turbolinks:load` - [@ThanhKhoaIT](https://github.com/ThanhKhoaIT). 64 | 65 | ### 2.1.1 (2019/8/17) 66 | 67 | * [#85](https://github.com/renderedtext/render_async/pull/85): Rename main JS function and support toggle feature with other features - [@nikolalsvk](https://github.com/nikolalsvk). 68 | * [#82](https://github.com/renderedtext/render_async/pull/82): Add toggle selector and event to render - [@ThanhKhoaIT](https://github.com/ThanhKhoaIT). 69 | * DEPRECATION WARNING - html_options is now a hash that you pass to render_async instead of an argument. If you passed for example a nonce: '21312aas...', you will need to pass 70 | html_options: { nonce: '21312aas...' } 71 | 72 | ### 2.1.0 (2019/5/6) 73 | 74 | * [#77](https://github.com/renderedtext/render_async/pull/77): Call render_async logic every N seconds - [@nikolalsvk](https://github.com/nikolalsvk). 75 | * [#76](https://github.com/renderedtext/render_async/pull/76): Retry render_async on failure - [@nikolalsvk](https://github.com/nikolalsvk). 76 | 77 | ### 2.0.2 (2019/1/4) 78 | 79 | * [#74](https://github.com/renderedtext/render_async/pull/74): Remove bundler as a dependency - [@nikolalsvk](https://github.com/nikolalsvk). 80 | 81 | ### 2.0.1 (2018/12/10) 82 | 83 | * [#69](https://github.com/renderedtext/render_async/pull/69): Adding CHANGELOG.md - [@gil27](https://github.com/gil27). 84 | * [#66](https://github.com/renderedtext/render_async/pull/66): Adding support for Turbolinks 5+ - [@eclemens](https://github.com/eclemens). 85 | * [#65](https://github.com/renderedtext/render_async/pull/65): Invalid jQuery Promise method name - [@eclemens](https://github.com/eclemens). 86 | 87 | ### 2.0.0 (2018/10/06) 88 | 89 | * [#64](https://github.com/renderedtext/render_async/pull/64): Document new features - [@nikolalsvk](https://github.com/nikolalsvk). 90 | * [#63](https://github.com/renderedtext/render_async/pull/63): Configure gem - [@nikolalsvk](https://github.com/nikolalsvk). 91 | * [#62](https://github.com/renderedtext/render_async/pull/62): Handle request errorss - [@nikolalsvk](https://github.com/nikolalsvk). 92 | * [#61](https://github.com/renderedtext/render_async/pull/61): Add option to pass in container_class and container_id - [@nikolalsvk](https://github.com/nikolalsvk). 93 | * [#60](https://github.com/renderedtext/render_async/pull/60): Support event creation in IE - [@nikolalsvk](https://github.com/nikolalsvk). 94 | 95 | ### 1.3.0 (2018/10/06) 96 | 97 | * [#58](https://github.com/renderedtext/render_async/pull/58): Tune different method requests - [@nikolalsvk](https://github.com/nikolalsvk). 98 | * [#52](https://github.com/renderedtext/render_async/pull/52): Configurable AJAX method, headers and body - [@reneklacan](https://github.com/reneklacan). 99 | * [#54](https://github.com/renderedtext/render_async/pull/54): Add recipe for using with respond_to/format.js - [@filipewl](https://github.com/filipewl). 100 | * [#50](https://github.com/renderedtext/render_async/pull/50): Add Turbolinks usage note- [@richardvenneman](https://github.com/richardvenneman). 101 | 102 | ### 1.2.0 (2018/01/25) 103 | 104 | * [#49](https://github.com/renderedtext/render_async/pull/49): Pass in html_element_name - [@nikolalsvk](https://github.com/nikolalsvk). 105 | * [#48](https://github.com/renderedtext/render_async/pull/48): Move JS code to a partial - [@nikolalsvk](https://github.com/nikolalsvk). 106 | * [#47](https://github.com/renderedtext/render_async/pull/47): Trigger request when document is ready - [@nikolalsvk](https://github.com/nikolalsvk). 107 | * [#36](https://github.com/renderedtext/render_async/pull/36): Update README with instructions for nested async - [@SaladFork](https://github.com/SaladFork). 108 | * [#41](https://github.com/renderedtext/render_async/pull/41): Only bother when the dom is actually ready to be changed - [@krainboltgreene](https://github.com/nikolalsvk). 109 | * [#43](https://github.com/renderedtext/render_async/pull/43): [ci skip] Get more Open Source Helpers - [@schneems](https://github.com/schneems). 110 | * [#42](https://github.com/renderedtext/render_async/pull/42): Integration tests - [@nikolalsvk](https://github.com/nikolalsvk). 111 | 112 | ### 1.1.3 (2017/11/23) 113 | 114 | * [#40](https://github.com/renderedtext/render_async/pull/40): Replace render_async's container with the response data - [@nikolalsvk](https://github.com/nikolalsvk). 115 | 116 | ### 1.1.2 (2017/11/17) 117 | 118 | * [#35](https://github.com/renderedtext/render_async/pull/35): Fix async without jQuery - [@SaladFork](https://github.com/SaladFork). 119 | * [#33](https://github.com/renderedtext/render_async/pull/33): Don't html-escape the path when outputting JS - [@elsurudo](https://github.com/elsurudo). 120 | * [#31](https://github.com/renderedtext/render_async/pull/31): Allow `render_async_cache` to take a placeholder - [@elsurudo](https://github.com/elsurudo). 121 | 122 | ### 1.1.1 (2017/10/14) 123 | 124 | * [#29](https://github.com/renderedtext/render_async/pull/29): Use jQuery if available - [@nikolalsvk](https://github.com/nikolalsvk). 125 | 126 | ### 1.1.0 (2017/10/14) 127 | 128 | * [Fix event name explanation](https://github.com/renderedtext/render_async/commit/bd1ebb7011be6868dce9da76c5db9ca1133ec71d) - [@nikolalsvk](https://github.com/nikolalsvk). 129 | * [#22](https://github.com/renderedtext/render_async/pull/22): Dispatch JS Event when AJAX is finished- [@nikolalsvk](https://github.com/nikolalsvk). 130 | 131 | ### 1.0.1 (2017/10/14) 132 | 133 | * [#28](https://github.com/renderedtext/render_async/pull/28): Make vanilla JS more readable - [@nikolalsvk](https://github.com/nikolalsvk). 134 | * [#25](https://github.com/renderedtext/render_async/pull/25): Convert to vanilla js - [@sasharevzin](https://github.com/sasharevzin). 135 | * [#27](https://github.com/renderedtext/render_async/pull/27): Add turbolinks recipe - [@colinxfleming](https://github.com/colinxfleming). 136 | * [#23](https://github.com/renderedtext/render_async/pull/23): Test placeholder - [@nikolalsvk](https://github.com/nikolalsvk). 137 | * [#21](https://github.com/renderedtext/render_async/pull/21): Add specs for render_async_cache - [@nikolalsvk](https://github.com/nikolalsvk). 138 | 139 | ### 1.0.0 (2017/09/21) 140 | 141 | * [#20](https://github.com/renderedtext/render_async/pull/20): Rename block to placeholder - [@nikolalsvk](https://github.com/nikolalsvk). 142 | 143 | ### 0.4.1 (2017/09/07) 144 | 145 | * [#19](https://github.com/renderedtext/render_async/pull/19): Fix replace with - [@nikolalsvk](https://github.com/nikolalsvk). 146 | 147 | ### 0.4.0 (2017/09/07) 148 | 149 | * [#18](https://github.com/renderedtext/render_async/pull/18): Use replaceWith instead of html - [@nikolalsvk](https://github.com/nikolalsvk). 150 | 151 | ### 0.3.3 (2017/09/04) 152 | 153 | * [#17](https://github.com/renderedtext/render_async/pull/17): Prepare and test caching - [@nikolalsvk](https://github.com/nikolalsvk). 154 | * [#16](https://github.com/renderedtext/render_async/pull/16): Improvements and fixes for cached view - [@nightsurge](https://github.com/nightsurge). 155 | * [#15](https://github.com/renderedtext/render_async/pull/15): Add all contributors - [@nikolalsvk](https://github.com/nikolalsvk). 156 | * [#14](https://github.com/renderedtext/render_async/pull/14): Add view caching support - [@nightsurge](https://github.com/nightsurge). 157 | 158 | ### 0.2.3 (2017/07/11) 159 | 160 | * [#10](https://github.com/renderedtext/render_async/pull/10): Adjust to use javascript_tag and take html_options - [@colinxfleming](https://github.com/colinxfleming). 161 | 162 | ### 0.1.3 (2017/06/28) 163 | 164 | * [#9](https://github.com/renderedtext/render_async/pull/9): Use commit history from kaspergrubbe/render_async - [@nikolalsvk](https://github.com/nikolalsvk). 165 | * [#7](https://github.com/renderedtext/render_async/pull/7): Update README badges - [@nikolalsvk](https://github.com/nikolalsvk). 166 | * [#5](https://github.com/renderedtext/render_async/pull/5): Update README.md - [@nikolalsvk](https://github.com/nikolalsvk). 167 | * [#4](https://github.com/renderedtext/render_async/pull/4): Update installation info - [@nikolalsvk](https://github.com/nikolalsvk). 168 | * [#3](https://github.com/renderedtext/render_async/pull/3): Allow push to rubygems.org - [@nikolalsvk](https://github.com/nikolalsvk). 169 | * [#2](https://github.com/renderedtext/render_async/pull/2): Remove railtie and engine files - [@nikolalsvk](https://github.com/nikolalsvk). 170 | * [#1](https://github.com/renderedtext/render_async/pull/1): Use commit history from kaspergrubbe/render_async - [@nikolalsvk](https://github.com/nikolalsvk). 171 | 172 | ### 0.0.2 (2013/08/23) 173 | 174 | * It is now safe to call the method render_async instead of render_asynk (Billetto namespace issue) - [@kaspergrubbe](https://github.com/kaspergrubbe) 175 | 176 | ### 0.0.1 (2013/08/23) 177 | 178 | * Add a webpage description to the Gemspec - [@kaspergrubbe](https://github.com/kaspergrubbe) 179 | -------------------------------------------------------------------------------- /spec/render_async/view_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "json" 3 | 4 | describe RenderAsync::ViewHelper do 5 | let(:helper) { Class.new { extend RenderAsync::ViewHelper } } 6 | 7 | describe "#render_async_cache_key" do 8 | it "returns cache key" do 9 | expect(helper.render_async_cache_key("users")).to eq("render_async_users") 10 | end 11 | end 12 | 13 | describe "#render_async_cache" do 14 | let(:cached_view) { double("CachedView", 15 | html_safe: "

I'm cache

", 16 | present?: true) } 17 | before do 18 | stub_const("Rails", double("Rails")) 19 | end 20 | 21 | context "cache is present" do 22 | before do 23 | allow(Rails).to receive_message_chain(:cache, :read).and_return(cached_view) 24 | end 25 | 26 | it "renders cached HTML" do 27 | expect(helper).to receive(:render).with( 28 | html: "

I'm cache

" 29 | ) 30 | 31 | helper.render_async_cache("users") 32 | end 33 | end 34 | 35 | context "cache is not present" do 36 | let(:empty_cache) { double("EmptyCache", present?: false) } 37 | 38 | before do 39 | allow(Rails).to receive_message_chain(:cache, :read).and_return(empty_cache) 40 | end 41 | 42 | it "renders render_async partial with proper parameters" do 43 | expect(helper).to receive(:render).with( 44 | "render_async/render_async", 45 | { 46 | html_element_name: "div", 47 | container_id: /render_async_/, 48 | container_class: nil, 49 | path: "users", 50 | html_options: { nonce: false }, 51 | event_name: nil, 52 | toggle: nil, 53 | placeholder: nil, 54 | replace_container: true, 55 | method: 'GET', 56 | data: nil, 57 | headers: {}, 58 | error_message: nil, 59 | error_event_name: nil, 60 | retry_count: 0, 61 | retry_count_header: nil, 62 | retry_delay: nil, 63 | interval: nil, 64 | content_for_name: :render_async 65 | } 66 | ) 67 | 68 | helper.render_async_cache("users") 69 | end 70 | end 71 | end 72 | 73 | describe "#render_async" do 74 | before do 75 | allow(helper).to receive(:render) 76 | end 77 | 78 | context "called with just path" do 79 | it "renders render_async partial with proper parameters" do 80 | expect(helper).to receive(:render).with( 81 | "render_async/render_async", 82 | { 83 | html_element_name: "div", 84 | container_id: /render_async_/, 85 | container_class: nil, 86 | path: "users", 87 | html_options: { nonce: false }, 88 | event_name: nil, 89 | toggle: nil, 90 | placeholder: nil, 91 | replace_container: true, 92 | method: 'GET', 93 | data: nil, 94 | headers: {}, 95 | error_message: nil, 96 | error_event_name: nil, 97 | retry_count: 0, 98 | retry_count_header: nil, 99 | retry_delay: nil, 100 | interval: nil, 101 | content_for_name: :render_async 102 | } 103 | ) 104 | 105 | helper.render_async("users") 106 | end 107 | end 108 | 109 | context "container_id is given inside options hash" do 110 | it "renders render_async partial with proper parameters" do 111 | expect(helper).to receive(:render).with( 112 | "render_async/render_async", 113 | { 114 | html_element_name: "div", 115 | container_id: "users-container", 116 | container_class: nil, 117 | path: "users", 118 | html_options: { nonce: false }, 119 | event_name: nil, 120 | toggle: nil, 121 | placeholder: nil, 122 | replace_container: true, 123 | method: 'GET', 124 | data: nil, 125 | headers: {}, 126 | error_message: nil, 127 | error_event_name: nil, 128 | retry_count: 0, 129 | retry_count_header: nil, 130 | retry_delay: nil, 131 | interval: nil, 132 | content_for_name: :render_async 133 | } 134 | ) 135 | 136 | helper.render_async("users", container_id: "users-container") 137 | end 138 | end 139 | 140 | context "container_class is given inside options hash" do 141 | it "renders render_async partial with proper parameters" do 142 | expect(helper).to receive(:render).with( 143 | "render_async/render_async", 144 | { 145 | html_element_name: "div", 146 | container_id: /render_async_/, 147 | container_class: 'users-placeholder', 148 | path: "users", 149 | html_options: { nonce: false }, 150 | event_name: nil, 151 | toggle: nil, 152 | placeholder: nil, 153 | replace_container: true, 154 | method: 'GET', 155 | data: nil, 156 | headers: {}, 157 | error_message: nil, 158 | error_event_name: nil, 159 | retry_count: 0, 160 | retry_count_header: nil, 161 | retry_delay: nil, 162 | interval: nil, 163 | content_for_name: :render_async 164 | } 165 | ) 166 | 167 | helper.render_async("users", container_class: "users-placeholder") 168 | end 169 | end 170 | 171 | context "html_element_name is given inside options hash" do 172 | it "renders render_async partial with proper parameters" do 173 | expect(helper).to receive(:render).with( 174 | "render_async/render_async", 175 | { 176 | html_element_name: "tr", 177 | container_id: /render_async_/, 178 | container_class: nil, 179 | path: "users", 180 | html_options: { nonce: false }, 181 | event_name: nil, 182 | toggle: nil, 183 | placeholder: nil, 184 | replace_container: true, 185 | method: 'GET', 186 | data: nil, 187 | headers: {}, 188 | error_message: nil, 189 | error_event_name: nil, 190 | retry_count: 0, 191 | retry_count_header: nil, 192 | retry_delay: nil, 193 | interval: nil, 194 | content_for_name: :render_async 195 | } 196 | ) 197 | 198 | helper.render_async("users", html_element_name: "tr") 199 | end 200 | end 201 | 202 | context "called with html hash inside options hash" do 203 | it "renders render_async partial with proper parameters" do 204 | expect(helper).to receive(:render).with( 205 | "render_async/render_async", 206 | { 207 | html_element_name: "div", 208 | container_id: /render_async_/, 209 | container_class: nil, 210 | path: "users", 211 | html_options: { nonce: true }, 212 | event_name: nil, 213 | toggle: nil, 214 | placeholder: nil, 215 | replace_container: true, 216 | method: 'GET', 217 | data: nil, 218 | headers: {}, 219 | error_message: nil, 220 | error_event_name: nil, 221 | retry_count: 0, 222 | retry_count_header: nil, 223 | retry_delay: nil, 224 | interval: nil, 225 | content_for_name: :render_async 226 | } 227 | ) 228 | 229 | helper.render_async("users", html_options: { nonce: true }) 230 | end 231 | end 232 | 233 | context "event_name is given inside options hash" do 234 | it "renders render_async partial with proper parameters" do 235 | expect(helper).to receive(:render).with( 236 | "render_async/render_async", 237 | { 238 | html_element_name: "div", 239 | container_id: /render_async_/, 240 | container_class: nil, 241 | path: "users", 242 | html_options: { nonce: false }, 243 | event_name: "render_async_done", 244 | toggle: nil, 245 | placeholder: nil, 246 | replace_container: true, 247 | method: 'GET', 248 | data: nil, 249 | headers: {}, 250 | error_message: nil, 251 | error_event_name: nil, 252 | retry_count: 0, 253 | retry_count_header: nil, 254 | retry_delay: nil, 255 | interval: nil, 256 | content_for_name: :render_async 257 | } 258 | ) 259 | 260 | helper.render_async("users", event_name: "render_async_done") 261 | end 262 | end 263 | 264 | context "toggle is given inside options hash" do 265 | it "renders render_async partial with proper parameters" do 266 | expect(helper).to receive(:render).with( 267 | "render_async/render_async", 268 | { 269 | html_element_name: "div", 270 | container_id: /render_async_/, 271 | container_class: nil, 272 | path: "users", 273 | html_options: { nonce: false }, 274 | event_name: nil, 275 | toggle: { selector: 'el-id', event: :click }, 276 | placeholder: nil, 277 | replace_container: true, 278 | method: 'GET', 279 | data: nil, 280 | headers: {}, 281 | error_message: nil, 282 | error_event_name: nil, 283 | retry_count: 0, 284 | retry_count_header: nil, 285 | retry_delay: nil, 286 | interval: nil, 287 | content_for_name: :render_async 288 | } 289 | ) 290 | 291 | helper.render_async("users", toggle: { selector: 'el-id', event: :click }) 292 | end 293 | end 294 | 295 | context "placeholder is given" do 296 | let(:placeholder) { "I'm a placeholder" } 297 | 298 | before do 299 | allow(helper).to receive(:capture).and_return(placeholder) 300 | end 301 | 302 | it "renders render_async partial with proper parameters" do 303 | expect(helper).to receive(:render).with( 304 | "render_async/render_async", 305 | { 306 | html_element_name: "div", 307 | container_id: /render_async_/, 308 | container_class: nil, 309 | path: "users", 310 | html_options: { nonce: false }, 311 | event_name: nil, 312 | toggle: nil, 313 | placeholder: placeholder, 314 | replace_container: true, 315 | method: 'GET', 316 | data: nil, 317 | headers: {}, 318 | error_message: nil, 319 | error_event_name: nil, 320 | retry_count: 0, 321 | retry_count_header: nil, 322 | retry_delay: nil, 323 | interval: nil, 324 | content_for_name: :render_async 325 | } 326 | ) 327 | 328 | helper.render_async("users") { placeholder } 329 | end 330 | end 331 | 332 | context "JSON POST request" do 333 | it "renders render_async partial with proper parameters" do 334 | expect(helper).to receive(:render).with( 335 | "render_async/render_async", 336 | { 337 | html_element_name: "div", 338 | container_id: /render_async_/, 339 | container_class: nil, 340 | path: "users", 341 | html_options: { nonce: false }, 342 | event_name: nil, 343 | toggle: nil, 344 | placeholder: nil, 345 | replace_container: true, 346 | method: 'POST', 347 | data: { 'foor' => 'bar' }.to_json, 348 | headers: { 'Content-Type' => 'application/json' }, 349 | error_message: nil, 350 | error_event_name: nil, 351 | retry_count: 0, 352 | retry_count_header: nil, 353 | retry_delay: nil, 354 | interval: nil, 355 | content_for_name: :render_async 356 | } 357 | ) 358 | 359 | helper.render_async( 360 | "users", 361 | method: 'POST', 362 | data: { 'foor' => 'bar' }.to_json, 363 | headers: { 'Content-Type' => 'application/json' } 364 | ) 365 | end 366 | end 367 | 368 | context "retry_count is given" do 369 | it "renders render_async partial with proper parameters" do 370 | expect(helper).to receive(:render).with( 371 | "render_async/render_async", 372 | { 373 | html_element_name: "div", 374 | container_id: /render_async_/, 375 | container_class: nil, 376 | path: "users", 377 | html_options: { nonce: false }, 378 | event_name: nil, 379 | toggle: nil, 380 | placeholder: nil, 381 | replace_container: true, 382 | method: 'GET', 383 | data: nil, 384 | headers: {}, 385 | error_message: nil, 386 | error_event_name: nil, 387 | retry_count: 5, 388 | retry_count_header: nil, 389 | retry_delay: nil, 390 | interval: nil, 391 | content_for_name: :render_async 392 | } 393 | ) 394 | 395 | helper.render_async( 396 | "users", 397 | retry_count: 5 398 | ) 399 | end 400 | end 401 | 402 | context "retry_count_header is given" do 403 | it "renders render_async partial with proper parameters" do 404 | expect(helper).to receive(:render).with( 405 | "render_async/render_async", 406 | { 407 | html_element_name: "div", 408 | container_id: /render_async_/, 409 | container_class: nil, 410 | path: "users", 411 | html_options: { nonce: false }, 412 | event_name: nil, 413 | toggle: nil, 414 | placeholder: nil, 415 | replace_container: true, 416 | method: 'GET', 417 | data: nil, 418 | headers: {}, 419 | error_message: nil, 420 | error_event_name: nil, 421 | retry_count: 0, 422 | retry_count_header: "Retry-Count-Current", 423 | retry_delay: nil, 424 | interval: nil, 425 | content_for_name: :render_async 426 | } 427 | ) 428 | 429 | helper.render_async( 430 | "users", 431 | retry_count_header: "Retry-Count-Current", 432 | ) 433 | end 434 | end 435 | 436 | context "retry_delay is given" do 437 | it "renders render_async partial with proper parameters" do 438 | expect(helper).to receive(:render).with( 439 | "render_async/render_async", 440 | { 441 | html_element_name: "div", 442 | container_id: /render_async_/, 443 | container_class: nil, 444 | path: "users", 445 | html_options: { nonce: false }, 446 | event_name: nil, 447 | toggle: nil, 448 | placeholder: nil, 449 | replace_container: true, 450 | method: 'GET', 451 | data: nil, 452 | headers: {}, 453 | error_message: nil, 454 | error_event_name: nil, 455 | retry_count: 0, 456 | retry_count_header: nil, 457 | retry_delay: 3000, 458 | interval: nil, 459 | content_for_name: :render_async 460 | } 461 | ) 462 | 463 | helper.render_async( 464 | "users", 465 | retry_delay: 3000, 466 | ) 467 | end 468 | end 469 | 470 | context "interval is given" do 471 | it "renders render_async partial with proper parameters" do 472 | expect(helper).to receive(:render).with( 473 | "render_async/render_async", 474 | { 475 | html_element_name: "div", 476 | container_id: /render_async_/, 477 | container_class: nil, 478 | path: "users", 479 | html_options: { nonce: false }, 480 | event_name: nil, 481 | toggle: nil, 482 | placeholder: nil, 483 | replace_container: true, 484 | method: 'GET', 485 | data: nil, 486 | headers: {}, 487 | error_message: nil, 488 | error_event_name: nil, 489 | retry_count: 0, 490 | retry_count_header: nil, 491 | retry_delay: nil, 492 | interval: 5000, 493 | content_for_name: :render_async 494 | } 495 | ) 496 | 497 | helper.render_async( 498 | "users", 499 | interval: 5000 500 | ) 501 | end 502 | end 503 | 504 | context "content_for_name is given" do 505 | it "renders render_async partial with proper parameters" do 506 | expect(helper).to receive(:render).with( 507 | "render_async/render_async", 508 | { 509 | html_element_name: "div", 510 | container_id: /render_async_/, 511 | container_class: nil, 512 | path: "users", 513 | html_options: { nonce: false }, 514 | event_name: nil, 515 | toggle: nil, 516 | placeholder: nil, 517 | replace_container: true, 518 | method: 'GET', 519 | data: nil, 520 | headers: {}, 521 | error_message: nil, 522 | error_event_name: nil, 523 | retry_count: 0, 524 | retry_count_header: nil, 525 | retry_delay: nil, 526 | interval: nil, 527 | content_for_name: :render_async_other_name 528 | } 529 | ) 530 | 531 | helper.render_async( 532 | "users", 533 | content_for_name: :render_async_other_name 534 | ) 535 | end 536 | end 537 | 538 | context "replace_container is false" do 539 | it "renders render_async partial with proper parameters" do 540 | expect(helper).to receive(:render).with( 541 | "render_async/render_async", 542 | { 543 | html_element_name: "div", 544 | container_id: /render_async_/, 545 | container_class: nil, 546 | path: "users", 547 | html_options: { nonce: false }, 548 | event_name: nil, 549 | toggle: nil, 550 | placeholder: nil, 551 | replace_container: false, 552 | method: 'GET', 553 | data: nil, 554 | headers: {}, 555 | error_message: nil, 556 | error_event_name: nil, 557 | retry_count: 0, 558 | retry_count_header: nil, 559 | retry_delay: nil, 560 | interval: nil, 561 | content_for_name: :render_async 562 | } 563 | ) 564 | 565 | helper.render_async( 566 | "users", 567 | replace_container: false 568 | ) 569 | end 570 | end 571 | 572 | end 573 | end 574 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | render_async 3 | 4 |

👋 Welcome to render_async

5 | 6 |

Let's make your Rails pages fast again :racehorse:

7 | 8 |
9 | 10 |

11 | 12 | Donate 13 | 14 | 15 | Downloads 16 | 17 | 18 | All contributors 19 | 20 | 21 | Gem Version 22 | 23 |
24 | 25 | Discord Server 26 | 27 | 28 | Code Climate Maintainablity 29 | 30 | 31 | Test Coverage 32 | 33 | 34 | License 35 | 36 | 37 | Help Contribute to Open Source 38 | 39 |

40 |

41 | 42 | ### `render_async` is here to make your pages show faster to users. 43 | 44 | Pages become faster seamlessly by rendering partials to your views. 45 | 46 | Partials render **asynchronously** and let users see your page **faster** 47 | than using regular rendering. 48 | 49 | It works with Rails and its tools out of the box. 50 | 51 | :sparkles: A quick overview of how `render_async` does its magic: 52 | 53 | 1. user visits a page 54 | 2. `render_async` makes an AJAX request on the controller action 55 | 3. controller renders a partial 56 | 4. partial renders in the place where you put `render_async` view helper 57 | 58 | JavaScript is injected straight into `<%= content_for :render_async %>` so you choose 59 | where to put it. 60 | 61 | :mega: P.S. Join our [Discord channel](https://discord.gg/SPfbeRm) for help and discussion, and let's make `render_async` even better! 62 | 63 | ## :package: Installation 64 | 65 | Add this line to your application's Gemfile: 66 | 67 | ```ruby 68 | gem 'render_async' 69 | ``` 70 | 71 | And then execute: 72 | 73 | $ bundle install 74 | 75 | ## :hammer: Usage 76 | 77 | 1. Include `render_async` view helper somewhere in your views (e.g. `app/views/comments/show.html.erb`): 78 | ```erb 79 | <%= render_async comment_stats_path %> 80 | ``` 81 | 82 | 2. Then create a route for it `config/routes.rb`: 83 | ```ruby 84 | get :comment_stats, controller: :comments 85 | ``` 86 | 87 | 3. Fill in the logic in your controller (e.g. `app/controllers/comments_controller.rb`): 88 | ```ruby 89 | def comment_stats 90 | @stats = Comment.get_stats 91 | 92 | render partial: "comment_stats" 93 | end 94 | ``` 95 | 96 | 4. Create a partial that will render (e.g. `app/views/comments/_comment_stats.html.erb`): 97 | ```erb 98 |
99 | <%= @stats %> 100 |
101 | ``` 102 | 103 | 5. Add `content_for` in your base view file in the body part (e.g. `app/views/layouts/application.html.erb`): 104 | ```erb 105 | <%= content_for :render_async %> 106 | ``` 107 | 108 | ## :hammer_and_wrench: Advanced usage 109 | 110 | Advanced usage includes information on different options, such as: 111 | 112 | - [Passing in a container ID](#passing-in-a-container-id) 113 | - [Passing in a container class name](#passing-in-a-container-class-name) 114 | - [Passing in HTML options](#passing-in-html-options) 115 | - [Passing in an HTML element name](#passing-in-an-html-element-name) 116 | - [Passing in a placeholder](#passing-in-a-placeholder) 117 | - [Passing in an event name](#passing-in-an-event-name) 118 | - [Using default events](#using-default-events) 119 | - [Refreshing the partial](#refreshing-the-partial) 120 | - [Retry on failure](#retry-on-failure) 121 | - [Retry after some time](#retry-after-some-time) 122 | - [Toggle event](#toggle-event) 123 | - [Control polling with a toggle](#control-polling-with-a-toggle) 124 | - [Polling](#polling) 125 | - [Controlled polling](#controlled-polling) 126 | - [Handling errors](#handling-errors) 127 | - [Caching](#caching) 128 | - [Doing non-GET requests](#doing-non-get-requests) 129 | - [Using with Turbolinks](#using-with-turbolinks) 130 | - [Using with Turbo](#using-with-turbo) 131 | - [Using with respond_to and JS format](#using-with-respond_to-and-js-format) 132 | - [Nested async renders](#nested-async-renders) 133 | - [Customizing the content_for name](#customizing-the-content_for-name) 134 | - [Configuration options](#configuration-options) 135 | 136 | ### Passing in a container ID 137 | 138 | `render_async` renders an element that gets replaced with the content 139 | of your request response. In order to have more control over the element 140 | that renders first (before the request), you can set the ID of that element. 141 | 142 | To set ID of the container element, you can do the following: 143 | ```erb 144 | <%= render_async users_path, container_id: 'users-container' %> 145 | ``` 146 | 147 | Rendered code in the view: 148 | ```html 149 |
150 |
151 | 152 | ... 153 | ``` 154 | 155 | ### Passing in a container class name 156 | 157 | `render_async` renders an element that gets replaced with the content of your 158 | request response. If you want to style that element, you can set the class name 159 | on it. 160 | 161 | ```erb 162 | <%= render_async users_path, container_class: 'users-container-class' %> 163 | ``` 164 | 165 | Rendered code in the view: 166 | ```html 167 |
168 |
169 | 170 | ... 171 | ``` 172 | 173 | ### Passing in HTML options 174 | 175 | `render_async` can accept `html_options` as a hash. 176 | `html_options` is an optional hash that gets passed to a Rails' 177 | `javascript_tag`, to drop HTML tags into the `script` element. 178 | 179 | Example of utilizing `html_options` with a [nonce](https://edgeguides.rubyonrails.org/security.html#content-security-policy): 180 | 181 | ```erb 182 | <%= render_async users_path, html_options: { nonce: true } %> 183 | ``` 184 | 185 | Rendered code in the view: 186 | ```html 187 | 192 | 193 | ... 194 | 195 |
196 |
197 | ``` 198 | 199 | > :bulb: You can enable `nonce` to be set everywhere by using [configuration option](#configuration-options) render_async provides. 200 | 201 | ### Passing in an HTML element name 202 | 203 | `render_async` can take in an HTML element name, allowing you to control 204 | what type of container gets rendered. This can be useful when you're using 205 | [`render_async` inside a table](https://github.com/renderedtext/render_async/issues/12) 206 | and you need it to render a `tr` element before your request gets loaded, so 207 | your content doesn't get pushed out of the table. 208 | 209 | Example of using HTML element name: 210 | ```erb 211 | <%= render_async users_path, html_element_name: 'tr' %> 212 | ``` 213 | 214 | Rendered code in the view: 215 | ```html 216 | 217 | 218 | ... 219 | ``` 220 | 221 | ### Passing in a placeholder 222 | 223 | `render_async` can be called with a block that will act as a placeholder before 224 | your AJAX call finishes. 225 | 226 | Example of passing in a block: 227 | 228 | ```erb 229 | <%= render_async users_path do %> 230 |

Users are loading...

231 | <% end %> 232 | ``` 233 | 234 | Rendered code in the view: 235 | ```html 236 |
237 |

Users are loading...

238 |
239 | 240 | 245 | ``` 246 | 247 | After AJAX is finished, placeholder will be replaced with the request's 248 | response. 249 | 250 | ### Passing in an event name 251 | 252 | `render_async` can receive `:event_name` option which will emit JavaScript 253 | event after it's done with fetching and rendering request content to HTML. 254 | 255 | This can be useful to have if you want to add some JavaScript functionality 256 | after your partial is loaded through `render_async`. 257 | 258 | You can also access the associated container (DOM node) in the event object 259 | that gets emitted. 260 | 261 | Example of passing it to `render_async`: 262 | ```erb 263 | <%= render_async users_path, event_name: "users-loaded" %> 264 | ``` 265 | 266 | Rendered code in view: 267 | ```html 268 |
269 |
270 | 271 | 278 | ``` 279 | 280 | Then, in your JavaScript code, you could do something like this: 281 | ```javascript 282 | document.addEventListener("users-loaded", function(event) { 283 | console.log("Users have loaded!", event.container); // Access the container which loaded the users 284 | }); 285 | ``` 286 | 287 | > :bulb: Dispatching events is also supported for older browsers that don't support Event constructor. 288 | 289 | ### Using default events 290 | 291 | `render_async` will fire the event `render_async_load` when an async partial 292 | has loaded and rendered on the page. 293 | 294 | In case there is an error, the event `render_async_error` will fire instead. 295 | 296 | This event will fire for all `render_async` partials on the page. For every 297 | event, the associated container (DOM node) will be passed along. 298 | 299 | This can be useful to apply JavaScript to content loaded after the page is 300 | ready. 301 | 302 | Example of using events: 303 | 304 | ```js 305 | // Vanilla javascript 306 | document.addEventListener('render_async_load', function(event) { 307 | console.log('Async partial loaded in this container:', event.container); 308 | }); 309 | document.addEventListener('render_async_error', function(event) { 310 | console.log('Async partial could not load in this container:', event.container); 311 | }); 312 | 313 | // with jQuery 314 | $(document).on('render_async_load', function(event) { 315 | console.log('Async partial loaded in this container:', event.container); 316 | }); 317 | $(document).on('render_async_error', function(event) { 318 | console.log('Async partial could not load in this container:', event.container); 319 | }); 320 | ``` 321 | 322 | ### Refreshing the partial 323 | 324 | `render_async` lets you refresh (reload) the partial by letting you dispatch 325 | the 'refresh' event on the `render_async`'s container. An example: 326 | 327 | ```erb 328 | <%= render_async comments_path, 329 | container_id: 'refresh-me', 330 | replace_container: false %> 331 | 332 | 333 | 334 | 345 | ``` 346 | 347 | If you follow the example above, when you click "Refresh comments" button, 348 | `render_async` will trigger again and reload the `comments_path`. 349 | 350 | > :bulb: Note that you need to pass `replace_container: false` so you can later dispatch an event on that container. 351 | 352 | ### Retry on failure 353 | 354 | `render_async` can retry your requests if they fail for some reason. 355 | 356 | If you want `render_async` to retry a request for number of times, you can do 357 | this: 358 | ```erb 359 | <%= render_async users_path, retry_count: 5, error_message: "Couldn't fetch it" %> 360 | ``` 361 | 362 | Now render_async will retry `users_path` for 5 times. If it succeeds in 363 | between, it will stop with dispatching requests. If it fails after 5 times, 364 | it will show an [error message](#handling-errors) which you need to specify. 365 | 366 | This can show useful when you know your requests often fail, and you don't want 367 | to refresh the whole page just to retry them. 368 | 369 | #### Retry after some time 370 | 371 | If you want to retry requests but with some delay in between the calls, you can 372 | pass a `retry_delay` option together with `retry_count` like so: 373 | 374 | ```erb 375 | <%= render_async users_path, 376 | retry_count: 5, 377 | retry_delay: 2000 %> 378 | ``` 379 | 380 | This will make `render_async` wait for 2 seconds before retrying after each 381 | failure. In the end, if the request is still failing after 5th time, it will 382 | dispatch a [default error event](#using-default-events). 383 | 384 | > :candy: If you are catching an event after an error, you can get `retryCount` from 385 | the event. `retryCount` will have the number of retries it took before the event was dispatched. 386 | 387 | Here is an example on how to get `retryCount`: 388 | 389 | ```erb 390 | <%= render_async users_path, 391 | retry_count: 5, 392 | retry_delay: 2000, 393 | error_event_name: 'it-failed-badly' %> 394 | 395 | 400 | ``` 401 | 402 | If you need to pass retry count to the backend, you can pass `retry_count_header` in `render_async`'s options: 403 | 404 | ```erb 405 | <%= render_async users_path, 406 | retry_count: 5, 407 | retry_count_header: 'Retry-Count-Current' %> 408 | ``` 409 | 410 | And then in controller you can read the value from request headers. 411 | 412 | ``` 413 | request.headers['Retry-Count-Current']&.to_i 414 | ``` 415 | 416 | ### Toggle event 417 | 418 | You can trigger `render_async` loading by clicking or doing another event to a 419 | certain HTML element. You can do this by passing in a selector and an event 420 | name which will trigger `render_async`. If you don't specify an event name, the 421 | default event that will trigger `render_async` will be 'click' event. You can 422 | do this by doing the following: 423 | 424 | ```erb 425 | Load comments 426 | <%= render_async comments_path, toggle: { selector: '#comments-button', event: :click } %> 427 | ``` 428 | 429 | This will trigger `render_async` to load the `comments_path` when you click the `#comments-button` element. 430 | If you want to remove an event once it's triggered, you can pass `once: true` in the toggle options. 431 | The `once` option is false (`nil`) by default. 432 | 433 | You can also pass in a placeholder before the `render_async` is triggered. That 434 | way, the element that started `render_async` logic will be removed after the 435 | request has been completed. You can achieve this behaviour with something like this: 436 | 437 | ```erb 438 | <%= render_async comments_path, toggle: { selector: '#comments-button', event: :click } do %> 439 | Load comments 440 | <% end %> 441 | ``` 442 | 443 | #### Control polling with a toggle 444 | 445 | Also, you can mix interval and toggle features. This way, you can turn polling 446 | on, and off by clicking the "Load comments" button. In order to do this, you need to 447 | pass `toggle` and `interval` arguments to `render_async` call like this: 448 | 449 | ```erb 450 | Load comments 451 | <%= render_async comments_path, toggle: { selector: '#comments-button', event: :click }, interval: 2000 %> 452 | ``` 453 | 454 | If you want `render_async` to render the request on load, you can pass `start: 455 | true`. Passing the `start` option inside the `toggle` hash will trigger 456 | `render_async` on page load. You can then toggle off polling by interacting 457 | with the element you specified. An example: 458 | 459 | ```erb 460 | Toggle comments loading 461 | <%= render_async comments_path, 462 | toggle: { selector: '#comments-button', 463 | event: :click, 464 | start: true }, 465 | interval: 2000 %> 466 | ``` 467 | 468 | In the example above, the comments will load as soon as the page is rendered. 469 | Then, you can stop polling for comments by clicking the "Toggle comments 470 | loading" button. 471 | 472 | ### Polling 473 | 474 | You can call `render_async` with interval argument. This will make render_async 475 | call specified path at the specified interval. 476 | 477 | By doing this: 478 | ```erb 479 | <%= render_async comments_path, interval: 5000 %> 480 | ``` 481 | You are telling `render_async` to fetch comments_path every 5 seconds. 482 | 483 | This can be handy if you want to enable polling for a specific URL. 484 | 485 | > :warning: By passing interval to `render_async`, the initial container element 486 | > will remain in the HTML tree and it will not be replaced with request response. 487 | > You can handle how that container element is rendered and its style by 488 | > [passing in an HTML element name](#passing-in-an-html-element-name) and 489 | > [HTML element class](#passing-in-a-container-class-name). 490 | 491 | ### Controlled polling 492 | 493 | You can controller `render_async` [polling](#polling) in 2 manners. 494 | First one is pretty simple, and it involves using the [toggle](#toggle-event) 495 | feature. To do this, you can follow instructions in the 496 | [control polling with a toggle section](#control-polling-with-a-toggle). 497 | 498 | The second option is more advanced and it involves emitting events to the `render_async`'s 499 | container element. From your code, you can emit the following events: 500 | - 'async-stop' - this will stop polling 501 | - 'async-start' - this will start polling. 502 | 503 | > :bulb: Please note that events need to be dispatched to a render_async container. 504 | 505 | An example of how you can do this looks like this: 506 | 507 | ```erb 508 | <%= render_async wave_render_async_path, 509 | container_id: 'controllable-interval', # set container_id so we can get it later easily 510 | interval: 3000 %> 511 | 512 | 513 | 514 | 515 | 534 | ``` 535 | 536 | We are rendering two buttons - "Stop polling" and "Start polling". Then, we 537 | attach an event listener to catch any clicking on the buttons. When the buttons 538 | are clicked, we either stop the polling or start the polling, depending on which 539 | button a user clicks. 540 | 541 | ### Handling errors 542 | 543 | `render_async` lets you handle errors by allowing you to pass in `error_message` 544 | and `error_event_name`. 545 | 546 | - `error_message` 547 | 548 | passing an `error_message` will render a message if the AJAX requests fails for 549 | some reason 550 | ```erb 551 | <%= render_async users_path, 552 | error_message: '

Sorry, users loading went wrong :(

' %> 553 | ``` 554 | 555 | - `error_event_name` 556 | 557 | calling `render_async` with `error_event_name` will dispatch event in the case 558 | of an error with your AJAX call. 559 | ```erb 560 | <%= render_asyc users_path, error_event_name: 'users-error-event' %> 561 | ``` 562 | 563 | You can then catch the event in your code with: 564 | ```js 565 | document.addEventListener('users-error-event', function() { 566 | // I'm on it 567 | }) 568 | ``` 569 | 570 | ### Caching 571 | 572 | `render_async` can utilize view fragment caching to avoid extra AJAX calls. 573 | 574 | In your views (e.g. `app/views/comments/show.html.erb`): 575 | ```erb 576 | # note 'render_async_cache' instead of standard 'render_async' 577 | <%= render_async_cache comment_stats_path %> 578 | ``` 579 | 580 | Then, in the partial (e.g. `app/views/comments/_comment_stats.html.erb`): 581 | ```erb 582 | <% cache render_async_cache_key(request.path), skip_digest: true do %> 583 |
584 | <%= @stats %> 585 |
586 | <% end %> 587 | ``` 588 | 589 | - The first time the page renders, it will make the AJAX call. 590 | - Any other times (until the cache expires), it will render from cache 591 | instantly, without making the AJAX call. 592 | - You can expire cache simply by passing `:expires_in` in your view where 593 | you cache the partial 594 | 595 | ### Doing non-GET requests 596 | 597 | By default, `render_async` creates AJAX GET requests for the path you provide. 598 | If you want to change this behaviour, you can pass in a `method` argument to 599 | `render_async` view helper. 600 | 601 | ```erb 602 | <%= render_async users_path, method: 'POST' %> 603 | ``` 604 | 605 | You can also set `body` and `headers` of the request if you need them. 606 | 607 | ```erb 608 | <%= render_async users_path, 609 | method: 'POST', 610 | data: { fresh: 'AF' }, 611 | headers: { 'Content-Type': 'text' } %> 612 | ``` 613 | 614 | ### Using with Turbolinks 615 | 616 | On Turbolinks applications, you may experience caching issues when navigating 617 | away from, and then back to, a page with a `render_async` call on it. This will 618 | likely show up as an empty div. 619 | 620 | If you're using Turbolinks 5 or higher, you can resolve this by setting Turbolinks 621 | configuration of `render_async` to true: 622 | 623 | ```rb 624 | RenderAsync.configure do |config| 625 | config.turbolinks = true # Enable this option if you are using Turbolinks 5+ 626 | end 627 | ``` 628 | 629 | This way, you're not breaking Turbolinks flow of loading or reloading a page. 630 | It is more efficient than the next option below. 631 | 632 | Another option: 633 | If you want, you can tell Turbolinks to reload your `render_async` call as follows: 634 | 635 | ```erb 636 | <%= render_async events_path, html_options: { 'data-turbolinks-track': 'reload' } %> 637 | ``` 638 | 639 | This will reload the whole page with Turbolinks. 640 | 641 | > :bulb: If Turbolinks is misbehaving in some way, make sure to put `<%= content_for :render_async %>` in your base view file in 642 | the `` and not the ``. 643 | 644 | ### Using with Turbo 645 | 646 | On Turbo applications, you may experience caching issues when navigating 647 | away from, and then back to, a page with a `render_async` call on it. This will 648 | likely show up as an empty div. 649 | 650 | If you're using Turbo, you can resolve this by setting Turbo 651 | configuration of `render_async` to true: 652 | 653 | ```rb 654 | RenderAsync.configure do |config| 655 | config.turbo = true # Enable this option if you are using Turbo 656 | end 657 | ``` 658 | 659 | This way, you're not breaking Turbos flow of loading or reloading a page. 660 | It is more efficient than the next option below. 661 | 662 | Another option: 663 | If you want, you can tell Turbo to reload your `render_async` call as follows: 664 | 665 | ```erb 666 | <%= render_async events_path, html_options: { 'data-turbo-track': 'reload' } %> 667 | ``` 668 | 669 | This will reload the whole page with Turbo. 670 | 671 | > :bulb: If Turbo is misbehaving in some way, make sure to put `<%= content_for :render_async %>` in your base view file in 672 | the `` and not the ``. 673 | 674 | ### Using with respond_to and JS format 675 | 676 | If you need to restrict the action to only respond to AJAX requests, you'll 677 | likely wrap it inside `respond_to`/`format.js` blocks like this: 678 | 679 | ```ruby 680 | def comment_stats 681 | respond_to do |format| 682 | format.js do 683 | @stats = Comment.get_stats 684 | 685 | render partial: "comment_stats" 686 | end 687 | end 688 | end 689 | ``` 690 | 691 | When you do this, Rails will sometimes set the response's `Content-Type` header 692 | to `text/javascript`. This causes the partial not to be rendered in the HTML. 693 | This usually happens when there's browser caching. 694 | 695 | You can get around it by specifying the content type to `text/html` in the 696 | render call: 697 | 698 | ```ruby 699 | render partial: "comment_stats", content_type: 'text/html' 700 | ``` 701 | 702 | ### Nested async renders 703 | 704 | It is possible to nest async templates within other async templates. When doing 705 | so, another `content_for` is required to ensure the JavaScript needed to load 706 | nested templates is included. 707 | 708 | For example: 709 | ```erb 710 | <%# app/views/comments/show.html.erb %> 711 | 712 | <%= render_async comment_stats_path %> 713 | ``` 714 | 715 | ```erb 716 | <%# app/views/comments/_comment_stats.html.erb %> 717 | 718 |
719 | <%= @stats %> 720 |
721 | 722 |
723 | <%= render_async comment_advanced_stats_path %> 724 |
725 | 726 | <%= content_for :render_async %> 727 | ``` 728 | 729 | ### Customizing the content_for name 730 | 731 | The `content_for` name may be customized by passing the `content_for_name` 732 | option to `render_async`. This option is especially useful when doing [nested async 733 | renders](#nested-async-renders) to better control the location of the injected JavaScript. 734 | 735 | For example: 736 | ```erb 737 | <%= render_async comment_stats_path, content_for_name: :render_async_comment_stats %> 738 | 739 | <%= content_for :render_async_comment_stats %> 740 | ``` 741 | 742 | ### Configuration options 743 | 744 | `render_async` renders Vanilla JS (regular JavaScript, non-jQuery code) 745 | **by default** in order to fetch the request from the server. 746 | 747 | If you want `render_async` to use jQuery code, you need to configure it to do 748 | so. 749 | 750 | You can configure it by doing the following anywhere before you call 751 | `render_async`: 752 | 753 | ```rb 754 | RenderAsync.configure do |config| 755 | config.jquery = true # This will render jQuery code, and skip Vanilla JS code. The default value is false. 756 | config.turbolinks = true # Enable this option if you are using Turbolinks 5+. The default value is false. 757 | config.turbo = true # Enable this option if you are using Turbo. The default value is false. 758 | config.replace_container = false # Set to false if you want to keep the placeholder div element from render_async. The default value is true. 759 | config.nonces = true # Set to true if you want render_async's javascript_tag always to receive nonce: true. The default value is false. 760 | end 761 | ``` 762 | 763 | Also, you can do it like this: 764 | ```rb 765 | # This will render jQuery code, and skip Vanilla JS code 766 | RenderAsync.configuration.jquery = true 767 | ``` 768 | 769 | Aside from configuring whether the gem relies on jQuery or VanillaJS, you can 770 | configure other options: 771 | 772 | - `turbolinks` option - If you are using Turbolinks 5+, you should enable this option since it supports Turbolinks way of loading data. The default value for this option is false. 773 | - `turbo` option - If you are using Turbo, you should enable this option since it supports Turbo way of loading data. The default value for this option is false. 774 | - `replace_container` option - If you want render_async to replace its container with the request response, turn this on. You can turn this on globally for all render_async calls, but if you use this option in a specific render_async call, it will override the global configuration. The default value is true. 775 | - `nonces` - If you need to pass in `nonce: true` to the `javascript_tag` in your application, it might make sense for you to turn this on globally for all render_async calls. To read more about nonces, check out [Rails' official guide on security](https://edgeguides.rubyonrails.org/security.html). The default value is false. 776 | 777 | ## :hammer_and_pick: Development 778 | 779 | After checking out the repo, run `bin/setup` to install dependencies. Then, run 780 | `rake spec` to run the tests. You can also run `bin/console` for an interactive 781 | prompt that will allow you to experiment. To run integration tests, use 782 | `bin/integration-tests`. For more information, check out [CONTRIBUTING](.github/CONTRIBUTING.md) file, please. 783 | 784 | Got any questions or comments about development (or anything else)? 785 | Join [render_async's Discord channel](https://discord.gg/SPfbeRm) 786 | and let's make `render_async` even better! 787 | 788 | ## :pray: Contributing 789 | 790 | Check out [CONTRIBUTING](.github/CONTRIBUTING.md) file, please. 791 | 792 | Got any issues or difficulties? 793 | Join [render_async's Discord channel](https://discord.gg/SPfbeRm) 794 | and let's make `render_async` even better! 795 | 796 | ## :memo: License 797 | 798 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 799 | 800 | ## Contributors 801 | 802 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 803 | 804 | 805 | 806 | | [
Nikola Đuza](https://nikolalsvk.github.io)
[💬](#question-nikolalsvk "Answering Questions") [💻](https://github.com/renderedtext/render_async/commits?author=nikolalsvk "Code") [📖](https://github.com/renderedtext/render_async/commits?author=nikolalsvk "Documentation") [👀](#review-nikolalsvk "Reviewed Pull Requests") | [
Colin](http://www.colinxfleming.com)
[💻](https://github.com/renderedtext/render_async/commits?author=colinxfleming "Code") [📖](https://github.com/renderedtext/render_async/commits?author=colinxfleming "Documentation") [💡](#example-colinxfleming "Examples") | [
Kasper Grubbe](http://kaspergrubbe.com)
[💻](https://github.com/renderedtext/render_async/commits?author=kaspergrubbe "Code") | [
Sai Ram Kunala](https://sairam.xyz/)
[📖](https://github.com/renderedtext/render_async/commits?author=sairam "Documentation") | [
Josh Arnold](https://github.com/nightsurge)
[💻](https://github.com/renderedtext/render_async/commits?author=nightsurge "Code") [📖](https://github.com/renderedtext/render_async/commits?author=nightsurge "Documentation") | [
Elad Shahar](https://eladshahar.com)
[💻](https://github.com/renderedtext/render_async/commits?author=SaladFork "Code") [💡](#example-SaladFork "Examples") | [
Sasha](http://www.revzin.co.il)
[💻](https://github.com/renderedtext/render_async/commits?author=sasharevzin "Code") [📖](https://github.com/renderedtext/render_async/commits?author=sasharevzin "Documentation") | 807 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 808 | | [
Ernest Surudo](http://elsurudo.com)
[💻](https://github.com/renderedtext/render_async/commits?author=elsurudo "Code") | [
Kurtis Rainbolt-Greene](https://kurtis.rainbolt-greene.online)
[💻](https://github.com/renderedtext/render_async/commits?author=krainboltgreene "Code") | [
Richard Schneeman](https://www.schneems.com)
[📖](https://github.com/renderedtext/render_async/commits?author=schneems "Documentation") | [
Richard Venneman](https://www.cityspotters.com)
[📖](https://github.com/renderedtext/render_async/commits?author=richardvenneman "Documentation") | [
Filipe W. Lima](https://github.com/filipewl)
[📖](https://github.com/renderedtext/render_async/commits?author=filipewl "Documentation") | [
Jesús Eduardo Clemens Chong](https://github.com/eclemens)
[💻](https://github.com/renderedtext/render_async/commits?author=eclemens "Code") | [
René Klačan](https://github.com/reneklacan)
[💻](https://github.com/renderedtext/render_async/commits?author=reneklacan "Code") [📖](https://github.com/renderedtext/render_async/commits?author=reneklacan "Documentation") | 809 | | [
Gil Gomes](http://gilgomes.com.br)
[📖](https://github.com/renderedtext/render_async/commits?author=gil27 "Documentation") | [
Khoa Nguyen](https://github.com/ThanhKhoaIT)
[💻](https://github.com/renderedtext/render_async/commits?author=ThanhKhoaIT "Code") [📖](https://github.com/renderedtext/render_async/commits?author=ThanhKhoaIT "Documentation") | [
Preet Sethi](https://www.linkedin.com/in/preetsethila/)
[💻](https://github.com/renderedtext/render_async/commits?author=preetsethi "Code") | [
fangxing](https://github.com/fffx)
[💻](https://github.com/renderedtext/render_async/commits?author=fffx "Code") | [
Emmanuel Pire](http://blog.lipsumarium.com)
[💻](https://github.com/renderedtext/render_async/commits?author=lipsumar "Code") [📖](https://github.com/renderedtext/render_async/commits?author=lipsumar "Documentation") | [
Maxim Geerinck](https://github.com/maximgeerinck)
[💻](https://github.com/renderedtext/render_async/commits?author=maximgeerinck "Code") | [
Don](https://github.com/vanboom)
[💻](https://github.com/renderedtext/render_async/commits?author=vanboom "Code") | 810 | | [
villu164](https://github.com/villu164)
[📖](https://github.com/renderedtext/render_async/commits?author=villu164 "Documentation") | [
Mitchell Buckley](https://github.com/Mbuckley0)
[💻](https://github.com/renderedtext/render_async/commits?author=Mbuckley0 "Code") [📖](https://github.com/renderedtext/render_async/commits?author=Mbuckley0 "Documentation") | [
yhirano55](https://github.com/yhirano55)
[💻](https://github.com/renderedtext/render_async/commits?author=yhirano55 "Code") [📖](https://github.com/renderedtext/render_async/commits?author=yhirano55 "Documentation") | 811 | 812 | 813 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 814 | --------------------------------------------------------------------------------