The main purpose of the task I'm working on is to create a form for admins that includes multiple time ranges, so I could save those time ranges in my database as a jsonb attribute. I could add as many time ranges as I want. The front-end dynamic is done, but I have struggled a lot with the back-end and the params I would like to get.
The params I would like to get from my simple form should be similar as follows:
admin: {first_name, last_name, age, time_ranges: [{start_time, end_time}, {start_time, end_time},{start_time, end_time}...]}
I have been reading a lot about nested forms, but I could not achieve what I want. I have tried creating a tables-less model for the DateRanges, so in my SuperAdmin model (that has a table in my db) I could use has_many: time_ranges and accepts_nested_attributes_for: date_ranges.
class Admin < ApplicationRecord
has_many :time_ranges
accepts_nested_attributes_for :time_ranges
end
class TimeRange < ApplicationRecord
include ActiveModel::Model
include ActiveModel::Attributes
attribute :end_time
attribute :start_time
attr_accessor :start_time, :end_time
belongs_to :admin
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
and in my admin_controller I have this in my params:
def admin_params
params.require(:admin).permit(:first_name, :last_name, :age, time_ranges_attributes: [:start_date, :end_date])
end
I'm using simple form in my view, so my front-end looks like this:
<%= simple_form_for @admin, url: admin_path(@admin), method: :put do |f|%>
<tr>
<td>Name</td>
<td><%= f.input :first_name %></td>
<td>Last Name</td>
<td><%= f.input :last_name %></td>
</tr>
<%= f.simple_fields_for :time_ranges do |p|%>
<tr>
<td>
<p class='ml-1'>Start</p>
<%= p.input :start_time, as: :time, ignore_date: true %>
</td>
<td>
<p class='ml-1'>End</p>
<%= p.input :end_time, as: :time, ignore_date: true %>
</td>
<td>
</td>
</tr>
<% end %>
--------------...I could add as many simple_fields_for as I want (front-end logic using StimulusJS) ---------------------------------------------
<%= f.button :submit, class: 'button is-info' %>
<% end %>
This the way I found could give me the results I want, but I get the following error as soon as I try to access my view:
PG::UndefinedTable: ERROR: relation "time_ranges" does not exist
LINE 1: SELECT "time_ranges".* FROM "time_ranges" WHERE "time_ranges...
I know this is because time_range entity doesn't exist in my DB, but I would like to get your help to work around this problem, or I would appreciate it if you give me another solution to get the params I want. I'm using rails 5.2.
has_many
and nested attributes are for associations. This isn't an association, it's just a column that contains a string which Rails will serialize and deserialize. That's about all Rails knows about JSONB.
Instead of nested attributes, you're passing an array parameter.
def admin_params
params.require(:admin).permit(:first_name, :last_name, :age, time_ranges: [])
end
It's up to Admin to validate time_ranges contains the correct objects.
Your form would take inputs with the name admin[time_ranges][][start_date]
and admin[time_ranges][][end_date]
. See Parameter Naming Conventions.
Since Rails does not know about JSONB associations it's going to be a lot more work. This could be done simpler as a regular association with a real table with a single tztsrange or daterange column (it's unclear if you're storing times or dates).
create_table :admin_availability do
t.belongs_to :admin
t.tztsrange :availability, null: false
end
class AdminAvailability < ApplicationRecord
belongs_to :admin
end
class Admin < ApplicationRecord
has_many :admin_availabilities
accepts_nested_attributes_for :admin_availability
end
Now the nested attribute machinery will work. You'll likely still have to translate from start_time/end_time parameters to a single Range object. It might be simpler to add convenience accessors to your controller.
class AdminAvailability < ApplicationRecord
belongs_to :admin
def start_time
availability.begin
end
def start_time=(start)
self.availability = Range.new(start, end_time)
end
def end_time
availability.end
end
def end_time=(end)
self.availability = Range.new(start_time, end)
end
end