PREPARE TO REPEL BOARDERS!!!!!!!

with Rails 5.2 Attributes API

What we'll cover

  • What is a form object?
  • When would you use one?
  • ActiveModel::Attributes
  • ActiveRecord::Attributes

What is a form object?

# this is an old style form object
# we'll be improving on this.

class Contact
  include ActiveModel::Model

  attr_accessor :email, :message, :name

  validates_presence_of :message...
end
<%= form_for @contact do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>

  <%= f.label :email %>
  <%= f.email_field :email %>

  <%= f.label :message %>
  <%= f.text_field :message %>

  <%= f.submit "Send message" %>
<% end %>

When would you use one?

OK but seriously.. what do you use them for?

  • Non AR backed model
  • Updating multiple objects at once
  • When you don't want to bother with accepts_nested_attributes_for
  • Also, they're fun to overuse ;) 

First.. a quick quiz

Text

What types are the params?

Text

textfield

number_field

{
  "textfield"=>"some text",
  "numberish"=>"1",
}

STRINGS!!!!!!!!!!

class BarbleForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :foo, :datetime
  attribute :bar, :integer
  attribute :baz, :string

  ...
end

You can have defaults... 

attribute :start_time, :datetime, default: -> { Time.now }

But wait! there's more....

Domain/Value objects

# app/models/spot.rb
class Spot
  def initialize(lat, long)
    @lat = lat
    @long = long
  end

  attr_reader :lat, :long

  def ==(other)
    lat == other.lat && long == other.long
  end
end
# app/types/spot_type.rb
class SpotType < ActiveRecord::Type::Value
  def cast(value)
    #used to take input from params
    case value
    when String
      Spot.new(*value.split(","))
    when Spot
      value
    end
  end
end

# config/initializers/types.rb
ActiveModel::Type.register(:spot, SpotType)
class TreasureForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :x, :spot
foo = TreasureForm.new(x: "123.23, 45.123")
foo.x.lat => "123.23"
foo.x.class => Spot

foo = TreasureForm.new(x: Spot.new("123.23", "45.123"))
foo.x.lat => "123.23"
foo.x.class => Spot

ActiveRecord::Attributes

ActiveRecord::Attributes (change schema type)

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.decimal :price_in_cents
end

# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end

store_listing = StoreListing.new(price_in_cents: '10.1')

# before
store_listing.price_in_cents # => BigDecimal(10.1)
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :integer
end

# after
store_listing.price_in_cents # => 10
rails g model product name:string price_in_pieces_of_eight:integer

Use your value objects to query the database..

# app/models/money.rb
class Money < Struct.new(:amount, :currency)
end
class MoneyType < ActiveRecord::Type::Value
  def initialize(gold_converter:)
    @gold_converter = gold_converter
  end

  # value will be the result of +deserialize+ or
  # +cast+. Assumed to be an instance of +Money+ in
  # this case.
  def serialize(value)
    value_in_pieces_of_eight = @gold_converter.convert_to_pieces_of_eight(value)
    value_in_pieces_of_eight.amount
  end
end
Product.create(price_in_pieces_of_eight: Money.new(5000, "USD"))
# INSERT INTO `products` (`price_in_pieces_of_eight`, `created_at`, `updated_at`)
# VALUES (123, '2019-08-10 01:08:15', '2019-08-10 01:08:15')






Product.where(price_in_pieces_of_eight: Money.new(4000, "CAD"))
# SELECT  `products`.* FROM `products`
# WHERE `products`.`price_in_pieces_of_eight` = 456 LIMIT 11

Finally...

class BarbleForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :foo, :integer

  validates_presence_of :foo

  def self.model_name
    ActiveModel::Name.new(self, nil, "Barble")
  end

  def save
    return false unless valid?

    # do all the things
  end
end

Great place to do too many things

Finallier...

class Contact
  ...

  def persisted?
    # defaults to false
  end
end
class Contact
  def id
    "sdf"
  end

  def persisted?
    true
  end
end
  • Internationalization lives on active_model
  • Use ActiveModel::Attributes for non AR backed models
  • Use ActiveRecord::Attributes for AR backed models
  • ActiveModel::Type::Value == ActiveRecord::Type::Value
  • postgres `attribute :foo, :integer, array: true`
  • Virtus & dry-rb (dry-struct dry-types) ecosystem

Finalliest...

Thanks & Questions?

@psdavey

 

https://bit.ly/2YZ16CA (Slides)

PREPARE TO REPEL BOARDERS (Rails 5.2 Attributes API)

By Patrick Davey

PREPARE TO REPEL BOARDERS (Rails 5.2 Attributes API)

Rails 5.2 Attributes API

  • 809