Coder Social home page Coder Social logo

rootstrap / yaaf Goto Github PK

View Code? Open in Web Editor NEW
349.0 15.0 14.0 130 KB

Easing the form object pattern in Rails applications

Home Page: https://rootstrap.com

License: MIT License

Ruby 99.71% Shell 0.29%
form-object rails form ruby yaaf form-validation rails-model hacktoberfest

yaaf's Introduction

YAAF

YAAF (Yet Another Active Form) is a gem that let you create form objects in an easy and Rails friendly way. It makes use of ActiveRecord and ActiveModel features in order to provide you with a form object that behaves pretty much like a Rails model, and still be completely configurable.

We were going to name this gem ActiveForm to follow Rails naming conventions but given there are a lot of form object gems named like that we preferred to go with YAAF.

CI Maintainability Test Coverage

Table of Contents

Motivation

Form Objects is a design pattern that allows us to:

  1. Keep views, models and controllers clean
  2. Create/update multiple models at the same time
  3. Keep business logic validations out of models

There are some other form objects gems but we felt none of them provided us all the features that we expected:

  1. Form objects that behave like Rails models
  2. Simple to use and to understand the implementation (no magic)
  3. Easy to customize
  4. Gem is well tested and maintained

For this reason we decided to build our own Form Object implementation. After several months in production without issues we decided to extract it into a gem to share it with the community.

If you want to learn more about Form Objects you can check out these great articles.

Why YAAF?

  • It is 71 lines long. As you can imagine, we did no magic in such a few lines of code, we just leveraged Rails modules in order to provide our form objects with a Rails-like behavior. You can review the code, it's easy to understand.

  • It provides a similar API to ActiveModel models so you can treat them interchangeably.

  • You can customize it 100%. We encourage you to have your own ApplicationForm which inherits from YAAF::Form and make the customizations you'd like for your app.

  • It helps decoupling the frontend from the database. This is particularly important when using Rails as a JSON API with a frontend in React/Ember/Vue/Angular/you name it. If you were to use accepts_nested_attributes_for your frontend would need to know your database structure in order to build the request. With YAAF you can provide a the interface you think it's best.

  • It easily supports nested models, collection of models and associated models. You have full control on their creation.

  • It helps you keep your models, views and controllers thin by providing a better place where to put business logic. In the end, this will improve the quality of your codebase and make it easier to maintain and extend.

  • It is an abstraction from production code. It has been working well for us, I'm confident it will work well for you too :)

Installation

Add this line to your application's Gemfile:

gem 'yaaf'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install yaaf

Usage

In the following sections we explain some basic usage and the API provided by the gem. You can also find some recipes here.

Setting up a form object

In order to use a YAAF form object, you need to inherit from YAAF::Form and define the @models of the form, for example:

# app/forms/registration_form.rb

class RegistrationForm < YAAF::Form
  attr_accessor :user_attributes

  def initialize(attributes)
    super(attributes)
    @models = [user]
  end

  def user
    @user ||= User.new(user_attributes)
  end
end

By doing that you can work with your form object in your controller such as you'd do with a model.

# app/controllers/registrations_controller.rb

class RegistrationsController < ApplicationController
  def create
    registration_form = RegistrationForm.new(user_attributes: user_params)

    if registration_form.save
      redirect_to registration_form.user
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

Form objects supports calls to valid?, invalid?, errors, save, save!, such as any ActiveModel model. The return values match the corresponding ActiveModel methods.

When saving or validating a form object, it will automatically validate all its models and promote the error to the form object itself, so they are accessible to you directly from the form object.

Form objects can also define validations like:

# app/forms/registration_form.rb

class RegistrationForm < YAAF::Form
  validates :phone, presence: true
  validate :a_custom_validation

  # ...

  def a_custom_validation
    # ...
  end
end

Validations can be skipped the same way as for ActiveModel models:

# app/controllers/registrations_controller.rb

