Module: InlineForms

Defined in:
lib/inline_forms.rb,
lib/inline_forms/tabs.rb,
lib/inline_forms/version.rb,
lib/inline_forms/searchable.rb,
lib/inline_forms/schema_apply.rb,
lib/inline_forms/schema_label.rb,
lib/inline_forms/form_elements.rb,
lib/inline_forms/schema_intent.rb,
lib/inline_forms/attribute_list.rb,
lib/inline_forms/schema_preview.rb,
lib/inline_forms/turbo_tabs_builder.rb,
lib/generators/inline_forms_generator.rb,
lib/inline_forms/form_element_registry.rb,
lib/inline_forms/archived_form_elements.rb,
lib/inline_forms/form_element_from_callee.rb,
lib/inline_forms/form_elements/date_helper.rb,
lib/inline_forms/form_elements/info_helper.rb,
lib/inline_forms/form_elements/time_helper.rb,
lib/generators/inline_forms_addto_generator.rb,
lib/inline_forms/form_elements/header_helper.rb,
lib/inline_forms/form_elements/dropdown_helper.rb,
lib/inline_forms/form_elements/helper_includes.rb,
lib/inline_forms/form_elements/pdf_link_helper.rb,
lib/inline_forms/form_elements/check_box_helper.rb,
lib/inline_forms/form_elements/info_list_helper.rb,
lib/inline_forms/form_elements/rich_text_helper.rb,
lib/inline_forms/form_elements/text_area_helper.rb,
lib/inline_forms/form_elements/check_list_helper.rb,
lib/inline_forms/form_elements/file_field_helper.rb,
lib/inline_forms/form_elements/plain_text_helper.rb,
lib/inline_forms/form_elements/text_field_helper.rb,
lib/inline_forms/form_elements/audio_field_helper.rb,
lib/inline_forms/form_elements/color_field_helper.rb,
lib/inline_forms/form_elements/image_field_helper.rb,
lib/inline_forms/form_elements/money_field_helper.rb,
lib/inline_forms/form_elements/month_select_helper.rb,
lib/inline_forms/form_elements/radio_button_helper.rb,
lib/inline_forms/form_elements/decimal_field_helper.rb,
lib/inline_forms/form_elements/integer_field_helper.rb,
lib/inline_forms/form_elements/plain_text_area_helper.rb,
lib/inline_forms/form_elements/month_year_picker_helper.rb,
lib/inline_forms/form_elements/multi_image_field_helper.rb,
lib/inline_forms/form_elements/scale_with_values_helper.rb,
lib/inline_forms/form_elements/simple_file_field_helper.rb,
lib/inline_forms/form_elements/slider_with_values_helper.rb,
lib/inline_forms/form_elements/dropdown_with_other_helper.rb,
lib/inline_forms/form_elements/scale_with_integers_helper.rb,
lib/inline_forms/form_elements/dropdown_with_values_helper.rb,
lib/inline_forms/form_elements/devise_password_field_helper.rb,
lib/inline_forms/form_elements/dropdown_with_integers_helper.rb,
lib/inline_forms/form_elements/dropdown_with_values_with_stars_helper.rb

Overview

InlineForms is a Rails Engine that let you setup an admin interface quick and easy. Please install it as a gem or include it in your Gemfile.

Defined Under Namespace

Modules: FormElementRegistry, FormElements, SchemaLabel, SchemaPreview, Searchable, Tabs Classes: ArchivedFormElementError, AttributeList, Engine, InlineFormsGenerator, PlainTextColumnMissingError, SchemaApply, SchemaIntent, TurboTabsBuilder

Constant Summary collapse

DEFAULT_COLUMN_TYPES =

DEFAULT_COLUMN_TYPES holds the standard ActiveRecord::Migration column types. This list provides compatability with the standard types, but we add our own later in 'Special Column Types'.

These types will override Special Column Types of the same name.\

Example: rails g inline_forms Example name:string price:integer will result in:

class InlineFormsCreateExamples < ActiveRecord::Migration
def self.up
  create_table :examples do |t|
    t.string  :name
    t.integer :price
    t.timestamps
  end
end
def self.down
  drop_table :examples
end
end
{
  string: :string,
  text: :text,
  integer: :integer,
  float: :float,
  decimal: :decimal,
  datetime: :datetime,
  timestamp: :timestamp,
  time: :time,
  date: :date,
  binary: :binary,
  boolean: :boolean
  # :references => :belongs_to,
  # :belongs_to => :belongs_to,
}
DEFAULT_FORM_ELEMENTS =

