ok, ok, I know that is not the correct line :-)
But making a slightly complicated model relationship and form sure seemed to be like that in Ruby on Rails.
Here is the model - pretty simple at that:
1. You have players who can play 1-n number of sports. represented by a model called Players
2. You have a number of sports. represented by a model called Sports
3. Players can play number of sports, similarly a given Sport could be played by a number of players. the relationship between them is called PlayerSports - it is many to many, through this relationship table, which also stores certain attributes, Rating and StartYear for the relationship itself
We want to create a single form for Players as well as allowing them to be associated with Sports in the same form, along with some of the details about this relationship as given in point 3 above.
Now the form only displays details about one mandatory sport, while registering yourself as a player (after all if you do not play even one Sport, you can't really be a Player, right?). Using jquery/javascript you can add the form inputs for more Sports dynamically - in case a Player plays more than one Sport. How to create the form and the rest of the code to do this in relatively straight forward manner? Given below is the step by step implementation, narrowed down by reading tons of blogs, documentation., hit and trial. Also listing some of the fails, pitfalls to beware of.
Disclaimer: Not a ruby/rails expert, learned all this starting 2 months back. but found it difficult to implement so thought should document it somewhere.
Statement 1: I found (for a newbie perhaps) that using simple_form, other form gems, and cocoon isn't worth the time, especially since my problem was solved relatively quickly using standard Rails means and some jquery. also note that cocoon etc expects some format for it to work, if you have a custom html form, it might mean more work than needed.
Statement 2: Tried and tested on Rails version 4.2.3
Statement 3: We do not create Sport rows/data through the form. Just new Players and their relationship with Sports.
Statement 4: Pay attention to the singular and plural forms of the words. they are significant in Rails.
Step 1 - Models:
First we need to create a many-to-many through relationship. if you don't know what that is, please read the rails guides for an intro (it's relatively easy). The code needed for that is given below:
But making a slightly complicated model relationship and form sure seemed to be like that in Ruby on Rails.
Here is the model - pretty simple at that:
1. You have players who can play 1-n number of sports. represented by a model called Players
2. You have a number of sports. represented by a model called Sports
3. Players can play number of sports, similarly a given Sport could be played by a number of players. the relationship between them is called PlayerSports - it is many to many, through this relationship table, which also stores certain attributes, Rating and StartYear for the relationship itself
We want to create a single form for Players as well as allowing them to be associated with Sports in the same form, along with some of the details about this relationship as given in point 3 above.
Now the form only displays details about one mandatory sport, while registering yourself as a player (after all if you do not play even one Sport, you can't really be a Player, right?). Using jquery/javascript you can add the form inputs for more Sports dynamically - in case a Player plays more than one Sport. How to create the form and the rest of the code to do this in relatively straight forward manner? Given below is the step by step implementation, narrowed down by reading tons of blogs, documentation., hit and trial. Also listing some of the fails, pitfalls to beware of.
Disclaimer: Not a ruby/rails expert, learned all this starting 2 months back. but found it difficult to implement so thought should document it somewhere.
Statement 1: I found (for a newbie perhaps) that using simple_form, other form gems, and cocoon isn't worth the time, especially since my problem was solved relatively quickly using standard Rails means and some jquery. also note that cocoon etc expects some format for it to work, if you have a custom html form, it might mean more work than needed.
Statement 2: Tried and tested on Rails version 4.2.3
Statement 3: We do not create Sport rows/data through the form. Just new Players and their relationship with Sports.
Statement 4: Pay attention to the singular and plural forms of the words. they are significant in Rails.
Step 1 - Models:
First we need to create a many-to-many through relationship. if you don't know what that is, please read the rails guides for an intro (it's relatively easy). The code needed for that is given below:
class Player < ActiveRecord::Base
# relationship for sports for a single playerhas_many :player_sports, foreign_key: "player_id"has_many :sports, through: :player_sportsaccepts_nested_attributes_for :player_sports,reject_if: :all_blank, allow_destroy: true# ... other code not relevant here ...endclass PlayerSport < ActiveRecord::Basebelongs_to :playerbelongs_to :sportvalidates :player_id, presence: true#validates :sport_id, presence: true # accepts_nested_attributes_for :sportsendclass Sport < ActiveRecord::Basehas_many :player_sports, foreign_key: "sport_id"has_many :players, through: :player_sportsendFew clarifications here:1. Some blogs mentioned you need inverse_of in other models for the through :player_sports relationship. Not true, as the code above shows for the purposes here.2. The validates and accepts_nested_attributes_for in the relationship table PlayerSport is not needed. In fact validates gave me an issue when creating a new Player (and one or more PlayerSport relationship rows) since the player_id would be blank when the Player record is not yet created. Seems common sense.3. Some sites mentioned attr_accessor to be created for the PlayerSport attributes in the Player model. I did not find that is required and remember reading somewhere that accepts_nested_attributes_for automatically creates that. autosave isn't needed too.Step 2 - Controllers:For creating and saving a new record: Nothing!!! I did nothing (new) in the Player controller when I added this relationship other than what is needed for the core Player itself. If you do not know what that is, then this is probably not the right place for you to start from. But the code should essentially look something like this (player_params is a method for allowing the params to pass through as given in the point below):def create @player = Player.new(player_params) if @player.save # do something else flash[:danger] = @player.errors # do something else endendPermitting the attributes (strong parameters in Rails) - this is interesting. you need to add player_sports_attributes as an array in the parameters allowed for Player - not player_sport or player_sport_attributes but exactly the same format-pluralized as given below. the fields in the permit array can vary depending upon what you have in the PlayerSport model, but _destroy is a special one needed for deleting the relationship records.def player_params params.require(:player).permit(:first_name, :last_name, ...other_fields...,:player_sports_attributes => [:id, :done, :player_id, :sport_id, :rating, :_destroy]) endStep 3 - Form:The main form - containing fields for Player is the standard one. In my case, I created the html layout in a table, with a <tr> representing a field. What is interesting is the part where you need to put the PlayerSport fields. That is achieved with the code below:tr> <td><%= f.label :comments %><br></td> <td><%= f.text_area :comments, class: 'form-control' %></td> </tr> <!-- some html code to show the Player model fields --> <%= f.fields_for 'player_sports_attributes', index: 0 do |builder| %> <%= render 'player_sport_fields', :f => builder %> <% end %> <tr><tr id="addAnchorRow"><td><a href="javascript: void(0)" id="addSportAnchor" onClick="addSport(this)"> Add Another Sport</a></td></tr><!-- the remaining code if any -->Things to note here:1. If you create a form using form_for and are using a html <table> to layout the fields, the form_for must be outside the table, since we would be adding <tr> dynamically and the new <tr> rows (and thus the extra PlayerSport rows apart from the one added directly) would NOT be submitted in params to the controller. It is not a Rails issue, it is html/DOM issue - I am not sure if it is an issue even, I think it behaves as it should - just mentioning it for the sake of completeness here.2. Index is not needed as Rails should automatically generate the form fields for the single PlayerSport row present statically (i.e. not created through jquery) with a "0". Again, just mentioned for clarity.3. If you try giving 'player_sports' instead of 'player_sports_attributes', the static fields also would disappear - Rails would NOT generate anything. Again this is standard behavior I think.4. The last line is to generate an <a> tag, clicking which should generate more fields. It calls a javascript function when clicked as you can see, and while it is not unobtrusive javascript, to me it seemed simple enough.Step 4 - Jquery/Javascript:For the static/mandatory in our case, PlayerSport, the generated html from Rails is of the format (one field shown below):<select class="form-control" name="player[player_sports_attributes][0][sport_id]" id="player_player_sports_attributes_0_sport_id">..options...</select>Through jquery we need to:0. Generate a set of almost identical row - containing the form fields for additional PlayerSport records, and attach it to the <table> within the form_for1. Change or remove the ID so that we don't have form fields (<select> in this case) with duplicate ID. Now, I have seen blogs talking about changing the ID value, but I tried just removing it altogether and still the form was submitted and records created properly for me. so your choice here I think. Would even duplicate IDs matter? I am not sure, since I saw Rails complaining more about the name of the fields than ID. But it is possible I missed something.2. Change the [0] part in the name player[player_sports_attributes][0][sport_id] for the newly generated fields - you can change it to anything unique - [1] would be fine. I used $.now() to get current time in milliseconds and replaced the "0" with that. This is important, since the params are submitted like:Parameters: {..other params..., "player_sports_attributes"=>{"0"=>{"sport_id"=>"1", "rating"=>"7.0"}, "1451889076747"=>{"sport_id"=>"2", "rating"=>"1990"}}}, "commit"=>"Register Player"}As you can see the number inside the middle [] become a key in the params hash submitted.
No comments:
Post a Comment