Angular 2: Using the HTTP Service to Write Data to an API
Update, November 27, 2017: This post explains the Http service used in Angular 2. This is now deprecated in favor of the newer HttpClient released in...
I love how easy ActiveAdmin is to use out of the box, but it can force you into using a panoply of workarounds for forms with any significant level of complexity.
I love how easy ActiveAdmin is to use out of the box, but it can force you into using a panoply of workarounds for forms with any significant level of complexity. The challenge I faced was making this:
In my app, the line item new/edit form was where I needed autocompletion. A line item has many fees. In turn, a fee belongs to a line item, belongs to a track, and belongs to either a User or a Vendor via a payable_type
and payable_id
(that's the polymorphic part). I tackled this challenge in three steps: autocompletion on the has many association, autocompletion on a polymorphic association, and inclusion of objects from multiple models in the options for the polymorphic association.
I got a great start on adding basic autocomplete functionality thanks to Tyler Hunt's gist, but I needed to customize it to work for a nested form with a has_many
association.
To start, some basic setup was required to install jQuery UI autocomplete. I already had the active_model_serializers gem, so all I needed was the jquery-ui-rails gem.
# Gemfile gem jquery-ui-rails
Then, I added the autocomplete css to ActiveAdmin.
// app/assets/stylesheets/active_admin.css.scss @import "jquery.ui.autocomplete";
Next up was adding the autocomplete javascript to ActiveAdmin.
// app/assets/javascripts/active_admin.js //= require jquery.ui.autocomplete //= require_tree ./active_admin
(Require_tree is important so the custom javascript in the next section will be included.)
For the ActiveAdmin form, I used the built-in has_many
functionality so the user could dynamically add or delete as many fees as they wanted. I assigned each fee's track input to the autocomplete class, without a name (so it wouldn't be processed), and I also passed a url from which to get the JSON through the data attribute. Then I added a hidden track_id
input that would be populated when the user made a selection and saved to the database when they submitted the form.
# app/admin/line_item_order.rb f.has_many :fees do |fee| fee.input :track, as: :string, input_html: { class: 'autocomplete', name: '', value: fee.object.track.try(:title), data: { url: autocomplete_manage_tracks_path } } fee.input :track_id, as: :hidden end
How does one get the JSON that will populate the autocomplete? I had to create the autocomplete_manage_tracks_path
by creating a batch action. The search term entered by the user will be passed via the term
parameter (which is built into JQuery UI's autocomplete).
# app/admin/track.rb collection_action :autocomplete, method: :get do tracks = Track.where('LOWER(title) ILIKE ?', "#{params[:term]}%") render json: tracks, each_serializer: TrackAutocompleteSerializer, root: false end
This was where Active Model Serializers came into play. I created a serializer for Track specifically for this autocomplete functionality. This passed along only the ID and the label to be rendered as JSON and received and processed into the matching autocompleted options.
# app/serializers/track_autocomplete_serializer.rb class TrackAutocompleteSerializer < ActiveModel::Serializer attribute :id attribute :label private def label # Because some tracks have the same title, I wanted to display the options with their IDs tagged on at the end object.title + " - " + object.id.to_s end end
Then I created a new javascript file to add custom autocomplete functionality.
// app/assets/javascripts/active_admin/autocomplete.js $(document).ready(function() { var autocomplete = function() { // Loop through elements that have the autocomplete class $('.autocomplete').each(function(index, input) { var $hiddenInput, $input; $input = $(input); // Find the dynamically generated hidden form element $hiddenInput = $('#' + $input.attr('id') + '_input').find(':hidden'); return $input.autocomplete({ minLength: 3, delay: 600, // Do an ajax call to a url to get back the JSON that will populate the autocomplete options source: function(request, response) { return $.ajax({ url: $input.data('url'), dataType: 'json', data: { // Pass the search term term: request.term }, success: function(data) { return response(data); } }); }, // Upon select, designate what to do with the selected item. In this case, display the label of their selection // and populate the hidden track field with the ID select: function(event, ui) { $input.val(ui.item.label); $hiddenInput.val(ui.item.id); return false; } // Populate the autocomplete options in a drop down unordered list }).data('ui-autocomplete')._renderItem = function(ul, item) { return $('<li></li>').data('item.autocomplete', item).append('<a>' + item.label + '</a>').appendTo(ul); }; }); }; autocomplete(); // Make sure that inputs generated on the fly (via the has many association) also have autocomplete functionality $('.has_many .button').click(function() { autocomplete(); }); });
Voila! A user could add as many fees as they wanted and the form field for track would successfully autocomplete.
Next up was adding autocomplete to the payable field on Fee, which is polymorphic. I had a bit of an unusual case here; I wanted the user to be able to enter either a Vendor, or the specific User associated with the line item's order. I tackled adding autocomplete as I did before with all Vendors as options, leaving the User for later.
# app/admin/line_item_order.rb fee.input :payable, as: :string, input_html: { class: 'autocomplete', name: '', value: fee.object.payable.try(:name), data: { url: autocomplete_fee_manage_vendors_path } } fee.input :payable_identifier, as: :hidden
Next I needed to build the route...
# app/admin/vendor.rb collection_action :autocomplete_fee, method: :get do vendors = Vendor.where('LOWER(name) ILIKE ?', "#{params[:term]}%") render json: vendors, each_serializer: VendorUserAutocompleteSerializer, root: false end
...and the serializer. (I got ahead of myself on the naming, anticipating that a user would also be involved soon.)
As before, I structured the JSON with an ID and a label. But this time, I couldn't just pass the Vendor's ID, because a Fee needs both the payable_id
and the payable_type
. Here I built the ID attribute to match the format of the getter and setter I'd previously created for payable_identifier
on the fee model. This was based on the ActiveAdmin wiki article on nested forms with polymorphic associations.
# app/serializers/vendor_user_autocomplete_serializer.rb class VendorUserAutocompleteSerializer < ActiveModel::Serializer attribute :id attribute :label private def label object.name + " - " + object.id.to_s end def id object.class.to_s + "-" + object.id.to_s end end # app/models/fee.rb class Fee < ActiveRecord::Base belongs_to :payable, polymorphic: true belongs_to :line_item belongs_to :track attr_accessible :amount, :line_item_id, :payable_type, :payable_id, :payable_identifier, :track_id validates_numericality_of :amount validates_presence_of :payable_id def payable_identifier "#{payable_type.to_s}-#{payable_id.to_s}" end def payable_identifier=(payable_data) if payable_data.present? payable_data = payable_data.split('-') self.payable_type = payable_data[0] self.payable_id = payable_data[1] end end end
Then I had to figure out how to find the appropriate inputs and hidden inputs on the page with the ActiveAdmin/Formtastic-assigned IDs for the fees that were dynamically generated when a user clicked "Add a fee." I was working with a slightly different attribute than the previous patterns for Track (payable_identifier
rather than payable_id
). I wanted this javascript to work for all autocomplete inputs, so I checked whether this was the payable_id
input, and if it was, I set $hiddenInput
to grab the hidden element with the ID of #payable_identifier_input
rather than the standard #payable_id_input
.
// app/assets/javascripts/active_admin/autocomplete.js var $hiddenInput, $input; $input = $(input); if($input.attr('id').indexOf('payable_id') == -1) { $hiddenInput = $('#' + $input.attr('id') + '_input').find(':hidden'); } else { $hiddenInput = $('#' + $input.attr('id') + 'entifier_input').find(':hidden'); }
Ok! I was good to go for autocompletion on a fee's vendor.
Only a few tweaks were necessary to always include the option of the specific User. Because the user was two steps away from the line item in the schema, I thought it would be easiest to include the user_id
in a hidden field in the form.
# app/admin/line_item_order.rb f.input :user, as: :hidden, value: Order.find(params[:order_id]).user.id, input_html: { name: ''}
Then I included that in the ajax request.
// app/assets/javascripts/active_admin/autocomplete.js data: { term: request.term, user_id: $('input#line_item_user').val() },
That's how params[:user_id]
became accessible to the collection action.
# app/admin/vendor.rb collection_action :autocomplete_fee, method: :get do user = User.find(params[:user_id]) vendors = Vendor.where('LOWER(name) ILIKE ?', "#{params[:term]}%") render json: vendors.unshift(user), each_serializer: VendorUserAutocompleteSerializer, root: false end
The serializer also needed a slight adjustment because User didn't have a name attribute.
# app/serializers/vendor_user_autocomplete_serializer.rb def label if object.is_a?(User) "Charge to the client" else object.name + " - " + object.id.to_s end end
I also had to throw in a conditional on the form itself because of the same problem.
# app/admin/line_item_order.rb if fee.object.payable.is_a?(Vendor) fee.input :payable, as: :string, input_html: { class: 'autocomplete', name: '', value: fee.object.payable.try(:name), data: { url: autocomplete_fee_manage_vendors_path } } elsif fee.object.payable.is_a?(User) fee.input :payable, as: :string, input_html: { class: 'autocomplete', name: '', value: fee.object.payable.try(:email), data: { url: autocomplete_fee_manage_vendors_path } } else fee.input :payable, as: :string, input_html: { class: 'autocomplete', name: '', data: { url: autocomplete_fee_manage_vendors_path } } end fee.input :payable_identifier, as: :hidden
That's it! While it was fun for me to figure out, I think the client was even happier to have this added functionality. Do you have any ideas on how I could do this better? I'd love to hear them in the comments.
Learn more about JavaScript in our JavaScript Blog Archive.
Update, November 27, 2017: This post explains the Http service used in Angular 2. This is now deprecated in favor of the newer HttpClient released in...
During the theming process for the Emmys, specifically Emmys.com and Emmys.tv, we were presented with 2 differently styled marquees. On Emmys.com,...
A previous post described how to reposition node comments with Drupal's hook_menu_alter(), to facilitate a tabbed interface. One side effect that...
Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.