{"id":13400234,"url":"https://github.com/trailblazer/reform","last_synced_at":"2025-12-18T05:49:26.637Z","repository":{"id":8275139,"uuid":"9764362","full_name":"trailblazer/reform","owner":"trailblazer","description":"Form objects decoupled from models.","archived":false,"fork":false,"pushed_at":"2025-08-25T12:50:07.000Z","size":1931,"stargazers_count":2494,"open_issues_count":39,"forks_count":179,"subscribers_count":41,"default_branch":"master","last_synced_at":"2025-11-30T06:45:20.967Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://trailblazer.to/2.1/docs/reform.html","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/trailblazer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2013-04-30T05:02:35.000Z","updated_at":"2025-11-13T09:47:45.000Z","dependencies_parsed_at":"2024-01-05T21:59:28.110Z","dependency_job_id":null,"html_url":"https://github.com/trailblazer/reform","commit_stats":{"total_commits":1275,"total_committers":86,"mean_commits":"14.825581395348838","dds":"0.21568627450980393","last_synced_commit":"ab73e1813a2e00ca409b71f7db5ccb56138775d5"},"previous_names":["apotonick/reform"],"tags_count":54,"template":false,"template_full_name":null,"purl":"pkg:github/trailblazer/reform","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trailblazer%2Freform","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trailblazer%2Freform/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trailblazer%2Freform/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trailblazer%2Freform/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/trailblazer","download_url":"https://codeload.github.com/trailblazer/reform/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trailblazer%2Freform/sbom","scorecard":{"id":896616,"data":{"date":"2025-08-11","repo":{"name":"github.com/trailblazer/reform","commit":"ab73e1813a2e00ca409b71f7db5ccb56138775d5"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.1,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":3,"reason":"Found 8/22 approved changesets -- score normalized to 3","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Warn: no topLevel permission defined: .github/workflows/ci_jruby.yml:1","Warn: no topLevel permission defined: .github/workflows/ci_legacy.yml:1","Warn: no topLevel permission defined: .github/workflows/ci_truffleruby.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_jruby.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci_jruby.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci_jruby.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci_jruby.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_legacy.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci_legacy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci_legacy.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci_legacy.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci_truffleruby.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci_truffleruby.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci_truffleruby.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/trailblazer/reform/ci_truffleruby.yml/master?enable=pin","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   4 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 17 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-24T13:57:27.601Z","repository_id":8275139,"created_at":"2025-08-24T13:57:27.601Z","updated_at":"2025-08-24T13:57:27.601Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27753776,"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","status":"online","status_checked_at":"2025-12-15T02:00:09.782Z","response_time":96,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2024-07-30T19:00:49.799Z","updated_at":"2025-12-18T05:49:26.599Z","avatar_url":"https://github.com/trailblazer.png","language":"Ruby","funding_links":[],"categories":["Ruby","Uncategorized","Form Builder"],"sub_categories":["Uncategorized"],"readme":"# Reform\n\n[![Gitter Chat](https://badges.gitter.im/trailblazer/chat.svg)](https://gitter.im/trailblazer/chat)\n[![TRB Newsletter](https://img.shields.io/badge/TRB-newsletter-lightgrey.svg)](http://trailblazer.to/newsletter/)\n[![Build\nStatus](https://travis-ci.org/trailblazer/reform.svg)](https://travis-ci.org/trailblazer/reform)\n[![Gem Version](https://badge.fury.io/rb/reform.svg)](http://badge.fury.io/rb/reform)\n\n_Form objects decoupled from your models._\n\nReform gives you a form object with validations and nested setup of models. It is completely framework-agnostic and doesn't care about your database.\n\nAlthough reform can be used in any Ruby framework, it comes with [Rails support](#rails-integration), works with simple_form and other form gems, allows nesting forms to implement has_one and has_many relationships, can [compose a form](#compositions) from multiple objects and gives you coercion.\n\n## Full Documentation\n\nReform is part of the [Trailblazer](http://trailblazer.to) framework. [Full documentation](http://trailblazer.to/2.1/docs/reform.html) is available on the project site.\n\n## Reform 2.2\n\nTemporary note: Reform 2.2 does **not automatically load Rails files** anymore (e.g. `ActiveModel::Validations`). You need the `reform-rails` gem, see [Installation](#installation).\n\n## Defining Forms\n\nForms are defined in separate classes. Often, these classes partially map to a model.\n\n```ruby\nclass AlbumForm \u003c Reform::Form\n  property :title\n  validates :title, presence: true\nend\n```\n\nFields are declared using `::property`. Validations work exactly as you know it from Rails or other frameworks. Note that validations no longer go into the model.\n\n\n## The API\n\nForms have a ridiculously simple API with only a handful of public methods.\n\n1. `#initialize` always requires a model that the form represents.\n2. `#validate(params)` updates the form's fields with the input data (only the form, _not_ the model) and then runs all validations. The return value is the boolean result of the validations.\n3. `#errors` returns validation messages in a classic ActiveModel style.\n4. `#sync` writes form data back to the model. This will only use setter methods on the model(s).\n5. `#save` (optional) will call `#save` on the model and nested models. Note that this implies a `#sync` call.\n6. `#prepopulate!` (optional) will run pre-population hooks to \"fill out\" your form before rendering.\n\nIn addition to the main API, forms expose accessors to the defined properties. This is used for rendering or manual operations.\n\n\n## Setup\n\nIn your controller or operation you create a form instance and pass in the models you want to work on.\n\n```ruby\nclass AlbumsController\n  def new\n    @form = AlbumForm.new(Album.new)\n  end\n```\n\nThis will also work as an editing form with an existing album.\n\n```ruby\ndef edit\n  @form = AlbumForm.new(Album.find(1))\nend\n```\n\nReform will read property values from the model in setup. In our example, the `AlbumForm` will call `album.title` to populate the `title` field.\n\n## Rendering Forms\n\nYour `@form` is now ready to be rendered, either do it yourself or use something like Rails' `#form_for`, `simple_form` or `formtastic`.\n\n```haml\n= form_for @form do |f|\n  = f.input :title\n```\n\nNested forms and collections can be easily rendered with `fields_for`, etc. Note that you no longer pass the model to the form builder, but the Reform instance.\n\nOptionally, you might want to use the `#prepopulate!` method to pre-populate fields and prepare the form for rendering.\n\n\n## Validation\n\nAfter form submission, you need to validate the input.\n\n```ruby\nclass SongsController\n  def create\n    @form = SongForm.new(Song.new)\n\n    #=\u003e params: {song: {title: \"Rio\", length: \"366\"}}\n\n    if @form.validate(params[:song])\n```\n\nThe `#validate` method first updates the values of the form - the underlying model is still treated as immutuable and *remains unchanged*. It then runs all validations you provided in the form.\n\nIt's the only entry point for updating the form. This is per design, as separating writing and validation doesn't make sense for a form.\n\nThis allows rendering the form after `validate` with the data that has been submitted. However, don't get confused, the model's values are still the old, original values and are only changed after a `#save` or `#sync` operation.\n\n\n## Syncing Back\n\nAfter validation, you have two choices: either call `#save` and let Reform sort out the rest. Or call `#sync`, which will write all the properties back to the model. In a nested form, this works recursively, of course.\n\nIt's then up to you what to do with the updated models - they're still unsaved.\n\n\n## Saving Forms\n\nThe easiest way to save the data is to call `#save` on the form.\n\n```ruby\nif @form.validate(params[:song])\n  @form.save  #=\u003e populates album with incoming data\n              #   by calling @form.album.title=.\nelse\n  # handle validation errors.\nend\n```\n\nThis will sync the data to the model and then call `album.save`.\n\nSometimes, you need to do saving manually.\n\n## Default values\n\nReform allows default values to be provided for properties.\n\n```ruby\nclass AlbumForm \u003c Reform::Form\n  property :price_in_cents, default: 9_95\nend\n```\n\n## Saving Forms Manually\n\nCalling `#save` with a block will provide a nested hash of the form's properties and values. This does **not call `#save` on the models** and allows you to implement the saving yourself.\n\nThe block parameter is a nested hash of the form input.\n\n```ruby\n  @form.save do |hash|\n    hash      #=\u003e {title: \"Greatest Hits\"}\n    Album.create(hash)\n  end\n```\n\nYou can always access the form's model. This is helpful when you were using populators to set up objects when validating.\n\n```ruby\n  @form.save do |hash|\n    album = @form.model\n\n    album.update_attributes(hash[:album])\n  end\n```\n\n\n## Nesting\n\nReform provides support for nested objects. Let's say the `Album` model keeps some associations.\n\n```ruby\nclass Album \u003c ActiveRecord::Base\n  has_one  :artist\n  has_many :songs\nend\n```\n\nThe implementation details do not really matter here, as long as your album exposes readers and writes like `Album#artist` and `Album#songs`, this allows you to define nested forms.\n\n\n```ruby\nclass AlbumForm \u003c Reform::Form\n  property :title\n  validates :title, presence: true\n\n  property :artist do\n    property :full_name\n    validates :full_name, presence: true\n  end\n\n  collection :songs do\n    property :name\n  end\nend\n```\n\nYou can also reuse an existing form from elsewhere using `:form`.\n\n```ruby\nproperty :artist, form: ArtistForm\n```\n\n## Nested Setup\n\nReform will wrap defined nested objects in their own forms. This happens automatically when instantiating the form.\n\n```ruby\nalbum.songs #=\u003e [\u003cSong name:\"Run To The Hills\"\u003e]\n\nform = AlbumForm.new(album)\nform.songs[0] #=\u003e \u003cSongForm model: \u003cSong name:\"Run To The Hills\"\u003e\u003e\nform.songs[0].name #=\u003e \"Run To The Hills\"\n```\n\n### Nested Rendering\n\nWhen rendering a nested form you can use the form's readers to access the nested forms.\n\n```haml\n= text_field :title,         @form.title\n= text_field \"artist[name]\", @form.artist.name\n```\n\nOr use something like `#fields_for` in a Rails environment.\n\n```haml\n= form_for @form do |f|\n  = f.text_field :title\n\n  = f.fields_for :artist do |a|\n    = a.text_field :name\n```\n\n## Nested Processing\n\n`validate` will assign values to the nested forms. `sync` and `save` work analogue to the non-nested form, just in a recursive way.\n\nThe block form of `#save` would give you the following data.\n\n```ruby\n@form.save do |nested|\n  nested #=\u003e {title:  \"Greatest Hits\",\n         #    artist: {name: \"Duran Duran\"},\n         #    songs: [{title: \"Hungry Like The Wolf\"},\n         #            {title: \"Last Chance On The Stairways\"}]\n         #   }\n  end\n```\n\nThe manual saving with block is not encouraged. You should rather check the Disposable docs to find out how to implement your manual tweak with the official API.\n\n\n## Populating Forms\n\nVery often, you need to give Reform some information how to create or find nested objects when `validate`ing. This directive is called _populator_ and [documented here](http://trailblazer.to/2.1/docs/reform.html#reform-populators).\n\n## Installation\n\nAdd this line to your Gemfile:\n\n```ruby\ngem \"reform\"\n```\n\nReform works fine with Rails 3.1-5.0. However, inheritance of validations with `ActiveModel::Validations` is broken in Rails 3.2 and 4.0.\n\nSince Reform 2.2, you have to add the `reform-rails` gem to your `Gemfile` to automatically load ActiveModel/Rails files.\n\n```ruby\ngem \"reform-rails\"\n```\n\nSince Reform 2.0 you need to specify which **validation backend** you want to use (unless you're in a Rails environment where ActiveModel will be used).\n\nTo use ActiveModel (not recommended because very out-dated).\n\n```ruby\nrequire \"reform/form/active_model/validations\"\nReform::Form.class_eval do\n  include Reform::Form::ActiveModel::Validations\nend\n```\n\nTo use dry-validation (recommended).\n\n```ruby\nrequire \"reform/form/dry\"\nReform::Form.class_eval do\n  feature Reform::Form::Dry\nend\n```\n\nPut this in an initializer or on top of your script.\n\n\n## Compositions\n\nReform allows to map multiple models to one form. The [complete documentation](https://github.com/apotonick/disposable#composition) is here, however, this is how it works.\n\n```ruby\nclass AlbumForm \u003c Reform::Form\n  include Composition\n\n  property :id,    on: :album\n  property :title, on: :album\n  property :songs, on: :cd\n  property :cd_id, on: :cd, from: :id\nend\n```\nWhen initializing a composition, you have to pass a hash that contains the composees.\n\n```ruby\nAlbumForm.new(album: album, cd: CD.find(1))\n```\n\n## More\n\nReform comes many more optional features, like hash fields, coercion, virtual fields, and so on. Check the [full documentation here](http://trailblazer.to/2.1/docs/reform.html).\n\n[![](http://trailblazer.to/images/3dbuch-freigestellt.png)](https://leanpub.com/trailblazer)\n\nReform is part of the [Trailblazer project](http://trailblazer.to). Please [buy my book](https://leanpub.com/trailblazer) to support the development and learn everything about Reform - there's two chapters dedicated to Reform!\n\n\n## Security And Strong_parameters\n\nBy explicitly defining the form layout using `::property` there is no more need for protecting from unwanted input. `strong_parameter` or `attr_accessible` become obsolete. Reform will simply ignore undefined incoming parameters.\n\n## This is not Reform 1.x!\n\nTemporary note: This is the README and API for Reform 2. On the public API, only a few tiny things have changed. Here are the [Reform 1.2 docs](https://github.com/trailblazer/reform/blob/v1.2.6/README.md).\n\nAnyway, please upgrade and _report problems_ and do not simply assume that we will magically find out what needs to get fixed. When in trouble, join us on [Gitter](https://gitter.im/trailblazer/chat).\n\n[Full documentation for Reform](http://trailblazer.to/2.1/docs/reform.html) is available online, or support us and grab the [Trailblazer book](https://leanpub.com/trailblazer). There is an [Upgrading Guide](http://trailblazer.to/2.1/docs/reform.html#reform-upgrading-guide) to help you migrate through versions.\n\n### Attributions!!!\n\nGreat thanks to [Blake Education](https://github.com/blake-education) for giving us the freedom and time to develop this project in 2013 while working on their project.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrailblazer%2Freform","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftrailblazer%2Freform","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrailblazer%2Freform/lists"}