简体   繁体   中英

Load model association dynamically in Rails form before submit

So I'm not sure how to achieve this or if it's even possible and having a tough time searching for similar examples/answers. But is there a way to dynamically load a model association within a form before you submit it, like without knowing what the instance variable would be ahead of time?

I have models as follows:

Property
  has_many :owners, inverse_of: :property
  has_many :orders

Owners
  belongs_to :property

Orders
  belongs_to :property
  has_many :owners, through: :property

In my orders.new form I have select boxes to choose a property to create the association for the new order (using Select2 plugin):

<%= bootstrap_form_for(@orders, ...) do |f| %>
    ...
    <%= f.select :property_id, options_from_collection_for_select(Property.all, "id", "full_address"),
             { label: "Property:", include_blank: true }, { id: "orderPropSelect", data: { placeholder: "Select a Property"} } %>
    ...
<% end %>

So when someone SELECTS a property for the new order, is there a way I can load what owners that property already has associated to it before the form is submitted? Even just being able to see what owners are already there is okay (being able to edit them would be even better but I realize thats probably more complex)

The following code of course doesn't work but looking for something along the lines of:

<%= f.static_control label: "Property Owners" do %>
  <% :property_id.owners.each do |owner| %>
    <%= owner.name %>
  <% end %>
<% end %>

I've tried variations of fields_for but I don't know how to tell the nested fields to be based off what is chosen in the above select (of course loading different owners based on what property is chosen. The errors that I get are undefined method owners for nil:NilClass which are appropriate because I know I'm not telling rails where to look correctly.

So is this possible and if so, how would I achieve this?

(I use bootstrap forms gem incase anyone is wondering about form syntax. I also have cocoon loaded for other forms so if there's a way to use that then I'm not opposed.)

Update Working code, slightly modified for extraneous conditions.

$("#orderPropSelect").off().on('change', function() {
    var id = $(this).val();
    console.log(id);
    if (id !== '') {
        $.ajax({
            url: '/properties/' + id + '/owners',
            dataType: "json",
            success: function (data) {
                owners_html = '';
                $.each(data['owners'], function () {
                    owners_html += '<p>' + this + '</p>';
                });
                if (owners_html === '') {
                    $("#propOwnersShow").html('<p>No owner added yet.</p>');
                } else {
                    $("#propOwnersShow").html($(owners_html));
                }
            },
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                console.log(errorThrown);
            }
        });
    } else {
        $('#propOwnersShow').html('Please select a Property below to view its associated owners.')
    }

});

You need to make sure it matches your underlying routes & stuff, and probably handle the case where there aren't owners, so to hide the owners div. If you want to do more complex stuff you could instead of .pluck build a better array/hash with id's that you can then also use to build elements that you can interact with (eg remove them from the list)

# your property controller

before_action :allowed, only [:owners]

def owners
  owners = Property.find(params[:id]).owners.pluck(:name)
  respond_to |format|
    format.json { render json: { owners: owners, success: true } }
  end
end

def allowed
  # logic to define if this user is allowed to request this data, if you have devise, it could be
  user_signed_in? && request.xhr?
end

# your routes.rb

get "properties/:id/owners", to: "properties#owners", as: "property_owners"

# or if you have resources
resources :properties do
  member do
    get :owners
  end
end

# js file

$("#property_id").off().on('change', function() {
  var id = $(this).val();
  $.ajax({
    url: '/properties/'+id+'/owners',
    dataType: "json",
    success: function(data) {
      owners_html = '';
      $.each(data['owners'], function() {
        owners_html += '<p>'+this+'</p>';
      });
      $("selector_where_you_want_to_show_owners").html($(owners_html));
    },
    error: function(XMLHttpRequest, textStatus, errorThrown) {
      console.log(errorThrown);
    }
  });
}

UPDATE:

You can prevent the issue with no Owners by using find_by instead and making sure that you always return an [] , this way simplifying the logic on the front-side as well.

# on your controller instead use:

def owners
  # .find_by will return "nil" if no properties are found, which 
  # then we can leverage with .try. .try will "try" calling those
  # chained methods without raising an error. It usually returns
  # "nil" if the parent is "nil" but .try with .pluck though will
  # always return an [] no matter what the ancestors returned. 

  owners = Property.find_by(id: params[:id]).try(:owners).try(:pluck, :name]

  # actually looking at your relationships it seems you could instead
  # .where, since that will return an empty array, like so:

  # owners = Owner.where(property_id: params[:id]).pluck(:name)

  # this only does one database query where the other one does 2

  # here we're simply making sure we add something to the array so
  # that then on the front-end you can always deal with an array
  # without worrying with the "no properties found". .blank? will
  # return "true" if the value is nil, empty string, empty hash or
  # empty array which works fine for this. So if there's no value in
  # the array we add the "no owners" string.

  owners << 'No owner added yet.' if owners.blank?

  respond_to |format|
    format.json { render json: { owners: owners, success: true } }
  end
end

# And then since you'll always be returning an array you no longer
# have to worry about an empty array and your ajax function will
# look like this:

$.ajax({
  url: '/properties/' + id + '/owners',
  dataType: "json",
  success: function (data) {
    owners_html = '';
    $.each(data['owners'], function () {
      owners_html += '<p>' + this + '</p>';
    });
    $("#propOwnersShow").html($(owners_html));
  },
  error: function (XMLHttpRequest, textStatus, errorThrown) {
    console.log(errorThrown);
  }
});

Hope this helps

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM