|
| 1 | +# Nested Forms |
| 2 | + |
| 3 | +Matestack provides functionality for reactive nested forms. |
| 4 | + |
| 5 | +This works in conjunction with rails' `accepts_nested_attributes_for`. From the rails documentation on [nested attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html): |
| 6 | + |
| 7 | +> 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. |
| 8 | +
|
| 9 | +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. |
| 10 | + |
| 11 | + |
| 12 | +Consider the following model setup, which is the same model found in the dummy app in the spec directory (active in this dummy app): |
| 13 | + |
| 14 | +```ruby |
| 15 | +class DummyModel < ApplicationRecord |
| 16 | + validates :title, presence: true, uniqueness: true |
| 17 | + has_many :dummy_child_models, index_errors: true |
| 18 | + accepts_nested_attributes_for :dummy_child_models, allow_destroy: true |
| 19 | +end |
| 20 | + |
| 21 | +class DummyChildModel < ApplicationRecord |
| 22 | + validates :title, presence: true, uniqueness: true |
| 23 | +end |
| 24 | +``` |
| 25 | + |
| 26 | +## Index Errors |
| 27 | + |
| 28 | +Note the `has_many :dummy_child_models, index_errors: true` declaration in the `Dummy Model` declaration above. |
| 29 | + |
| 30 | +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. |
| 31 | + |
| 32 | +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. |
| 33 | + |
| 34 | + |
| 35 | +## ActiveRecord Patch |
| 36 | + |
| 37 | +Matestack nested forms support requires an ActiveRecord patch. This is because `index_errors` does not consider indexes of the correct existing sub records. |
| 38 | + |
| 39 | +See rails [issue #24390](https://github.com/rails/rails/issues/24390) |
| 40 | + |
| 41 | +Add this monkey patch to your rails app |
| 42 | + |
| 43 | +```ruby |
| 44 | +module ActiveRecord |
| 45 | + module AutosaveAssociation |
| 46 | + def validate_collection_association(reflection) |
| 47 | + if association = association_instance_get(reflection.name) |
| 48 | + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) |
| 49 | + all_records = association.target.find_all |
| 50 | + records.each do |record| |
| 51 | + index = all_records.find_index(record) |
| 52 | + association_valid?(reflection, record, index) |
| 53 | + end |
| 54 | + end |
| 55 | + end |
| 56 | + end |
| 57 | + end |
| 58 | +end |
| 59 | +``` |
| 60 | + |
| 61 | +## Example |
| 62 | + |
| 63 | +```ruby |
| 64 | +class ExamplePage < Matestack::Ui::Page |
| 65 | + |
| 66 | + def prepare |
| 67 | + @dummy_model = DummyModel.new |
| 68 | + @dummy_model.dummy_child_models.build(title: "init-value") |
| 69 | + @dummy_model.dummy_child_models.build |
| 70 | + end |
| 71 | + |
| 72 | + def response |
| 73 | + matestack_form form_config do |
| 74 | + form_input key: :title, type: :text, label: "dummy_model_title_input", id: "dummy_model_title_input" |
| 75 | + |
| 76 | + @dummy_model.dummy_child_models.each do |dummy_child_model| |
| 77 | + dummy_child_model_form dummy_child_model |
| 78 | + end |
| 79 | + |
| 80 | + form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do |
| 81 | + # type: :button is important! otherwise remove on first item is triggered on enter |
| 82 | + button "add", type: :button |
| 83 | + end |
| 84 | + |
| 85 | + form_fields_for_remove_item do |
| 86 | + # id is just required in this spec, but type: :button is important! otherwise remove on first item is triggered on enter |
| 87 | + button "remove", ":id": "'remove'+nestedFormRuntimeId", type: :button |
| 88 | + end |
| 89 | + |
| 90 | + button "Submit me!" |
| 91 | + |
| 92 | + toggle show_on: "success", hide_after: 1000 do |
| 93 | + plain "success!" |
| 94 | + end |
| 95 | + toggle show_on: "failure", hide_after: 1000 do |
| 96 | + plain "failure!" |
| 97 | + end |
| 98 | + end |
| 99 | + end |
| 100 | + |
| 101 | + def dummy_child_model_form dummy_child_model = DummyChildModel.new |
| 102 | + form_fields_for dummy_child_model, key: :dummy_child_models_attributes do |
| 103 | + form_input key: :title, type: :text, label: "dummy-child-model-title-input" |
| 104 | + form_fields_for_remove_item do |
| 105 | + # id is just required in this spec, but type: :button is important! otherwise remove on first item is triggered on enter |
| 106 | + button "remove", ":id": "'remove'+nestedFormRuntimeId", type: :button |
| 107 | + end |
| 108 | + end |
| 109 | + end |
| 110 | +end |
| 111 | +``` |
| 112 | + |
| 113 | +### Dynamically Adding Nested Items |
| 114 | + |
| 115 | +As in the example above, you can dynamically add nested items. As the comment in the code suggests `type: :button` is important, otherwise remove on first item is triggered on enter. |
| 116 | + |
| 117 | +```ruby |
| 118 | +form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do |
| 119 | +# type: :button is important! otherwise remove on first item is triggered on enter |
| 120 | +button "add", type: :button |
| 121 | +end |
| 122 | +``` |
| 123 | + |
| 124 | +### Dynamically Removing Nested Items |
| 125 | + |
| 126 | +As in the example above, as well as dynamically adding items, you can dynamically remove nested items. Again, important: `type: :button` is important, otherwise remove on first item is triggered on enter. |
| 127 | + |
| 128 | +```ruby |
| 129 | +form_fields_for_remove_item do |
| 130 | + # id is just required in this spec, but type: :button is important! otherwise remove on first item is triggered on enter |
| 131 | + button "remove", ":id": "'remove'+nestedFormRuntimeId", type: :button |
| 132 | +end |
| 133 | +``` |
0 commit comments