I am going through a software engineering book called Modern Front-End Development for Rails, Second Edition by Noel Rappin. In one section of the book, I am trying to create a search form with turbo and stimulus so that when I type in input, it will automatically submit through javascript and the page that I am on will show the desired search result that I want. I've followed the book and double checked my code but it seems that the search bar is not working for one reason or another.
What makes this feature difficult to troubleshoot is that I do not see any errors in the rails terminal, no output in the browser console tab, no error logging, etc. It simply does not do a thing when I type. Below is the following pieces of code that is supposed to make the search work.
app/views/schedules/_search_form.html.erb
<%= turbo_frame_tag("search-form") do%>
<div data-controller="search">
<%= form_with(
url: concerts_url,
method: "get",
data: {
"turbo-frame": "search-results",
"search-target": "form",
action: "input->search#submit"
}
) do%>
<div class="flex justify-center">
<div class="w-4/5">
<%= text_field_tag(
"query", "",
placeholder: "Search concerts",
type: "search",
id: "search_query",
"data-search-target": "input",
class: "w-full px-3 py-2
border border-gray-400 rounded-lg"
) %>
</div>
</div>
<% end %>
<%= turbo_frame_tag("search-results") %>
</div>
<%end%>
app/javascript/controllers/search_controller.ts
import { Controller } from "#hotwired/stimulus"
import "form-request-submit-polyfill"
export default class SearchController extends Controller {
static targets = ["form", "input"]
formTarget: HTMLFormElement
submit(): void {
this.formTarget.requestSubmit()
}
}
app/javascript/controllers/index.js
import { application } from "./application"
import SearchController from "./search_controller.ts"
application.register("search", SearchController)
app/controllers/concerts_controller.rb
def index
#query = params[:query]
#concerts = Concert.all
end
app/views/concerts/_search_result.html.erb
<article class="my-6 max-w-screen-lg">
<div>
<%= concert.start_time.by_example("Jan 2 #3:04PM") %>
<div class="font-bold text-xl">
<%= link_to(concert, data: {"turbo-frame": "_top"}) do %>
<%= highlight(concert.name, /#{query}/i) %>
<%end %>
<div class="float-right">
<% if concert.sold_out? %>
Sold out
<% else %>
<%= pluralize(concert.unsold_ticket_count, "Tickets") %>
Remaining
<%end%>
</div>
</div>
</div>
<div>
<%= highlight(concert.bands.map(&:name).join(", "), /#{query}/i) %>
</div>
<div><%= concert.genre_tags.split(",").to_sentence %></div>
<div><%= concert.venue.name %></div>
</article>
These 5 pieces should work together and should highlight on my page the search term that I want. But like I mentioned before, nothing is being submitted, going to the controller, or updating. It simply doesn't do anything with no errors reported. How can I troubleshoot this feature effectively? If there is something glaring that I am not seeing I would appreciate the help.
Related
I've been testing Hotwire, using the demo that was put up by DHH. I have a default rails 6 setup & I know that it re-creates the javascript folder structure from the rails 5 < asset pipeline. The issue I am having is the form will not reset the text field after submission - despite setting up the stimulus controller for using this particular action. How can I reset the form for hotwire after the form is submitted by the user? My code is below
new.html.erb
<%= turbo_frame_tag 'new_conversation_comment', target: '_top' do %>
<%= form_with(model: [#conversation, #conversation_comment],
data: { controller: "reset_form", action: 'turbo:submit-end->reset_form#reset' }, html: {class: 'form-inline'}) do |form| %>
<%= form.submit 'Post', class: 'btn btn-primary mb-2', style: 'float: right;', 'data-reset_form-target': 'button' %>
<div style="overflow: hidden; padding-right: .5em;">
<%= form.text_field :content, class: 'form-control' %>
</div>
<% end %>
_conversation_comment.html.erb
<div class="p-1">
<%= conversation_comment.content %>
</div>
show.html.erb
<div class="p-2">
<%= turbo_stream_from #conversation %>
<%= turbo_frame_tag 'conversation' do %>
....
<% end %>
<div class="conversation-comments-container" id="conversation_comments">
<%= render #conversation.conversation_comments %>
</div>
<hr>
<%= turbo_frame_tag 'new_conversation_comment', src: new_conversation_conversation_comment_path(#conversation), target: :_top %>
</div>
assets/javascripts/controllers/reset_form_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
reset() {
this.element.reset()
}
}
Disclaimer: I'm using Webpack 4.0
Depending on setup underscore between controller name can be an issue. Replacing it with dash worked in my case.
turbo:submit-end->reset-form#reset
Disclaimer: I'm using Webpack 4.0
I ran into the same thing, also using Webpack.
The solution for me was:
Change the underscores to dashes, like Canbey said.
Move the stimulus controller from app/assets/javascripts/controllers/ to app/javascripts/controllers/ (somewhat per the stimulus docs so that webpack imported it properly — my application.js file was importing controllers from that directory rather than app/assets/javascripts.
A half SJR, half Hotwire approach would be this:
Working example here: https://rails-react-bootstrap.herokuapp.com/rooms
new.html.erb
<%= turbo_frame_tag 'new_note' do %>
<%= form_with model: [#room, #note] do |form| %>
<!-- Allow body validation error without message -->
<div class="form-group">
<%= form.rich_text_area :body, class: 'form-control comments', id: 'bodyText' %>
</div><!-- .form-group -->
<div class="form-group d-flex justify-content-center">
<%= form.submit 'Add Note', class: 'btn btn-lg btn-tayberry btn-block' %>
</div><!-- .form-group -->
<% end %>
<% end %>
create_js.erb
// clear note textarea after submit
var input = document.getElementById("bodyText");
input.value = '';
notes_controller
def create
#note = #room.notes.create!(note_params)
# #note.user = current_user
respond_to do |format|
if #note.save
format.turbo_stream
format.html { redirect_to #room }
format.js
else
format.html { render action: 'new' }
end
end
end
If you still need an answer, here is how you can achieve what you need:
In your controller respond to requests like that:
format.html { redirect_to conversations_url }
Note: redirect to the page where new_conversation_comment frame is defined (i.e same page from where you are submitting form). Redirect will happen, but Hotwire will substitute form for you with a new one and append comment where you specified.
It is a bit magic.
I suggest you to check this video:
https://gorails.com/episodes/hotwire-rails?autoplay=1
I am trying to apply AJAX on a toggle "Like" button. This setup updates the button status without refreshing the entire page, but the former <%= pluralize(#company.get_likes.size, "people liked it") %> is not removed ("0" people liked it, is added to the new "1" people liked it")
How can remove the previous "#company.get_likes.size" people liked it sentence?
How can I improve the code?
controller:
def toggle_favorite(company)
if user_signed_in?
if current_user.liked? company
link_to raw("<i class='fa fa-star'></i>"), unlike_company_path(company), remote: true, method: :put
else
link_to raw("<i class='far fa-star'></i>"), like_company_path(company), remote: true, method: :put
end
else
link_to 'sign in to like', new_user_session_path
end
end
route:
resources :companies do
member do
put "like" => "companies#like"
put "unlike", to: "companies#unlike"
end
end
view:
<div class="text-center">
<div id="<%= dom_id(company) %>">
<%= pluralize(company.get_likes.size, "people liked it")) %>
<%= toggle_favorite(company) %>
</div>
</div>
like.js.erb
let starIcon = document.querySelector("#company_<%= #company.id %>").querySelector('.fa-star')
starIcon.parentElement.outerHTML = "<%= pluralize(#company.get_likes.size, "people liked it") %> <%= escape_javascript(toggle_favorite(#company)) %>"
unlike.js.erb
let farstarIcon = document.querySelector("#company_<%= #company.id %>").querySelector('.fa-star')
farstarIcon.parentElement.outerHTML = " <%= pluralize(#company.get_likes.size, t('views.company.bookmark.count_bookmark')) %> <%= escape_javascript(toggle_favorite(#company)) %>"
You can achieve this from the application_helper.rb file:
For this particular case, I'd rather use .count instead of .size (It's up to you). For more info, you can check count vs length vs size in a collection Stack Overflow question.
1- Define method:
def pluralize_get_likes(get_likes)
if get_likes.count > 0
"#{get_likes.count} people liked this"
else
"Here the text you want to be displayed (e.g.: Be the first one to vote this)"
end
end
2- Apply method:
Replace "pluralize" with "pluralize_get_likes" in like.js.erb and unlike.js.erb files.
like.js.erb:
let starIcon = document.querySelector("#company_<%= #company.id %>").querySelector('.fa-star')
starIcon.parentElement.outerHTML = "<%= pluralize_get_likes(#company.get_likes) %> <%= escape_javascript(toggle_favorite(#company)) %>"
The same applies to the view:
<div class="text-center">
<div id="<%= dom_id(company) %>">
<%= pluralize_get_likes(company.get_likes) %>
<%= toggle_favorite(company) %>
</div>
</div>
That should do it.
I'm trying to create a form that will generate copies of a nested form depending on a user's selection. I have 3 models: Visualizations, which has_many Rows, which has_many Panes.
I want to be able to allow the user to click a small dropdown menu that will generate a row and then select 1, 2, or 3 panes. Depending on their selection, this will generate the form of their choice from a render.
here is an example where a user chose 3 panes (3 upload boxes for images) with just styled divs in place of the form
For starters. I know I would need to change up my controller a bit, but I'm not sure how to do that with a nested form since in every example I've seen so far, you need to predetermine how many times you would "build" a nested model form (not sure if that is the correct terminology). I'm not sure how to make that part dynamic.
Here is what I mean:
//visualization_controller.rb
def new
#visualization = Visualization.new
rows = #visualization.rows.build
panes = rows.panes.build
end
here is an example of the render that would be called if the usr wanted to generate a row with just 1 Pane:
<div class="medium-12 columns one_box_wrapper first" style="display:none">
<%= f.fields_for :rows, Row.new do |r| %>
<%= r.label :rowtitle %>
<%= r.text_field :rowtitle %>
<div class="nested" style="display:inline-block">
<%= r.fields_for :panes, Pane.new do |p| %>
<%= p.label :text %>
<%= p.text_field :text %>
<div class="mehv">
<img id="img_prev" width=300 height=300 src="#" alt="your image" class="img-thumbnail hidden"/> <br/>
<span class="meh">
Upload Pane Image<%= p.file_field :pane_photo, id: "pane_photo" %>
</span>
<%= p.hidden_field :pane_photo_cache %>
</div>
<% end %>
</ul>
</div>
<% end %>
</div>
Is this possible in rails? I feel like javascript would come into play but I don't think you are allowed to render server-side code through JS.
I feel a bit in over my head with this and any help would be greatly appreciated!
EDIT
I ended up using cocoon gem as it seems to have made my life a lot easier however.. I can't get the visualizations to all display on the index page! I'm not sure what the issue is.. The cover Title and Image show up but nothing else does!
//schema.rb
create_table "panes", force: :cascade do |t|
t.integer "row_id", null: false
t.text "text"
t.string "pane_photo"
end
create_table "rows", force: :cascade do |t|
t.integer "visualization_id", null: false
t.string "rowtitle"
end
create_table "visualizations", force: :cascade do |t|
t.string "title", null: false
t.string "cover_image"
end
//controller.rb
class VisualizationsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :authorize_user, except: [:index, :show]
helper_method :resource
def index
#visualizations = Visualization.all
#visualization = Visualization.new
end
def new
#visualization = Visualization.new
rows = #visualization.rows.build
panes = rows.panes.build
end
def create
#visualization = Visualization.new(visualization_params)
if #visualization.save
redirect_to edit_visualization_path(#visualization)
else
render :new
end
end
def edit
#visualizations = Visualization.all
#visualization = Visualization.find(params[:id])
end
protected
def visualization_params
params.require(:visualization).permit(:title, :cover_image, rows_attributes: [:visualization_id, :rowtitle, panes_attributes:[:text, :pane_photo]])
end
//index.html
<div class="visual_wrap text-center">
<ul class="no-bullet text-center">
<% #visualizations.each do |visualization| %>
<li class="text-center">
<div class="row visualization text-center">
<%= image_tag visualization.cover_image.to_s, id: "visualization_image", class: "output_image" %>
<p class="visualization_title"><%= visualization.title %></p>
<p>+</p>
</div>
<div class="placeholder">
<% visualization.rows.each do |row| %>
<%= row.rowtitle %>
<% row.panes.each do |p| %>
<%= p.text %>
<%= image_tag p.pane_photo.to_s, id: "pane_image", class: "output_image" %>
<% end %>
<% end %>
</div>
</li>
<% end %>
</ul>
</div>
//new.html.erb
<% if user_signed_in? %>
<% if current_user.admin? %>
<div class="try">
<div class="breadcrumb-wrapper">
<ul class="breadcrumbs">
<%= form_for #visualization, :html => {:multipart => true} do |f| %>
<li>Preview</li>
<li><div class="separating-bar">|</div>
<li><input type="submit" name="commit" value="Save" class="save button secondary"></li>
</ul>
</div>
<div class="cover_image text-center">
<div class="row pre_img_prev">
<div class="gray_bar"></div>
<div class="img_prev">
<img id="img_preview" src="#" alt="your image" class="img-thumbnail hidden text-center"/>
<input type="text" name="visualization[title]" id="visualization[title]" class="inputtext" />
<label for="visualization[title]" id="text_preview">+ Add Title</label>
<div class="trash-can_title">
<i class="fa fa-trash fa-3x" aria-hidden="true"></i>
</div>
</div>
<input type="file" name="visualization[cover_image]" id="visualization_cover_image" class="inputfile" />
<label for="visualization_cover_image">+ Add Cover Image</label>
<%= f.hidden_field :cover_image_cache %>
<div class="trash-can_cover">
<i class="fa fa-trash fa-3x" aria-hidden="true"></i>
</div>
</div>
</div>
<div class="row linkstest">
<div class="rows">
<%= f.fields_for :rows, Row.new do |task| %>
<%= render 'row_fields', f: task %>
<% end %>
<div class="links">
<%= link_to_add_association "Add Row", f, :rows %>
</div>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
You don't need really need JS except to make it more "snappy". I think what you are looking for are Nested Forms.
Take a look at RaisCast it's a bit outdated but it's a good foundation.
You might want to look into Cocoon gem to help you create the new fields dynamically.
Rails nested_form inside nested_form
Update
If you don't want to use Javascript at all, you need to make an extra request to the controller to tell it to initialize (build) the rows and panes for you so that it renders the empty fields in the view.
For example:
def new
#visualization = Visualization.new
num_rows = params[:num_rows]
num_rows.each { #visualization.rows.build }
end
You would also need to have the number of panes in each row, so the parameter would have to be inside the row attributes, e.g. params[:visualization][:rows][0][:num_panes], and using that parameter you initialize the panes in the row. It's probably best to put that logic in a Factory service.
However if you create the empty fields using JS you can save yourself from making that extra request and all that logic. This is because Rails will automatically initialize (build) the rows and panes if you give it the right attributes, such as:
{
visualization: {
rows_attributes: {
# These are the attributes from an existing record
0: {
id: 39,
panes_attributes: {
0: {
id: 992
}
}
},
# This is a new set of attributes for a new record
# passed by the fields generated by cocoon where xxx
# and yyy are random numbers
xxxx: {
panes_attributes: {
yyyy: {
}
}
}
}
}
}
Then all you need to do in your controller is:
def new
# visualization_params are the permitted attributes
#visualization = Visualization.new(visualization_params)
end
I'm new to programming and teaching myself Rails as I build an app.
I have a page with a list of jobs. Each job is rendered via a job partial which has a div which renders a status partial via a "Update Status" button with ajax.
The button works as expected and refreshes each job's status when clicked.
I am now trying to make the status div auto-refresh every x seconds.
After trying different methods, I've implemented a javascript which simply periodically refreshes the button (which I can later hide if I want). It was suggested here by #de_an777: Rails 3 - Periodic Refresh of DIV.
This seems to almost work for me.
The problem is that it doesn't refresh ALL jobs in the list. So if I have 2 jobs in the list, I see two refresh operations happening in the server log, but both of them refresh only the first job.
When inspecting the job id for each button element in the browser inspector, they all seem to have the correct id via job[:id].
I've been struggling with this for days, changing my routes and methods but cannot make it work. Hoping that someone can direct me in the right way...
Jobs_controller.rb:
def update
#job = Job.find(params[:id])
#some updating logic
respond_to do |format|
format.js
end
end
index.html.erb:
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>Job List</h1>
</section>
<section class="job_form">
<%= link_to "New Job", upload_path, class: "btn btn-lg btn-primary" %>
</section>
</aside>
</div>
<%= render #jobs %>
_job.html.rb:
<span class="jobs">
<p> Job ID: <%= job.id %></p>
<p> Job Name: <%= job.name != "" ? job.name : "Untitled" %>
<p> Source File: <%= job.source_file %></p>
<% #job = job %>
<div id="status_div_<%= #job.id %>">
<%= render :partial => 'jobs/status', locals: {job: job} %>
</div>
<%= button_to("Update Status", jobs_update_path(id: job.id), remote: true, method: :patch, id: "refresh_#{job[:id]}") %>
</span>
</li>
<script type="text/javascript">
function doSomething() {
$('#refresh_<%= job[:id] %>').click();
}
setInterval('doSomething()',10000);
</script>
routes:
get 'new_job' => 'jobs#new'
get 'upload' => 'jobs#upload'
patch "jobs/update", to: "jobs#update"
resources :jobs
update.js.erb:
$('#status_div_<%= #job.id %>').html("<%= j render :partial => 'status', object: #job, as: :job %>");
_status.html.erb:
<p> Status: <%= job[:status] %></p>
In my development environment, everything is okay.
editor work so great.
but when app run on heroku, the WYSIWYG editor Redactor display twice.
one default style Redactor editor, and one custom style Redactor editor.
I try to delete custom setup,
not working,
it display two, two default style editor,
ban turbolinks or modify config, it's still not working.
wherever I do, I got two editor on the page where use Redactor.
on Heroku the page look like:
<div class="redactor-box">
<ul class="redactor-toolbar" id="redactor-toolbar-2">
</ul>
<div class="redactor-editor">
</div>
<div class="redactor-box">
<ul class="redactor-toolbar" id="redactor-toolbar-3">
</ul>
<div class="redactor-editor">
</div>
</div>
</div>
In my development env, it look like:
<div class="redactor-box">
<ul class="redactor-toolbar" id="redactor-toolbar-2">
</ul>
<div class="redactor-editor">
</div>
</div>
this is my code.
#app/views/articles/new.html.erb
<div class="col-md-8 col-xs-8 col-xs-offset-2 col-md-offset-2">
<%= form_for([current_user, #article]) do |f| %>
<%= render 'shared/error_messages', var: #article %>
<%= f.label :title %>
<%= f.text_field :title, class: 'form-control', id: "article-title" %></br>
<%= f.label :content %>
<%= f.text_area :content, class: :redactor %></br>
<%= f.submit "Save", class: "btn btn-default btn-custom" %>
<% end %>
</div>
and this is my Redactor config.
#app/assets/javascripts/redactor-rails/config.js
window.init_redactor = function(){
var csrf_token = $('meta[name=csrf-token]').attr('content');
var csrf_param = $('meta[name=csrf-param]').attr('content');
var params;
if (csrf_param !== undefined && csrf_token !== undefined) {
params = csrf_param + "=" + encodeURIComponent(csrf_token);
}
$('.redactor').redactor({
"imageUpload":"/redactor_rails/pictures?" + params,
"imageGetJson":"/redactor_rails/pictures",
"fileUpload":"/redactor_rails/documents?" + params,
"fileGetJson":"/redactor_rails/documents",
"path":"/assets/redactor-rails",
"css":"style.css",
"minHeight" : 300
});
}
$(document).on( 'ready page:load', window.init_redactor );
rails '4.2.3'
redactor-rails '0.5.0'
Thanks for read.
update
when I delete all config file, not just change some setup,
I got right display.
but if I do that, I can't custom my editor.
finally, I use CSS solve the problem.
it's a stupid way,
The extra editor just be hidden, not disappear.
.fix-box {
margin-top: 110px;
.redactor-box {
#redactor-toolbar-2 {
display: none;
}
.redactor-box {
margin-top: -110px;
}
}
}
You should change
$(document).on( 'ready page:load', window.init_redactor );
to
$(document).ready(function(){window.init_redactor});
pageLoad may be called more than once in some circumstances, causing multiple Redactor instances in your case