Module: InlineForms::SchemaPreview

Defined in:
lib/inline_forms/schema_preview.rb

Overview

Cheap preview of a SchemaIntent, WITHOUT running a migration or booting a second app. The trick (see the staging doc): declare the proposed attribute as a virtual typed attribute on a throwaway subclass of the target model, so it reads/writes in memory with no column, and render the stock inline forms against an AttributeList that has the proposed row spliced in at the requested position. The real class and its table are untouched.

obj, list = InlineForms::SchemaPreview.build(intent, existing_record)
# render "inline_forms/show" with @object = obj,
#   @inline_forms_attribute_list = list  (the controller already supports
#   this per-request override — see attr_writer :inline_forms_attribute_list)

Scope of the stab: scalar attributes preview fully. Relation dropdowns (backed by a foreign key + association) and uploader/rich_text fields need model-side wiring the preview does not synthesize; #supported? reports this so a caller can fall back to a "available after apply" placeholder.

Constant Summary collapse

COLUMN_TYPE_TO_VIRTUAL =

Map an AR column type (from the engine's form-element maps) to an ActiveModel::Type symbol for a virtual attribute.

{
  string: :string,
  text: :string,
  integer: :integer,
  decimal: :decimal,
  float: :float,
  boolean: :boolean,
  date: :date,
  time: :time,
  datetime: :datetime,
  timestamp: :datetime
}.freeze
PREVIEW_UNSUPPORTED_FORM_ELEMENTS =

Form elements the GUI must not offer as "add a scalar field":

  • :header / :info are not real fields — :header is a display-only separator (adding one via addto would create a pointless string column), :info is read-only over an existing column.
  • uploaders need a mounted CarrierWave uploader; :money_field a monetize declaration; :devise_password_field a Devise model; :pdf_link a route — a virtual attribute alone cannot faithfully preview them.
%i[
  header
  info
  image_field
  audio_field
  file_field
  multi_image_field
  simple_file_field
  pdf_link
  money_field
  devise_password_field
].freeze

Class Method Summary collapse

Class Method Details

.attribute_list_for(base_class, intent) ⇒ Object

The AttributeList the preview should render: the model's current list with the proposed row inserted at --after/--before (falling back to append).



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/inline_forms/schema_preview.rb', line 87

def attribute_list_for(base_class, intent)
  current = base_class.new.inline_forms_attribute_list
  list    = InlineForms::AttributeList.wrap(current.map(&:dup))
  opts    = { values: intent.values, disabled: intent.disabled }.compact

  # Value-bearing choice elements need a values hash or their _edit/_show
  # helper raises. Mirror the addto generator: inject a placeholder the user
  # edits after apply (so preview and apply produce the same row).
  if opts[:values].nil? && InlineForms::VALUE_BEARING_FORM_ELEMENTS.include?(intent.form_element)
    opts[:values] = InlineForms::PLACEHOLDER_VALUES
  end

  if intent.after && list.include_attribute?(intent.after)
    list.insert_after(intent.after, intent.attribute, intent.form_element, **opts)
  elsif intent.before && list.include_attribute?(intent.before)
    list.insert_before(intent.before, intent.attribute, intent.form_element, **opts)
  else
    list.field(intent.attribute, intent.form_element, **opts)
  end
  list
end

.build(intent, base_record = nil) ⇒ Object

Returns [preview_object, attribute_list]. base_record seeds the preview object's existing attributes when given (so the rest of the form shows real data); otherwise a blank instance is used.



61
62
63
64
65
66
67
68
69
# File 'lib/inline_forms/schema_preview.rb', line 61

def build(intent, base_record = nil)
  base_class = intent.model_class
  list       = attribute_list_for(base_class, intent)
  preview    = preview_class_for(base_class, intent)

  object = base_record ? preview.new(base_record.attributes) : preview.new
  object.inline_forms_attribute_list = list
  [ object, list ]
end

.preview_class_for(base_class, intent) ⇒ Object

A throwaway subclass with the proposed attribute declared virtual (when supported), masquerading as the base class so partials/routes resolve.



111
112
113
114
115
116
117
118
119
120
121
# File 'lib/inline_forms/schema_preview.rb', line 111

def preview_class_for(base_class, intent)
  type = virtual_type(intent.form_element)
  klass = Class.new(base_class)
  klass.attribute(intent.attribute, type) if type

  base_name       = base_class.name
  base_model_name = base_class.model_name
  klass.define_singleton_method(:name) { base_name }
  klass.define_singleton_method(:model_name) { base_model_name }
  klass
end

.supported?(intent) ⇒ Boolean

False when the intent's form element needs model-side wiring the preview cannot synthesize (relations, uploaders, rich_text).

Returns:

  • (Boolean)


73
74
75
# File 'lib/inline_forms/schema_preview.rb', line 73

def supported?(intent)
  !virtual_type(intent.form_element).nil?
end

.supported_form_elementsObject

The sorted list of form elements the GUI offers (those that preview via a virtual attribute): scalar text/number/date/boolean/choice elements.



79
80
81
82
83
# File 'lib/inline_forms/schema_preview.rb', line 79

def supported_form_elements
  candidates = InlineForms::SPECIAL_COLUMN_TYPES.keys +
               InlineForms::DEFAULT_FORM_ELEMENTS.values
  candidates.uniq.select { |fe| !virtual_type(fe).nil? }.sort
end

.virtual_type(form_element) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
# File 'lib/inline_forms/schema_preview.rb', line 123

def virtual_type(form_element)
  fe = form_element.to_sym
  return nil if PREVIEW_UNSUPPORTED_FORM_ELEMENTS.include?(fe)

  column_type =
    InlineForms::SPECIAL_COLUMN_TYPES[fe] ||
    (InlineForms::DEFAULT_FORM_ELEMENTS.value?(fe) ? :string : nil)
  return nil if column_type.nil? || column_type == :no_migration || column_type == :belongs_to

  COLUMN_TYPE_TO_VIRTUAL[column_type]
end