How can I improve this dynamic partial load - ruby-on-rails

I am dynamically loading partials based on the occurrence drop down that is select like this:
<select name="scheduleevent[occurrence" id="occurrence" data-controller="remote-select"
data-remote-select-url-value="<%= scheduleevents_select_path %>"
data-remote-select-name-value="form[occurrence]"
data-action="change->remote-select#update" class="mb-4 w-48 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<%= Scheduleevent.occurrences.each do |occurrence| %>
<option value="<%= new_scheduleevent_path(range: occurrence.second) %>"><%= occurrence.first %></option>
<% end %>
</select>
<%= turbo_frame_tag :ranges do %>
<%= render partial: params[:range] ||= "0", locals: {form: form} %>
<% end %>
Stimulus controller:
import { Controller } from "#hotwired/stimulus"
export default class extends Controller {
static values = {
url: String,
name: String,
}
connect() {
}
update() {
const frame = document.getElementById('ranges');
frame.src = window.event.target.value;
}
}
This works fine to load the partial dynamically based on what value, 0, 1, etc. is passed. The only issue is that when I submit the controller the ocurrence value isn’t being saved which I need it to be. It is trying to write the URL /scheduleevents/new?range=1 into an integer field for occurrence. Is there a better way to rewrite this so I am not passing the URL through the select dropdown value so I can retain the selected value?

I ended up hard coding the URL in the stimulus controller:
frame.src = '/scheduleevents/new?range=' + window.event.target.value;

Related

Rails Hotwire - How to search within existing form