class RegistrationsController < ApplicationController
  def create
    registration_form = RegistrationForm.new(user_attributes: user_params)

    registration_form.save!(validate: false)
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

Form objects support the saving of multiple models at the same time, to prevent leaving the system in a bad state all the models are saved within a DB transaction.

A good practice would be to create an empty ApplicationForm and make your form objects inherit from it. This way you have a centralized place to customize any YAAF default behavior you would like.

class ApplicationForm < YAAF::Form
  # Customized behavior
end

#initialize

The .new method should be called with the arguments that the form object needs.

When initializing a YAAF form object, there are two things to keep in mind

  1. You need to define the @models instance variables to be an array of all the models that you want to be validated/saved within the form object.
  2. To leverage ActiveModel's features, you can call super to automatically make the attributes be stored in instance variables. If you use it, make sure to also add attr_accessors, otherwise ActiveModel will fail.

#valid?

The #valid? method will perform both the form object validations and the models validations. It will return true or false and store the errors in the form object.

By default YAAF form objects will store model errors in the form object under the same key. For example if a model has an email attribute that had an error, the form object will provide an error under the email key (e.g. form_object.errors[:email]).

#invalid?

The #invalid? method is exactly the same as the .valid? method but will return the opposite boolean value.

#errors

The #errors method will return an ActiveModel::Errors object such as any other ActiveModel model.

#save

The #save method will run validations. If it's invalid it will return false, otherwise it will save all the models within a DB transaction and return true. Models that were #marked_for_destruction? will be destroyed instead of saved, this can be done by calling #mark_for_destruction on an ActiveRecord model.

Defined callbacks will be called in the following order:

  • before_validation
  • after_validation
  • before_save
  • after_save
  • after_commit/after_rollback

Options:

  • If validate: false is send as options to the save call, it will skip validations.

#save!

The #save! method is exactly the same as the .save method, just that if it is invalid it will raise an exception.

Validations

YAAF form objects support validations the same way as ActiveModel models. For example:

class RegistrationForm < YAAF::Form
  validates :email, presence: true
  validate :some_custom_validation

  # ...
end

Callbacks

YAAF form objects support callbacks the same way as ActiveModel models. For example:

class RegistrationForm < YAAF::Form
  before_validation :normalize_attributes
  after_commit :send_confirmation_email

  # ...
end

Available callbacks are (listed in execution order):

  • before_validation
  • after_validation
  • before_save
  • after_save
  • after_commit/after_rollback

Sample app

You can find a sample app making use of the gem here. Its code is also open source, and you can find it here.

Links

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rspec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/rootstrap/yaaf. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the YAAF project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Credits

YAAF is maintained by Rootstrap with the help of our contributors.

YAAF

yaaf's People

Contributors

coorasse avatar emirdaponte avatar geeknees avatar grosendo2006 avatar juanmanuelramallo avatar ken3ypa avatar santib avatar shaglock avatar thewatts avatar ur5us avatar vitogit avatar vpiau avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

yaaf's Issues

Deprecation warning in Rails 6.1

Is your feature request related to a problem? Please describe.
So I am using rails 6.1 and get this deprecation warning.

In Rails 6.1, `errors` is an array of Error objects,
therefore it should be accessed by a block with a single block
parameter like this:

person.errors.each do |error|
  attribute = error.attribute
  message = error.message
end

You are passing a block expecting two parameters,
so the old hash behavior is simulated. As this is deprecated,
this will result in an ArgumentError in Rails 6.2.

Describe the solution you'd like
I guess we have to change promote_errors a bit, but as I understand this new behavior will not work in rails < 6.1? I could make a PR, but I don't know how to make it compatible with rails versions < 6.1.

Additional context
rails/rails#32313

[Usage Questions] How could I set foreign key on Parent and Child Form?

Hi, I'm yaaf beginner, I'm interested in this gem.

