Kendall Weihe Kendall Weihe - 4 months ago 22
jQuery Question

Rails 4 update instance variables without page reload

Is there a way I can reload an instance variable in the view without reloading the page?

I have an AJAX post, and it creates a new record. I want that record to then be added to the existing instance variable.

So I have an action such as

action
and it handles the view I am in
action.html.erb
:

def action
@variable = Variable.where().to_a
end


And then I have an AJAX request that executes an action such as
action2
:

def action2
@new_record = Variable.create(params)
respond_to do |format|
format.html
format.json {render :json => @new_record.to_json}
end
end


Is there a way I can then refresh the
@variable
instance variable to include the newly created record?

Could I make another AJAX request to
action
? If so, how would I detect the AJAX request in
action
?

I have tried this^^ with
request.xhr?
but it is throwing a weird
StandardError
after I execute a JS
.click()
?

I'm not sure if I am going in the right direction, but if I am, here is all the code that I put together...

I am building an instant messaging service, and I admit that I am totally hacking together a prototype. I understand there are cleaner ways to do this that will handle high traffic loads, but I am working on a prototype right now.

The view code looks like this... there is: the HTML chatbox, I make an AJAX request to find new messages every 2 seconds, in the same function I make the AJAX request to what I called
action
above (actually names something different), after the "refresh" function I have a
.click()
function to update the users view (the one who just submitted).

<div id="wrapper">
<div id="menu">
<p class="welcome">Welcome, <b></b></p>
<p class="logout"><a id="exit" href="#">Exit Chat</a></p>
<div style="clear:both"></div>
</div>

<div id="chatbox">
<% @messages.each do |message| %>
<% if session[:email] == message.email %>
<!-- post to the right side -->
<div id="right-side">
<p>Email: <%= message.email %></p>
<p>Message: <%= message.message %></p>
</div>
<% else %>
<!-- post to the left side -->
<div id="left-side">
<p>Email: <%= message.email %></p>
<p>Message: <%= message.message %></p>
</div>
<% end %>
<% end %>
</div>

<form id="frm1" action="">
Message: <input id="message_input" type="text" name="fname"><br>
<!-- <input type="submit" value="Submit"> -->
<button name="submitmsg" type="submit" id="submitmsg">Try it</button>
</form>
</div>

<p id="demo"></p>
<script>

function retrieveMessages(){
var message;
$.ajax({
type:"GET",
url:"<%= get_messages_path %>",
dataType:"json",
data: {chat_id: <%= @message_info[:chat_id] %>,
last_message: <%= @messages.last.created_at.to_i %>},
success:function(data){
if (data != null){
console.log(data);
console.log(data.message);
message = data.message;
document.getElementById("chatbox").innerHTML += message + '<br>';
}
}
});

$.ajax({
type:"GET",
url:"<%= new_message_path %>",
dataType:"json",
success:function(data){
console.log("success!");
}
}
});
setTimeout(retrieveMessages, 2000);
}

$(document).ready(function(){
//get messages
setTimeout(retrieveMessages, 2000);

//send messages
$("#submitmsg").on('click', function(e){
e.preventDefault();
var x = document.getElementById("frm1"); //This is the form, and not the value of the textbox
var text = "";
var i;
for (i = 0; i < x.length ;i++) {
text += x.elements[i].value;
}
// document.getElementById("chatbox").innerHTML += text;
text_with_br = text + "<br>"
document.getElementById("chatbox").innerHTML += text_with_br;
document.getElementById("frm1").reset();
// console.log(document.getElementById("right"));
//Actual message is in 'text'
$.ajax({
url: "/messages",
method: "post",
data: { message: text,
chat_id: <%= @message_info[:chat_id] %>,
message_counter: <%= @message_info[:message_counter] + 1 %> },
dataType: "JSON",
success: function(response){
//do something,maybe notify user successfully posted their message
},
error: function(error){
console.log(error);
}
});

});
});
</script>


