• Stars
    star
    1,079
  • Rank 42,873 (Top 0.9 %)
  • Language
    Ruby
  • License
    MIT License
  • Created almost 8 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

render_async lets you include pages asynchronously with AJAX

render_async

๐Ÿ‘‹ Welcome to render_async

Let's make your Rails pages fast again ๐ŸŽ


Donate Downloads All contributors Gem Version
Discord Server Build Status Code Climate Maintainablity Test Coverage License Help Contribute to Open Source

render_async is here to make your pages show faster to users.

Pages become faster seamlessly by rendering partials to your views.

Partials render asynchronously and let users see your page faster than using regular rendering.

It works with Rails and its tools out of the box.

โœจ A quick overview of how render_async does its magic:

  1. user visits a page
  2. render_async makes an AJAX request on the controller action
  3. controller renders a partial
  4. partial renders in the place where you put render_async view helper

JavaScript is injected straight into <%= content_for :render_async %> so you choose where to put it.

๐Ÿ“ฃ P.S. Join our Discord channel for help and discussion, and let's make render_async even better!

๐Ÿ“ฆ Installation

Add this line to your application's Gemfile:

gem 'render_async'

And then execute:

$ bundle install

๐Ÿ”จ Usage

  1. Include render_async view helper somewhere in your views (e.g. app/views/comments/show.html.erb):

    <%= render_async comment_stats_path %>
  2. Then create a route for it config/routes.rb:

    get :comment_stats, controller: :comments
  3. Fill in the logic in your controller (e.g. app/controllers/comments_controller.rb):

    def comment_stats
      @stats = Comment.get_stats
    
      render partial: "comment_stats"
    end
  4. Create a partial that will render (e.g. app/views/comments/_comment_stats.html.erb):

    <div class="col-md-6">
      <%= @stats %>
    </div>
  5. Add content_for in your base view file in the body part (e.g. app/views/layouts/application.html.erb):

    <%= content_for :render_async %>

๐Ÿ› ๏ธ Advanced usage

Advanced usage includes information on different options, such as:

Passing in a container ID

render_async renders an element that gets replaced with the content of your request response. In order to have more control over the element that renders first (before the request), you can set the ID of that element.

To set ID of the container element, you can do the following:

<%= render_async users_path, container_id: 'users-container' %>

Rendered code in the view:

<div id="users-container">
</div>

...

Passing in a container class name

render_async renders an element that gets replaced with the content of your request response. If you want to style that element, you can set the class name on it.

<%= render_async users_path, container_class: 'users-container-class' %>

Rendered code in the view:

<div id="render_async_18b8a6cd161499117471" class="users-container-class">
</div>

...

Passing in HTML options

render_async can accept html_options as a hash. html_options is an optional hash that gets passed to a Rails' javascript_tag, to drop HTML tags into the script element.

Example of utilizing html_options with a nonce:

<%= render_async users_path, html_options: { nonce: true } %>

Rendered code in the view:

<script nonce="2x012CYGxKgM8qAApxRHxA==">
//<![CDATA[
  ...
//]]>
</script>

...

<div id="render_async_18b8a6cd161499117471" class="">
</div>

๐Ÿ’ก You can enable nonce to be set everywhere by using configuration option render_async provides.

Passing in an HTML element name

render_async can take in an HTML element name, allowing you to control what type of container gets rendered. This can be useful when you're using render_async inside a table and you need it to render a tr element before your request gets loaded, so your content doesn't get pushed out of the table.

Example of using HTML element name:

<%= render_async users_path, html_element_name: 'tr' %>

Rendered code in the view:

<tr id="render_async_04229e7abe1507987376">
</tr>
...

Passing in a placeholder

render_async can be called with a block that will act as a placeholder before your AJAX call finishes.

Example of passing in a block:

<%= render_async users_path do %>
  <h1>Users are loading...</h1>
<% end %>

Rendered code in the view:

<div id="render_async_14d7ac165d1505993721">
  <h1>Users are loading...</h1>
</div>

<script>
//<![CDATA[
  ...
//]]>
</script>

After AJAX is finished, placeholder will be replaced with the request's response.

Passing in an event name

