Goal of this article - feature which can allow you easy extend your AR models with custom fields.
You can set type, value, name and for example label for each field and use them later let’s say on uniq forms for each customer.
Adding Fields class
First let’s add Fields class, and initialize it with array or hash which we passing to constructor.
class Fields
extend Forwardable
def_delegators :@collection, *[].public_methods
def initialize(array_or_hash = [])
collection = case array_or_hash
when Hash
[CustomField.new(array_or_hash)]
else
Array(array_or_hash).map do |field|
field.is_a?(CustomField) ? field : CustomField.new(field)
end
end
@collection = collection.reject(&:empty?)
end
def to_a
@collection
end
end
Attributable module
Next, Attributable module.
This module responsible for dynamic methods creation, and looks pretty simple and clear, right?
For each custom field we creating setter and getter and method itself.
module Attributable
def create_method(name, &block)
self.class.send(:define_method, name.to_sym, &block)
end
def create_setter(m)
create_method("#{m}=".to_sym) { |v| instance_variable_set("@#{m}", v) }
end
def create_getter(m)
create_method(m.to_sym) { instance_variable_get("@#{m}") }
end
def set_attr(method, value)
create_setter method
send "#{method}=".to_sym, value
create_getter method
end
end
Model
Ok, cool - but what’s next?
Let’s check our model file, let say Customer.rb.
First - let’s include our Attributable module and add custom_fields attribute.
Here you can see custom_fields_values method, with this method you can easy get hash of keys and values for your model:
{"location"=>"KZN", "ig"=>"IG", "age"=>"24", "city"=>"KZN", "team"=>"ASR", "size"=>"8", "code"=>"SSS"}
Cool - right?
Next, let’s add custom_fields setter and write_attribute methods.
First one just creates Fields instance and calls super. But write_attribute returns value for each CustomField.
And finally - we should initialize each value (define_default_values method) on model after_initialize callback.
class Customer < ApplicationRecord
include Attributable
attribute :custom_fields, FieldsType.new
COLUMNS = ['first_name', 'last_name', 'email'].freeze
def custom_fields_values
data = {}
custom_fields.each do |attr|
unless COLUMNS.include?(attr.name)
data["#{attr.name}"] = attr.value
end
end
data
end
def custom_fields=(value)
value = Fields.new(value)
super
end
def write_attribute(name, value)
if name == :custom_fields
value = Fields.new(value)
end
super
end
after_initialize :define_default_values
def define_default_values(fields = custom_fields)
custom_fields.each do |attr|
unless COLUMNS.include?(attr.name)
set_attr(attr.name, attr.value)
end
end
end
end
See it in action
Let’s select latest Customer model from database, and see how it works.
[2] pry(main)> cus = Customer.last
Customer Load (5.2ms) SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> #<Customer:0x00007fa22a72a758
id: 10000,
user_id: 3,
first_name: "Mike",
last_name: "Heatley",
email: "mike@outlook.com",
created_at: Sat, 11 Apr 2019 13:53:26 UTC +00:00,
updated_at: Sat, 11 Apr 2019 13:53:26 UTC +00:00,
custom_fields:
[#<CustomField:0x00007fa22cc4a7e0 @name="location", @value="USA">,
#<CustomField:0x00007fa22cc4a718 @name="age", @value="24">,
#<CustomField:0x00007fa22cc4a6a0 @name="city", @value="SFA">,
#<CustomField:0x00007fa22cc4a600 @name="size", @value="12">]>
And specific size field:
[4] pry(main)> cus = Customer.last.age
Customer Load (0.5ms) SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> "12"