Then for the
action
action I have (actually the
new
action, and what I called
@variable
is actually
@messages
:

def new
# debugger
# Message.create(:chat_id => 5)
if session[:email].nil?
#user not logged in
redirect_to new_session_path(:message => "need to login")
else
@user = User.where(:email => session[:email]).first
@message_info = Hash(email: @user.email, message_counter: 0, chat_id: Message.last.chat_id + 1)
@messages = Message.where(:chat_id => @message_info[:chat_id]).to_a

respond_to do |format|
format.html
format.json
end

end
end


Then the action that executes the AJAX request to GET the most recent message (so that viewers from other browsers can view) is as follow...

def get
@new_message = Message.where(["created_at > ?", Time.at(params[:last_message].to_i)]).first

respond_to do |format|
format.html
# format.json {render json: @new_message}
format.json {render :json => @new_message.to_json}
end
end

Answer

Starting from your code, below is my attempt at a solution. I've renamed elements in the hope that it will make this code more self-documenting and therefore easier for you to discern what I've done. It's not perfect, but it is working for me in Chrome, so hopefully it helps. I have assumed the starting chat_id is 1. If it is not, you can adjust it in the get_last_chat_id method in the controller.

(Note: There are a few lines of code that will be superfluous for you, but were necessary for me to be able to run this code inside of one of my existing apps. You should also know that I'm using jQuery, Devise, HAML and Bootstrap 4 alpha.)

app/controllers/messages_controller.rb

class MessagesController < ApplicationController

  before_action :authenticate_user!
  before_action :set_current_user_email, only: [:index, :display_all_messages]
  before_action :set_current_messages,   only: [:display_all_messages]

  def index
  end


  def get_last_chat_id
    last_chat_id = Message.pluck(:chat_id).max
    if last_chat_id
      puts '*** LAST CHAT ID: ' + last_chat_id.to_s
    else
      last_chat_id = 0
      puts '*** LAST CHAT ID: ' + last_chat_id.to_s
    end

    respond_to do |format|
      format.json { render json: last_chat_id }
    end
  end


  def save_new_message
    new_message = Message.new do |msg|
      msg.email        = params[:email]
      msg.message_text = params[:message_text]
      msg.chat_id      = params[:chat_id]
    end

    puts '*** NEW MESSAGE EMAIL:   ' + new_message.email
    puts '*** NEW MESSAGE TEXT:    ' + new_message.message_text
    puts '*** NEW MESSAGE CHAT ID: ' + new_message.chat_id.to_s

    # Source: https://makandracards.com/housetrip-deck/16879-jquery-ajax-success-done-will-not-run-callbacks-if-request-is-json-but-the-response-is-empty-typical-200
    respond_to do |format|
      if new_message.save
        puts '*** NEW MESSAGE WAS SAVED!!!'
        format.json { render json: { ok: true }, status: :ok }
      else
        puts '*** NEW MESSAGE WAS NOT SAVED!!!'
        format.json { render json: { ok: false }, status: :unprocessable_entity }
      end
    end
  end


  def display_all_messages
    respond_to do |format|
      if @current_messages
        format.js  {  }
      else
        puts '*** THERE ARE NO MESSAGES TO DISPLAY!!!'
      end
    end
  end


  private

    def set_current_messages
      @current_messages = Message.by_created_desc
    end


    def set_current_user_email
      @current_user_email = current_user.email
    end


    def message_params
      params.require(:message).
        permit(:email, :message_text)
    end


end

app/models/message.rb

class Message < ActiveRecord::Base

  scope :by_created_desc, -> { order(created_at: :desc) }

end

db/migrate/20160717000100_create_messages.rb

class CreateMessages < ActiveRecord::Migration
  def change

    create_table :messages do |t|

      t.string  :email,        null: false
      t.text    :message_text
      t.integer :chat_id,      null: false

      t.timestamps null: false

    end

    add_index :messages, :chat_id, unique: true

  end
end

app/assets/javascripts/messages.js

var messagesRefresher;

$(document).ready(function(){

  // AJAX error handling, outputting error messaging to the console
  $(document).ajaxError(function (event, jqxhr, settings, thrownError) {
    console.log('EVENT: '        + JSON.stringify(event, null, '\t'));
    console.log('JQXHR: '        + JSON.stringify(jqxhr));
    console.log('SETTINGS: '     + JSON.stringify(settings, null, '\t'));
    console.log('THROWN ERROR: ' + thrownError);
  });

  messagesRefresher = setInterval(refreshMessages, 2000);

  $('#new-message-create-btn').on('click', function(e){
    e.preventDefault();
    $('#new-message-create-btn').addClass('no-display');
    $('#new-messages-form-wrapper').removeClass('no-display');
  });

  $('#new-message-cancel-btn').on('click', function(e){
    e.preventDefault();
    $('#new-messages-form-wrapper').addClass('no-display');
    $('#new-message-create-btn').removeClass('no-display');
  });

  $('#new-message-submit-btn').on('click', function(e){
    e.preventDefault();
    var newMessageEmail  = $('#new-message-email').val();
    var newMessageText   = $('#new-message-text').val();

    console.log('*** NEW MESSAGE EMAIL: ' + newMessageEmail);
    console.log('*** NEW MESSAGE TEXT:');
    console.log(newMessageText);

    getLastChatId('/messages/get_last_chat_id').done(function(lastChatId) {
      var newMessageChatId = lastChatId + 1;
      console.log('*** NEW MESSAGE CHAT ID: ' + newMessageChatId)

      saveNewMessage('/messages/save_new_message?email=' + newMessageEmail + '&message_text=' + newMessageText + '&chat_id=' + newMessageChatId).done(function(data) {

        console.log('*** MESSAGE # ' + newMessageChatId + ' SAVED!!!')

        $('#new-messages-form-wrapper').addClass('no-display');
        document.getElementById('new-message-form').reset();
        $('#new-message-create-btn').removeClass('no-display');

        refreshMessages;
      });

    });

  });

});


function refreshMessages() {
  displayAllMessages('/messages/display_all_messages').done(function(data) {
    console.log('*** MESSAGES REFRESHED!!!');
  });
};


function getLastChatId(url) {
  return $.ajax({
    url:       url,
    type:     'get',
    dataType: 'json'
  })
  .fail(function() {
    alert('AJAX Get Last Chat Id Error');
  });
};


function saveNewMessage(url) {
  return $.ajax({
    url:       url,
    type:     'get',
    dataType: 'json'
  })
  .fail(function() {
    alert('AJAX Save New Message Error');
  });
};


function displayAllMessages(url) {
  return $.ajax({
    url:       url,
    type:     'get',
    dataType: 'script'
  })
  .fail(function() {
    alert('AJAX Display All Messages Error');
  });
};


$(window).unload(
  function(event) {
    clearInterval(messagesRefresher);
  }
);

app/views/messages/index.html.haml

- content_for :page_specific_javascript do
  = javascript_include_tag 'messages.js'

#chatbox
  %h1 Chatbox

  = link_to 'New Message', 'javascript:;', id: 'new-message-create-btn', class: 'btn btn-sm btn-primary'

  #new-message-form-wrapper.no-display
    = form_tag messages_path, id: 'new-message-form' do
      = hidden_field_tag 'new-message-email', @current_user_email
      #new-message-form-label-wrapper
        = label_tag 'new-message-text', 'Enter Your Message:'
      #new-message-form-text-wrapper
        = text_area_tag 'new-message-text', nil, rows: 6, cols: 70
      #new-message-form-buttons-wrapper
        = submit_tag 'Post Message', id: 'new-message-submit-btn', class: 'btn btn-sm btn-success'
        = link_to 'Cancel', 'javascript:;', id: 'new-message-cancel-btn', class: 'btn btn-sm btn-secondary'


  #display-messages-wrapper

    #messages-column-left.pull-md-left
      %h3 Messages From Others
      #messages-other-users

    #messages-column-right.pull-md-right
      %h3 My Messages
      #messages-current-user

app/views/messages/display_all_messages.js.haml

$('#messages-other-users').html('');
$('#messages-current-user').html('');
- @current_messages.each do |msg|
  - if msg.email == @current_user_email
    $('#messages-current-user').append("#{ escape_javascript render(partial: 'message', locals: { email: msg.email, message_created_at: msg.created_at, message_text: msg.message_text }) }");
  - else
    $('#messages-other-users').append("#{ escape_javascript render(partial: 'message', locals: { email: msg.email, message_created_at: msg.created_at, message_text: msg.message_text }) }");

app/views/messages/_message.html.haml

.message-wrapper

  .message-attribution-wrapper
    %span.message-attribution-label Posted by:
    %span.message-attribution-text=email + ' on ' + message_created_at.strftime('%Y-%m-%d') + ' at ' + message_created_at.strftime('%I:%M:%S %p')

  .message-text-wrapper
    .message-label Message:
    .message-text= message_text

app/assets/stylesheets/messages.scss

#chatbox {
  width: 90%;
  margin: .5em auto;
}

#new-message-form-wrapper {
  width: 48%;
  padding: 1em;
  border: 1px solid #ccc;
}

#new-message-form-label-wrapper > label {
  font-weight: 700;
}

#new-message-form-buttons-wrapper {
  margin-top: .5em;
}

#new-message-submit-btn {
  margin-right: .3em;
}

#new-message-submit-btn,
#new-message-cancel-btn {
  width: 8em;
}

#new-messages-form-wrapper,
#display-messages-wrapper {
  margin-top: 1.5em;
}

#messages-column-left,
#messages-column-right {
  width: 48%;
}

.message-wrapper {
  width: 96%;
  margin: 1em auto;
  padding: .5em;
  border: 1px solid #ccc;
}

.message-attribution-label,
.message-label {
  font-weight: 700;
}

config/routes.rb

Rails.application.routes.draw do

  resources :messages, only: [:index]
  get  'messages/get_last_chat_id',     to: 'messages#get_last_chat_id'
  get  'messages/save_new_message',     to: 'messages#save_new_message'
  get  'messages/display_all_messages', to: 'messages#display_all_messages'

end