Rails 6 Cascading Select List - javascript

In Rails 6, I want the profiles/_form to have two dropdown lists, country and city. When I pick a value from country this is supposed to change the city choices. I want this to happen without
refreshing the page. My solution is below, and it kind of works for the new action, but it doesn't work for the edit action. Is this the right approach or am I totally missing the idiomatic rails 6 solution?
A route to return the option tags for the city select box:
# config/routes.rb
get 'cities_by_country/:id', to: 'profiles#cities_by_country'
The action that runs
# profiles_controller
def cities_by_country
#city_list = Profile::CITY_LIST[params[:id].to_i]
respond_to do |format|
format.js { render :cities_by_country}
end
end
The js file to generate the option tags
#views/profiles/cities_by_country.js.erb
<%= options_for_select(#city_list) %>
The javascript to attach the "change" event on the country select tag:
# app/javascript/packs/country_cities.js
import Rails from '#rails/ujs';
var country_select, city_select, selected_country;
window.addEventListener("load", () => {
country_select = document.querySelector("select#profile_country");
city_select = document.querySelector("select#profile_city");
country_select.addEventListener('change', (event) => {
selected_country = country_select.selectedIndex;
Rails.ajax({
url: "/cities_by_country/" + selected_country,
type: "get",
data: "",
success: function (data) {
city_select.innerHTML = data;
},
error: function (data) { }
})
})
});

Sorry to pile it on you but this is really broken.
Lets start with the controller/route. The idiomatic way to do this through a nested route - GET /countries/:country_id/cities. Nor should you really be shoehorning this into your Profile model / ProfilesController.
You can declare the route with:
get '/countries/:country_id/cities', to: 'countries/cities#index'
Or by using resources with a block:
resources :countries, only: [] do
resources :cities, only: [:index], module: :countries
end
And the controller like so:
module Countries
class CitiesController < ApplicationController
# GET /countries/:country_id/cities
def index
#cities = Profile::CITY_LIST[params[:city_id].to_i]
end
end
end
Not sure I can really get behind why you would want to use a constant in a model that should not be responsible for this at all instead of actually creating Country and City models.
The biggest issue with your JavaScript is that its completely non-idempotent. It all runs on window.addEventListener("load") so that it works on the intitial page load and then breaks down completely when Turbolinks replaces the page contents with AJAX since those event handlers were attached directly to the elements themselves.
To write JavaScript that works with Turbolinks you need to think differently. Create idempotent handlers that catch the event as it bubbles up the DOM.
# app/javascript/packs/country_cities.js
import Rails from '#rails/ujs';
document.addEventListener('change', (event) => {
let input = event.target;
if (input.matches('#profile_country')) {
Rails.ajax({
url: `/cities/${input.value}/country`,
type: 'get',
dataType : 'script'
});
}
});
If you want to use a js.erb template you also need to rewrite your view so that it transforms the page:
// app/views/countries/cities.js.erb
document.getElementById("#profile_city").innerHTML = "<%= j options_for_select(#cities) %>";
But you could also just use JSON and create the option elements on the client if you want to avoid making your server responsible for client side transformations.

Related

Dynamically add new element to specific list

In my Rails 4 application I have a Task model which can be sorted/grouped in various ways (group by user, group by transaction, order by due date, etc...) on several different pages. Users can create a new Task via an AJAX popup modal from anywhere in the application, and if they are on a page that is displaying a list of tasks, I would like to append the task to the list if appropriate.
# NOTE: tasks should only ever be displayed one of the ways below, not multiple
# Displaying tasks grouped by user
<div id="user-1-tasks">
<div class="task">...</div>
...
</div>
<div id="user-2-tasks">
<div class="task">...</div>
...
</div>
# Displaying tasks grouped by transaction
<div id="transaction-1-tasks">
<div class="task">...</div>
...
</div>
<div id="transaction-2-tasks">
<div class="task">...</div>
...
</div>
# Displaying all tasks together (ordered by due date)
<div id="tasks">
<div class="task">...</div>
...
</div>
The logic for determining which proper list to update is complex, because there may not be any list on the page (no update) or it could be sorted in several different ways like the examples I list above. It all depends on what page the user was on when they created the Task and how they had their tasks sorted (if any).
I came up with a "hack", whereby I pass all the possible DOM IDs that can be updated in a specific order and it updates the proper one on the page, or none if there aren't any.
Calculating what page the user is currently viewing and how the list is sorted is complicated, so it seemed much simpler to tell the JS to update "all" of the lists, knowing that only 1 should be present on the DOM at a time. The order of the DOM IDs is important, using the most-specific first and the fallback option last.
Note I am using server-generated JS via Rails' js.erb. I'm aware of the discussion surrounding this practice, but for this application it was much cleaner to maintain 1 copy of my HTML templates on the server instead of trying to do it all client-side and pass JSON back and forth.
# app/models/task.rb
class Task < ApplicationRecord
belongs_to :user
belongs_to :transaction
def domids_to_update
"#transaction-#{transaction.id}-tasks, #user-#{user.id}-tasks, #tasks"
end
end
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
# Only one of several pages that can display tasks a variety of ways
def index
if params[:sort] == "by-transaction"
#tasks = Task.includes(:transaction).group_by(&:transaction_record)
elsif params[:sort] == "by-user"
#tasks = Task.includes(:user).group_by(&:user)
else # default is order by due date
#tasks = Task.order(:due_date)
end
end
def create
#task = Task.create(task_params.merge(user: current_user))
# create.js.erb
end
private
def task_params
params.require(:task).permit(
:title,
:transaction_id,
:due_date
)
end
end
# app/views/tasks/create.js.erb
$("<%= #task.domids_to_update %>").append("<%=j render(partial: 'tasks/task', locals: { task: #task } %>");
As crazy as it seems, this actually works, because there "should" only be 1 of the lists present on the page at a time.
However, when I do things that seem crazy, it's usually because they are.
Is there a way to tell jQuery (or even with plan JavaScript) to only update the first occurrence of the #task.domids_to_update? The returned IDs are in a specific order, with the most specific first and the catch-all at the end.
Or is there a better way to implement the functionality I'm trying to achieve?
I'm slowly trying to remove my jQuery dependency, so my preference is vanilla JavaScript solutions. I only need to support modern browsers, so I don't have to worry about IE6, etc.
Or if the solution is resolved server-side by calculating the appropriate list ID to update I'm open to that as well.
You could use plain js to check for element existence and assign the first existing element to a variable:
# app/models/task.rb
# first, in your assignment, lose the spaces and the hashes,
# to make it simple to convert to array:
def domids_to_update
"transaction-#{transaction.id}-tasks,user-#{user.id}-tasks,tasks"
end
# app/controllers/tasks_controller.rb
var ids = "<%= #task.domids_to_update %>".split(',')
var theElement = document.getElementById(ids[0]) || document.getElementById(ids[1]) || document.getElementById(ids[2])
if (theElement !== null)
jQuery(theElement).append(...)
// or with plain js:
// var html = theElement.innerHTML
// html = html + "appended html"
// theElement.innerHTML = html
end

Moving AEM Touch UI pages based on path on page properties

We are given a requirement such that the page should be moved to a location based on the path provided at the page properties.
How to implement that in Touch UI?
In Classic UI we can use edit config and may use listeners and write respective JS code on that.Correct me if I am wrong.
Your question alludes to wanting to use JavaScript to move the page. I've put together an example using the Touch UI dialog. It works, but would require polish to validate user input and prevent string manipulation errors.
In this example, I'm using the dialog-success event that triggers after a dialog is saved. See Touch UI Events. I check to see if the textfield with the CSS selector is populated, and if it is, I post back to the Sling Post Servlet using the #MoveFrom suffix to move the node and its children (the page and the jcr:content, etc...). If that operation is successful, navigate the user to the new page.
In your dialog, add a textfield and give it a unique class name:
<movePage
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldLabel="Move page to:"
class="move-page"/>
Then add this JavaScript to a ClientLib used only in authoring mode such as cq.authoring.editor:
(function ($, $document) {
'use strict';
$document.on("dialog-success", function(e) {
var newPath,
lastSlash,
moveFromSuffix,
newDirectory,
currentPath,
data;
e.stopImmediatePropagation();
newPath = $('.move-page').val();
if (newPath) {
lastSlash = newPath.lastIndexOf('/');
moveFromSuffix = newPath.substring(lastSlash + 1) + Granite.Sling.MOVE_SUFFIX;
newDirectory = newPath.substring(0, lastSlash);
currentPath = Granite.HTTP.getPath().replace('/editor.html', '');
data = {};
data[moveFromSuffix] = currentPath;
$.post(newDirectory, data)
.done(function(){
window.location = '/editor.html' + newPath + '.html';
})
.fail(function(){
$(window).adaptTo('foundation-ui').alert('Alert', 'Could not move page');
});
}
});
})($, $(document));
However, another option would be to do it server side by implementing a custom Sling Post Processor.

pusher and rails updating a div

When a push is recieved I want it to update a div.
I don't understand javascript that well, but this is what i've got so far.
#subscription
var pusher = new Pusher('<%= Pusher.key %>');
var channel = pusher.subscribe('test_channel');
channel.bind('greet', function(data) {
$("#data.greeting").load(location.href + " #data.greeting");
});
#trigger
<%= Pusher['test_channel'].trigger('greet', { :greeting => "present"}) %>
#present is the div im trying to update in this example. the trigger works, but nothing happens on the sub end
Are you rendering the trigger in the view, as in, in your .erb file?
Trying keeping the subscription code the same and then running the trigger code in your rails console, as in, just this bit:
Pusher['test_channel'].trigger('greet', { :greeting => "present"})
I solved it like this.
channel.bind('greet', function(data) {
$("#present").load(location.href + " #present");
});
it just ignores the input from the trigger and runs that commands, works well. might be a little more work implementing in this way, but it will do.

What is the best practice for maintain a menu after a page reload? I have this custom code which feels wrong

I have a custom Menu which loads a new MVC View for each click as I want.
I load the new View by setting window.location.href. To make it work I have to set the baseURL (the name of the website) each time. To Store the state of the menu I use URL's querystring.
My concerns is in the use of:
'/WebConsole53/' // hardcode baseurl i have to apply each time manually
Setting window.location.href to load the new View from JavaScript // Is this the best way or should I use some URL/Html helpers instead?
I store the state of the selected menuItem in the querystring ("menu") // Is it more common to store that kind in Session/Cookie?
Any thoughts, corrections and suggestions would be much appriciated - thanks.
_Layout.cshtml
var controller = $self.data('webconsole-controller');
var action = $self.data('webconsole-action');
var menu = "?menu=" + $self.attr('id');
var relUrl = controller + "/" + action + menu;
var url = urlHelper.getUrl(relUrl);
window.location.href = url;
UrlHelper.js
var urlHelper = function () {
var getBaseUrl = '/WebConsole53/',
buildUrl = function(relUrl) {
return getBaseUrl + relUrl;
};
var getUrl = function(relUrl) { // relUrl format: 'controller/action'
return buildUrl(relUrl);
};
return {
getUrl: getUrl
};
}();
I Use MVC 5.
You can save this problem using Route. Through the route you know exactly where you are located in you application.
_Layout.cshtml is definetely not the place to have this javascript. Maybe you are missing some MVC concepts, I would recommend you to read a bit more about routes and routelinks
I hope this helps you a bit: RouteLinks in MVC
'/WebConsole53/' // hardcode baseurl I have to apply each time manually
sometimes you need to access your root from javascript where you don't have access to server-side code (eg #Html). While refactoring may be the best option you can get around this by storing the baseurl once, using server-side code, eg in _layout.cshtml:
<head>
<script type="text/javascript">
var basePath = '#Url.Content("~")'; // includes trailing /
</script>
... load other scripts after the above ...
</head>
you can then reference this everywhere and it will always be valid even if you move the base / migrate to test/live.
Setting window.location.href to load the new View from JavaScript // Is this the best way or should I use some URL/Html helpers instead?
Depends on your requirements - you could use $.ajax (or shortcuts $.get or $.load) to load PartialViews into specific areas on your page. There's plenty of examples on SO for this and the jquery api help.
Or just use <a> anchors or #Html.ActionLink as already suggested. Without needing menu= (see next) you don't need to control all your links.
I store the state of the selected menuItem in the querystring ("menu") // Is it more common to store that kind in Session/Cookie?
If you change the page, then you could query the current url to find which menu item points to it and highlight that one (ie set the menu dynamically rather than store it).
This would also cover the case where you user enters the url directly without the menu= part ... or where your forget to add this... not that that would happen :)
Additional: You can specify which layout to use in your view by specifying the Layout at the top of the view, eg:
#{
Layout = "~/Views/Shared/AltLayout.cshtml";
}
(which is also one of the options when you right click Views and Add View in visual studio)
Without this, MVC uses a configuration-by-convention and looks at Views/_ViewStart.cshtml which specifies the default _layout.cshtml.
If you don't want a layout at all, then just return PartialView(); instead

