Rails: Using input from dropdown menu without posting - javascript

I'm pretty new to Rails and web dev in general. I need to display two dropdown menus, states and schools, such that schools is only displayed after the user has chosen the state, and schools should only display the schools in the chosen state. What I don't know is how I can use the states choice to decide dynamically what schools to display, without the user having to click Submit. I understand that I may need to use JavaScript, but not knowing JS well, I'm not really sure how to do that. Hope I'm making sense. Thanks!

Here is a simple example of dynamically populating a select based on data structures already in your JavaScript. If you need to perform a server request after the user selects a state and return the list of schools, you'll need different code (and helpfully a library like jQuery).

I think you want to do this with AJAX. I'm not going to customize this for Rails 1 but you should be able to follow the idea. Your first dropdown has a list of states, and each state has a list of schools.
// some js file that's loaded from your layout
// When your states dropdown is changed it fires an ajax call
var success = function(response) {
for (var school in response.schools) {
$('#schools_dropdown').html('');
var option = $(document.createElement('option')).html(school.name).val(school.id);
option.appendTo($('#schools_dropdown'));
}
});
$('#states_dropdown').change(function() {
$.get('/state/' + $(this).val() + '/schools', success);
});
# your schools controller
def index
#schools = State.find(params[:id]).schools
respond_to do |format|
format.js { render :json => #schools }
end
end
So maybe you don't have jQuery and maybe rendering json is different in Rails 1... but the idea is the same. You have some javascript attached to your states dropdown so that when it changes, you pull off the id of that state and make an AJAX call to your controller. The last parameter to that AJAX call is a success function that loops through all the schools sent back by the controller, clears the schools dropdown, and adds options into the dropdown one by one.

Related

Implications on Speed/Performance When Producing Multiple HTML Templates from Javascript Classes

I'm building a travel website with Ruby on Rails 4 that makes heavy use of Javascript (or Coffeescript) for Google Maps and other APIs. This involves making a single call to the server, creating a javascript object with the results, then immediately rendering some HTML. A bit later, I will need to render different HTML using the same data.
A typical use case might be:
User searches for transportation between two different destinations
Coffeescript sends ajax post request to rails server
Rails server returns a JSON object with the results. Let's call this searchResults, which is an array of routes (e.g. searchResults['routes'][0]['path'] or searchResults['routes'][0]['price'])
The application immediately renders the results of this search as HTML (format 1)
Later, based on user action, the application must render data about one of the routes in the search result. This action requires rendering of different HTML than in step 4 (format 2).
Currently, in Step 3, I'm creating an instance of a SearchResults class in Coffeescript:
#holds all of the information for a single the transporation search call
class TransportationSearch
constructor: (oLat, oLng, dLat, dLng, oName, dName) ->
#origin = oName
#destination = dName
response = #search(oLat, oLng, dLat, dLng).responseJSON
#longestRoute = response.longestRoute #in minutes
#routes = response.routes
The reason I'm creating a Coffeescript class is because I'm trying to avoid hitting the server again. That is slow and I have an API limits to consider. My question is about steps 4 and 5. I've come across two different methods of doing what I need and wondering what the implications of each on speed/performance are.
Method 1: Cloning Hidden Div
I have methods in TransportationSearch that clone a hidden div, set the attributes, and insert it into the DOM:
renderFormatOne: ->
for route in routes
content = $('.div-one-template').clone().removeClass('hidden')
#...sets content for template. For example:
content.find('.price').html(route['price'])
#Insert template into Dom
$('#results-one').append(content)
renderFormatTwo: ->
...
Method 2: Using AJAX/Rails to Render the HTML
The other approach is to have house the HTML templates in a Rails partial, then use AJAX to send data to the controller and render the result.
Coffeescript:
#sets the content of the side-menu with the HTML from transportation call
showTransportation: (searchResults) =>
#first, get HTML
$.ajax '/segments/side_menu_transportation',
type: 'post'
data:
searchResults: JSON.stringify(searchResults)
success: (data) ->
$('#add-transport-box').html(data)
return true
error: ->
alert 'passDestinationToTransportation Unsuccessful'
return
#show()
Controller:
def side_menu_transportation
#searchResults = JSON.parse(params[:searchResults])
render partial: 'trips/transport_search'
end
Method 1 seems a little sloppy to me as it places a lot of the HTML structure in Coffeescript. However, speed is my priority and will probably dictate my decision. I'd prefer to use Method 2, but I'm wondering if the AJAX POST request is slow even if I'm not hitting my rails server.
Please let me know the speed / performance implications of these approaches, or if I'm missing something totally obvious :D.
Thanks in advance!
I don't think you should be sending data back to server to generate some HTML - if you do wouldn't that be generating frontend on the backend? Sounds a little bit odd to me. And it is a no-no from UX point of view because of lower responsiveness of the UI.
The speed of Javascript should not be a concern for you. Angular for example renders HTML all the time and unless developer was really sloppy, the impact on browser is not that big.
The HTML in Javascript. Well... again, this is frontend, you can't avoid it that much. But what might work for you is to have interpolated templates rather than copying, traversing and modifying DOM nodes. Just like this: <some><markup> #{route.price} </markup><some>. Having it that way would (possibly) reduce number of DOM operations (esp. costly traversing and lookup) and also would define body of the templates, so you see them full, as they are in one place.
Btw, it looks like I described what an Angular directive is with this paragraph - did you try to investigate it?
If you really need to render HTML server side (I advise you to not, but maybe I'm missing something) please don't make user wait for it - how about to render both/few templates at the same time of first call?
Lastly a hint:
class TransportationSearch
constructor: (oLat, oLng, dLat, dLng, oName, dName) ->
#origin = oName
#destination = dName
is equal to
class TransportationSearch
constructor: (oLat, oLng, dLat, dLng, #origin, #destination) ->

Building pagination controls with Knockout.JS

I've inherited a project which uses Knockout.JS to render a listing of posts. The client has asked that this listing be paginated and I'm wondering if this is possible and appropriate using Knockout.JS. I could easily achieve this in pure JavaScript but I'd like to use Knockout (if appropriate) for consistency.
From what I can tell, the page uses a Native Template in the HTML of the page. There is a ViweModel which stores the posts in a ko.ObservableArray() and a post model.
The data is loaded via a jQuery ajax call where the returned JSON is mapped to post model objects and then passed into the ObservableArray which takes care of the databinding.
Is it possible to amend the ViewModel to bind pagination links (including "previous" and "next" links when required) or would I be better off writing this in plain JS?
It should be easy enough to build a computed observable in knockout that shows a "window" of the full pagelist. For example add to the view model:
this.pageIndex = ko.observable(1);
this.pagedList = ko.computed(function() {
var startIndex = (this.pageIndex()-1) * PAGE_SIZE;
var endIndex = startIndex + PAGE_SIZE;
return this.fullList().slice(startIndex, endIndex);
}, this);
Then bind the "foreach" binding showing the record to pagedList instead of the full list, and in the forward and back links, simply change the value of pageIndex. Starting from there, you should be able to make it more robust/provide more functionality.
Also, this assumes you preload all data to the client anyway. It's also possible to make JSON calls on the previous and next link and update the model with the returned items. The "next" function (to be added to the view model prototype), could look like this:
ViewModel.prototype.next = function() {
var self = this;
this.pageIndex(this.pageIndex()+1);
$.ajax("dataurl/page/" + this.pageIndex(), {
success: function(data) {
self.dataList(data);
}
});
}
(using jQuery syntax for the ajax call for brevity, but any method is fine)
Writing features in KO always tend to generate less code and cleaner code than doing the same in "plain JS", jQuery or similar. So go for it!
I implemented a combobox with paging like this
https://github.com/AndersMalmgren/Knockout.Combobox/blob/master/src/knockout.combobox.js#L229
In my blog post, I have explained in very detail how to do it. you can find it (here. http://contractnamespace.blogspot.com/2014/02/pagination-with-knockout-jquery.html). It's very easy to implement and you can do it with a simple JQuery plugin.
Basically, I have used normal knockout data binding with AJAX and after data has been retrieved from the server, I call the plugin. You can find the plugin here. its called Simple Pagination.

How to redirect to different controller?

I have an application in ASP.MVC. The requirement is that I select a person from a list of people and click 'Info' and it should load the details of the person in that page. I have the Info controller and everything works fine if I go to the Info page from a different controller. In the page I am trying to make it work with JavaScript and it doesn't seem to take me to the desired page but to a different controller.
I have a ToDoList controller and in the .cshtml I have this code on click of the Info link.
function DoInfo#(i.ToString())() {
$("#sessionid").val("#Model.cSessionId[i]");
alert("hey");
$("#PageController").val(66);
$("#formID").submit();
}
I go to the ToDoList controller to do the redirection like this
if (viewModel.PageController == 66)
{
pass = new PassingData();
pass.personid = TSSessionService.ReadPersonId(viewModel.SessionId);
TempData["pass"] = pass;
return RedirectToAction("Index", "Info");
}
It never goes there and instead goes to a different controller. I cannot seem to find how they are linked and why is it not going back to controller where the Info link button is i.e. back to the ToDoList controller.
Let me know if it is not clear and I will try to explain again and I will give any other details.
I guess I'm confused as to why you are doing this as a combination of form and JavaScript. Are there other properties that you need to pass along that you are not posting above? Why do you need to use JavaScript to do this if you are just returning a new view?
You indicate in your post that when a person is selected from a list you need to go to a controller and display a view. This seems fairly straightforward, and I would like to suggest simplifying the problem.
Start with this: change your link to not use a form or JavaScript. Just make it a link. If it is text, you can use #Html.ActionLink() and even pass in the parameters you need.
If you're not displaying text, just use #Url.ActionLink() in your href property of the anchor you're wrapping your element with. Both of these allow you to leverage routing to ensure the correct path is being constructed.
If the controller that you are trying to get to has access to whatever TSSessionService is, then you don't need to pass through the TempData["pass"] you are trying to push through, so it makes it cleaner in that way as well.
If you do need to submit a more complicated value set, I would recommend coming up with a generic .click() event handler in jQuery that can respond to any of the clicks, bound by a common class name. You can use a data-val attribute in your link and read from $(this).attr('data-val') in your handler to store/fetch other important info. This allows you to more easily build up an object to POST to a controller.
Hope this helps some, but if I'm missing a critical point then please update the question above.

backbone.js cache collections and refresh

I have a collection that can potentially contain thousands of models. I have a view that displays a table with 50 rows for each page.
Now I want to be able to cache my data so that when a user loads page 1 of the table and then clicks page 2, the data for page 1 (rows #01-50) will be cached so that when the user clicks page 1 again, backbone won't have to fetch it again.
Also, I want my collection to be able to refresh updated data from the server without performing a RESET, since RESET will delete all the models in a collection, including references of existing model that may exist in my app. Is it possible to fetch data from the server and only update or add new models if necessary by comparing the existing data and the new arriving data?
In my app, I addressed the reset question by adding a new method called fetchNew:
app.Collection = Backbone.Collection.extend({
// fetch list without overwriting existing objects (copied from fetch())
fetchNew: function(options) {
options = options || {};
var collection = this,
success = options.success;
options.success = function(resp, status, xhr) {
_(collection.parse(resp, xhr)).each(function(item) {
// added this conditional block
if (!collection.get(item.id)) {
collection.add(item, {silent:true});
}
});
if (!options.silent) {
collection.trigger('reset', collection, options);
}
if (success) success(collection, resp);
};
return (this.sync || Backbone.sync).call(this, 'read', this, options);
}
});
This is pretty much identical to the standard fetch() method, except for the conditional statement checking for item existence, and using add() by default, rather than reset. Unlike simply passing {add: true} in the options argument, it allows you to retrieve sets of models that may overlap with what you already have loaded - using {add: true} will throw an error if you try to add the same model twice.
This should solve your caching problem, assuming your collection is set up so that you can pass some kind of page parameter in options to tell the server what page of options to send back. You'll probably want to add some sort of data structure within your collection to track which pages you've loaded, to avoid doing unnecessary requests, e.g.:
app.BigCollection = app.Collection.extend({
initialize: function() {
this.loadedPages = {};
},
loadPage: function(pageNumber) {
if (!this.loadedPages[pageNumber]) {
this.fetchNew({
page: pageNumber,
success: function(collection) {
collection.loadedPages[pageNumber] = true;
}
})
}
}
});
Backbone.Collection.fetch has an option {add:true} which will add models into a collection instead of replacing the contents.
myCollection.fetch({add:true})
So, in your first scenario, the items from page2 will get added to the collection.
As far as your 2nd scenario, there's currently no built in way to do that.
According to Jeremy that's something you're supposed to do in your App, and not part of Backbone.
Backbone seems to have a number of issues when being used for collaborative apps where another user might be updating models which you have client side. I get the feeling that Jeremy seems to focus on single-user applications, and the above ticket discussion exemplifies that.
In your case, the simplest way to handle your second scenario is to iterate over your collection and call fetch() on each model. But, that's not very good for performance.
For a better way to do it, I think you're going to have to override collection._add, and go down the line dalyons did on this pull request.
I managed to get update in Backbone 0.9.9 core. Check it out as it's exactly what you need http://backbonejs.org/#Collection-update.

Creating helper tag with UJS

Firstly, sorry for my English. I'm Brazilian guy that is improving yet.
I want create a helper tag called "collection_cascading_select".
That helper is similar to "collection_select", but he has one more argument called "source".
The "source" is the other collection in the view.
Ever that other option is select in the "source", a JavaScript function needs run to gets his value. Then populate the "collection_cascading_select" collection agreed of that value.
That gets confusing! I'm one week in this problem and my Brazilian brothers aren't help me.
Thanks!
[EDIT]
#Samo
I get it to work, but with some changes.
var success = function(response) {
for (var item in response){
var id = response[item].breed.id <--------------------
var name = response[item].breed.name <-------------------
var option = $(document.createElement('option')).val(id).html(name)
dependentDropDown.append(option)
}
}
I don't understand how FOR IN works.
It sounds like the answer you're looking for is a custom form builder. You could create your form builder and inherit from the Rails form builder, and then set that form builder as the default across your application. Then you could define an element called dependent_dropdown or cascading_selection, etc. This element would probably take the id of the source dropdown. Your helper would output a collection_select but it would also output some JavaScript that would fire an AJAX call when the source dropdown changes.
Of course, you don't have to do it this way. You could just use a collection_select, add some attributes to the source dropdown (i.e. :class => 'source_for_dependent', :dependent => some_id), and then hook up some JavaScript in your application.js that looks for collections with the source_for_dependent class, and when the onchange event fires it grabs the id from the dependent attribute and fires an AJAX call.
Either way, here's an example of your JavaScript (using jQuery)
$('select.source_for_dependent').change(function() {
var id = // get the id of the dependent dropdown, perhaps by $(this).attr('dependent')
var dependentDropDown = $('#' + id);
dependentDropDown.empty();
var success = function(response) {
for (var item in response) {
var option = $(document.createElement('option')).val(item.val).html(item.text);
dependentDropDown.append(option);
}
}
$.get('/some_controller/some_action/' + $(this).val(), success);
}
The success handler gets passed into jQuery's get method. It takes a JSON response as an argument. We loop through the response, and for each item, we create an option, pulling the value and the text off the item, and we append that to the dependent dropdown. Your controller might look something like this:
def some_action
#obj = SomeClass.find(params[:id])
respond_to do |format|
format.js { render :json => #obj }
end
end
Edit
Which controller you target is up to you. Let's say Dropdown A targets resource A, and Dropdown B targets resource B. An object of type A should have a list of objects of type B. If you're going after the show action for object A, then your as_json method for object A needs to include its B associations. This article shows examples of this.
Otherwise, you could target the index action for resource B. Making B a nested resource of A would be an easy way to key off the id of A to get all objects of type B which have a foreign key that points to A.

Categories