Skip to content

Commit

Permalink
Address field: Add super-scaffolding support, document new field and …
Browse files Browse the repository at this point in the history
…document updating dependent-fields in a form (#534)

* start adding address_field to super-scaffolding

* scaffold strong params for address_field, initialize empty before_action

* fix set_default_(address) callback

* linter

* add showcase preview for address_field

* docs: start address-field page, add to field-partials

* fix available field partials table

* title case the headings

* add Attribute Partials section in showcase (under Field Partials)

* add showcase preview for atttribute_partials/address

* document customizing address output

* showcase: pare down options for address_field

* address_field: remove Address.new from sample code

* add _dependent_fields_turbo_frame partial

* address_field: use dependent_fields_turbo_frame

* dependent_fields_turbo_frame: yield stimulus_controller name

* address_field: dependent_fields_controller_name

* address_field: use form.field_id for id of turbo_frame

* docs/field-partials: add address_field link at the bottom

* rename refresh-fields to dependent-fields-frame

* docs: add dynamic-forms-dependent-fields

* add showcase preview for _dependent_fields_frame

* about_attribute_partials: compress to partial.body.optional.yield

Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>

* about_attribute_partials: compress another partial.options.optional.yield

Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>

* docs: add missing `do` on render

Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>

* dependent_fields_frame showcase: clarify code example comment

* super-select doc: add link to dynamic forms doc

* buttons doc: add link to dynamic forms doc

---------

Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>
  • Loading branch information
pascallaliberte and kaspth committed Oct 3, 2023
1 parent 06ffffd commit a37318e
Show file tree
Hide file tree
Showing 17 changed files with 387 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module RefreshFieldsHelper
module DependentFieldsFrameHelper
def accept_query_string_override_for(form, method)
field_name = form.field_name(method)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class extends Controller {
}
static classes = [ "loading" ]

updateFrameFromDependentField(event) {
updateFrameFromDependableField(event) {
const field = event?.detail?.event?.detail?.event?.target || // super select nests its original jQuery event, contains <select> target
event?.detail?.event?.target || // dependable_controller will include the original event in detail
event?.target // maybe it was fired straight from the field
Expand Down
6 changes: 3 additions & 3 deletions bullet_train-fields/app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import PasswordController from './fields/password_controller'
import PhoneController from './fields/phone_controller'
import SuperSelectController from './fields/super_select_controller'
import DependableController from './dependable_controller'
import RefreshFieldsController from './refresh_fields_controller'
import DependentFieldsFrameController from './dependent_fields_frame_controller'

export const controllerDefinitions = [
[FieldController, 'fields/field_controller.js'],
Expand All @@ -27,7 +27,7 @@ export const controllerDefinitions = [
[PhoneController, 'fields/phone_controller.js'],
[SuperSelectController, 'fields/super_select_controller.js'],
[DependableController, 'dependable_controller.js'],
[RefreshFieldsController, 'refresh_fields_controller.js'],
[DependentFieldsFrameController, 'dependent_fields_frame_controller.js'],
].map(function(d) {
const key = d[1]
const controller = d[0]
Expand All @@ -50,5 +50,5 @@ export {
PhoneController,
SuperSelectController,
DependableController,
RefreshFieldsController,
DependentFieldsFrameController,
}
1 change: 1 addition & 0 deletions bullet_train-super_scaffolding/lib/scaffolding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def self.mysql?

def self.valid_attribute_type?(type)
[
"address_field",
"boolean",
"buttons",
# TODO: We're leaving cloudinary_image here for now for backwards compatibility.
Expand Down
2 changes: 2 additions & 0 deletions bullet_train-super_scaffolding/lib/scaffolding/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ def partial_name
"text"
when "number_field"
"number"
when "address_field"
"address"
else
raise "Invalid field type: #{type}."
end
Expand Down
44 changes: 43 additions & 1 deletion bullet_train-super_scaffolding/lib/scaffolding/transformer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -870,8 +870,11 @@ def valid_#{attribute.collection_name}
<td#{cell_attributes}><%= render 'shared/attributes/#{attribute.partial_name}', attribute: :#{attribute.is_vanilla? ? attribute.name : attribute.name_without_id_suffix}#{", #{table_cell_options.join(", ")}" if table_cell_options.any?} %></td>
ERB

if attribute.type == "password_field"
case attribute.type
when "password_field"
field_content.gsub!(/\s%>/, ", options: { password: true } %>")
when "address_field"
field_content.gsub!(/\s%>/, ", one_line: true %>")
end

unless ["Team", "User"].include?(child)
Expand Down Expand Up @@ -958,6 +961,20 @@ def valid_#{attribute.collection_name}
if attribute.type == "file_field"
scaffold_add_line_to_file(file, "#{attribute.name}_removal: [],", RUBY_NEW_ARRAYS_HOOK, prepend: true)
end
elsif attribute.type == "address_field"
address_strong_params = <<~RUBY
#{attribute.name}_attributes: [
:id,
:_destroy,
:address_one,
:address_two,
:city,
:country_id,
:region_id,
:postal_code
],
RUBY
scaffold_add_line_to_file(file, address_strong_params, RUBY_NEW_ARRAYS_HOOK, prepend: true)
else
scaffold_add_line_to_file(file, ":#{attribute.name},", RUBY_NEW_FIELDS_HOOK, prepend: true)
if attribute.type == "file_field"
Expand All @@ -969,6 +986,28 @@ def valid_#{attribute.collection_name}
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", attribute.special_processing, RUBY_NEW_FIELDS_PROCESSING_HOOK, prepend: true) if attribute.special_processing
end

#
# ASSOCIATED MODELS
#

unless cli_options["skip-form"] || attribute.options[:readonly]

# set default values for associated models.
case attribute.type
when "address_field"
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", "before_action :set_default_#{attribute.name}, except: :index", "ApplicationController", increase_indent: true)

method_content = <<~RUBY
def set_default_#{attribute.name}
@tangible_thing.#{attribute.name} ||= Address.new
end
RUBY
scaffold_add_line_to_file("./app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb", method_content, "end", prepend: true, increase_indent: true, exact_match: true)
end

end

#
# API SERIALIZER
#
Expand Down Expand Up @@ -1246,6 +1285,9 @@ def remove_#{attribute.name}
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "after_validation :remove_#{attribute.name}, if: :#{attribute.name}_removal?", CALLBACKS_HOOK, prepend: true)
when "trix_editor"
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_rich_text :#{attribute.name}", HAS_ONE_HOOK, prepend: true)
when "address_field"
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "has_one :#{attribute.name}, class_name: \"Address\", as: :addressable", HAS_ONE_HOOK, prepend: true)
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "accepts_nested_attributes_for :#{attribute.name}", HAS_ONE_HOOK, prepend: true)
when "buttons"
if attribute.is_boolean?
scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "validates :#{attribute.name}, inclusion: [true, false]", VALIDATIONS_HOOK, prepend: true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<% showcase.description do %>
When you're building forms, use Bullet Train's Field Partials for your form fields. They DRY-up all the presentation logic without needing a third-party dependency like Formtastic.
<br/>
<br/>
Read much more about them in the extensive <a href="https://bullettrain.co/docs/field-partials" target="_blank">developer documentation</a>.
<% end %>
<% form_with model: Scaffolding::CompletelyConcrete::TangibleThing.new, url: "#" do |form| %>
<%
form.object.address_value = Address.new
%>
<% showcase.sample "Basic" do %>
<%= render 'shared/fields/address_field', form: form, method: :address_value %>
<% end %>
<% end %>
<%# To display further options use `showcase.options.x` as options with a block will clear the old options. See `_options.html.erb` for an example. %>
<% showcase.options do |o| %>
<% o.required :form, "Reference to the form object", type: "ActionView::Helpers::FormBuilder" %>
<% o.required :method, "Attribute of the model" %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<% showcase.description do %>
Follows the <a href="/docs/field-partials/dynamic-forms-dependent-fields.md" target="_blank">Dependent Fields Pattern (See developer documentation)</a> to display form fields that depend on the value of another field.
<% end %>
<% form_with model: Scaffolding::CompletelyConcrete::TangibleThing.new, url: "#" do |form| %>
<%
form.object.boolean_button_value = false
%>
<% showcase.sample "Basic" do %>
<%# here we're wrapping the field to trap the `change` event %>
<%= tag.div data: {
'controller': "dependable",
'action': 'change->dependable#updateDependents',
'dependable-dependents-selector-value': "##{form.field_id(:button, :dependent_fields)}"
} do %>
<%= render "shared/fields/buttons",
form: form,
method: :boolean_button_value,
other_options: { label: "Should I present more fields?" } %>
<% end %>
<%= render "shared/fields/dependent_fields_frame",
id: form.field_id(:button, :dependent_fields),
form: form,
dependable_fields: [:boolean_button_value] do %>

<div class="my-3">
<% if form.object.boolean_button_value %>
<strong>More fields would be shown here.</strong>
<% else %>
<em>No fields should be shown here.</em>
<% end %>
</div>

<% end %>
<% end %>
<% end %>
<%# To display further options use `showcase.options.x` as options with a block will clear the old options. See `_options.html.erb` for an example. %>
<% showcase.options do |o| %>
<% o.required :id, "id of the turbo_frame element" %>
<% o.required :form, "Reference to the form object", type: "ActionView::Helpers::FormBuilder" %>
<% o.required :dependable_fields, "Attributes of the model for the fields on whose values this frame depends", type: "Array" %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<% showcase.description do %>
Attribute partials are used to display a single attribute of a model, within an `index` table colum or the resource's `show` screen.

Read more about them and about the form field partials in the extensive <a href="https://bullettrain.co/docs/field-partials" target="_blank">developer documentation</a>.
<% end %>
<%= partial.body.optional.yield Scaffolding::CompletelyConcrete::TangibleThing.new %>
<%# To display further options use `showcase.options.x` as options with a block will clear the old options. See `_options.html.erb` for an example. %>
<% showcase.options do |o| %>
<% o.required :object, "Reference to the model object", type: "Object" %>
<% o.required :attribute, "Attribute of the model" %>
<%= partial.options.optional.yield o %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<%= render "showcase/previews/field_partials/attribute_partials/about_attribute_partials", showcase: showcase do |partial| %>
<% partial.body do |object| %>
<% object.address_value = Address.new(
country_id: 233,
address_one: "2800 East Observatory Road",
city: "Los Angeles",
region_id: 1416,
postal_code: "90027"
)
%>
<% showcase.sample "Multi-line" do %>
<%= render 'shared/attributes/address', object: object, attribute: :address_value %>
<% end %>
<% showcase.sample "One-line" do %>
<%= render 'shared/attributes/address', object: object, attribute: :address_value, one_line: true %>
<% end %>
<% end %>
<% partial.options do |o| %>
<% o.optional :one_line, "Render into a single line", type: "Boolean" %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@
form ||= current_fields_form
options ||= {}
other_options ||= {}
id_of_dependent_fields_frame = form.field_id(method, :dependent_fields)
%>
<%= form.fields_for(method) do |address_form| %>
<% with_field_settings form: address_form do %>
<% # For self-updating the fields in the turbo_frame further down %>
<% accept_query_string_override_for(address_form, :country_id) %>
<%= render 'shared/fields/super_select',
method: :country_id,
choices: populate_country_options,
Expand All @@ -23,7 +19,7 @@ id_of_dependent_fields_frame = form.field_id(method, :dependent_fields)
data: {
'controller': "dependable",
'action': '$change->dependable#updateDependents',
'dependable-dependents-selector-value': "##{id_of_dependent_fields_frame}"
'dependable-dependents-selector-value': "##{form.field_id(:country, :dependent_fields)}"
}
}
%>
Expand All @@ -38,13 +34,11 @@ id_of_dependent_fields_frame = form.field_id(method, :dependent_fields)
%>
</div>
<div class="sm:col-span-2">
<%= turbo_frame_tag id_of_dependent_fields_frame,
class: "block space-y-5",
data: {
'controller': "refresh-fields",
'action': "dependable:updated->refresh-fields#updateFrameFromDependentField turbo:frame-render->refresh-fields#finishFrameUpdate",
'refresh-fields-loading-class': 'opacity-60'
} do
<%= render "shared/fields/dependent_fields_frame",
id: form.field_id(:country, :dependent_fields),
form: address_form,
dependable_fields: [:country_id],
html_options: { class: "block space-y-5" } do |dependent_fields_controller_name|
%>

<div class="grid grid-cols-1 gap-y gap-x sm:grid-cols-2">
Expand All @@ -61,7 +55,7 @@ id_of_dependent_fields_frame = form.field_id(method, :dependent_fields)
html_options: {
disabled: address_form.object.country_id.nil?,
data: {
"refresh-fields-target": "field"
"#{dependent_fields_controller_name}-target": "field"
}
}
%>
Expand All @@ -71,7 +65,7 @@ id_of_dependent_fields_frame = form.field_id(method, :dependent_fields)
<%= render 'shared/fields/text_field', method: :postal_code,
options: {
data: {
"refresh-fields-target": "field"
"#{dependent_fields_controller_name}-target": "field"
}
},
other_options: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<%
stimulus_controller = "dependent-fields-frame"
html_options ||= {}
html_options[:data] ||= {}
html_options[:data][:controller] ||= ""
html_options[:data][:controller] += " #{stimulus_controller}"
html_options[:data][:action] ||= ""
html_options[:data][:action] += " dependable:updated->#{stimulus_controller}#updateFrameFromDependableField turbo:frame-render->#{stimulus_controller}#finishFrameUpdate"
html_options[:data]["#{stimulus_controller}-loading-class"] ||= "opacity-60"

dependable_fields ||= []
%>
<%= turbo_frame_tag id, **html_options do %>
<%
dependable_fields.each do |method|
accept_query_string_override_for(form, method)
end
%>
<%= yield stimulus_controller %>
<% end %>

0 comments on commit a37318e

Please sign in to comment.