{"id":18513328,"url":"https://github.com/jbox-web/action_form","last_synced_at":"2025-04-09T06:33:10.678Z","repository":{"id":146100991,"uuid":"171366538","full_name":"jbox-web/action_form","owner":"jbox-web","description":"Create nested forms, easy ;)","archived":false,"fork":false,"pushed_at":"2024-10-29T03:29:02.000Z","size":414,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-29T04:26:53.810Z","etag":null,"topics":["form","form-object","form-validation","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jbox-web.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-02-18T22:38:12.000Z","updated_at":"2024-10-29T03:29:05.000Z","dependencies_parsed_at":"2024-08-11T05:11:19.013Z","dependency_job_id":"1792224d-c379-48ad-96e8-19724087715b","html_url":"https://github.com/jbox-web/action_form","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jbox-web%2Faction_form","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jbox-web%2Faction_form/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jbox-web%2Faction_form/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jbox-web%2Faction_form/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jbox-web","download_url":"https://codeload.github.com/jbox-web/action_form/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223367238,"owners_count":17134060,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["form","form-object","form-validation","rails","ruby"],"created_at":"2024-11-06T15:37:36.137Z","updated_at":"2024-11-06T15:37:37.060Z","avatar_url":"https://github.com/jbox-web.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ActionForm\n\n[![GitHub license](https://img.shields.io/github/license/jbox-web/action_form.svg)](https://github.com/jbox-web/action_form/blob/master/LICENSE)\n[![GitHub release](https://img.shields.io/github/release/jbox-web/action_form.svg)](https://github.com/jbox-web/action_form/releases/latest)\n[![CI](https://github.com/jbox-web/action_form/workflows/CI/badge.svg)](https://github.com/jbox-web/action_form/actions)\n[![Code Climate](https://codeclimate.com/github/jbox-web/action_form/badges/gpa.svg)](https://codeclimate.com/github/jbox-web/action_form)\n[![Test Coverage](https://codeclimate.com/github/jbox-web/action_form/badges/coverage.svg)](https://codeclimate.com/github/jbox-web/action_form/coverage)\n\nSet your models free from the `accepts_nested_attributes_for` helper. ActionForm provides an object-oriented approach to represent your forms by building a form object, rather than relying on Active Record internals for doing this. Form objects provide an API to describe the models involved in the form, their attributes and validations. A form object deals with create/update actions of nested objects in a more seamless way.\n\n## Installation\n\nPut this in your `Gemfile` :\n\n```ruby\ngit_source(:github){ |repo_name| \"https://github.com/#{repo_name}.git\" }\n\ngem 'action_form', github: 'jbox-web/action_form', tag: '1.4.0'\n```\n\nthen run `bundle install`.\n\n## Defining Forms\n\nConsider an example where you want to create/update a conference that can have many speakers which can present a single presentation with one form submission. You start by defining a form to represent the root model, `Conference`:\n\n```ruby\nclass ConferenceForm \u003c ActionForm::Base\n  self.main_model = :conference\n\n  attributes :name, :city\n\n  validates :name, :city, presence: true\nend\n```\n\nYour form object has to subclass `ActionForm::Base` in order to gain the necessary API. When defining the form, you have to specify the main_model the form represents with the following line:\n\n```ruby\nself.main_model = :conference\n```\n\nTo add fields to the form, use the `attributes` or `attribute` class method. The form can also define validation rules for the model it represents. For the `presence` validation rule there is a short inline syntax:\n\n```ruby\nclass ConferenceForm \u003c ActionForm::Base\n  attributes :name, :city, required: true\nend\n```\n\n## The API\n\nThe `ActionForm::Base` class provides a simple API with only a few instance/class methods. Below are listed the instance methods:\n\n1. `initialize(model)` accepts an instance of the model that the form represents.\n2. `submit(params)` updates the main form's model and nested models with the posted parameters. The models are not saved/updated until you call `save`.\n3. `errors` returns validation messages in a classy Active Model style.\n4. `save` will call `save` on the model and nested models. This method will validate the model and nested models and if no error arises then it will save them and return true.\n\nThe following are the class methods:\n\n1. `attributes` accepts the names of attributes to define on the form. If you want to declare a presence validation rule for the given attributes, you can pass in the `required: true` option as showcased above. The `attribute` method is aliased to the `attributes` method.\n2. `association(name, options={}, \u0026block)` defines a nested form for the `name` model. If the model is a `has_many` association you can pass in the `records: x` option and fields to create `x` objects will be rendered. If you pass a block, you can define another nested form the same way.\n\nIn addition to the main API, forms expose accessors to the defined attributes. This is used for rendering or manual operations.\n\n## Setup\n\nIn your controller you create a form instance and pass in the model you want to work on.\n\n```ruby\nclass ConferencesController\n  def new\n    conference = Conference.new\n    @conference_form = ConferenceForm.new(conference)\n  end\nend\n```\n\nYou can also setup the form for editing existing items.\n\n```ruby\nclass ConferencesController\n  def edit\n    conference = Conference.find(params[:id])\n    @conference_form = ConferenceForm.new(conference)\n  end\nend\n```\n\nActionForm will read property values from the model in setup. Given the following form class.\n\n```ruby\nclass ConferenceForm \u003c ActionForm::Base\n  attribute :name\nend\n```\n\nInternally, this form will call `conference.name` to populate the name field.\n\n## Rendering Forms\n\nYour `@conference_form` is now ready to be rendered, either do it yourself or use something like Rails' `form_for`, `simple_form` or `formtastic`.\n\n```erb\n\u003c%= form_for @conference_form do |f| %\u003e\n  \u003c%= f.text_field :name %\u003e\n  \u003c%= f.text_field :city %\u003e\n\u003c% end %\u003e\n```\n\nNested forms and collections can be easily rendered with `fields_for`, etc. Just use ActionForm as if it would be an Active Model instance in the view layer.\n\n## Syncing Back\n\nAfter setting up your form object, you can populate the models with the submitted parameters.\n\n```ruby\nclass ConferencesController\n  def create\n    conference = Conference.new\n    @conference_form = ConferenceForm.new(conference)\n    @conference_form.submit(conference_params)\n  end\nend\n```\n\nThis will write all the properties back to the model. In a nested form, this works recursively, of course.\n\n## Saving Forms\n\nAfter the form is populated with the posted data, you can save the model by calling `save`.\n\n```ruby\nclass ConferencesController\n  def create\n    conference = Conference.new\n    @conference_form = ConferenceForm.new(conference)\n    @conference_form.submit(conference_params)\n\n    if @conference_form.save\n      redirect_to @conference_form, notice: \"Conference: #{@conference_form.name} was successfully created.\" }\n    else\n      render :new\n    end\n  end\nend\n```\n\nIf the `save` method returns false due to validation errors defined on the form, you can render it again with the data that has been submitted and the errors found.\n\n## Nesting Forms: 1-n Relations\n\nActionForm also gives you nested collections.\n\nLet's define the `has_many :speakers` collection association on the `Conference` model.\n\n```ruby\nclass Conference \u003c ActiveRecord::Base\n  has_many :speakers\n  validates :name, uniqueness: true\nend\n```\n\nThe form should look like this.\n\n```ruby\nclass ConferenceForm \u003c ActionForm::Base\n  attributes :name, :city, required: true\n\n  association :speakers do\n    attributes :name, :occupation, required: true\n  end\nend\n```\n\nBy default, the `association :speakers` declaration will create a single `Speaker` object. You can specify how many objects you want in your form to be rendered with the `new` action as follows: `association: speakers, records: 2`. This will create 2 new `Speaker` objects, and of course fields to create 2 `Speaker` objects. There are also some link helpers to dynamically add/remove objects from collection associations. Read below.\n\nThis basically works like a nested `property` that iterates over a collection of speakers.\n\n### has_many: Rendering\n\nActionForm will expose the collection using the `speakers` method.\n\n```erb\n\u003c%= form_for @conference_form |f| %\u003e\n  \u003c%= f.text_field :name %\u003e\n  \u003c%= f.text_field :city %\u003e\n\n  \u003c%= f.fields_for :speakers do |s| %\u003e\n    \u003c%= s.text_field :name %\u003e\n    \u003c%= s.text_field :occupation %\u003e\n  \u003c% end %\u003e\n\u003c% end %\u003e\n```\n\n## Nesting Forms: 1-1 Relations\n\nSpeakers are allowed to have 1 Presentation.\n\n```ruby\nclass Speaker \u003c ActiveRecord::Base\n  has_one :presentation\n  belongs_to :conference\n  validates :name, uniqueness: true\nend\n```\n\nThe full form should look like this:\n\n```ruby\nclass ConferenceForm \u003c ActionForm::Base\n  attributes :name, :city, required: true\n\n  association :speakers do\n    attribute :name, :occupation, required: true\n\n    association :presentation do\n      attribute :topic, :duration, required: true\n    end\n  end\nend\n```\n\n### has_one: Rendering\n\nUse `fields_for` in a Rails environment to correctly setup the structure of params.\n\n```erb\n\u003c%= form_for @conference_form |f| %\u003e\n  \u003c%= f.text_field :name %\u003e\n  \u003c%= f.text_field :city %\u003e\n\n  \u003c%= f.fields_for :speakers do |s| %\u003e\n    \u003c%= s.text_field :name %\u003e\n    \u003c%= s.text_field :occupation %\u003e\n\n    \u003c%= s.fields_for :presentation do |p| %\u003e\n      \u003c%= p.text_field :topic %\u003e\n      \u003c%= p.text_field :duration %\u003e\n    \u003c% end %\u003e\n  \u003c% end %\u003e\n\u003c% end %\u003e\n```\n\n## Dynamically Adding/Removing Nested Objects\n\nActionForm comes with two helpers to deal with this functionality:\n\n1. `link_to_add_association` will display a link that renders fields to create a new object.\n2. `link_to_remove_association` will display a link to remove a existing/dynamic object.\n\nIn order to use it you have to insert this line: `//= require action_form` to your `app/assets/javascript/application.js` file.\n\nIn our `ConferenceForm` we can dynamically create/remove `Speaker` objects. To do that we would write in the `app/views/conferences/_form.html.erb` partial:\n\n```erb\n\u003c%= form_for @conference_form do |f| %\u003e\n  \u003c% if @conference_form.errors.any? %\u003e\n    \u003cdiv id=\"error_explanation\"\u003e\n      \u003ch2\u003e\u003c%= pluralize(@conference_form.errors.count, \"error\") %\u003e prohibited this conference from being saved:\u003c/h2\u003e\n\n      \u003cul\u003e\n      \u003c% @conference_form.errors.full_messages.each do |message| %\u003e\n        \u003cli\u003e\u003c%= message %\u003e\u003c/li\u003e\n      \u003c% end %\u003e\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  \u003c% end %\u003e\n\n  \u003ch2\u003eConference Details\u003c/h2\u003e\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :name, \"Conference Name\" %\u003e\u003cbr\u003e\n    \u003c%= f.text_field :name %\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :city %\u003e\u003cbr\u003e\n    \u003c%= f.text_field :city %\u003e\n  \u003c/div\u003e\n\n  \u003ch2\u003eSpeaker Details\u003c/h2\u003e\n  \u003c%= f.fields_for :speakers do |speaker_fields| %\u003e\n    \u003c%= render \"speaker_fields\", :f =\u003e speaker_fields %\u003e\n  \u003c% end %\u003e\n\n  \u003cdiv class=\"links\"\u003e\n    \u003c%= link_to_add_association \"Add a Speaker\", f, :speakers %\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"actions\"\u003e\n    \u003c%= f.submit %\u003e\n  \u003c/div\u003e\n\u003c% end %\u003e\n```\n\nOur `app/views/conferences/_speaker_fields.html.erb` would be:\n\n```erb\n\u003cdiv class=\"nested-fields\"\u003e\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :name, \"Speaker Name\" %\u003e\u003cbr\u003e\n    \u003c%= f.text_field :name %\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"field\"\u003e\n    \u003c%= f.label :occupation %\u003e\u003cbr\u003e\n    \u003c%= f.text_field :occupation %\u003e\n  \u003c/div\u003e\n\n  \u003ch2\u003ePresentantions\u003c/h2\u003e\n  \u003c%= f.fields_for :presentation do |presentations_fields| %\u003e\n    \u003c%= render \"presentation_fields\", :f =\u003e presentations_fields %\u003e\n  \u003c% end %\u003e\n\n  \u003c%= link_to_remove_association \"Delete\", f %\u003e\n\u003c/div\u003e\n```\n\nAnd `app/views/conferences/_presentation_fields.html.erb` would be:\n\n```erb\n\u003cdiv class=\"field\"\u003e\n  \u003c%= f.label :topic %\u003e\u003cbr\u003e\n  \u003c%= f.text_field :topic %\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"field\"\u003e\n  \u003c%= f.label :duration %\u003e\u003cbr\u003e\n  \u003c%= f.text_field :duration %\u003e\n\u003c/div\u003e\n```\n\n## Plain Old Ruby Object Forms\n\nActionForm also can accept `ActiveModel::Model` instances as a model.\n\n```ruby\nclass Feedback\n  include ActiveModel::Model\n\n  attr_accessor :name, :body, :email\n\n  def save\n    FeedbackMailer.send_email(email, name, body)\n  end\nend\n```\n\nThe form should look like this.\n\n```ruby\nclass FeedbackForm \u003c ActionForm::Base\n  attributes :name, :body, :email, required: true\nend\n```\n\nAnd then in controller:\n\n```ruby\nclass FeedbacksController\n  def create\n    feedback = Feedback.new\n    @feedback_form = FeedbackForm.new(feedback)\n    @feedback_form.submit(feedback_params)\n\n    if @feedback_form.save\n      head :ok\n    else\n      render json: @feedback_form.errors\n    end\n  end\n```\n\n## Demos\n\nYou can find a list of applications using this gem in this repository: https://github.com/m-Peter/nested-form-examples .\nAll the examples are implemented in before/after pairs. The before is using the `accepts_nested_attributes_for`, while the after uses this gem to achieve the same functionality.\n\n## Credits\n\nSpecial thanks to the owners of the great gems that inspired this work:\n\n* [Nick Sutterer](https://github.com/apotonick) - creator of [reform](https://github.com/apotonick/reform)\n* [Nathan Van der Auwera](https://github.com/nathanvda) - creator of [cocoon](https://github.com/nathanvda/cocoon)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjbox-web%2Faction_form","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjbox-web%2Faction_form","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjbox-web%2Faction_form/lists"}