I'm working on building my template and have the initial steps down. I'm on Rails 7.0.2.4, turbo-rails 1.1.1, Bootstrap 5.
In my layouts template's I have the following:
= javascript_include_tag "application", data:{turbo:{track: "reload"}}, defer: true
Which links to my app/javascript/application.js, and the relevant parts here are:
import * as bootstrap from "bootstrap"
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
The second part above enabled Bootstraps tooltips.
Back in my template, I break it down into three Turbo Frames:
%turbo-frame{id: "main_frame", data:{turbo:{action: "advance"}}}
.container-fluid
.row.flex-nowrap
.col-auto.col-md-3.col-xl-2.px-sm-2.px-0.bg-dark
%turbo-frame{id: "sidebar", data:{turbo:{action: "advance"}}}
.col
%turbo-frame{id: "content", data:{turbo:{action: "advance"}}}
= yield
First, there is the main_frame, which is then divided into a Sidebar and a Content frame. Clicking on links in the sidebar, which have date-turbo-frame: "content" attributes, swaps out the Content frame with that necessary content.
Now, on most full Content frames, the top of that frame has a navigation header which has a button on the left and right sides and text in the middle:
%turbo-frame{id: "content", data:{turbo:{action: "advance"}}}
.row
.d-flex
.p-2
%a{href: path, data:{turbo:{frame: "content"},
bs:{toggle: "tooltip", placement: "bottom",
title: "Back"}},
aria:{label: "Back"}}
%i.bi.bi-arrow-left-circle-fill{aria:{hidden: "true"}}
.flex-grow-1
// Some Text
.p-2
%a{href: new_path, data:{turbo:{frame: "new"},
bs:{toggle: "tooltip", placement: "bottom",
title: "New"}},
aria:{label: "New"}}
%i.bi.bi-plus{aria:{hidden: "true"}}
When you navigate to this replaced Content, the tooltips do not work. However, when you do a FULL page reload via the browser, the tooltips are suddenly working because the code is in the head.
Is there a way I can get the frames to load the necessary Javascript?
Basically with Turbo your JS is added into the head and not modified across pages loads.
So when you call :
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
Basically it will search the DOM for requested DOM nodes at the exact time your JS is appended to the application. And if your app refreshes a turbo Frame, then it won't be played. Please see this thread I replied recently : How to load page specific custom Javascript functions in a Rails 7 app with Turbo, ImportMaps and Stimulus?
If you want your Tooltips being refreshed when new content is loaded then you have to use Stimulus, put the code into the connect() method of a Stimulus controller, and attach that controller to every part of your application that may contain tooltips.
You can also attach your controller to the <html> tag and add a mutation observer (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) inside the connect() method if you don't want to attach your Stimulus controller at many places across your app. (You can also add the Mutation Observer in javascript/application.js if you wish. Stimulus purpose is actually to prevent spamming Mutation Observers but if you have many Tooltips, it is still a viable solution)
Related
As the title suggests, my main objective is to render a dynamic scss(.erb) file after an ajax call.
assets/javascripts/header.js
// onChange of a checkbox, a database boolean field should be toggled via AJAX
$( document ).ready(function() {
$('input[class=collection_cb]').change(function() {
// get the id of the item
var collection_id = $(this).parent().attr("data-collection-id");
// show a loading animation
$("#coll-loading").removeClass("vhidden");
// AJAX call
$.ajax({
type : 'PUT',
url : "/collections/" + collection_id + "/toggle",
success : function() {
// removal of loading animation, a bit delayed, as it would be too fast otherwise
setTimeout(function() {
$("#coll_loading").addClass("vhidden");
}, 300);
},
});
});
});
controller/collections_controller.rb
def toggle
# safety measure to check if the user changes his collection
if current_user.id == Collection.find(params[:id]).user_id
collection = Collection.find(params[:id])
# toggle the collection
collection.toggle! :auto_add_item
else
# redirect the user to error page, alert page
end
render :nothing => true
end
All worked very smooth when I solely toggled the database object.
Now I wanted to add some extra spices and change the CSS of my 50+ li's accordingly to the currently selected collections of the user.
My desired CSS looks like this, it checks li elements if they belong to the collections and give them a border color if so.
ul#list > li[data-collections~='8'][data-collections~='2']
{
border-color: #ff2900;
}
I added this to my controller to generate the []-conditions:
def toggle
# .
# .
# toggle function
# return the currently selected collection ids in the [data-collections]-format
#active_collections = ""
c_ids = current_user.collections.where(:auto_add_item => true).pluck('collections.id')
if c_ids.size != 0
c_ids.each { |id| #active_collections += "[data-collections~='#{id}']" }
end
# this is what gets retrieved
# #active_collections => [data-collections~='8'][data-collections~='2']
end
now I need a way to put those brackets in a scss file that gets generated dynamically.
I tried adding:
respond_to do |format|
format.css
end
to my controller, having the file views/collections/toggle.css.erb
ul#list<%= raw active_collections %> > li<%= raw active_collections %> {
border-color: #ff2900;
}
It didn't work, another way was rendering the css file from my controller, and then passing it to a view as described by Manuel Meurer
Did I mess up with the file names? Like using css instead of scss? Do you have any ideas how I should proceed?
Thanks for your help!
Why dynamic CSS? - reasoning
I know that this should normally happen by adding classes via JavaScript. My reasoning to why I need a dynamic css is that when the user decides to change the selected collections, he does this very concentrated. Something like 4 calls in 3 seconds, then a 5 minutes pause, then 5 calls in 4 seconds. The JavaScript would simply take too long to loop through the 50+ li's after every call.
UPDATE
As it turns out, JavaScript was very fast at handling my "long" list... Thanks y'all for pointing out the errors in my thinking!
In my opinion, the problem you've got isn't to do with CSS; it's to do with how your system works
CSS is loaded static (from the http request), which means when the page is rendered, it will not update if you change the CSS files on the server
JS is client side and is designed to interact with rendered HTML elements (through the DOM). This means that JS by its nature is dynamic, and is why we can use it with technologies like Ajax to change parts of the page
Here's where I think your problem comes in...
Your JS call is not reloading the page, which means the CSS stays static. There is currently no way to reload the CSS and have them render without refreshing (sending an HTTP request). This means that any updating you do with JS will have to include per-loaded CSS
As per the comments to your OP, you should really look at updating the classes of your list elements. If you use something like this it should work instantaneously:
$('li').addClass('new');
Hope this helps?
If I understood your feature correctly, actually all you need can be realized by JavaScript simply, no need for any hack.
Let me organize your feature at first
Given an user visiting the page
When he checks a checkbox
He will see a loading sign which implies this is an interaction with server
When the loading sign stopped
He will see the row(or 'li") he checked has a border which implies his action has been accepted by server
Then comes the solution. For readability I will simplify your loading sign code into named functions instead of real code.
$(document).ready(function() {
$('input[class=collection_cb]').change(function() {
// Use a variable to store parent of current scope for using later
var $parent = $(this).parent();
// get the id of the item
var collection_id = $parent.attr("data-collection-id");
show_loading_sign();
// AJAX call
$.ajax({
type : 'PUT',
url : "/collections/" + collection_id + "/toggle",
success : function() {
// This is the effect you need.
$parent.addClass('green_color_border');
},
error: function() {
$parent.addClass('red_color_border');
},
complete: function() {
close_loading_sign(); /*Close the sign no matter success or error*/
}
});
});
});
Let me know if my understanding of feature is correct and if this could solve the problem.
What if, when the user toggles a collection selection, you use jquery change one class on the ul and then define static styles based on that?
For example, your original markup might be:
ul#list.no_selection
li.collection8.collection2
li.collection1
And your css would have, statically:
ul.collection1 li.collection1,
ul.collection2 li.collection2,
...
ul.collection8 li.collection8 {
border-color: #ff2900;
}
So by default, there wouldn't be a border. But if the user selects collection 8, your jquery would do:
$('ul#list').addClass('collection8')
and voila, border around the li that's in collection8-- without looping over all the lis in javascript and without loading a stylesheet dynamically.
What do you think, would this work in your case?
I have a rails project that has a standard rails delete link inside a popup.
I have multiple popups on the page so I have a hide action attached to the body (onclick) and a stopPropagation event attached to popups so that clicking inside the popup doesn't hide it, but clicking anywhere else will hide all popups.
However, this seems to be affecting my delete links, which are inside the popups. It seems to be forcing them to get instead of post, which means they're not deleting. And the confirm dialog doesn't appear.
If I move the delete links outside of the popup, it works as expected.
Some code:
$('body').click(function() {
$('.popover').hide();
});
$('.popover').click(function(ev){
ev.stopPropagation();
});
And the rails (Haml) view:
.popover
= link_to [task], :confirm => 'Are you sure you want to delete?', :method => :delete, :remote => true
How do I get delete to work inside a parent with stopPropagation?
Appreciate the help.
Use the jQuery .not() function in combination with the CSS * selector:
$('body *').not('.popover, .popover *').click(function() {
$('.popover').hide();
});
Full documentation: http://api.jquery.com/not/
Working JSFiddle: http://jsfiddle.net/ryanbrodie/nGcaP/7/
The reason why it is not working, is because these of these lines.
$('.popover').click(function(ev){
ev.stopPropagation();
});
They stop all javascript from being executed on anything clicked on inside your popups. This will turn your links into standard links without any javascript, thus not performing an post request.
You can solve this by using jQuery's not along with the * selector as RyanBrodie suggested.
$('body *').not('.popover').click(function() {
$('.popover').hide();
});
To ensure elements within the popover don't trigger hiding the popover do this:
$('body *').not('.popover').not('.popover *').click(function() {
$('.popover').hide();
});
I've been racking my head against this for 2 days now. I'm massively frustrated, and I can't seem to find any information on this with searching.
The issue. I'm using a :remote => true link to load some html from a different controller.
$('.managed_locations').bind('ajax:complete', function(evt, xhr, status){
$('#locations_modal').modal('show')
$('#locations_modal').html(xhr.responseText);
});
So it gets the html, dumps it into the bootstrap modal and displays the modal. This is working fine.
But inside of the modal I ALSO have a form which also uses :remote => true. Now to make life harder, when a button is pressed I clone the form and display it. So the user could have many forms.
Now the issue. Whenever the form is submitted it just loads it like a normal page. It's as if the :remote => true is being ignored. But this only in the modal. If I just load the modal controller by itself it works just fine. I also had this developed before using another jquery lightbox where it was working fine. I'm just switching in bootstrap for consistency.
So my initial thoughts are that the jquery_ujs.js isn't finding the new forms. So I added some code to output the form elements.
$("#log_events").click(function () {
$(document).find(".new_stored_physical_location").each(function() {
console.log( $(this).data() );
console.log( $(this).data('events') );
});
return false;
});
Which outputs in the console:
Object { type="html", remote=true}
Object { ajax:complete=[1]}
So I see that the events are being set in jQuery. Each of these forms has :remote => true and has the ajax event for when the request is complete. But it's just not doing an ajax request when I hit submit.
Is there something I'm missing that is required to make sure an ajax request will happen from the form???? The data() looks fine, the data('events') look fine. But is there some other event/binding that I need to look at?
The html that is loaded in from the modal right now is loading a layout. But i've done it both with a layout, without a layout. It's driving me nuts. Thanks for the help guys.
Edit: Some extra weirdness. The modal also loads some additional remote links, all of which are working correctly. It's only the form links which don't seem to work.
I got a solution. The big issue was within jquery_ujs.js Especially this line:
$(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
FYI, rails.formSubmitSelector = 'form'. So this code found all of the forms in the document, overwrote the submit with this function. But the issue was that once you loaded in some ajax, and that ajax contained a it wouldn't add this fancy event to it. You need to re-add it.
So this is what I did.
Inside of jquery_ujs there is a bunch of functions that are accessible outside of it using $.rails. So things like: $.rails.enableElement, $.rails.nonBlankInputs. And the code for the submit event was sitting around all willy nilly. It only executes once when the page is loaded. So I put that in a function addSubmitEvent():
// Add the form submit event
addSubmitEvent: function(element) {
//$(element) was before $(document) but I changed it
$(element).delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
var form = $(this),
remote = form.data('remote') !== undefined,
blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector),
nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
if (!rails.allowAction(form)) return rails.stopEverything(e);
// skip other logic when required values are missing or file upload is present
if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
return rails.stopEverything(e);
}
if (remote) {
if (nonBlankFileInputs) {
return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
}
// If browser does not support submit bubbling, then this live-binding will be called before direct
// bindings. Therefore, we should directly call any direct bindings before remotely submitting form.
if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e);
rails.handleRemote(form);
return false;
} else {
// slight timeout so that the submit button gets properly serialized
setTimeout(function(){ rails.disableFormElements(form); }, 13);
}
});
}
This is basically the exact same code. But now it's $(element) instead of $(document). This was changed because now I can sniff for when the modal has loaded in the html. Then I can call:
$.rails.addSubmitEvent('#my_modal');
I then had an issue of it adding the event too many times from when I opened/closed the modal multiple times. So I just put a simple true/false if around it to call it once only.
I defined my jQuery tabs like this:
$('#serviceTabs').tabs({
idPrefix: 'ui-subtabs-',
spinner: 'Retrieving data...',
cache: false,
select: function(event, ui) {
if(checkServiceTabs(ui.index))
{
$('#ui-subtabs-'+(currentDetailTab+1)).html(" ");
currentDetailTab = ui.index;
return true;
}
else
return false;
},
collapsible: true
});
Unfortunately after reloading my page the index of my tabs is raised incrementally.
So on first request my TabID's look like:
#ui-subtabs-1, #ui-subtabs-2, #ui-subtabs-3
after reloading my page it looks like:
#ui-subtabs-4, #ui-subtabs-5, #ui-subtabs-6
The side effect is, that the tabs are locked after reload. The select event doesn't work anymore.
FYI: The tabs are in a DIV and merged with $.get function.
So i don't reload the whole page but only the div.
Before the new request I already blank the div with .html(" ") and I also tried
$('#serviceTabs').tabs("destroy");
Does anybody have an idea how to delete the TabID cache ?
Unfortunately, the tabIndex variable from which the TabID is coming from is encapsulated in the anonymous function of the Tabs plugin, making it totally invisible to the outside world. It is only incremented and the plugin offers no method to be able to reset it. Even destroying the instance of the plugin would not help.
On the overview page of the plugin's documentation though, it is specified that the tab containers can be references using the Title attribute of the <a> elements serving as tab buttons.
You would have then this kind of markup for the tab buttons:
<li> ... </li>
This will create tab containers with the title as ID (replacing spaces with underscores):
<div id="My_Tab_1"> ... </div>
You would have then to modify you select method accordingly.
I'm writing a bookmarklet which needs to work in the context of pages whose design I don't control. Some of the pages I need the bookmarklet to function in use frames (in framesets). Is it possible for a jQuery-UI dialog to work inside a frame?
Currently, when I encounter a page with a frameset, I creating my dialog like this:
var frame = window.frames[0];
var div = $(frame.document.createElement("div"));
div.html("My popup contents");
div.dialog( ... );
The result is that jQuery appends the ui-widget div to the main document, rather than the frame's document. Since the main document is just a frameset, nothing is displayed. I can't find any options in the jquery-ui API to specify which document the widgets should be constructed in. The bookmarklet will necessarily be running (or at least starting) from within the context of the outer document.
I'm aware that it won't be possible to display an overlay over the frames; I'm comfortable with display just in a single frame. Also, some other notable bookmarklets fail to function on pages with framesets, so this may be a common problem.
Suggestions?
Bookmarklets typically don't use jQuery. Most bookmarklets open a window which has jQuery.
Here's what I ended up doing: rather than attempting to display within or over a frame, I just had the bookmarklet rewrite the page to remove the framesets and add my own body and content to the page. This allows the bookmarklet to still introspect the frames and get data that it needs from them to construct the overlay prior to removing the framesets, but allows the overlay to still work.
Something like this:
if (window.frames) {
for (var i = 0; i < window.frames.length; i++) {
// ... grab data from the frame ...
}
}
if ($("frameset")) {
$("html").children("frameset").remove();
document.body = document.createElement("body");
$("html").append(document.body);
// ... add my stuff to body ...
}