DEFAULT_FORM_ELEMENTS holds a mapping from Default Column Types to Form Elements. Form Elements are defined in lib/app/helpers/form_elements and are pieces of code that display a form for a field.

Example: rails g inline_forms Example name:string price:integer will result in the following model:

class Example < ApplicationRecord
 def inline_forms_attribute_list
   [
     [ :name,  :text_field ],
     [ :price, :text_field ],
   ]
 end
end

as you see, both :string and :integer are mapped to a :text_field

{
  string: :text_field,
  text: :plain_text,
  integer: :text_field,
  float: :text_field,
  decimal: :text_field,
  datetime: :datetime_select,
  timestamp: :datetime_select,
  time: :time_select,
  date: :date_select,
  binary: :text_field,
  boolean: :check_box
}
SPECIAL_COLUMN_TYPES =

SPECIAL_COLUMN_TYPES maps the column types that we define here and in lib/app/helpers/form_elements to the standard ActiveRecord::Migration column types

Example: in lib/app/helpers/form_elements/dropdown.rb InlineForms::SPECIAL_COLUMN_TYPES=:belongs_to this maps the :dropdown form element to the :belongs_to column type.

If you call the generator with country:dropdown, it will add t.belongs_to :country to the migration. (In fact AR will add t.integer :country_id). And it will add [ :country, :dropdown ], to the inline_forms_attribute_list in the model.

{
  associated: :no_migration
}
PLAIN_TEXT_FORM_ELEMENTS =
%i[
  plain_text
  plain_text_area
].freeze
VALUE_BEARING_FORM_ELEMENTS =

Form elements that render a set of choices and therefore REQUIRE a values hash as the 3rd element of their attribute_list row (their _show/_edit helpers call attribute_values, which raises when it is missing). Shared by the addto generator and the schema GUI/preview so both insert a placeholder.

%i[
  dropdown_with_values
  dropdown_with_integers
  dropdown_with_values_with_stars
  radio_button
  check_box
  scale_with_integers
  scale_with_values
  slider_with_values
].freeze
PLACEHOLDER_VALUES =

Placeholder values hash inserted when a value-bearing element is added programmatically (generator / GUI); the user edits it to real values after.

{ 1 => "one", 2 => "two" }.freeze
PENDING_GATE_EXEMPT_FORM_ELEMENTS =

Form elements exempt from the pending-migration gate. They either render a label only (:header), or read a virtual attribute whose name is not its backing column — :devise_password_field (backed by encrypted_password), :money_field (backed by a *_cents column via money-rails), :info (conventionally bound to pre-existing columns like created_at). None of these are produced by inline_forms_addto in the transient pre-migrate window, so gating them would only ever mis-fire.

%i[
  header
  info
  devise_password_field
  money_field
].freeze
RELATIONS =

RELATIONS defines a mapping between AR::Migrations columns and the Model.

When a column has the type of :references or :belongs_to, then there will be a line in the migration reflecting that, but not in the model.

Why?

  • Let's say we have a customer that has_many phone_numbers.
  • Let's say that a phone_number belongs_to a customer.
  • Let's say that every number has_one type_of_number (like 'private','gsm' etc.)
  • Let's say a type_of_number belongs_to a number.

Wait a minute... thats sounds right... but it ain't!

In fact, a type_of_number has_many phone_numbers and a phone_number belongs_to a type_of_number!

In a form, it's quite logical to use a dropdown for type_of_number. So, in the generator, use type_of_number:dropdown This creates the correct migration (t.integer :type_of_number_id) and the correct model. (It adds 'belongs_to :type_of_number' and adds a dropdown in the inline_forms_attribute_list)

But, you also want to have a client_id in the migration, and a 'belongs_to :client' in the model. In such cases, you need to use :belongs_to, like this: rails g inline_forms Example phone_number:string type_of_number:dropdown client:belongs_to

{
  belongs_to: :belongs_to,
  references: :belongs_to
}
SPECIAL_RELATIONS =

SPECIAL_RELATIONS maps AR relations to migrations. In most cases, these relations have no migration at all, but they do need a line in the model.

{
  has_many: :no_migration,
  has_many_destroy: :no_migration,
  has_one: :no_migration,
  has_and_belongs_to_many: :no_migration,
  habtm: :no_migration
}
VERSION =
"8.1.47"
ARCHIVED_FORM_ELEMENTS =

