I have created a chat app with rails 6 and I'm using actionable along with stimulus in order to broadcast the messages and notifications in realtime. The setup I have thus far works just fine!
Now I'm trying to add a user is typing feature as well, but I'm having trouble implementing it! The ideas is to create a typing_channel with a is_typing method in order to broadcast the event and then show the output data via the received method inside the stimulus controller.
With all the previous implementations I call the broadcast action after the creation of a message and then I show the data via the received method inside the stimulus controller.
But for the is_typing feature the precess is different since I will not be creating a message this time.
So my main issue here is, how to trigger the broadcast event when I start typing inside the form??
class TypingChannel < ApplicationCable::Channel
def subscribed
stop_all_streams
stream_from "typing_channel"
end
def unsubscribed
stop_all_streams
end
def is_typing(data)
ActionCable.server.broadcast "typing_channel", typing: data['typing'], user: current_user
end
end
Th stimulus controller:
import { Controller } from "stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static targets = ['status'];
connect() {
this.channel = consumer.subscriptions.create('TypingChannel', {
connected: this._connected.bind(this),
disconnected: this._disconnected.bind(this),
received: this._received.bind(this),
});
}
disconnect() {
consumer.subscriptions.remove(this.subscription)
}
_connected() {
}
_disconnected() {
}
_received() {
alert("receiving data")
}
}
And this is the form that I create the messages and I want to show when a user is typing feature:
<div data-controller="typing" data-target="typing.status" id="user_is_typing"></div>
<%= simple_form_for #message, url: consumer_create_conversation_message_path, html: { data: { action: "ajax:success->channel#clearMessage" }, method: :post, remote: true, id: :chat_form } do |f| %>
<%= f.input :body, label: false, placeholder: "Type your message here", :input_html => { id:"text_send", :rows => 2, class: "text_area_none" } %>
<%= button_tag type: 'submit', id:"button", class: "btn btn-primary btn-m fa-pull-right" do %><i class="fas fa-paper-plane"></i><% end %>
<% end %>
Related
In my Home controller, I basically have a variable inside of def index: target = 13.5. I use this to do a lot of calculations which I send to my index.html view. My aim is to allow the user to input a value that directly updates this variable. I am using the below Stimulus controller, but want it so that if someone types in '14', the variable becomes target = 14, which my controller then updates the calculation and is updated to the user in real time.
In ajax_controller.js:
import { Controller } from "#hotwired/stimulus"
// Connects to data-controller="ajax"
export default class extends Controller {
static targets = ['form', 'output']
connect() {
console.log(this.formTarget);
console.log(this.outputTarget);
}
send(event) {
event.preventDefault();
fetch(this.formTarget.action, {
method: 'POST',
headers: { "Accept": "application/json" },
body: new FormData(this.formTarget)
})
.then(response => response.json())
.then(data => {
this.outputTarget.innerHTML = data.body.number;
});
}
}
In index.html.erb:
<div class="input-form" data-controller="ajax">
<%= form_with url: receive_path, data: { ajax_target: 'form', action: 'submit->ajax#send' } do |form| %>
<%= form.text_field :number %>
<%= form.submit "Submit" %>
<% end %>
<p data-ajax-target="output"></p>
</div>
In home_controller.rb:
def index
target = 13.5
#DO LOTS OF CALULATIONS HERE THAT EVENTUALLY OUTPUT TO A VIEW. I WOULD LIKE THE ABOVE TARGET UPDATED BY INPUT THAT THEN UPDATES THE CALCULATIONS
end
def receive
render json: { body: params }
end
Routes.rb:
root 'home#index'
post 'receive', to: 'home#receive'
I have OrdersController show action (with order_info partial), which displays current status of the order ("paid", "canceled", etc.).
meanwhile, I have callback action order_callback, which executes update action and changes status of the order in the database when it receives the callback from a payment processor.
What I want to achieve is to update show action in real-time to capture changes in order status (e.g. order paid successfully).
I tried to use unobtrusive javascript, but did not succeed.
update.js.erb
$("#order").html("<%= escape_javascript(render 'order_info') %>")
show.html.erb
<div id="order">
<%= render 'order_info' %>
</div>
orders_controller.rb
def update
if #order.update_attributes(order_params)
flash[:success] = "Order updated."
redirect_to #order
else
render 'edit'
end
end
api/orders_controller.rb
def order_callback
signature = request.headers['X-Signature']
request_uri = URI(env['REQUEST_URI']).request_uri rescue env['REQUEST_URI']
if $processor.callback_valid?(signature, request_uri)
#order = Order.find(params["id"])
#order.update_attributes(status: params["status"])
render status: :ok,
json: { success: true,
info: "Successfully updated order." }
else
render status: :unprocessable_entity,
json: { success: false }
end
end
I am using rails 4.2.2 with turbolinks enabled.
I was able to resolve it with javascript polling. The critical line was to explicitly say which .js partial to render in respond_to block
show.js.erb
$("#order").html("<%= j render 'orders/order_info', locals: {order: #order} %>");
OrderPoller.poll();
orders_controller.rb
def show
#order = Order.find(params[:id])
respond_to do |format|
format.js { render "orders/show.js.erb" }
format.html
end
end
orders.coffee
#OrderPoller =
poll: ->
setInterval #request, 5000
request: ->
$.get($('#order').data('url'))
jQuery ->
if $('#order').length > 0
OrderPoller.poll()
show.html.erb
<%= content_tag :div, id: "order", data: { url: order_path(#order) } do %>
<%= render 'order_info' %>
<% end %>
Using simple_form, I'm trying to get the entry of an id input (team_id) to autofill a couple of the other inputs (team_city and team_name) in a form creating a new team_game. I've gone through every implementation I can find, but to no avail (my most recent reference: Jquery ajax and controller method). This one is like quicksand for me, the more I struggle with, the farther I get pulled down. My code is below. Currently, its throwing the AJAX error in the javascript. I'm also getting this in the console:
Started GET "/team_games/populate_team_city_and_name?team_id=1" for ::1 at 2016-02-12 13:28:50 -0800
Processing by TeamGamesController#show as JSON
Parameters: {"team_id"=>"1", "id"=>"populate_team_city_and_name"}
TeamGame Load (0.3ms) SELECT "team_games".* FROM "team_games" WHERE "team_games"."id" = $1 LIMIT 1 [["id", 0]]
Completed 404 Not Found in 2ms (ActiveRecord: 0.3ms)
ActiveRecord::RecordNotFound - Couldn't find TeamGame with 'id'=populate_team_city_and_name:
So, in addition to my AJAX issue, I appear to be doing something wrong with my routing (used this as a guide: How to add a custom action to the controller in Rails 3).
Using Ruby 2.3.0 and Rails 4.2.5. Any help would be greatly appreciated.
models/team_game.rb
class TeamGame < ActiveRecord::Base
attr_accessor :team_city
attr_accessor :team_name
attr_accessor :opposing_team_city
attr_accessor :opposing_team_name
belongs_to :team, class_name: 'Team',
inverse_of: :team_games
end
models/team.rb
class Team < ActiveRecord::Base
has_many :team_games, class_name: 'TeamGame',
inverse_of: :team
end
view/team_games/_form.html.haml
= simple_form_for #team_game do |f|
= f.input :team_id, input_html: { id: 'team-id' }
= f.input :team_city, input_html: { id: 'team-city' }
= f.input :team_name, input_html: { id: 'team-name' }
controllers/team_games_controller.rb
def populate_team_city_and_name(team_id)
#team = Team.where(id: team_id)
respond_to do |format|
format.json { render json: #team }
end
end
private
def team_game_params
params.require(:team_game).
permit(:team_id,
:team_city,
:team_name,
:is_home_team,
:opposing_team_id,
:opposing_team_city,
:opposing_team_name,
:stadium_name
:game_date,
:game_time)
end
assets/javascripts/team_games.js
$(document).ready(function() {
$('#team-id').change(function(e) {
e.preventDefault();
var teamId = $('#team-id').val();
request = void 0;
request = $.ajax({
url: 'populate_team_city_and_name',
type: 'GET',
dataType: 'json',
data: { team_id: teamId }
});
request.done(function(data, textStatus, jqXHR) {
if (data.length > 0) {
$('#team-city').val(data.city);
$('#team-name').val(data.name);
} else {
$('#team-name').val('There is no team with entered Id');
}
console.log("Success!!")
});
request.error(function(jqXHR, textStatus, errorThrown) {
console.log("AJAX Error: #{textStatus}");
});
});
});
config/routes.rb
resources :team_games do
member do
get :populate_team_city_and_name
end
end
UPDATE: WORKING
Following are the changes I made to get the autofill to work. They may not all be necessary, but it works, so that's something. Thanks to Jeff F. for heading me down the right road.
view/team_games/_form.html.haml (revised)
= simple_form_for #team_game do |f|
= f.input :team_id, input_html: { id: 'team-id' }
#no-team-with-id-msg There is no team with entered id
= f.input :team_city, input_html: { id: 'team-city' }
= f.input :team_name, input_html: { id: 'team-name' }
controllers/team_games_controller.rb (revised)
def populate_team_city_and_name
#team = Team.where(id: params[:team_id])
respond_to do |format|
format.json { render json: #team }
end
end
private
def team_game_params
params.require(:team_game).
permit(:team_id,
:team_city,
:team_name,
:is_home_team,
:opposing_team_id,
:opposing_team_city,
:opposing_team_name,
:stadium_name
:game_date,
:game_time)
end
assets/javascripts/team_games.js (revised)
$(document).ready(function() {
$("#no-team-with-id-msg").hide();
$('#team-id').change(function(e) {
e.preventDefault();
var teamId = $('#team-id').val();
request = void 0;
request = $.ajax({
url: '/team_games/populate_team_city_and_name?team_id=' + teamId,
type: 'GET',
dataType: 'json'
});
request.done(function(data, textStatus, jqXHR) {
if (data.length > 0) {
$("#no-team-with-id-msg").hide();
$('#team-city').val(data[0].city);
$('#team-name').val(data[0].name);
} else {
$("#no-team-with-id-msg").show();
$('#team-city').val('');
$('#team-name').val('');
}
});
request.error(function(jqXHR, textStatus, errorThrown) {
console.log("AJAX Error: #{textStatus}");
});
});
});
config/routes.rb (revised)
get 'team_games/populate_team_city_and_name',
to: 'team_games#populate_team_city_and_name',
as: 'populate_team_city_and_name',
defaults: { format: 'json' }
resources :team_games
resources :teams
Part of my routing problem appears to have been that I previously had the 'get' route after the resources routes, which I'm guessing is why the javascript 'GET' was routing to the team_games#show action.
I also changed the handling of the message 'There is no team with entered id' since it seemed inappropriate for it to appear in an input.
You're using the url for that route incorrectly. Try changing your ajax to:
request = $.ajax({
url: 'team_games/' + teamId + '/populate_team_city_and_name',
type: 'GET',
dataType: 'json'
});
Then in your controller:
#team = Team.where(id: params[:id])
I edited the answer above a bit, but it was actually a little too far from correct, so I'm answering anew. Upvoted as it's on the right track, though.
Add a line to your form like:
= simple_form_for #team_game do |f|
= f.input :id, as: hidden, :input_html => { id: 'team-game-id', :value => #team_game.id }
= f.input :team_id, input_html: { id: 'team-id' }
= f.input :team_city, input_html: { id: 'team-city' }
= f.input :team_name, input_html: { id: 'team-name' }
Then in your JS:
var teamId = $('#team-id').val();
var teamGameId = $("#team-game-id").val();
{ ... }
request = $.ajax({
url: '/team_games/' + teamGameId + '/populate_team_city_and_name?team_id=' + teamId,
type: 'GET'
});
The reason for the query parameter is because you can't specify the teamId in the URL unless you make a route that allows you to. The membership is on the TeamGame model, not the team. Alternatively, you could specify a route like:
get '/team_games/:id/populate_team_city_and_name/:team_id' => 'team_games#populate_team_city_and_name'
But as-is, you don't have a matching route. However, the query parameter will show up in params.
Then in your controller:
#team = Team.find params[:team_id]
You may want to also ensure that this Team not only exists, but also that Team.team_games includes the TeamGame in question. But I'll leave that exercise to you!
I am trying to upload files using a Rails form where the remote is set to true. I'm using Rails 4.1.1. Let's say that my model is a Message, and it is using JavaScript so that the user could easily send multiple messages without reloading the page. The form is set like this:
<%= form_for #message, url: {action: "create"}, html: {:class => "message-form", multipart: true}, remote: true do |f| %>
The user can upload images with the Message, if they wish to do so. MessageImage acts as a nested attribute in the form, and is declared like this (http://railscasts.com/episodes/196-nested-model-form-revised way):
<%= f.fields_for :message_images do |builder| %>
<%= render 'message_image_fields', f: builder %>
<%= link_to_add_fields "Add an image", f, :message_images %>
<% end %>
On my controller the action is roughly like this:
if #message.save
flash.now[:success] = "Message sent"
else
flash.now[:alert] = "Error sending the message"
end
respond_to do |format|
format.html { render 'new' }
format.js { render 'new' }
end
Now, this works perfectly as long as the user doesn't send any images, but if they do, it uses format.html instead of format.js. Removing the format.html gives ActionController::UnknownFormat-exception.
Now, this obviously has to do with the fact that you can't submit files with remote set to true. I tried searching a bit, and found this gem https://github.com/JangoSteve/remotipart , which seems to be exactly what I'm looking for. I installed it following the instructions, but for some reason it still doesn't work and gives ActionController::UnknownFormat-exception if I remove the format.html. However, I couldn't find any example of it involving nested attributes. Are there any alternatives for this gem or any other way to fix this, or should I just set that it renders HTML if the user submits files?
JQuery
I don't know how to get the nested model aspect of this, but we've done file uploading with JQuery / asynchronicity before here (register for account, log into profile):
We used the jquery-file-upload gem - basically allowing you to pass the files through Ajax to your controller backend. To give you a clear idea of how we did this:
--
Code
#app/assets/javascripts/application.js
$('#avatar').fileupload({
url: '/profile/' + $(this).attr('data_id'),
dataType: 'json',
type: 'post',
add: function (e, data) {
$(this).avatar_loading('avatar_loading');
data.submit();
},
success: function (data, status) {;
$("#avatar_img").fadeOut('fast', function() {
$(this).attr("src", data.avatar_url).fadeIn('fast', function(){
$(this).avatar_loading('avatar_loading');
});
});
}
});
#app/views/users/index.html.erb
<%= form_for :upload, :html => {:multipart => true, :id => "avatar"}, :method => :put, url: profile_path(current_user.id), "data_id" => current_user.id do |f| %>
<div class="btn btn-success fileinput-button avatar" id="avatar_container">
<%= f.file_field :avatar, :title => "Upload New" %>
<%= image_tag(#user.profile.avatar.url, :width=> '100%', :id => "avatar_img", :alt => name?(#user)) %>
</div>
<% end %>
#app/controllers/profile_controller.rb
Class ProfileController < ApplicationController
def update
def update
#profile = User.find(current_user.id)
#profile.profile.update(upload_params)
respond_to do |format|
format.html { render :nothing => true }
format.js { render :partial => 'profiles/update.js' }
format.json {
render :json => #profile.profile.as_json(:only => [:id, :avatar], :methods => [:avatar_url])
}
end
def upload_params
params.require(:upload).permit(:avatar, :public, :description)
end
end
end
--
Implementation
For your implementation, I would recommend firstly creating the message, and then getting the user to append some images to it in another action
After you've got that working, you could get it to work as one form
I've got a form that should render error messages but I'm not sure how to render the error messages since my form is being displayed with Javascript after clicking a button. I'm using Devise for the registration which is working just not displaying the error messages. I'm using Rails 4.
This is the code that hides the register button after it's clicked and shows the form:
<script type="text/javascript">
$(function() {
$('#prelaunchbutton').click(function() {
$('#prelaunchregistrationbutton').fadeToggle('slow');
$('.prelaunchhide').fadeToggle('slow');
return false;
});
});
</script>
Here is the Devise registration form (Shortened for brevity):
<div class="prelaunch_preregisterbutton" id="prelaunchregistrationbutton">
<%= link_to image_tag("pre-register.png", :border=>0), '#', id:'prelaunchbutton' %>
</div>
<div class="span10 offset1 prelaunchhide">
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :class => "form-inline") do |f| %>
...
<% end %>
</div>
Here is the Devise registration controller:
class RegistrationsController < Devise::RegistrationsController
before_filter :authenticate_user!
respond_to :html, :json
def new
end
def create
#user = User.new(user_params)
#user.build_store(store_params)
if #user.save
UserMailer.preregister_confirmation(#user).deliver
sign_in #user
redirect_to prelaunch_gear_path, :flash => {:success => "Welcome"}
else
render root_path
end
end
...
protected
def after_update_path_for(resource)
user_path(resource)
end
private
def user_params
params.require(:user).permit(:firstname, :lastname, :email, :password)
end
def store_params
params.require(:store).permit(:storename)
end
end
You might look into using toastr for displaying javascript notifications.
Here's an SO question I answered before that may help set it up: Rails 4 - Toaster notifications rather than flash notifications -- you would just need to make your flash notification be flash.now[:toastr] and then be sure to call the initializer for that in the JS returned by the AJAX request.