Url.Action only render controller and action but not id

I need to render a URL for a JavaScript search that I am doing. Unfortunately Url.Action renders not only the action but the current id. This occurs when presently on page utilizing the action with the id.
To illustrate Url.Action("List", "Org"); will first render Org/List from which I can append an org to be listed. However, after the location has been moved to Org/List/12345 Url.Action("List", "Org"); will render Org/List/12345 and appending to that creates an issue where I end up with Org/List/12345/6789.
Is there a different method I can use other than Url.Action? I've thought about using JavaScript to check for the number of / and removing part of the string but that seems a bit hackish.
// appears in my Site.Master & utilizes the AutoComplete plugin for jQuery
$(document).ready(function() {
$("input#FindOrg").autocomplete('<%= Url.Action("Find", "Org") %>', {
minChars: 3,
maxItemsToShow: 25
}).result(function(evt, data, formatted) {
var url = '<%= Url.Action("List", "Org") %>/';
window.location.href = url + data;
});
});
Couple of suggestions:
What happens if you use Url.Action("Find", "Org", new { id = "" })?
Alternately, try manually building the url with Url.Content("~/Find/Org").
You could 'cheat' and access the 'Controller' and 'Action' entries in the Routing Dictionary. This would then allow you to construct only the part of the url you need. The only caveat is that if you change your routing model these routines may become incorrect.

Categories