{"id":17656528,"url":"https://github.com/imdrasil/form_object","last_synced_at":"2025-05-07T11:43:49.113Z","repository":{"id":81841543,"uuid":"154114154","full_name":"imdrasil/form_object","owner":"imdrasil","description":"Form objects decoupled from models.","archived":false,"fork":false,"pushed_at":"2020-04-25T18:56:04.000Z","size":38,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-21T08:14:05.179Z","etag":null,"topics":["crystal","form-objects"],"latest_commit_sha":null,"homepage":null,"language":"Crystal","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/imdrasil.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2018-10-22T08:59:18.000Z","updated_at":"2024-03-04T07:21:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"63026077-b790-470b-8306-6779aeabe0fa","html_url":"https://github.com/imdrasil/form_object","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fform_object","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fform_object/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fform_object/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/imdrasil%2Fform_object/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/imdrasil","download_url":"https://codeload.github.com/imdrasil/form_object/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252873871,"owners_count":21817708,"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":["crystal","form-objects"],"created_at":"2024-10-23T14:33:22.837Z","updated_at":"2025-05-07T11:43:49.106Z","avatar_url":"https://github.com/imdrasil.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FormObject [![Build Status](https://travis-ci.org/imdrasil/form_object.svg)](https://travis-ci.org/imdrasil/form_object) [![Latest Release](https://img.shields.io/github/release/imdrasil/form_object.svg)](https://github.com/imdrasil/form_object/releases) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://imdrasil.github.io/form_object/versions)\n\nThis shard gives you an opportunity to separate form data from your model. Also you can move ny data-specific validation to form object level and be free from coercing data from the request instance - it will take care of it.\n\n\u003e ATM FormObject is designed to be used in air with [Jennifer](https://github.com/imdrasil/jennifer.cr) ORM but can be also used as ORM-agnostic tool but with some limitations.\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  form_object:\n    github: imdrasil/form_object\n```\n\n## Usage\n\nRequire FromObject somewhere after Jennifer:\n\n```crystal\nrequire \"jennifer\"\n# ...\nrequire \"form_object\"\nrequire \"form_object/coercer/pg\" # if you are going to use PG::Numeric\n```\n\nAlso it is important to notice that `form_object` modifies `HTTP::Request` core class to store body in private variable `@cached_body : IO::Memory?` of maximum size 1 GB. This is done because to allow request body multiple reading.\n\n### Defining Form\n\nForms are defined in the separate classes. Often (but not necessary) these classes are pretty similar to related models:\n\n```crystal\nclass PostForm \u003c FormObject::Base(Post)\n  attr :title, String\nend\n```\n\nUse `.attr` macro to define a field.\n\nAlso you can specify [any validation](https://github.com/imdrasil/jennifer.cr/blob/master/docs/validation.md) supported by Jennifer model.\n\n```crystal\nclass PostForm \u003c FormObject::Base(Post)\n  attr :title, String\n\n  validates_length :title, in: 1...255\nend\n\nf = PostForm.new(Post.new)\nf.title = \"a\" * 255\nf.valid? # false\nf.errors # Jennifer::Model::Errors\n```\n\nResource model translation messages are used for the form.\n\n#### Nesting\n\nTo define nested object use `.object` macro:\n\n```crystal\nclass AddressForm \u003c FormObject::Base(Address)\n  attr :street, Address\nend\n\nclass ContactForm \u003c FormObject::Base(Contact)\n  object :address, Address\nend\n```\n\nFor collection use `.collection` macro.\n\n##### Populators\n\nIn `#verify`, nested hash is passed. Form object by default will try to match nested hashes to the nested forms. But sometimes the incoming hash and the existing object graph are not matching 1-to-1. That's where populators will help you.\n\nYou have to declare a populator when the form has to deserialize nested input. ATM populator may be only a method name.\n\nPopulator is called only if an incoming part for particular object is present.\n\n```crystal\n# request with { addresses: [{ :street =\u003e \"Some street\" }]} payload\nform.verify(request) # will call populator once\n# request with { addresses: [] of String} payload\nform.verify(request) # will not call populator\n```\n\nPopulator for collection is executed for every collection part in the incoming hash.\n\n```crystal\nclass ContactForm \u003c FormObject::Base(Contact)\n  collection :addresses, Address, populator: :address_populator\n\n  def address_populator(collection, index, **opts)\n    if item = collection[index]?\n      item\n    else\n      item = AddressForm.new(Address.new({contact_id: resource.id}))\n      collection \u003c\u003c item\n      item\n    end\n  end\n```\n\nThis populator checks if a nested form is already existing by using `collection[index]?`. While the `index` argument represents where we are in the incoming array traversal, `collection` is identical to `self.addresses`.\n\nIt is very important that each populator invocation returns the *form* not the model.\n\n##### Delete\n\nPopulators can not only create, but also destroy. Let's say the following input is passed in.\n\n```crystal\n# request with the { addresses: [{:street =\u003e \"Street\", :id =\u003e 2, :_delete =\u003e \"1\" }] } payload\nform.verify(request)\n```\n\nYou can implement your own deletion:\n\n```crystal\nclass ContactForm \u003c FormObject::Base(Contact)\n  collection :addresses, Address, populator: :address_populator\n\n  property ids_to_destroy : Array(Int32)\n\n  def address_populator(context, **opts)\n    item = addresses.find { |address| address.id == context[\"id\"] }\n\n    if context[\"_delete\"]\n      addresses.delete(item)\n      ids_to_destroy \u003c\u003c item.id\n      skip\n    end\n\n    if item\n      item\n    else\n      item = AddressForm.new(Address.new)\n      collection \u003c\u003c item\n      item\n    end\n  end\n\n  def persist\n    super.tap do |result|\n      next unless result\n      ids = ids_to_destroy\n      Address.where { _id.in(ids) }.destroy\n    end\n  end\nend\n```\n\n##### Skip\n\nPopulators can skip processing of a part by invoking `#skip`. This method raises `FormObject::SkipException` which makes form object to ignore particular part.\n\n#### Reusability\n\nTo reuse common attributes or functionality you can use modules inclusion and inheritance:\n\n```crystal\nmodule PostTitle\n  include FormObject::Module\n\n  attr :title, String\nend\n\nmodule PostText\n  include FormObject::Module\n\n  attr :text, String\nend\n\nmodule BasePostAttributes\n  include PostTitle\n  include PostText\nend\n\nclass PostForm \u003c FormObject::Base(Post)\n  include BasePostAttributes\n\n  attr :release_date, Time\n\n  validates_length :title, in: 1...255\nend\n\nclass AdvancedPostForm \u003c PostForm\n  attr :likes, Int32\nend\n```\n\n### Create Form\n\n```crystal\nclass PostsController \u003c ApplicationController\n  def edit\n    @form = PostForm.new(Post.find!(params[\"id\"]))\n    render(\"edit.slang\")\n  end\nend\n```\n\nForm will automatically read attributes from the model.\n\n### Validation\n\nTo save model you should validate input data:\n\n```crystal\nclass PostsController \u003c ApplicationController\n  def create\n    @form = PostForm.new(Post.new)\n    if @form.verify(request) \u0026\u0026 @form.save\n      flash[\"success\"] = \"Created Post successfully.\"\n      redirect_to \"/posts\"\n    else\n      flash[\"danger\"] = \"Could not create Post!\"\n      render(\"new.slang\")\n    end\n  end\nend\n```\n\nThe `#verify` method parses data from the given request object and updates form attributes - the underlying model at this step remains unchanged. Next if runs defined validations and returns whether they succeed.\n\n### Data Synching\n\nAfter validation you can call `#save` (as in example above) and let FormObject take care of model persistence. Also you can use `#sync` to only write attributes from form to the resource and do everything else by your own.\n\n#### Custom Persistence Mechanism\n\nYou can define your own way of model persistence at the form level implementing own `#persist` method:\n\n```crystal\nclass PostForm \u003c FormObject::Base(Post)\n  attr :title, String\n\n  def persist\n    resource.save\n    # some other logic goes here\n  end\nend\n```\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/imdrasil/form_object/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## Contributors\n\n\u003e FormObject is heavily inspired by [reform](https://github.com/trailblazer/reform) ruby gem.\n\n- [imdrasil](https://github.com/imdrasil) Roman Kalnytskyi - creator, maintainer\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimdrasil%2Fform_object","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fimdrasil%2Fform_object","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fimdrasil%2Fform_object/lists"}