render_async can receive :event_name option which will emit JavaScript event after it's done with fetching and rendering request content to HTML.

This can be useful to have if you want to add some JavaScript functionality after your partial is loaded through render_async.

You can also access the associated container (DOM node) in the event object that gets emitted.

Example of passing it to render_async:

<%= render_async users_path, event_name: "users-loaded" %>

Rendered code in view:

<div id="render_async_04229e7abe1507987376">
</div>

<script>
//<![CDATA[
  ...
    document.dispatchEvent(new Event("users-loaded"));
  ...
//]]>
</script>

Then, in your JavaScript code, you could do something like this:

document.addEventListener("users-loaded", function(event) {
  console.log("Users have loaded!", event.container); // Access the container which loaded the users
});

๐Ÿ’ก Dispatching events is also supported for older browsers that don't support Event constructor.

Using default events

render_async will fire the event render_async_load when an async partial has loaded and rendered on the page.

In case there is an error, the event render_async_error will fire instead.

This event will fire for all render_async partials on the page. For every event, the associated container (DOM node) will be passed along.

This can be useful to apply JavaScript to content loaded after the page is ready.

Example of using events:

// Vanilla javascript
document.addEventListener('render_async_load', function(event) {
  console.log('Async partial loaded in this container:', event.container);
});
document.addEventListener('render_async_error', function(event) {
  console.log('Async partial could not load in this container:', event.container);
});

// with jQuery
$(document).on('render_async_load', function(event) {
  console.log('Async partial loaded in this container:', event.container);
});
$(document).on('render_async_error', function(event) {
  console.log('Async partial could not load in this container:', event.container);
});

Refreshing the partial

render_async lets you refresh (reload) the partial by letting you dispatch the 'refresh' event on the render_async's container. An example:

<%= render_async comments_path,
                 container_id: 'refresh-me',
                 replace_container: false %>

<button id="refresh-button">Refresh comments</button>

<script>
  var button = document.getElementById('refresh-button')
  var container = document.getElementById('refresh-me');

  button.addEventListener('click', function() {
    var event = new Event('refresh');

    // Dispatch 'refresh' on the render_async container
    container.dispatchEvent(event)
  })
</script>

If you follow the example above, when you click "Refresh comments" button, render_async will trigger again and reload the comments_path.

๐Ÿ’ก Note that you need to pass replace_container: false so you can later dispatch an event on that container.

Retry on failure

render_async can retry your requests if they fail for some reason.

If you want render_async to retry a request for number of times, you can do this:

<%= render_async users_path, retry_count: 5, error_message: "Couldn't fetch it" %>

Now render_async will retry users_path for 5 times. If it succeeds in between, it will stop with dispatching requests. If it fails after 5 times, it will show an error message which you need to specify.

This can show useful when you know your requests often fail, and you don't want to refresh the whole page just to retry them.

Retry after some time

If you want to retry requests but with some delay in between the calls, you can pass a retry_delay option together with retry_count like so:

<%= render_async users_path,
                 retry_count: 5,
                 retry_delay: 2000 %>

This will make render_async wait for 2 seconds before retrying after each failure. In the end, if the request is still failing after 5th time, it will dispatch a default error event.

๐Ÿฌ If you are catching an event after an error, you can get retryCount from the event. retryCount will have the number of retries it took before the event was dispatched.

Here is an example on how to get retryCount:

<%= render_async users_path,
                 retry_count: 5,
                 retry_delay: 2000,
                 error_event_name: 'it-failed-badly' %>

<script>
  document.addEventListener('it-failed-badly', function(event) {
    console.log("Request failed after " + event.retryCount + " tries!")
  });
</script>

If you need to pass retry count to the backend, you can pass retry_count_header in render_async's options:

<%= render_async users_path,
                 retry_count: 5,
                 retry_count_header: 'Retry-Count-Current' %>

And then in controller you can read the value from request headers.

request.headers['Retry-Count-Current']&.to_i

Toggle event

You can trigger render_async loading by clicking or doing another event to a certain HTML element. You can do this by passing in a selector and an event name which will trigger render_async. If you don't specify an event name, the default event that will trigger render_async will be 'click' event. You can do this by doing the following:

<a href='#' id='comments-button'>Load comments</a>
<%= render_async comments_path, toggle: { selector: '#comments-button', event: :click } %>

This will trigger render_async to load the comments_path when you click the #comments-button element. If you want to remove an event once it's triggered, you can pass once: true in the toggle options. The once option is false (nil) by default.

You can also pass in a placeholder before the render_async is triggered. That way, the element that started render_async logic will be removed after the request has been completed. You can achieve this behaviour with something like this:

<%= render_async comments_path, toggle: { selector: '#comments-button', event: :click } do %>
  <a href='#' id='comments-button'>Load comments</a>
<% end %>

Control polling with a toggle

Also, you can mix interval and toggle features. This way, you can turn polling on, and off by clicking the "Load comments" button. In order to do this, you need to pass toggle and interval arguments to render_async call like this:

<a href='#' id='comments-button'>Load comments</a>
<%= render_async comments_path, toggle: { selector: '#comments-button', event: :click }, interval: 2000 %>

If you want render_async to render the request on load, you can pass start: true. Passing the start option inside the toggle hash will trigger render_async on page load. You can then toggle off polling by interacting with the element you specified. An example:

<a href='#' id='comments-button'>Toggle comments loading</a>
<%= render_async comments_path,
                 toggle: { selector: '#comments-button',
                           event: :click,
                           start: true },
                 interval: 2000 %>

In the example above, the comments will load as soon as the page is rendered. Then, you can stop polling for comments by clicking the "Toggle comments loading" button.

Polling

You can call render_async with interval argument. This will make render_async call specified path at the specified interval.

By doing this:

<%= render_async comments_path, interval: 5000 %>

You are telling render_async to fetch comments_path every 5 seconds.

This can be handy if you want to enable polling for a specific URL.

โš ๏ธ By passing interval to render_async, the initial container element will remain in the HTML tree and it will not be replaced with request response. You can handle how that container element is rendered and its style by passing in an HTML element name and HTML element class.

Controlled polling

You can controller render_async polling in 2 manners. First one is pretty simple, and it involves using the toggle feature. To do this, you can follow instructions in the control polling with a toggle section.

The second option is more advanced and it involves emitting events to the render_async's container element. From your code, you can emit the following events:

  • 'async-stop' - this will stop polling
  • 'async-start' - this will start polling.

๐Ÿ’ก Please note that events need to be dispatched to a render_async container.

An example of how you can do this looks like this:

<%= render_async wave_render_async_path,
                 container_id: 'controllable-interval', # set container_id so we can get it later easily
                 interval: 3000 %>

<button id='stop-polling'>Stop polling</button>
<button id='start-polling'>Start polling</button>

<script>
  var container = document.getElementById('controllable-interval')
  var stopPolling = document.getElementById('stop-polling')
  var startPolling = document.getElementById('start-polling')

  var triggerEventOnContainer = function(eventName) {
    var event = new Event(eventName);

    container.dispatchEvent(event)
  }

  stopPolling.addEventListener('click', function() {
    container.innerHTML = '<p>Polling stopped</p>'
    triggerEventOnContainer('async-stop')
  })
  startPolling.addEventListener('click', function() {
    triggerEventOnContainer('async-start')
  })
</script>

We are rendering two buttons - "Stop polling" and "Start polling". Then, we attach an event listener to catch any clicking on the buttons. When the buttons are clicked, we either stop the polling or start the polling, depending on which button a user clicks.

Handling errors

render_async lets you handle errors by allowing you to pass in error_message and error_event_name.

  • error_message

    passing an error_message will render a message if the AJAX requests fails for some reason

    <%= render_async users_path,
                     error_message: '<p>Sorry, users loading went wrong :(</p>' %>
  • error_event_name

    calling render_async with error_event_name will dispatch event in the case of an error with your AJAX call.

    <%= render_asyc users_path, error_event_name: 'users-error-event' %>

    You can then catch the event in your code with:

    document.addEventListener('users-error-event', function() {
      // I'm on it
    })

Caching

render_async can utilize view fragment caching to avoid extra AJAX calls.

In your views (e.g. app/views/comments/show.html.erb):

