Nested Forms
Added in 2.1.0
Matestack provides functionality for reactive nested forms.
This works in conjunction with rails' accepts_nested_attributes_for. From the rails documentation on nested attributes:
Nested attributes allow you to save attributes on associated records through the parent. By default nested attribute updating is turned off and you can enable it using the accepts_nested_attributes_for class method. When you enable nested attributes an attribute writer is defined on the model.
There is a little bit of setup required to enable this. There's a need for accepts_nested_attributes_for, index_errors on a models' has_many associations and an ActiveRecord patch.
Consider the following model setup, which is the same model found in the dummy app in the spec directory (active in this dummy app):
1
class DummyModel < ApplicationRecord
2
validates :title, presence: true, uniqueness: true
3
has_many :dummy_child_models, index_errors: true
4
accepts_nested_attributes_for :dummy_child_models, allow_destroy: true
5
end
6
7
class DummyChildModel < ApplicationRecord
8
validates :title, presence: true, uniqueness: true
9
end
Copied!

Index Errors

Note the has_many :dummy_child_models, index_errors: true declaration in the Dummy Model declaration above.
Normally with rails, when rendering forms using Active Record models, errors are available on individual model instances. When using accepts_nested_attributes_for, error messages sent as JSON are not as useful because it is not possible to figure out which associated model object the error relates to.
From rails 5, we can add an index to errors on nested models. We can add the option index_errors: true to has_many association to enable this behaviour on individual association.

ActiveRecord Patch

Matestack nested forms support requires an ActiveRecord patch. This is because index_errors does not consider indexes of the correct existing sub records.
See rails issue #24390
Add this monkey patch to your rails app
1
module ActiveRecord
2
module AutosaveAssociation
3
def validate_collection_association(reflection)
4
if association = association_instance_get(reflection.name)
5
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
6
all_records = association.target.find_all
7
records.each do |record|
8
index = all_records.find_index(record)
9
association_valid?(reflection, record, index)
10
end
11
end
12
end
13
end
14
end
15
end
Copied!

Controller Setup

Adjusting strong params for nested form support does not differ from what needs to be done when using classsic Rails forms, e.g.:
1
def dummy_model_params
2
params.require(:dummy_model).permit(
3
:title,
4
dummy_child_models_attributes: [:id, :_destroy, :title]
5
)
6
end
Copied!

Example

1
class ExamplePage < Matestack::Ui::Page
2
3
def prepare
4
@dummy_model = DummyModel.new
5
# optional: build new instances before render:
6
@dummy_model.dummy_child_models.build(title: "init-value")
7
@dummy_model.dummy_child_models.build
8
end
9
10
def response
11
# use a normal form config, nothing new here
12
matestack_form form_config do
13
form_input key: :title, type: :text, label: "dummy_model_title_input"
14
15
# use all kind of input components for the parent model here!
16
17
@dummy_model.dummy_child_models.each do |dummy_child_model|
18
dummy_child_model_form(dummy_child_model)
19
end
20
21
# optional!
22
# lives outside of form_fields_for
23
form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do
24
# type: :button is important! otherwise form submission may be triggered when adding an item.
25
button "add", type: :button
26
end
27
28
button "Submit me!", type: :submit
29
end
30
end
31
32
# use a partial for all dummy child model forms
33
# used for existing child models or as a 'prototype' form for new child models
34
# when called from form_fields_for_add_item prototype, the first param is nil
35
def dummy_child_model_form dummy_child_model = DummyChildModel.new
36
form_fields_for dummy_child_model, key: :dummy_child_models_attributes do
37
# do not use IDs for input fields!
38
# IDs are assigned automatically in order to have unique IDs for each input
39
form_input key: :title, type: :text, label: "dummy-child-model-title-input"
40
41
# use all kind of input components for the child model here!
42
43
# optional!
44
# has to be placed within a form_fields_for component
45
form_fields_for_remove_item do
46
# type: :button is important! otherwise form submission may be triggered when removing an item.
47
button "remove", type: :button
48
end
49
end
50
end
51
end
Copied!

Dynamically Adding Nested Items (Optional)

As in the example above, you can dynamically add nested items. As the comment in the code suggests type: :button is important, otherwise form submission may be triggered when removing an item.
1
# lives outside of a form_fields_for component!
2
form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do
3
# type: :button is important! otherwise form submission may be triggered when removing an item
4
button "add", type: :button
5
end
Copied!

Dynamically Removing Nested Items (Optional)

As in the example above, as well as dynamically adding items, you can dynamically remove nested items. Again, important: type: :button is important, otherwise form submission may be triggered when adding an item.
1
# has to be placed within a form_fields_for component!
2
form_fields_for_remove_item do
3
# type: :button is important! otherwise form submission may be triggered when adding an item.
4
button "remove", type: :button
5
end
Copied!