Retired form-element symbols. Full source for entries with archive_path lives under archived/form_elements// — see archived/README.md.

{
  geo_code_curacao: {
    archived_in_version: "7.6.0",
    archive_path: "archived/form_elements/geo_code_curacao",
    summary: "Curaçao street geocode (MySQL Zones/Buurten/Straatcode, jQuery autocomplete, UJS list_streets)."
  },
  chicas_photo_list: {
    archived_in_version: "7.6.0",
    archive_path: "archived/form_elements/chicas",
    summary: "Chicas app read-only member photo gallery (show-only)."
  },
  chicas_family_photo_list: {
    archived_in_version: "7.6.0",
    archive_path: "archived/form_elements/chicas",
    summary: "Chicas app read-only family member photo gallery (show-only)."
  },
  chicas_dropdown_with_family_members: {
    archived_in_version: "7.6.0",
    archive_path: "archived/form_elements/chicas",
    summary: "Chicas client picker via family.clients; moves CarrierWave upload dir on update."
  },
  kansen_slider: {
    archived_in_version: "7.6.0",
    archive_path: "archived/form_elements/kansen_slider",
    summary: "jQuery UI slider for integer-coded chance scale; uses model attribute_values."
  },
  tree: {
    archived_in_version: "7.7.0",
    archive_path: "archived/form_elements/tree",
    summary: "Self-referential children list via parent.children; requires host tree APIs (see README)."
  },
  move: {
    archived_in_version: "7.7.0",
    archive_path: "archived/form_elements/tree",
    summary: "Reparent via hash_tree_to_collection + add_child (host must implement; pairs with :tree)."
  },
  absence_list: {
    removed_in_version: "6.3.0",
    archive_path: nil,
    summary: "Project-specific absence list UI; removed without a copy in this repo (see CHANGELOG 6.3.0)."
  },
  ckeditor: {
    archived_in_version: "8.1.21",
    archive_path: "archived/form_elements/ckeditor",
    summary: "Legacy CKEditor form element; use :rich_text (ActionText) instead."
  },
  text_area_without_ckeditor: {
    archived_in_version: "8.1.21",
    archive_path: "archived/form_elements/ckeditor",
    summary: "Legacy plain-text alias; use :plain_text or :plain_text_area instead."
  },
  question_list: {
    archived_in_version: "8.1.30",
    archive_path: "archived/form_elements/question_list",
    summary: "Survey-app checklist hardcoding a host Question model with subquestions."
  },
  # The helper methods were named dnsrecords_* (no underscore), so the
  # attribute-list symbol in host apps was :dnsrecords.
  dnsrecords: {
    archived_in_version: "8.1.30",
    archive_path: "archived/form_elements/dns_records",
    summary: "DNS-admin display of djbdns A-record lines via host a_records/djbdns_line APIs."
  }
}.freeze
InlineFormsAddtoGenerator =

Backwards-compatible alias (tests and any programmatic invokers that addressed the namespaced version while the generator was scoped to module InlineForms). Rails generator discovery uses the top-level constant above.

::InlineFormsAddtoGenerator

Class Method Summary collapse

Class Method Details

.assert_plain_text_column!(object:, attribute:, form_element:) ⇒ Object



140
141
142
143
144
145
146
147
# File 'lib/inline_forms.rb', line 140

def self.assert_plain_text_column!(object:, attribute:, form_element:)
  return unless plain_text_form_element?(form_element)
  return if object.class.column_names.include?(attribute.to_s)

  raise PlainTextColumnMissingError,
    "#{object.class.name}##{attribute} uses #{form_element} but has no DB column `#{attribute}`. " \
    "Use :rich_text for ActionText-backed attributes, or add a text column for :plain_text."
end

.attribute_list(&block) ⇒ Object

Convenience entry point mirroring AttributeList.build.



188
189
190
# File 'lib/inline_forms/attribute_list.rb', line 188

def self.attribute_list(&block)
  AttributeList.build(&block)
end

.attribute_pending_migration?(object, attribute, form_element) ⇒ Boolean

True when an attribute_list row references a column that its model's table does not have yet — i.e. the model file was edited (row added) but the migration adding the column has not run. Rendering or writing such a row raises (e.g. object[:foo] / foo_show on a missing column), so callers gate on this to show a "pending migration" placeholder and skip writes instead of 500ing during the window between rails g inline_forms_addto and rails db:migrate.