# note 'render_async_cache' instead of standard 'render_async'
<%= render_async_cache comment_stats_path %>

Then, in the partial (e.g. app/views/comments/_comment_stats.html.erb):

<% cache render_async_cache_key(request.path), skip_digest: true do %>
  <div class="col-md-6">
    <%= @stats %>
  </div>
<% end %>
  • The first time the page renders, it will make the AJAX call.
  • Any other times (until the cache expires), it will render from cache instantly, without making the AJAX call.
  • You can expire cache simply by passing :expires_in in your view where you cache the partial

Doing non-GET requests

By default, render_async creates AJAX GET requests for the path you provide. If you want to change this behaviour, you can pass in a method argument to render_async view helper.

<%= render_async users_path, method: 'POST' %>

You can also set body and headers of the request if you need them.

<%= render_async users_path,
                 method: 'POST',
                 data: { fresh: 'AF' },
                 headers: { 'Content-Type': 'text' } %>

Using with Turbolinks

On Turbolinks applications, you may experience caching issues when navigating away from, and then back to, a page with a render_async call on it. This will likely show up as an empty div.

If you're using Turbolinks 5 or higher, you can resolve this by setting Turbolinks configuration of render_async to true:

RenderAsync.configure do |config|
  config.turbolinks = true # Enable this option if you are using Turbolinks 5+
end

This way, you're not breaking Turbolinks flow of loading or reloading a page. It is more efficient than the next option below.

Another option: If you want, you can tell Turbolinks to reload your render_async call as follows:

<%= render_async events_path, html_options: { 'data-turbolinks-track': 'reload' } %>

This will reload the whole page with Turbolinks.

๐Ÿ’ก If Turbolinks is misbehaving in some way, make sure to put <%= content_for :render_async %> in your base view file in the <body> and not the <head>.

Using with Turbo

On Turbo applications, you may experience caching issues when navigating away from, and then back to, a page with a render_async call on it. This will likely show up as an empty div.

If you're using Turbo, you can resolve this by setting Turbo configuration of render_async to true:

RenderAsync.configure do |config|
  config.turbo = true # Enable this option if you are using Turbo
end

This way, you're not breaking Turbos flow of loading or reloading a page. It is more efficient than the next option below.

Another option: If you want, you can tell Turbo to reload your render_async call as follows:

<%= render_async events_path, html_options: { 'data-turbo-track': 'reload' } %>

This will reload the whole page with Turbo.

๐Ÿ’ก If Turbo is misbehaving in some way, make sure to put <%= content_for :render_async %> in your base view file in the <body> and not the <head>.

Using with respond_to and JS format

If you need to restrict the action to only respond to AJAX requests, you'll likely wrap it inside respond_to/format.js blocks like this:

def comment_stats
  respond_to do |format|
    format.js do
      @stats = Comment.get_stats

      render partial: "comment_stats"
    end
  end
end

When you do this, Rails will sometimes set the response's Content-Type header to text/javascript. This causes the partial not to be rendered in the HTML. This usually happens when there's browser caching.

You can get around it by specifying the content type to text/html in the render call:

render partial: "comment_stats", content_type: 'text/html'

Nested async renders

It is possible to nest async templates within other async templates. When doing so, another content_for is required to ensure the JavaScript needed to load nested templates is included.

For example:

<%# app/views/comments/show.html.erb %>

<%= render_async comment_stats_path %>
<%# app/views/comments/_comment_stats.html.erb %>

<div class="col-md-6">
  <%= @stats %>
</div>

<div class="col-md-6">
  <%= render_async comment_advanced_stats_path %>
</div>

<%= content_for :render_async %>

Customizing the content_for name

The content_for name may be customized by passing the content_for_name option to render_async. This option is especially useful when doing nested async renders to better control the location of the injected JavaScript.

For example:

<%= render_async comment_stats_path, content_for_name: :render_async_comment_stats %>

<%= content_for :render_async_comment_stats %>

Configuration options

render_async renders Vanilla JS (regular JavaScript, non-jQuery code) by default in order to fetch the request from the server.

If you want render_async to use jQuery code, you need to configure it to do so.

You can configure it by doing the following anywhere before you call render_async:

RenderAsync.configure do |config|
  config.jquery = true # This will render jQuery code, and skip Vanilla JS code. The default value is false.
  config.turbolinks = true # Enable this option if you are using Turbolinks 5+. The default value is false.
  config.turbo = true # Enable this option if you are using Turbo. The default value is false.
  config.replace_container = false # Set to false if you want to keep the placeholder div element from render_async. The default value is true.
  config.nonces = true # Set to true if you want render_async's javascript_tag always to receive nonce: true. The default value is false.
end

Also, you can do it like this:

# This will render jQuery code, and skip Vanilla JS code
RenderAsync.configuration.jquery = true

Aside from configuring whether the gem relies on jQuery or VanillaJS, you can configure other options:

  • 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.
  • 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.
  • 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.
  • 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. The default value is false.

โš’๏ธ Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment. To run integration tests, use bin/integration-tests. For more information, check out CONTRIBUTING file, please.

Got any questions or comments about development (or anything else)? Join render_async's Discord channel and let's make render_async even better!

๐Ÿ™ Contributing

Check out CONTRIBUTING file, please.

Got any issues or difficulties? Join render_async's Discord channel and let's make render_async even better!

๐Ÿ“ License

The gem is available as open source under the terms of the MIT License.

Contributors

Thanks goes to these wonderful people (emoji key):


Nikola ฤuza

๐Ÿ’ฌ ๐Ÿ’ป ๐Ÿ“– ๐Ÿ‘€

Colin

๐Ÿ’ป ๐Ÿ“– ๐Ÿ’ก

Kasper Grubbe

๐Ÿ’ป

Sai Ram Kunala

๐Ÿ“–

Josh Arnold

๐Ÿ’ป ๐Ÿ“–

Elad Shahar

๐Ÿ’ป ๐Ÿ’ก

Sasha

๐Ÿ’ป ๐Ÿ“–

Ernest Surudo

๐Ÿ’ป

Kurtis Rainbolt-Greene

๐Ÿ’ป

Richard Schneeman

๐Ÿ“–

Richard Venneman

๐Ÿ“–

Filipe W. Lima

๐Ÿ“–

Jesรบs Eduardo Clemens Chong

๐Ÿ’ป

Renรฉ Klaฤan

๐Ÿ’ป ๐Ÿ“–

Gil Gomes

๐Ÿ“–

Khoa Nguyen

๐Ÿ’ป ๐Ÿ“–

Preet Sethi

๐Ÿ’ป

fangxing

๐Ÿ’ป

Emmanuel Pire

๐Ÿ’ป ๐Ÿ“–

Maxim Geerinck

๐Ÿ’ป

Don

๐Ÿ’ป

villu164

๐Ÿ“–

Mitchell Buckley

๐Ÿ’ป ๐Ÿ“–

yhirano55

๐Ÿ’ป ๐Ÿ“–

This project follows the all-contributors specification. Contributions of any kind welcome!

More Repositories

1

base-app

An app to help jumpstart a new Rails 4 app. Features Ruby 2.0, PostgreSQL, jQuery, RSpec, Cucumber, user and admin system built with Devise, Facebook login.
Ruby
125
star
2

admin_view

Rails 4 generator of CRUD admin interfaces for existing models.
Ruby
118
star
3

coming-soon

Sinatra application to show a pre-launch page and collect emails.
Ruby
81
star
4

test-boosters

Auto Parallelization - runs test files in multiple jobs
Ruby
53
star
5

ex-tackle

๐Ÿ’ฏ percent reliable microservice communication
Elixir
48
star
6

tackle

๐Ÿ’ฏ percent reliable microservice communication
Ruby
47
star
7

wormhole

Captures anything that is emitted from the callback - Elixir library repo
Elixir
41
star
8

semaphore-docs-new

Documentation site for Semaphore.
CSS
24
star
9

cli

Semaphore Classic Command Line Interface
Ruby
15
star
10

snippets

Code snippets in Ruby.
Shell
12
star
11

dark-magic

Various sorcery for Elixir projects
Elixir
7
star
12

vim-elixir-alternative-files

Vim script for alternating between test and implementation files for Elixir
Vim Script
7
star
13

