Modal for Adding Records
At the moment the only way to add new records is using the scaffolded "New" page. Not only is it pretty uninspiring to look at it, using it means navigating away from the index page to do so.
Instead I'll let users add records directly from the index page using a modal that contains a much better version of the form, the same as the "Edit" page in fact. Much of the work to make all this happen has been put in place in previous episodes of this series so this should be pretty straightforward.
Here's what I'll end up with:
First I need to tweak the existing "New" button on the index page. I already have a PageHeading
component that takes care of rendering actions. I'll change the existing link_to
over to a Button
component:
app/views/item_sell_packs/index.html.erb
<%= render PageHeading::Component.new(title: 'Item Sell Packs', description: 'These are the names for how a supplier sells a complete package') do |c| %>
<%= c.with_actions do %>
<%= render Button::Component.new(
id: :item_sell_packs_new,
label: 'New',
options: {
icon: { name: :plus, colour: :white },
colour_classes: 'text-white bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-200',
data: {
action: 'click->resource#new',
params: [
{ name: 'resource-modal-name', value: :item_sell_packs_new }
]
}
}
) %>
<% end %>
<% end %>
The Stimulus action is set to call the resource controller new
method. I'm passing in the name of a modal too. I'll add that modal to the page next:
app/views/item_sell_packs/index.html.erb
<%= render Modal::Component.new(name: :item_sell_packs_new) do |modal| %>
<%= modal.with_form do %>
<%= render(partial: 'resource', locals: { action: :new, resource: @resource_class.new, readonly: false, token: form_authenticity_token }) %>
<% end %>
<%= modal.with_button(
id: :save_new,
label: 'Save',
options: {
icon: { name: :save, colour: :white },
colour_classes: 'text-white bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-200',
data: {
params: [
{ name: 'resource-url', value: polymorphic_url(@resource_class) },
{ name: 'resource-form-id', value: dom_id(@resource_class.new, :form) },
{ name: 'resource-modal-name', value: :item_sell_packs_new }
],
action: 'click->resource#create'
}
}) %>
<% end %>
Before wiring all that up I can add a Storybook story for the modal to see how it will look:
test/components/stories/modal/component_stories.rb
story :item_sell_packs_new do
constructor(name: :item_sell_packs_new, hidden: false)
form do
render(partial: 'item_sell_packs/resource', locals: { action: :new, resource: ItemSellPack.new, readonly: false, token: 'token' })
end
button(
id: :save_new,
label: 'Save',
options: {
icon: { name: :save, colour: :white },
colour_classes: 'text-white bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-200',
data: {
params: [
{ name: 'resource-url', value: '/item_sell_packs' },
{ name: 'resource-form-id', value: 'form_item_sell_pack' },
{ name: 'resource-modal-name', value: :item_sell_packs_new }
],
action: 'click->resource#create'
}
}
)
end
Looks not too bad. Now on to adding the Stimulus code that will make the modal open and allow the form data to be POST
ed. This is going to go into the resource controller I've used already, it has update
and delete
from previous sections I've covered.
app/javascript/controllers/resource_controller.js
new(event) {
const modalName = event.params.modalName
const modal = cash(`#${modalName}_modal`)
modal.show()
}
async create(event) {
const resourceUrl = event.params.url
const formId = event.params.formId
const modalName = event.params.modalName
const modal = cash(`#${modalName}_modal`)
const formData = new FormData(document.getElementById(formId));
const response = await post(resourceUrl, { body: formData, responseKind: 'json' })
if (response.ok) {
modal.hide()
}
}
There are two new methods, the first being new
which simply finds the correct modal and shows it. The second method is for creating the new record. It finds the form, produces a new FormData
from it and then POST
s to the correct URL. At the moment there is no error handling, it simply hides the modal if the create was successful.
Client side validation and error handling will be the topic of a future blog.
Creating the record is complete, the last user experience feature to add is to broadcast the creation of the new record. This will make the new record appear at the top of any screen showing the index page (including the user who made the record).
All this requires is adding another callback to the Broadcast
concern created in the previous blog. This time it is called after a create
has been committed on the model. And instead of a replace
like the other callbacks this is a prepend
which means it will insert a new row (in this case) at the beginning of the target collection_rows
.
app/models/concerns/broadcast.rb
after_create_commit lambda {
broadcast_prepend_later_to(
resource_name_plural,
partial: "#{resource_name_plural}/row",
locals: { resource: self },
target: 'collection_rows'
)
}
That collection_rows
target was added way back in the Infinite Scrolling blog entry.
app/components/collection/component.html.erb
<tbody id="collection_rows" class="bg-white" data-controller="resource">
<%= rows %>
</tbody>
Finally I can fix up the existing test cases for creating new records as they will no longer work now that the original "New" button has been hijacked by the modal version. I can also add a test to ensure that the broadcast is adding the new record on to the page as well.
test/system/item_sell_packs_test.rb
test 'should create item sell pack' do
visit item_sell_packs_url
click_on 'New'
find('#item_sell_pack_new_canonical--toggle').click()
fill_in 'Name', with: "a new pack"
click_on 'Save'
assert_selector("a", text: "a new pack")
end
Subscribe to my newsletter
Read articles from Andrew Foster directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Andrew Foster
Andrew Foster
I am a web developer who has been in the industry since 1995. My current tech stack preference is Ruby on Rails with JavaScript. Originally from Australia but now living in Scotland.