I have form that has a section that displays a series of checkboxes for what groups a user manages. I want to add a search box at the top of the checkboxes that allows them to search a name and have it filter the results. From there they can check whatever ones they need to and submit the main form.
The only way I have ever done this is by triggering a from submission. It is not working (not doing anything) I assume because it is within another form. Is there a way to get this to work like I have it below, or is there a better way to set up this filter?
cardrequest form:
<%= form_with(model: cardrequest, id: dom_id(cardrequest), class: "contents") do |form| %>
form fields for cardrequest ...
<div class="mb-4 space-y-4">
<%= form_with url: search_cardrequests_path, method: :get, data: { controller: "search-form", search_form_target: "form", turbo_frame: "cgs" } do |c| %>
<div>
<label for="add-guests">Select Cardholder Groups</label>
<div class="relative">
<%= c.search_field :search, data: { action: "input->search-form#search" }, value: params[:search] %>
</div>
<% end %>
</div>
<div data-controller="checkbox-select-all">
<label>
<input type="checkbox" data-checkbox-select-all-target="checkboxAll" />
<span class="btn_primary_small">Select All / Deselect All</span>
</label>
<%= turbo_frame_tag "cgs" do %>
<div class="space-y-3">
<%= form.collection_check_boxes :cardholdergroup_ids, current_user.cardholdergroups, :id, :friendlyname do |g| %>
<div class="flex items-center mr-4">
<%= g.check_box data: { checkbox_select_all_target: 'checkbox' } %>
<%= g.label %>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
Stimulus controller:
import { Controller } from "#hotwired/stimulus"
export default class extends Controller {
static targets = [ "form" ]
search() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.formTarget.requestSubmit()
}, 200)
}
}
cardrequests controller search method:
def search
cardholdergroups = current_user.cardholdergroups
#cardholdergroups = cardholdergroups.where("friendlyname LIKE ?", "%#{search}%") if params[:search].present?
render(partial: 'cgs', locals: { cardholdergroups: #cardholdergroups })
end
routes:
resources :cardrequests do
collection do
get 'search'
end
end

Modal for tr - different id index page

I have an index page with different software in a table.
I want to display additional information (in a modal) when we click on a tr.
Everything works but I have the information of a single software that appears in my modal and is the same for each tr.
I would like to display the information of each software in the corresponding modals.
My script :
$(".clickable").click(function(e) {
if (!$(e.target).hasClass('no-click')) {
$('#exampleModal').modal('show');
}
});
My view :
<% #nonpremium.each do |software| %>
<table>
<tr class="clickable">
<td class="hey1">
<%= link_to software_path(software), class: "no-click" do %>
<%= image_tag software.logo.url(:medium), class:"no-click"%>
<% end %>
</td>
<td class="hey3">
<h6><%= software.slogan %></h6>
<p><%= software.largeslogan %></p>
</td>
</tr>
</table>
<div class="modal fade bd-example-modal-lg" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<%= link_to software.software_name, software_path(software), class:"no-click" %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<% end %>
I tried something like this in my script, but it does not work ..
$(".clickable").click(function(e) {
if (!$(e.target).hasClass('no-click')) {
$('#exampleModal-<%= #software.id %>').modal('show');
}
});
Thx for you help
EDIT :
Controller/pages
class PagesController < ApplicationController
before_action :click, only: :index
def home
#softwares = Software.all.order(:cached_votes_up => :desc )
#premium = Software.includes(:user).where(users: { subscribed: true }).order("RANDOM()").limit(2)
#nonpremium = #softwares - #premium
end
def search
#softwares = Software.ransack(name_cont: params[:q]).result(distinct: true)
#categories = Category.ransack(name_cont: params[:q]).result(distinct: true)
respond_to do |format|
format.html {}
format.json {
#softwares = #softwares.limit(5)
#categories = #categories.limit(5)
}
end
end
end
EDIT 2 :
I have the desired result by putting in my table the information that I want to recover, then I put a "display: none".
<style>
td.test {
display:none;
}
</style>
<td class="test">
<span><%= software.software_description %></span>
<span><%= get_video_iframe(software.youtube_id) %></span>
</td>
Then I get the information from my table in my script:
$(".clickable").click(function(e) {
if (!$(e.target).hasClass('no-click')) {
var description = this.childNodes[3].innerHTML;
var name = this.childNodes[5].innerHTML;
document.getElementById("myModalName").innerHTML = name;
document.getElementById("myModalDesc").innerHTML = description;
$('#exampleModal').modal('show');
}
});
For then displayed in my modal:
...
<div class="modal-body" id="myModalName">
Name
</div>
<div class="modal-body" id="myModalDesc">
Description
</div>
...
There is probably better to do, but being a beginner is how I achieve the desired result.
However I would like to post videos in my modals.
Am I not going to overload my home page by hiding youtube videos with my display: none?
You won't be able to use erb in your script (unless this is in a script tag within your view, in which case your code should work) - better using a data attribute. For example, if you update your tr to the following:
<%= content_tag :tr, class: "clickable", data: { software_id: #software.id } do %>
# the rest of your code within the tr
<% end %>
# Equivalent of using:
# <tr class="clickable" data-software_id="<%= #software.id %>">
This attaches the relevant software_id to the tr in the DOM. You can then use the following in your script, accessing this new attribute:
$(".clickable").click(function(e) {
if (!$(e.target).hasClass('no-click')) {
$('#exampleModal-' + $(e.target).data('software_id')).modal('show');
}
});
And everything should work as desired.
Let me know how you get on or if you have any questions. Hope this helps!
Edit based on your comment:
That error you're seeing will come because #software is nil and you are, therefore, attempting to call id on nil.
It's a common error, and means to need to ensure #software is correctly set in your controller. If you post your controller code, I might be able to help with this.
Alternatively, you can 'safely' try the method, using #software&.id with newer versions of Ruby / Rails (or #software.try(:id) on older versions). However, that's not likely to be helpful here, more of a side note :)
Edit 2:
So, in your controller, you're not actually assigning the singular #software, rather the plural #softwares:
#softwares = Software.all.order(:cached_votes_up => :desc )
#premium = Software.includes(:user).where(users: { subscribed: true }).order("RANDOM()").limit(2)
#nonpremium = #softwares - #premium
Then, in your view, you're looping through #nonpremium using the local variable software. So, you can either:
assign #software in the controller if it should always use the same data in the modal
go back to the previous option, assigning a data attribute to the tr, which is what I'd recommend. Using that should work, although you'll need to alter the code to use software without the # to address the correct variable.
I.E.
<%= content_tag :tr, class: "clickable", data: { software_id: software.id } do %>
# the rest of your code within the tr
<% end %>
This ensures the script addresses the click based on the element clicked, and pulls the id directly from there, which is within the scope of your software loop.
That do it for ya?

How do I pass a specific string to my partial depending on the value of another variable also being passed to it?

I have the following partial (_card_brand.html.erb), which looks like this:
<div class="payment-card">
<i class="fa fa-cc-<%= brand.downcase %> payment-icon-big text-success"></i>
</div>
That renders HTML that looks like this:
<div class="payment-card">
<i class="fa fa-cc-visa payment-icon-big text-success"></i>
</div>
The above is rendered with this:
<%= render partial: "subscriptions/card_brand", locals: { brand: current_user.card_brand } %>
What I want to do is change the class text-success, to be either: text-warning, text-primary, text-danger, etc. depending on if the card has brand: visa, amex, mastercard, discovery, etc.
So:
Visa = Success
AMEX = Warning
Mastercard = Primary
Discovery = Danger
Any other cards would be other classes.
How do I elegantly represent that in my view that renders the partial?
You may create a helper and use it so that it will be easy to add new classes as well.
application_helper.rb
CARD_CLASS = {
'visa' => 'success',
'amex' => 'warning',
'mastercard' => 'primary',
'discovery' => 'danger'
}
def payment_class(type)
CARD_CLASS[type.downcase]
end
_card_brand.html.erb
<div class="payment-card">
<i class="fa fa-cc-<%= brand.downcase %> payment-icon-big text-text-<%= payment_class(brand.downcase) %>"></i>
</div>

Passing SelectList through ViewData to editor templates - not displayed properly

It's a bit complicated so bear with me.
Let's say I've got an example of a controller edit action defined like:
Node nd = _repo.getNode(id);
List<Category> ac = new List<Category>();
ac.AddRange(_repo.getCategories());
SelectList acl = new SelectList(ac, "category_id", "category_name", ac.Where(cat => cat.category_id == nd.category_id).First());
ViewData["category_id"] = acl;
return View(nd);
The view is templated like so:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Myapp.Models.Node>" %>
<% if (ViewData.TemplateInfo.TemplateDepth > 1)
{ %>
<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% }
else
{ %>
<table cellpadding="0" cellspacing="0" border="0">
<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(pm)))
{ %>
<% if (prop.HideSurroundingHtml)
{ %>
<%= Html.Editor(prop.PropertyName) %>
<% }
else
{ %>
<tr>
<td>
<div class="editor-label" style="text-align: right;">
<%= prop.IsRequired ? "*" : ""%>
<%= Html.Label(prop.PropertyName)%>
</div>
</td>
<td>
<div class="editor-field">
<% if (ViewData.Keys.Contains(prop.PropertyName))
{
if ((ViewData[prop.PropertyName]).GetType().Name == "SelectList")
{ %>
<%= Html.DropDownList(prop.PropertyName, (SelectList)ViewData[prop.PropertyName])%>
<% }
else
{ %>
<%= Html.Editor(prop.PropertyName)%>
<% } %>
<% }
else
{ %>
<%= Html.Editor(prop.PropertyName)%>
<% } %>
<%= Html.ValidationMessage(prop.PropertyName, "*")%>
</div>
</td>
</tr>
<% } %>
<% } %>
</table>
<% } %>
So, what the template does is display a dropdown list for every property for which ViewData["property_name"] exists.
I've also defined DisplayName metadata attributes for every property of my Node class.
Now, the dropdown lists display fine and are being populated correctly, but:
The first value from a list is always selected, even though the SelectList selected value predicate is fine and does set a proper value (in the debugger at least).
Html.Label in the template returns a proper DisplayName for properties, but when I define a ViewData for them so as to display the dropdown list, the label resets to normal property name (ie. category_id instead of Category).
What gives? Can you think of any "neater" way to accomplish this functionality?
Allright, no one's answering so there's my answer, maybe it comes in handy for someone:
Do not use your property names for ViewData keys! It messes up with the view model, so your views get confused and start to behave strangely.
Actually, best avoid the magic strings mess entirely, but if you insist, just use something like ex.: ViewData[prop.PropertyName+"_list"]. Your views are going to be fine now.

In a View, What is the most efficient way to hide HTML related to a null value from the Model?

This is likely a very simple question with a straightforward answer but I'm something of a newbie when it comes to ASP.NET (MVC).
I am returning an address (in pieces) from my model. Some of components are null. Is there a simple or fluent-like way to check for that null value without a lot of extra code to determine whether or not to display the associated surrounding HTML (not just the value)?
Example:
<% foreach (var item in Model)
{ %>
<h3>
<%= Html.ActionLink(item.name, "Details", new { id = item.ID})%></h3>
<div>
<%= Html.Encode(item.address) %><br />
<%= Html.Encode(item.city) %>,
<%= Html.Encode(item.state) %>
<%= Html.Encode(item.zip) %>
</div>
<% } %>
In the above example, if there is a null value for item.address, I want the <br/> tag to be hidden as well so that only the city, state zip string is displayed.
I'm looking for something more elegant than just putting a <% if () { %> conditional out there. Thanks.
You could write an extension method for HtmlHelper that checked to see if it was null or not, and would output nothing if it was, or field + <br /> if it wasn't.
public static string FieldLine(this HtmlHelper helper, object value, bool addBr)
{
if (value == null)
{
return string.Empty;
}
else if (addBr)
{
return helper.Encode(value) + "<br />";
}
else
{
return helper.Encode(value);
}
}
Remember to import the namespace of your extension class into your View aspx. For this example, if my namespace was "MvcApplication1.Extensions", I would use
<%# Import Namespace="MvcApplication1.Extensions" %>
at the top of my View. Then to use it, it would simply be:
<%= Html.FieldLine(item.address, true) %>
<%= Html.FieldLine(item.city, true) %>
etc.
I'm adding another answer based on what womp described above.. I'd make the helper a bit more generic than he did, and still honor the origional Encode as well...
public static string EncodeWithHtml(this HtmlHelper helper, object value, string html)
{
if (value == null)
{
return string.Empty;
}
else
{
return helper.Encode(value) + html;
}
}
This would allow you to do something like:
<%= Html.EncodeWithHtml(item.address, "<br />") %>
or
<%= Html.EncodeWithHtml(item.address, "<img src=\"images\home.gif\"><br />") %>
Assuming item.address is a string...
<%= Html.Encode(item.address) %>
<% if (!string.IsNullOrEmpty(item.address)) { %>
<br />
<% } %>
of course, this is typed out in the little comment box, so be wary of spelling, case, etc, etc.

Resources