Detection is column-presence based (robust): the expected column is derived from the form element via the same type maps the generator uses. nil (associations, rich_text, pdf_link, unknown elements) and virtual-backed elements (see PENDING_GATE_EXEMPT_FORM_ELEMENTS) are never gated. The gate self-heals: once the migration runs and the schema cache reflects the new column, the column is present and the row renders normally — no flag to clear, nothing to maintain.

Returns:

  • (Boolean)


178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/inline_forms.rb', line 178

def self.attribute_pending_migration?(object, attribute, form_element)
  fe = form_element.to_sym
  return false if PENDING_GATE_EXEMPT_FORM_ELEMENTS.include?(fe)

  klass = object.class
  return false unless klass.respond_to?(:table_exists?) && klass.respond_to?(:column_names)
  return false unless klass.table_exists?

  column = pending_gate_expected_column(attribute, fe)
  return false if column.nil?

  !klass.column_names.include?(column)
rescue StandardError
  # The gate must never itself break rendering; fail open (render as before).
  false
end

.form_element_string_from_callee(from_callee) ⇒ Object

Maps __callee__ from a *_show helper to the params[:form_element] string (e.g. :text_field_show"text_field").



6
7
8
9
10
# File 'lib/inline_forms/form_element_from_callee.rb', line 6

def self.form_element_string_from_callee(from_callee)
  s = from_callee.to_s
  s = s.sub(/\Ablock in /, "")
  s.delete_suffix("_show")
end

.pending_gate_expected_column(attribute, form_element) ⇒ Object

The column an attribute_list row expects on the model's own table, or nil when the form element needs no such column (has_many/habtm/has_one/ rich_text -> :no_migration; pdf_link/info_list/unknown -> nil). Relation dropdowns are backed by a foreign key (<attribute>_id); every other column-backed element uses a column named after the attribute.



200
201
202
203
204
205
206
207
208
209
# File 'lib/inline_forms.rb', line 200

def self.pending_gate_expected_column(attribute, form_element)
  column_type =
    SPECIAL_COLUMN_TYPES[form_element] ||
    (DEFAULT_FORM_ELEMENTS.value?(form_element) ? :__scalar__ : nil)

  return nil if column_type.nil? || column_type == :no_migration
  return "#{attribute}_id" if column_type == :belongs_to

  attribute.to_s
end

.plain_text_form_element?(form_element) ⇒ Boolean

Returns:

  • (Boolean)


134
135
136
137
138
# File 'lib/inline_forms.rb', line 134

def self.plain_text_form_element?(form_element)
  PLAIN_TEXT_FORM_ELEMENTS.include?(form_element.to_sym)
rescue NoMethodError
  false
end

.validate_no_archived_form_elements_for!(klass) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/inline_forms/archived_form_elements.rb', line 73

def self.validate_no_archived_form_elements_for!(klass)
  return unless klass.instance_methods.include?(:inline_forms_attribute_list)

  klass.new.inline_forms_attribute_list.each do |attribute, form_element|
    key = form_element.to_sym
    next unless ARCHIVED_FORM_ELEMENTS.key?(key)

    meta = ARCHIVED_FORM_ELEMENTS[key]
    version = meta[:archived_in_version] || meta[:removed_in_version]
    hint = if meta[:archive_path]
      "Restore from #{meta[:archive_path]}/README.md or vendor into your app."
    else
      "See archived/README.md and CHANGELOG #{version}."
    end

    raise ArchivedFormElementError,
      "#{klass.name} inline_forms_attribute_list declares #{attribute}:#{form_element}, " \
      "which was retired in inline_forms #{version} (#{meta[:summary]}). #{hint}"
  end
end

.validate_plain_text_configuration_for!(klass) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/inline_forms.rb', line 211

def self.validate_plain_text_configuration_for!(klass)
  return unless klass.respond_to?(:table_exists?) &&
                klass.respond_to?(:column_names) &&
                klass.instance_methods.include?(:inline_forms_attribute_list)
  return unless klass.table_exists?

  attributes = klass.new.inline_forms_attribute_list
  attributes.each do |attribute, form_element|
    next unless plain_text_form_element?(form_element)
    next if klass.column_names.include?(attribute.to_s)

    raise PlainTextColumnMissingError,
      "#{klass.name} inline_forms_attribute_list declares #{attribute}:#{form_element}, " \
      "but table `#{klass.table_name}` has no `#{attribute}` column. " \
      "Use :rich_text for ActionText-backed attributes."
  end
end