semaphore-status

CLI to get current status of your projects on Semaphore
Ruby
6
star
14

ex-config

Simple Elixir configuration managment.
Elixir
6
star
15

scripts

Handy scripts
Ruby
5
star
16

react-redux-demo

Demo application that demonstrates how to use React and Redux
JavaScript
5
star
17

docker-compose-example

An example Rails 4 project with Docker
Ruby
5
star
18

cheatsheets

Various cheatsheets in nice Markdown format.
4
star
19

grpc_health_check

Healthchecks for Elixir GRPC server apps
Elixir
4
star
20

when

Erlang
4
star
21

ruby-101

Ruby
4
star
22

ex-docker

Manage your docker images from your Elixir apps
Elixir
4
star
23

ex-watchman

Watchman is your friend who monitors your processes so you don't have to
Elixir
4
star
24

logman

Logman โ€” Lightweight abstraction for formatted logging
Ruby
4
star
25

vim-bdd

Vim functions to run RSpec and Cucumber on the current cursor or file.
Vim Script
4
star
26

hubot

Our witty bot
CoffeeScript
3
star
27

jquery-placeholders

jQuery plugin for input hints via the placeholder HTML5 attribute
JavaScript
3
star
28

semaphore-scripts

Script collection which might help with adjusting Semaphore environment for a specific project
Shell
3
star
29

elixir-util

Common utility functions for elixir services
Elixir
2
star
30

testapp-capybara-webkit

Ruby
2
star
31

hoist

campfire bot
Ruby
2
star
32

testapp-mysql

Ruby
2
star
33

mkvm

Scripts for setting up a new virtual machine for development
Ruby
2
star
34

helm-charts

Semaphore Helm charts repository
Smarty
2
star
35

agent-aws-stack

AWS CDK application to deploy an autoscaling fleet of agents to AWS
JavaScript
2
star
36

ssh_message

Render awesome motd messages for SSH sessions.
Ruby
2
star
37

green-worker

DB backed, FSM-like family of workers, distributed on multiple nodes
Elixir
2
star
38

ex-cacheman

Cacheman is a Redis backed Rails.cache equivalent for Elixir applications.
Elixir
2
star
39

grpc-mock

Easy gRPC server mocking for [grpc Elixir library](https://github.com/tony612/grpc-elixir)
Elixir
2
star
40

testapp-mongodb

Ruby
1
star
41

kafka-playground

Ruby
1
star
42

testapp-testunit

Ruby
1
star
43

go-watchman

go watchman
Go
1
star
44

grpc-stress

Stress testing tony612/grpc-elixir implementation
Elixir
1
star
45

ci-server

Ruby
1
star
46

testapp-sphinx

Ruby
1
star
47

alpine-release

Generate an exrm release for Alpine Docker with ease
Elixir
1
star
48

testapp-envvars

Ruby
1
star
49

javascript-101

1
star
50

kata-base

A minimal setup to do a code kata in Ruby with RSpec.
Ruby
1
star
51

s2-public-api-docs

Documentation for Sempahore 2.0 publlic HTTP API
1
star
52

mobile10k

Shell
1
star
53

fun-registry

Elixir
1
star
54

testapp-sqlite

Ruby
1
star
55

testapp-mongodb-mongomapper

Ruby
1
star
56

cont-monitoring

Continuous monitoring for Semaphore2 UI
Shell
1
star
57

stripe-example

implementing stripe features in a rails app
Ruby
1
star
58

log-tee

tee-ing data to logger
Elixir
1
star
59

semctl

Tool that make interactions with Semaphore API from jobs on Semaphore easier
Python
1
star
60

testapp-solr

Ruby
1
star
61

CurrencyTracker

Ruby
1
star
62

testapp-testunit-rspec

Ruby
1
star
63

test-boosters-tests

Ruby
1
star
64

test-results-cli

(Experimental) Test Results CLI โ€” Parse XML files and prepare JSON files that are consumable from the Front App
Elixir
1
star
65

open-source

Showcase of Open Source projects built by the Semaphore team
JavaScript
1
star
66

thrift-with-tackle

Use thrift models in tackle based communication
Elixir
1
star