I tried this gem, with 1 Parent and 1 Child Form on new_parent_path (http://localhost:3000/parents/new )

Here is my source code. You can run with Docker.
https://github.com/hamilton-keisuke/yaaf-sample


I wanna save 1 Parent and 1 Child Form, but couldn't save.
valid? is false, because of "Parent must exist".

I think the child needs the parent's key, but how can I set the parent's key smartly?

I couldn't understand it by looking at the README and sample recipes. Sorry to trouble you, but would you please help me?

Improve testing structure

Why

We want to avoid having large spec files in our test suite

What

Introduce a structure of testing so that each behaviour being tested has its own file.
Optional: Find a way to share the minimum set up for every behaviour.

Provide a save with bang method to match ActiveModel API

Why

We want to integrate as seamlessly as possible in Rails development environments and providing both #save and #save! methods would be a good experience for the developer

What

Provide two save methods: one that returns a boolean and another one that raises an exception if invalid. This should match common ActiveModel behaviour.

Notes

#1 (comment)

Exclude uneeded modules from ActiveModel

Why

We are currently including several modules we might not be using nor needing from ActiveModel

What

Manually include the modules being used/needed currently from ActiveModel

Ability to use callbacks as in Rails' models

Why

We want to match Rails behaviour about callbacks

What

Allow users to define callbacks just as it's done in models
For example:

class RegistrationForm < YAAF::Form
  after_save :add_role
 
  [snip]

  def add_role
    user.add_role(:normal)
  end
end

Update readme

Why

We want to let users know how to use this gem

What

Update readme file with the following information:

  • Table of contents: Displaying links to each section in the readme
  • Getting started: Explaining how to install and use this gem
  • Motivation: Explaining why to use this pattern
  • Real life examples: One or two examples showcasing the usage of the gem in a real life application
  • Notes: In order to support major form helpers gems like SimpleForm or even plain rails form helpers, we would want to define the methods:
    • persisted? => to let the gem know if the form object being saved is a new record or not
    • model_name => which will let the gem know where to look for translations model_name should return an ActiveModel::Name instance, most likely to be the model_name of the main model being saved.
  • Useful links: A list of links where the form object pattern is explained

https://docs.google.com/spreadsheets/d/1nilkCK2ZezTwR7rz6lazNG-5D-muK3WutOX4pps6zC4/edit#gid=2109604323

Return the saved model

What about some function that saves the record and returns the model? Kinda the create method, I would like to prevent additional steps or functions inside of the ModelForm to return that model.

Something like this:

class MyController < ApplicationController
  def create
    model_form = ModelForm.new(params)
    @event = model_form.create!
  end
end

[FEATURE]

Is your feature request related to a problem? Please describe.
So I am using rails 6.1 and get this deprecation warning.

In Rails 6.1, `errors` is an array of Error objects,
therefore it should be accessed by a block with a single block
parameter like this:

person.errors.each do |error|
  attribute = error.attribute
  message = error.message
end

You are passing a block expecting two parameters,
so the old hash behavior is simulated. As this is deprecated,
this will result in an ArgumentError in Rails 6.2.

Describe the solution you'd like
I guess we have to change promote_errors a bit, but as I understand this new behavior will not work in rails < 6.1? I could make a PR, but I don't know how to make it compatible with rails versions < 6.1.

Usage Questions: Transactional Commands + Edit

Hello again! ๐Ÿ‘‹

As we're evaluating YAAF - we're looking at how we would go about adjusting current implementations to leverage what YAAF provides.

We have a couple of specific scenarios:

1. Transactional Commands

In our current form implementations, we have (often order-dependent) methods that need to be called within the save transaction. Should they fail - we want the model persistence to rollback.

Ex:

def save
  if valid?
    ApplicationRecord.transaction do
      end_current_session # updates an existing Session record 
      remove_clients_from_other_sessions # calls out to Form that raises on error
      create_new_session # create a new Session record
      create_missing_documentation # creates Documentation records
      migrate_shared_data # updates other records
    end
  end
end

My only thought here is that - in order for this to work - each of those methods would have to delegate out to an object that in itself would live within the @models array, to be saved (?)

2. Editing

I feel like using form objects on an edit page isn't really talked about a lot in the examples given throughout the community.

In the case of editing, we keep having to lean on overriding the reader method for attributes so that we can either pull them from the params given, or from the record that was already saved.

Note: This is a trivial example, but highlights the use case of sharing the same form for new and edit

<%= form_for @registration do |form| %>
  <%= form.text_field :user_email %>
<% end %>
class EventRegistration
  include ActiveModel::Model

  attr_accessor :id, :event_id, :user_email

  validates :event_id, presence: true
  validates :user_email, presence: true

  def registration
    @registration ||= Registration.find_by(id: id) || Registration.new
  end

  def persisted?
    registration.persisted?
  end

  def user_email
    super.presence || registration.user_email
  end
end

Thanks for your time / help!

Ruby 3 support

Bug report:

  • Expected Behavior: Models are saved

  • Actual Behavior: Models are not saved due to error:
    *** ArgumentError Exception: wrong number of arguments (given 1, expected 0)

  • Steps to Reproduce:

    1. Install Ruby 3 and Rails 6
    2. Create a form object, instantiate it and send save
    3. An error is raised
  • Version of the repo: 2.0.0

  • Ruby and Rails Version: ruby-3.0.1 rails-6.1.3.2

Make form object callbacks private

Why

We don't want to have these methods to be public, given that they are not intended to be used directly, and in fact they are an implementation detail of the save method.

What

Make all callback methods private in YAAF::Form

Notes

#1 (comment)

Fix test coverage badge

Test coverage badge is ? . Seems like Travis is not reporting to CC the required data

Notes

From Travis CI logs

$ ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
Error: json: cannot unmarshal object into Go struct field input.coverage of type []formatters.NullInt
Usage:
  cc-test-reporter after-build [flags]
Flags:
  -s, --batch-size int               batch size for source files (default 500)
  -e, --coverage-endpoint string     endpoint to upload coverage information to (default "https://api.codeclimate.com/v1/test_reports")
  -t, --coverage-input-type string   type of input source to use [clover, cobertura, coverage.py, excoveralls, gcov, gocov, jacoco, lcov, simplecov, xccov]
      --exit-code int                exit code of the test run
  -r, --id string                    reporter identifier (default "aff2c7b9e07e54d5fc9e5588d2e2a8bab4f69950d35000edc2b6250bbaba477d")
      --insecure                     send coverage insecurely (without HTTPS)
  -p, --prefix string                the root directory where the coverage analysis was performed (default "/home/travis/build/rootstrap/yaaf")
Global Flags:
  -d, --debug   run in debug mode

Configure CI to run against several rails versions

Why

We want to test the gem against several rails versions so that we are aware the gem is working correctly for everyone

What

Update our Travis CI configuration to use Rails 4, 5 and 6 as the Rails target. And keep using Ruby 2.5, 2.6 and 2.7.

Notes

Github Actions is supposedly faster than Travis. Check if it's easier to configure Github Action instead of Travis CI

[Bug] Double Validation On Save (?)

This isn't quite a bug report - but wasn't sure if this is intended behavior.

We use form objects quite a bit - and have run into this same issue on our custom forms.

When calling save, validations are run - including model validations (which, is what we would expect). However, once those validations pass, the models are all saved via save! inside of a transaction.

That save!, however, will end up calling model validations a second time.

For basic validations, this doesn't matter that much as they are done in memory - but any type of validation (like a uniqueness validation) will hit the database a second time (the first being in the invalid? call, and the second in the save!).

Would it be better to call save!(validate: false) on each of the models, as they are already being validated just a few lines earlier?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.