Search code examples
ruby-on-railsrubyruby-on-rails-5simple-formancestry

Rails ancestry gem + Render and choose category/subcategory in new form


I have Rails 5.2.2 and I'm trying to implement the ancestry gem.

So what I want:

I want a user to create an offer for an automotive part, but in the new form I want to be able to choose the category/subcategories and then enter the other details that I have, and then submit the form to create the offer. Let's say that someone wants to add for selling Brake Pads. But first has to select the parent categories. E.g

Car -> Brakes -> Brake Pads

So after choosing Brake Pads, he can create the offer.

What I have:

#category.rb
class Category < ApplicationRecord
  has_ancestry
  has_many :parts
end

-

#part.rb
class Part < ApplicationRecord
  belongs_to :category
end

For now, I have already created one in the console just to make it work: Eg

car = Category.create!(name: "Car")
brakes = Category.create!(name: "Brakes", parent: car)
brake_pads = Category.create!(name: "Brake Pads", parent: brakes)

I already also run the migration rails g migration add_category_to_parts category:references.

And my view:

#views/parts/new.html.haml
.container
  %h2.center Create offer
  = simple_form_for(@part, html: {class: "form-group" }) do |f|
    .form-group.col-md-8.col-md-offset-2.well
      = f.input :title
      = f.input :make_name, label: "Make"
      = f.input :code, label: 'Part number'
      = f.association :condition, label_method: :name, prompt: "-"
      = f.input :description
      .form-actions
        = f.button :submit, class: "btn btn-primary btn-dark-blue"

The question is: How I can render the categories/subcategories in my views-> parts -> new.html.haml form with 3 dropdowns(one for each subcategory because I will have many categories/subcategories) so the user can choose them and then create the offer?


Solution

  • You need CategoriesController in any case, because you need dependent dropdowns here. When user selects category in the first one, you need to make AJAX request to get subcategories and so on

    # config/routes.rb
    resources :categories, only: [] do
      get :select_item, on: :collection
    end
    
    # app/assets/javascripts/application.js
    $('#first-dropdown').on('change', function() {
      $.ajax({
        url: 'categories/select_item?category_id=' + $(this).val(),
        dataType: 'json',
        success: function(data) {
          var childElement = $('#second-dropdown');
          childElement.html('');
          data.forEach(function(v) {
            var id = v.id;
            var name = v.name;
            childElement.append('<option value="' + id + '">' + name + '</option>');
          });
        }
      });
    });
    
    # app/controllers/categories_controller.rb
    Class CategoriesController < ApplicationController
      def select_item
        category = @category.find params[:category_id]
        render json: category.children
      end
    end 
    
    # app/inputs/fake_select_input.rb
    class FakeSelectInput < SimpleForm::Inputs::CollectionSelectInput
      def input(wrapper_options = nil)
        label_method, value_method = detect_collection_methods
    
        merged_input_options = merge_wrapper_options(input_html_options, wrapper_options).merge(input_options.slice(:multiple, :include_blank, :disabled, :prompt))
    
        template.select_tag(
          attribute_name, 
          template.options_from_collection_for_select(collection, value_method, label_method, selected: input_options[:selected], disabled: input_options[:disabled]), 
          merged_input_options
        )
      end
    end
    
    # views/parts/new.html.haml
    .form-group.col-md-8.col-md-offset-2.well
      = f.input :title
      = f.input :make_name, label: "Make"
      = f.input :code, label: 'Part number'
      = f.association :condition, label_method: :name, prompt: "-"
      = f.input :description
      = f.input :parent_category_id, as: :fake_select, collection: Category.roots, input_html: { id: 'first-dropdown' }
      = f.input category_id, collection: [], input_html: { id: 'second-dropdown' }
      .form-actions
        = f.button :submit, class: "btn btn-primary btn-dark-blue"
    

    Since simple_form relies on using a model you need to create custom input for non-model fields (parent categories). You need to save only one category_id to @part, parent categories can always be taken from it. If you need more, than 2 dropdowns, just add one more function or (better) change it, so you can pass dependent dropdown as a parameter