This card describes two variants, that add a more intuitive workflow when working with nested attributes in Rails + Unpoly.
Example
For the following examples we use a simple data model where a user has zero or more tasks.
class ExampleMigration < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :full_name
t.timestamps
end
create_table :tasks do |t|
t.string :title
t.references :user
t.timestamps
end
end
end
class Task < ApplicationRecord
belongs_to :user
validates :title, presence: true
end
class User < ApplicationRecord
has_many :tasks
accepts_nested_attributes_for :tasks, allow_destroy: true, reject_if: ->(attributes) { attributes['title'].blank? }
validates :full_name, presence: true
end
Without JS
Back in times we used to add n-empty nested records and rejected them in case they were not filled.
resources :variant_1_users, only: [:edit, :update]
class Variant1UsersController < ApplicationController
def edit
load_user
3.times { @user.tasks.build }
end
def update
load_user
@user.attributes = user_params
if @user.save
flash[:notice] = 'User saved successfully.'
redirect_to(edit_variant_1_user_path(@user))
else
flash[:notice] = 'User could not be saved.'
render :edit
end
end
private
def load_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(
:full_name,
tasks_attributes: [
:id,
:title,
:_destroy,
]
)
end
end
- if flash[:notice]
= flash[:notice]
%h1 Edit User
= form_with(model: @user, url: variant_1_user_path(@user)) do |form|
- if @user.errors.any?
%h2= pluralize(@user.errors.count, "error") + " prohibited this user from being saved:"
%ul
- @user.errors.full_messages.each do |message|
%li= message
= form.label :full_name
= form.text_field :full_name
%h3 Tasks
= form.fields_for :tasks do |task_form|
%div
= task_form.label :title
= task_form.text_field :title
= task_form.check_box :_destroy
= task_form.label :_destroy, "Remove task"
= form.submit
Adding nested records via template HTML with JS
resources :variant_2_users, only: [:edit, :update]
class Variant2UsersController < ApplicationController
def edit
load_user
end
def update
load_user
@user.attributes = user_params
if @user.save
flash[:notice] = 'User saved successfully.'
redirect_to(edit_variant_2_user_path(@user))
else
flash[:notice] = 'User could not be saved.'
render :edit
end
end
private
def load_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(
:full_name,
tasks_attributes: [
:id,
:title,
:_destroy,
]
)
end
end
- if flash[:notice]
= flash[:notice]
%h1 Edit User
= form_with(model: @user, url: variant_2_user_path(@user)) do |form|
- if @user.errors.any?
%h2= pluralize(@user.errors.count, "error") + " prohibited this user from being saved:"
%ul
- @user.errors.full_messages.each do |message|
%li= message
= form.label :full_name
= form.text_field :full_name
%h3 Tasks
.div(nested-records)
= form.fields_for :tasks do |task_form|
%div
= task_form.label :title
= task_form.text_field :title
= task_form.check_box :_destroy
= task_form.label :_destroy, "Remove task"
= form.fields_for :tasks, Task.new do |task_form|
%template(nested-records--template)
= task_form.label :title
= task_form.text_field :title
= task_form.check_box :_destroy
= task_form.label :_destroy, "Remove task"
%button(nested-records--add type="button") Add task
= form.submit
const NESTED_ID = /(?<=_attributes_)(\d+)/
const NESTED_NAME = /(?<=_attributes]\[)(\d+)/
function cloneTemplate(template) {
const uid = new Date().valueOf()
const clone = template.content.cloneNode(true)
const anchor = document.createElement('div')
for (let element of clone.querySelectorAll('[id]')) { element.id = element.id.replace(NESTED_ID, uid) }
for (let element of clone.querySelectorAll('[for]')) { element.htmlFor = element.htmlFor.replace(NESTED_ID, uid) }
for (let element of clone.querySelectorAll('[name]')) { element.name = element.name.replace(NESTED_NAME, uid) }
template.insertAdjacentElement('beforebegin', anchor)
anchor.appendChild(clone)
}
up.compiler('[nested-records]', function(container) {
const template = container.querySelector('[nested-records--template]')
const addButton = container.querySelector('[nested-records--add]')
addButton.addEventListener('click', () => cloneTemplate(template))
})
Adding nested records via XHR with JS
resources :variant_3_users, only: [:edit, :update]
class Variant3UsersController < ApplicationController
def edit
load_user
end
def update
load_user
@user.attributes = user_params
if up.validate?
render :edit
elsif @user.save
flash[:notice] = 'User saved successfully.'
redirect_to(edit_variant_3_user_path(@user))
else
flash[:notice] = 'User could not be saved.'
render :edit
end
end
private
def load_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(
:full_name,
tasks_attributes: [
:id,
:title,
:_destroy,
]
)
end
end
- if flash[:notice]
= flash[:notice]
%h1 Edit User
= form_with(model: @user, url: variant_3_user_path(@user)) do |form|
- if @user.errors.any?
%h2= pluralize(@user.errors.count, "error") + " prohibited this user from being saved:"
%ul
- @user.errors.full_messages.each do |message|
%li= message
= form.label :full_name
= form.text_field :full_name
%h3 Tasks
#task-fields
- @user.tasks.build
= form.fields_for :tasks do |task_form|
%div
= task_form.label :title
= task_form.text_field :title, 'up-validate' => '#task-fields'
= task_form.check_box :_destroy
= task_form.label :_destroy, "Remove task"
= form.submit
Posted by Emanuel to makandra dev (2024-05-15 07:08)