Is it possible to render a javascript file inside a js.erb file? I have a modal that's being render in my new.js.haml file. However I have some javascript code that needs to run only when that modal gets called.
new.js.haml
$('.modal-content').html("#{ j(render partial: 'new') }");
I want to be able to call this file or render this file when new.js.haml is called upload.js
$ ->
files = [];
$("#upload_button").click ->
$("#file_input").trigger "click";
$("#file_input").change (event) ->
file = event.target.files[0];
files.push(file);
uploadfile files.length - 1, file;
uploadfile = (index, file) ->
formData = new FormData();
formData.append "file", file;
fileitem = "
<li class='file-item success'>
<div class='active'>
<span class='remove'>×</span>
<span>#{file.name}</span>
</div>
<div class='lds-ellipsis'><div></div><div></div><div></div><div></div></div>
</li>
";
$('#filelist').append fileitem;
setRemove();
setRemove = ->
$(".remove").unbind();
$(".remove").click ->
index = $(#).parent().parent().index();
files.splice index, 1;
$('.file-item').eq(index).remove();
Here is my form file that gets rendered but the upload.js file doesn't get called because it's already loaded on the main page. When the the new form button gets clicked it renders this form in a modal.
- #ticket.errors.full_messages.each do |message|
.error= message
= render 'layouts/standard_flash'
%ZenTicketForm{ title: 'Submit Help Request', :multipart => true, data: { remote: true } }
.col-xs-12.col-sm-12.col-md-12.col-lg-12{style: 'padding:0;'}
.col-xs-12.col-sm-12.col-md-12.col-lg-6
%dl.dl-horizontal
%dt Type
%dd.show-more-container
%ZenSelect#ticket_type{options: ['Questions', 'Incidents', 'Problems', 'Tasks'],
class: 'select2',
placeholder: t('activerecord.placeholders.zen_support/ticket.ticket_type') }
.col-xs-12.col-sm-12.col-md-12.col-lg-12
%dl.dl-horizontal
%dt Subject
%dd.show-more-container
%input.form-control{ type: 'text',
name: 'zen_support_ticket[subject]',
placeholder: t('activerecord.placeholders.zen_support/ticket.subject') }
.col-xs-12.col-sm-12.col-md-12.col-lg-6
%dl.dl-horizontal
%dt Priority
%dd.show-more-container
%ZenSelect#priority{ options: ['Low', 'Normal', 'High', 'Urgent'],
data: { placeholder: "activerecord.placeholders.zen_support/ticket.priority" },
class: 'select2' }
.col-xs-12.col-sm-12.col-md-12.col-lg-12
%dl.dl-horizontal
%dt Comment
%dd.show-more-container
%ZenTextArea#comment{ placeholder: t('activerecord.placeholders.zen_support/ticket.comment'),
error: #zendesk_ticket.errors&.dig(:comment) }
%dl.dl-horizontal
.new-show-less
%button.fileUpload.btn.btn-default#upload_button{type: "button"}
Attach File
%input.upload#file_input{:name => "attachments[]", multiple: true, :type => "file"}
%ul#filelist
You can put the JS code directly inside the js.erb file and it'll only be executed when that partial is rendered.
See more:
https://guides.rubyonrails.org/working_with_javascript_in_rails.html
Related
I'm confused. I'm getting some inconsistent behaviour with a target in a stimulus controller.
Using StimulusJS via importmap pin "#hotwired/stimulus", to: "stimulus.min.js", preload: true
I have a basic form with stimulus controller.
<%= form_with model: #message, data: { controller: "message-form" } do |form| %>
<%= form.file_field :attachments, class: 'file-input', id: 'file-input', multiple: true, hidden: "hidden",
data: { message_form_target: "attachmentInput" } %>
<i class="fa-solid fa-paperclip fa-lg" data-action="click->message-form#openAttachments"></i>
<% end %>
import {Controller} from "#hotwired/stimulus"
export default class extends Controller {
static targets = ["messageInput", "sendBtn", "attachmentInput"]
connect() {
this.inputIsEmpty()
console.log(this.attachmentInputTarget) // Outputs HTML element
}
openAttachments(){
this.attachmentInputTarget.click()
this.attachmentInputTarget.removeAttribute("hidden")
setInterval(this.hideAttachments, 5000)
}
hideAttachments(){
let attachmentInput = document.querySelector('#file-input')
console.log(this.attachmentInputTarget) // Undefined
console.log(attachmentInput) // Outputs HTML element
console.log(document.getElementById('file-input') == this.attachmentInputTarget) // false
if (!document.getElementById('file-input').files[0]) {
attachmentInput.setAttribute("hidden", "hidden")
} else {
attachmentInput.removeAttribute("hidden")
}
}
}
So, attatchmentInputTarget in connect() acts as I would expect it to and outputs the HTML element, but when hideAttachments is called attatchmentInputTarget is undefined.
Okay, as I've written this all out I figured it out.
Because of the delayed call with setInterval, hideAttachments gets called outside of the stimulus controller as vanilla JS, so has no reference to the Target.
The button for file field keeps submitting the form when it should be opening a window to select files to upload. Does anyone have an idea why?
= render 'show'
.row
.col-12
%ZenForm{ path: zen_support_urls.ticket_path, id: #zendesk_ticket.id, method: :put, :multipart => true, data: { remote: true } }
%button.fileUpload.btn.btn-default#upload_button{data: {type: "button"}}
Attach File
%input.upload#file_input{:name => "attachments[]", multiple: true, :type => "file"}/
%ul#filelist
- #zendesk_ticket.comments.each do | comment |
= render 'files', comment: comment
%ZenTextArea#comment{ label: "Comments", placeholder: "Add any comments you think will help us address your issue.", error: #zendesk_ticket.errors&.dig(:comment) }
Here is my JavaScript in app.js:
//= require jquery
//= require bootstrap-sprockets
//= require select2
//= require ./base
//= require ./tickets
//= require_tree .
Here is my CoffeeScript for tickets:
$(document).on "ready page:load", () ->
# this method forwards clicks to the first <a> element in the row
$.activateRowClick = (selector) ->
$(selector).click (e) ->
if !$(e.target).is('a')
$(this).find('a').click()
$(selector).css('cursor', 'pointer')
$.activateRowClick('tr.clickable')
Here is my JavaScript for ticketform.js:
$(document).on "ready page:load", ->
$support = new ZenSupport.Form "zs-ticket-form"
$support.comment = $support.form.find('textarea')
$support.subject = $("#zs-ticket-form__subject")
$support.priority = $("#zs-ticket-form__priority")
# $support.actions = $support.form.find('.support-ticket__action-btns')
$support.fields =
inputs: [$support.comment]
select2: [
$support.subject,
$support.priority
]
$support.subject.select2
theme: "bootstrap"
placeholder: "Please select..."
width: "100%"
allowClear: true
tags: true
$support.priority.select2
theme: "bootstrap"
placeholder: "Please select..."
width: "100%"
allowClear: true
tags: false
# Provide the beforeSend
$support.beforeSend = ->
has_subject = $support.subject.val().length
has_comment = $support.comment.val().length
placeholder = $support.comment.attr('placeholder')
required_inputs = $support.form.find(".required:visible")
inputs_valid = true
$.each required_inputs, (index, element) ->
console.log $(element).val().length
if($(element).val().length == 0)
$(element).parent().addClass('has-error')
inputs_valid = false
else
$(element).parent().removeClass('has-error')
return
if has_subject and has_comment and has_comment isnt placeholder and inputs_valid
$support.showLoader false
return true
else
$support.comment.parent('.form-group').toggleClass 'has-error', !has_comment
$support.subject.parent('.form-group').toggleClass 'has-error', !has_subject
return false
Here is my code for uploads.js. This file is not getting called when tickets is being called so this code is currently not doing anything and I think that is the problem:
$ ->
files = [];
$("#upload_button").click ->
$("#file_input").trigger "click";
$("#file_input").change (event) ->
file = event.target.files[0];
files.push(file);
uploadfile files.length - 1, file;
uploadfile = (index, file) ->
formData = new FormData();
formData.append "file", file;
fileitem = "
<li class='file-item success'>
<div class='active'>
<span class='remove'>×</span>
<span>#{file.name}</span>
</div>
<div class='lds-ellipsis'><div></div><div></div><div></div><div></div></div>
</li>
";
$('#filelist').append fileitem;
setRemove();
setRemove = ->
$(".remove").unbind();
$(".remove").click ->
index = $(#).parent().parent().index();
files.splice index, 1;
$('.file-item').eq(index).remove();
Background
This is a crisp explanation of what I am trying to do:
I have a Rails form (am calling it a 'pre-search' form) which contains a dropdown
When the dropdown selection is changed, a search form is displayed to the user (the form allows the user to select some search criteria)
This form contains a "Run Search" button which is used to submit the form selections
When "Run Search" is clicked on the above form, a datatable is displayed right below the form
The Issue
And this is my issue:
When the search criteria in the above form is changed and "Run Search" is clicked again, I would like:
I. The datatable that was displayed earlier to be removed and
II. A new datatable to be displayed in the place where the old datatable was displayed.
But this does not happen. I see that additional datatables get added to the page instead of deleting the old one and replacing it everytime. So if I run search 4 times, I see 4 datatables in the page. I would like to see only 1 datatable how many ever times I run the search.
Code
This is my pre-search form with a dropdown:
<%= form_for :anything, remote: true, html: { id: 'pre_search_form' } do |f| %>
<div style="margin-left: 10px; margin-top: 10px">
<div class="col-lg-2">
<div class="form-group">
<label style="font-weight: 600;font-size:95%"><b>Category : </b> <%= select_tag(:search_category, options_for_select([['-Select-', ''], ['Option1', 'o1'], ['Option 2', 'o2'], ['Option 3', 'o3'], ['Option 4', 'o4']], :selected => 0)) %></label>
</div>
</div>
</div>
<!-- end of row 1 -->
</div>
<div id="dummy_div"> </div>
<div id="datatable_div"></div>
<% end %>
<script type='text/javascript'>
$(function() {
$(document).on("change", "#search_category", function(event){
$.ajax('show_search_form', {
type: 'GET',
dataType: 'script',
data: {
search_category: $("#search_category").val()
},
error: function(jqXHR, textStatus, errorThrown) {
return console.log("AJAX Error: " + textStatus);
}
});
});
});
</script>
This is the routes entry that gets called when the dropdown selection is changed in the above 'pre-search' form:
routes.rb
get "/show_search_form" => "search#show_search_form", :as => :show_search_form
This is the controller function that is executed due to the routes entry above:
search_controller.rb
class SearchController < ApplicationController
def show_search_form
render :partial => "search/show_search_form.js.erb"
end
This is the partial rendered by above controller function:
search/show_search_form.js.erb
<%= form_for :anything, remote: true, html: { id: 'search_form' } do |f| %>
<div class="row">
<b><%= label_tag "Entity" %><br/></b>
<%= select_tag(:search_entity, options_for_select([["Entity 1", 1], ["Entity 2", 2]]) %>
</div>
<%= button_tag 'Run Search', type: 'button', class: 'btn btn-primary btn-sm', id: "search_run_button" %>
<%end%>
<!-- Javascript code to handle events for dropdowns in the above form-->
<script type='text/javascript'>
/* Handle event when "Run Search" button is clicked */
$(document).on("click", "#search_run_button", function(event){
$.ajax('search_run', {
type: 'GET',
dataType: 'script',
data: {
search_entity: $("#search_entity").val()
},
error: function(jqXHR, textStatus, errorThrown) {
return console.log("AJAX Error: " + textStatus);
}
});
});
</script>
This is the routes entry for the button click in above form:
get "/search_run" => "search#run_search", :as => :search_run
This is the controller function called by the routes entry above:
def run_search
logger.info "SearchController run_search"
$search_entity = params[:search_entity]
data_table = SearchDatatable.new(view_context)
respond_to do |format|
format.html
format.json do
render json: {
"pageLength" => data_table.page_length,
"recordsTotal" => data_table.records_total,
"recordsFiltered" => data_table.records_filtered,
"data" => data_table.run_data
}
end
end
render :partial => "search/run_search.js.erb"
end
This is the partial called by the controller function above:
search/run_search.js.erb
$('#datatable_div').hide();
$('#datatable_div').hide().after('<%=escape_javascript(render(:partial => 'search/run_search')) %>');
As you can see, I am hiding datatable_div before rendering the datatable. But still, datatables get appended to the page instead of being overwritten.
How can I overwrite the datatable_div everytime so that irrespective of the number of searches done on the page, ONLY ONE datatable gets shown ALWAYS?
Please help!
This is the datatable rendering code just in case this is relevant:
search/run_search.html.erb
<div id="datatable_div">
<%= content_tag :table,
role: :search_datatable,
id: 'search_datatable',
style: 'height:500px; width: 100vw; overflow-y: auto;',
class: 'table table-striped table-bordered table-hover',
data: { url: search_datatable_path(format: :json)} do %>
<thead>
<tr>
<th>Column1</th>
<th>Column2</th>
</tr>
</thead>
<tbody>
</tbody>
<% end %>
</div>
<script>
$(document).ready(function(){
$("table[role='search_datatable']").each(function(){
var table = $(this).DataTable({
columnDefs: [
{ "orderable": true, "targets": 1},
],
aoColumns: [
{ mData: 'column1' },
{ mData: 'column2' }
],
autoWidth: true,
pageLength: 50,
processing: true,
serverSide: true,
sDom: 'ltipr',
ajax: $(this).data('url')
});
});
});
</script>
I tried these based on the comments from fellow stackoverflow users:
Approach 1 (based on #MikeHeft suggestion)
Approach 1. a)
$('#datatable_div').html('').html('<%=escape_javascript(render(:partial => 'search/dbr/run_search')) %>');
What happened with this approach?
This would not render the datatable even the very first time. It won't render any datatable - ever.
Approach 1. b)
$('#datatable_div').html('').after('<%=escape_javascript(render(:partial => 'search/dbr/run_search')) %>');
What happened after this approach?
This would render the datatable all right - but it still takes me back to my original issue - it won't replace the existing datatable the next time I run search. It just keeps adding more and more datatables to my search page.
I have a page that does a search, using javascript, and I want to take the list of users that it comes up with, and send that as a submit to the next page. What I have, is:
.search_client_users
= form_tag admin_clients_path, method: "get" , class: "search_form" do
= label_tag 'search_term', 'Old domain name:'
= text_field_tag 'search_term', nil, autocomplete: "off", size: "50"
.main_form.client_emails
= simple_form_for(:domainNameSwap, url: { action: "update" }, html: { method: :put }) do |f|
.input-row
= f.hidden_field :users, value: #clients
.submit-row
.row
.col-xs-5
.submit
= f.submit "Update domains", id: "submit", :class => "btn btn-primary submit"
.client_list
- content_for :javascript do
= javascript_include_tag 'admin/search_client_users'
[some of the formatting may not be quite right due to cut and paste, sorry]
The admin/search_client_users creates an #clients, I'm pretty sure, at least, with:
class App.ClientUserList
constructor: ->
#incrementalSearchAttempts = 0
search: (searchTerm, completeCallback) =>
handleResponseWithOrderAwareness = (attemptNumber, response) =>
if attemptNumber >= #incrementalSearchAttempts
completeCallback(response)
#incrementalSearchAttempts++
onComplete = _.partial(handleResponseWithOrderAwareness, #incrementalSearchAttempts)
$.get('/admin/manage_clients/client_list', { search_term: searchTerm }).complete(onComplete)
class App.Views.SearchClientUsers extends Backbone.View
events:
"keyup input[name='search_term']": "search",
"click .profile_attribute": "showClientUserProfile"
initialize: =>
#clientUserList = new App.ClientUserList()
search: =>
searchTerm = $('.search_form input[name=search_term]').val()
#clientUserList.search(searchTerm, #render)
showClientUserProfile: (event) =>
window.location = $(event.currentTarget).closest('tr').data('client-path')
render: (response) =>
#$el.find('.client_list').html(response.responseText)
$ ->
new App.Views.SearchClientUsers(el: $('.search_client_users')).search()
so, I'm trying to take the list of clients, and send it to the update method in the controller. However, due to when javascript and ruby take place, it doesn't seem to be working... is there a way to do this? or do I have to figure out how to do this in Ajax?
ETA: An alternative idea is, I suppose to just turn the initial text_field into a form, so that the text field is used both for the javascript, and THEN submitted to the form, and then the update can re-do the search... My dataset is small enough that doing the search twice is not a huge problem I suppose...
But I'm not quite sure exactly how to merge the two forms...
I have text block in main page, it located in _about.html.haml partial. When I click the link, text replaces by radar chart via AJAX (I use 'chart-js-rails' gem). Now the js function located in _my-chart.html.haml partial (which replaces first one) & I think it's not good enough.
So the question is: how to move this function code to assets folder in .coffee file or something like that?
P.S. I already try to move it to 'chart.js' file & use = javascript_include_tag 'chart.js' but it's not working graph doesn't render & I have empty canvas element.
views/welcome/_about.html.haml
%div{id: 'about'}
%h2 About
%p Some text
= link_to 'View chart', welcome_chart_path, remote: true
views/welcome/chart.js.haml
$('#about').replaceWith('#{j render(partial: 'welcome/my-chart')}');
views/welcome/_my-chart.html.haml
%div
%h2 Chart
%canvas{id: 'my-chart', width: '400', height: '400'}
:javascript
$(function() {
var ctx = $('#my-chart');
var data = {
labels: [a, b, c],
datasets: [
{data: [1, 2, 3]}
]
};
var my_chart = new Chart(ctx, {
type: 'radar',
data: data
})
});
controllers/welcome_controller.rb
class WelcomeController < ApplicationController
respond_to :js, only: [:chart]
...
def chart; end
end