Ajax loading spinner in Rails with Javascript's events & mutation observer
Published: 2020-01-19 23:35:07

This blog post is intended for intermediate programmers who have prior knowledge of rails ajax forms and javascript events.
 
There are tons of articles that explain the way to do the AJAX loader using **JQuery** but how can we solve it with javascript? This thought made me write this article.

## Layout
Here we will be building a layout having an account as input and search button to show the list of matching accounts and the loader icon would show and hide during DOM events.

![layout](https://user-images.githubusercontent.com/19718351/72675924-663d1180-3a59-11ea-938f-d73a99cc693b.png)

## Events
Our main objective is to display a loading spinner during page loading or the time when accounts are being fetched from the backend. So, to fit the spinner in, we can think of 2 two main scenarios 
1. To show a spinner, when a search button is pressed
2. and to hide it back, when a child is added to a DOM element via Rails AJAX

## Setup: Routes, Controllers and Views

*I'm not going through the code in detail as it would make the blog lengthy.*

### First, 
We define a route to new and search actions

```ruby
Rails.application.routes.draw do
  root to: 'accounts#new'
  
  resources :accounts do
    collection do
      post 'search'
    end
  end
end

```

### Second, 
Rendering js when a search action has occurred and sleep is added to delay the backend fetching time to see the loading spinner. 

```ruby
class AccountsController < ApplicationController

  def new
    @accounts = Account.new
  end
  
  def search
    @accounts = Account.where("name LIKE ?", "%#{new_params[:name]}%")
    sleep(2)
    respond_to do |format|
      format.js
    end
  end

  private

  def new_params
    params.require(:account).permit(:name)
  end

end

```

### Third, 
The View will make three things: one a search js, the other a partial and the last one for the main view

1) The partial **_accounts.html.erb** will be added as a child to the main DOM element via AJAX.

```html
<table class="table">
  <thead class="thead-dark">
    <tr>
      <th scope="col">Account Name</th>
      <th scope="col">Country</th>
	    <th scope="col"> State</th>

    </tr>
  </thead>

  <tbody>
    <% @accounts.each do |acct,v| %>
      <tr>
        <td scope="row"><%=acct.name%></td> 
        <td scope="row"><%=acct.country%></td> 
        <td scope="row"><%=acct.state%></td> 
      </tr>
    <% end %>
  </tbody>
</table>

```

2) The main view **new.html.erb**, will have an input field, a search button wrapped in a form with **remote: true** option, and an empty div **accounts-details** for displaying the accounts list

```html
<h1 class="text-info">Find Accounts</h1>

<div class="shadow-sm p-3 mb-5 bg-white rounded">
<%= form_for( @accounts, url: search_accounts_path, method: :post, remote: true ) do |f| %>

    <div class="form-group">
         <%= label_tag(:name, 'Account Name') %>
         <%= f.text_field :name, required: true%>
    </div>   
		
    <div class="form-group form-check"> 
        <div class="actions" class="btn btn-info">
            <%= f.submit("Search") %>
        </div>
    </div>

<% end %>

</div>

<!-- List will added as child when a search button is clicked--> 
<div id="accounts-details"></div>
```


3) The **search.js.erb** will do the magic by adding the partial ****_accounts.html.erb** as a child to an empty div **accounts-details** in the main view.

```javascript
$("#accounts-details").html("<%= j render partial: 'accounts', locals: { accounts: @accounts } %>");
```

4) A modal dialog box to hold the loading spinner
```html
  <body>
    <div class="container-md">
        <%= yield %>

      <div id="myModal" class="modal">
        <div class="text-center">
          <div class="spinner-border" role="status">
            <span class="sr-only">Loading...</span>
          </div>
        </div>
      </div>
    </div>
  </body>
```

## Finally a loading spinner 


a. Display a loading spinner on submit event listener


```javascript

// Get the Id of the form
  const searchButton = document.getElementById('new_account');
// Get the Id of a loading spinner modal
  const modal = document.getElementById('myModal');

  const displayModal = (e) => {
    //e.preventDefault();
    modal.style.display =  'block' ;
  }

// Listen the submit click on the form and call display spinner
searchButton.addEventListener('submit', displayModal);

```

 b. Hiding a loading spinner on when a DOM element is added by using Mutation observer


```javascript
// Get the ID of empty div
  const accountsDiv = document.getElementById('accounts-details');

// Get the Id of a loading spinner modal
  const modal = document.getElementById('myModal');

 const hideModal = (e) => {
    modal.style.display = 'none';
  }

// callback function will return the list of things changed an entry DIV. // In our case, a *childList* will be returned true, as we as adding partial.
  const callback = mutations => {
    mutations.forEach( (e) => {
            if (e.type === 'childList' ) {
              hideModal();
            };
    }); 
  };
  
// Configuration for the observer to listen to the DOM changes
  const config = {
    attributes: true,
    subtree: true,
    childList: true
  };

// Initializing a mutation observer
  const observer = new MutationObserver(callback);

// The main Listener to see DOM element changes
  observer.observe(accountsDiv, config);

```

*Note: As the event listener 'DOMNodeInserted' has drawbacks and so are using Mutation observer*
```javascript
 accountsDiv.addEventListener('DOMNodeInserted', hideModal);
```

and Finally, Wrap all the logic in **DOMContentLoaded** event listener.

```javascript
  window.addEventListener('DOMContentLoaded', domLoaded)

```

## Complete logic
{% github kattak2k/ajax-rails-js-observer no-readme %}



That's all, Thanks for reading!!


## References

https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
https://developer.mozilla.org/en-US/docs/Web/API/EventListener
https://www.rubyguides.com/2019/03/rails-ajax/