Skip to content

Commit

Permalink
Add address multi-field partials, add support for self-refreshing for…
Browse files Browse the repository at this point in the history
…m fields (#86)

* add addresses/countries.json and addresses/states.json

* add Addresses::Country, Addresses::Region models, concerns

* add Address model, Addresses::Base concern

* add address_field helpers to popuplate country, region options

* add addresses locale

* add address_field

* add address field to tangible_things

* permit address field for tangible things

* add admin_divisions to certain countries

* add locale strings for admin_divisions, zip_code

* adapt region name and postal code field labels per country

* switch tangible_things to default to Japan

* super-select: allow setting more wrapper_options

* add refresh_fields_helper

* add dependable_controller.js, refresh_fields_controller.js

* address_field: update region, postal code fields on country change

* refresh_fields_helper: clean up to_sym

* address_field: allow nil country_id

* address_field: restore previous field values after dependent fields refreshed

* refresh_fields: toggle loading styling, disable fields while loading

* refresh_fields: ensure super-select validates new value on restoreFieldValues

* refactor refresh_fields_controller

* fix Address model class

* address_field: lay out in three columns on desktop: city, state, zip

* address: rely on list of country_ids for knowing how to call admin divisions

* tangible_things: :address => :address_value

* bullet_train-fields: add snail gem

* add address_formatted using snail gem

* add address attributes partial

* add address attribute to tangible_things/show

* add all_blank? check to records/base for checking if address is blank

* override Address.all_blank? with custom check

* only show address attribute (and label) if not all_blank?

* tangible_thing: provide address to edit form if missing

* fix Address.all_blank?

* replace address_formatted with modified code from snail gem

* address_formatted: tweaks

* remove snail gem

* addresses.en.yml: remove default country, region options

* add Addresses::Continent and Addresses::Subcontinent models

* address_field_helper: add notes about different lookup approaches

* address_city_line_formatted: clarify cleanup steps

* Fixing Standard Ruby.

* add addresses/countries.json and addresses/states.json

* add Addresses::Country, Addresses::Region models, concerns

* add Address model, Addresses::Base concern

* add address_field helpers to popuplate country, region options

* add addresses locale

* add address_field

* add address field to tangible_things

* permit address field for tangible things

* add admin_divisions to certain countries

* add locale strings for admin_divisions, zip_code

* adapt region name and postal code field labels per country

* switch tangible_things to default to Japan

* super-select: allow setting more wrapper_options

* add refresh_fields_helper

* add dependable_controller.js, refresh_fields_controller.js

* address_field: update region, postal code fields on country change

* refresh_fields_helper: clean up to_sym

* address_field: allow nil country_id

* address_field: restore previous field values after dependent fields refreshed

* refresh_fields: toggle loading styling, disable fields while loading

* refresh_fields: ensure super-select validates new value on restoreFieldValues

* refactor refresh_fields_controller

* fix Address model class

* address_field: lay out in three columns on desktop: city, state, zip

* address: rely on list of country_ids for knowing how to call admin divisions

* tangible_things: :address => :address_value

* bullet_train-fields: add snail gem

* add address_formatted using snail gem

* add address attributes partial

* add address attribute to tangible_things/show

* add all_blank? check to records/base for checking if address is blank

* override Address.all_blank? with custom check

* only show address attribute (and label) if not all_blank?

* tangible_thing: provide address to edit form if missing

* fix Address.all_blank?

* replace address_formatted with modified code from snail gem

* address_formatted: tweaks

* remove snail gem

* addresses.en.yml: remove default country, region options

* add Addresses::Continent and Addresses::Subcontinent models

* address_field_helper: add notes about different lookup approaches

* address_city_line_formatted: clarify cleanup steps

* Fixing Standard Ruby.

* empty commit for CI

* remove duplicate all_blank? method

* re-remove to_api_json

* moar declarative

---------

Co-authored-by: Andrew Culver <andrew.culver@gmail.com>
Co-authored-by: Jeremy Green <jeremy@octolabs.com>
  • Loading branch information
3 people authored Sep 1, 2023
1 parent eb1758b commit 4f79050
Show file tree
Hide file tree
Showing 29 changed files with 55,431 additions and 2 deletions.
1 change: 1 addition & 0 deletions bullet_train-fields/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ PLATFORMS
arm64-darwin-22
ruby
x86_64-darwin-21
x86_64-darwin-22
x86_64-linux

DEPENDENCIES
Expand Down
127 changes: 127 additions & 0 deletions bullet_train-fields/app/helpers/fields/address_field_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
module Fields::AddressFieldHelper
def populate_country_options
([Addresses::Country.find_by(name: "United States"), Addresses::Country.find_by(name: "Canada")] + Addresses::Country.all).map { |country| [country.name, country.id] }.uniq
end

def populate_region_options(address_form)
return [] if address_form.object.country_id.nil?
Addresses::Region.where(country_id: address_form.object.country_id).map { |region| [region.name, region.id] }
end

def admin_division_label_for(address_form)
# using country_id because it's fastest, even if this case statement is hard to read
admin_divisions_key = case address_form.object.country_id
when 233, 31, 142, 239, 101
:states
when 39
:provinces_territories
when 109
:prefectures
when 107, 45, 116, 182, 207, 219, 230, 156, 204
:provinces
when 14
:states_territories
when 59
:regions
when 82, 15
:federal_states
when 75
:departments
when 105
:counties
when 214
:cantons
else
:default
end
path = [:addresses, :fields, :admin_divisions, admin_divisions_key]
t(path.compact.join("."))
end

def postal_code_label_for(address_form)
key = if address_form.object.country_id == 233
:zip_code
else
:postal_code
end
path = [:addresses, :fields, key, :label]
t(path.compact.join("."))
end

def address_formatted(address, one_line: false)
return "" if address.all_blank?

formatted = [
address.address_one,
address.address_two,
address_city_line_formatted(address),
address.country&.name&.upcase
].reject(&:blank?)

if one_line
formatted.join(", ") # simplistic
else
simple_format(formatted.join("\n"))
end
end

def address_city_line_formatted(address)
country_iso2 = address.country&.iso2 # can be nil or empty
city = address.city
region = address.region&.name
postal_code = address.postal_code

# adapted from https://github.com/cainlevy/snail/blob/master/lib/snail.rb
# using iso2 property here because it's a port of what's used in snail gem
# will be cleaned up below if parts missing
formatted = case country_iso2
when "CN", "IN"
"#{city}, #{region} #{postal_code}"
when "BR"
"#{postal_code} #{city}#{"-" unless city.nil? || city.empty? || region.nil? || region.empty?}#{region}"
when "MX", "SK"
"#{postal_code} #{city}, #{region}"
when "IT"
"#{postal_code} #{city} #{region}"
when "BY"
"#{postal_code} #{city}#{"-" unless city.nil? || city.empty? || region.nil? || region.empty?}#{region}"
when "US", "CA", "AU", nil, ""
"#{city} #{region} #{postal_code}"
when "IL", "DK", "FI", "FR", "DE", "GR", "NO", "ES", "SE", "TR", "CY", "PT", "MK", "BA"
"#{postal_code} #{city}"
when "KW", "SY", "OM", "EE", "LU", "BE", "IS", "CH", "AT", "MD", "ME", "RS", "BG", "GE", "PL", "AM", "HR", "RO", "AZ"
"#{postal_code} #{city}"
when "NL"
"#{postal_code} #{city}"
when "IE"
"#{city}, #{region}\n#{postal_code}"
when "GB", "RU", "UA", "JO", "LB", "IR", "SA", "NZ"
"#{city} #{postal_code}" # Locally these may be on separate lines. The USPS prefers the city line above the country line, though.
when "EC"
"#{postal_code} #{city}"
when "HK", "IQ", "YE", "QA", "AL"
city.to_s
when "AE"
"#{postal_code}\n#{city}"
when "JP"
"#{city}, #{region}\n#{postal_code}"
when "EG", "ZA", "IM", "KZ", "HU"
"#{city}\n#{postal_code}"
when "LV"
"#{city}, LV-#{postal_code}".gsub(/LV-\s*$/, "") # undo if no postal code
when "LT"
"LT-#{postal_code} #{city}".gsub(/^LT-\s*/, "") # undo if no postal code
when "SI"
"SI-#{postal_code} #{city}".gsub(/^SI-\s*/, "") # undo if no postal code
when "CZ"
"#{postal_code} #{region}\n#{city}"
else
"#{city} #{region} #{postal_code}"
end

# clean up separators when missing pieces
formatted.strip # remove extra spaces and newlines before and after
.gsub(/^,\s*/, "") # remove extra comma from start
.gsub(/\s*,$/, "") # remove extra comma from end
end
end
30 changes: 30 additions & 0 deletions bullet_train-fields/app/helpers/refresh_fields_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module RefreshFieldsHelper
def accept_query_string_override_for(form, method)
field_name = form.field_name(method)

new_value = new_value_from_query_string(field_name)
return if new_value.nil?

form.object[method] = new_value
end

private

def new_value_from_query_string(field_name)
params.dig(*params_dig_path_for_field_name(field_name))
end

def params_dig_path_for_field_name(field_name)
dig_path = []

nested_keys = Rack::Utils.parse_nested_query(field_name)

while !nested_keys.nil? && nested_keys.keys.size
key = nested_keys.keys.first
dig_path << key.to_sym
nested_keys = nested_keys[key]
end

dig_path
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static values = {
dependentsSelector: String
}

updateDependents(event) {
if (!this.hasDependents) { return false }

this.dependents.forEach((dependent) => {
dependent.dispatchEvent(new CustomEvent(`${this.identifier}:updated`, { detail: { event: event }, bubbles: true, cancelable: false }))
})
}

get hasDependents() {
return (this.dependents.length > 0)
}

get dependents() {
if (!this.dependentsSelectorValue) { return [] }
return document.querySelectorAll(this.dependentsSelectorValue)
}
}
6 changes: 6 additions & 0 deletions bullet_train-fields/app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import FileItemController from './fields/file_item_controller'
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'

export const controllerDefinitions = [
[FieldController, 'fields/field_controller.js'],
Expand All @@ -24,6 +26,8 @@ export const controllerDefinitions = [
[PasswordController, 'fields/password_controller.js'],
[PhoneController, 'fields/phone_controller.js'],
[SuperSelectController, 'fields/super_select_controller.js'],
[DependableController, 'dependable_controller.js'],
[RefreshFieldsController, 'refresh_fields_controller.js'],
].map(function(d) {
const key = d[1]
const controller = d[0]
Expand All @@ -45,4 +49,6 @@ export {
PasswordController,
PhoneController,
SuperSelectController,
DependableController,
RefreshFieldsController,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = [ "field" ]
static values = {
valuesStore: { type: Object, default: {} },
loading: { type: Boolean, default: false }
}
static classes = [ "loading" ]

updateFrameFromDependentField(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

if (!field) { return }

this.storeFieldValues()

this.loadingValue = true
this.disableFieldInputWhileRefreshing()

const frame = this.element
frame.src = this.constructNewUrlUpdatingField(field.name, field.value)
}

finishFrameUpdate(event) {
if (event.target !== this.element) { return }

this.restoreFieldValues()
this.loadingValue = false
}

constructNewUrlUpdatingField(fieldName, fieldValue) {
const url = new URL(window.location.href)
url.searchParams.set(fieldName, fieldValue)

return url.href
}

disableFieldInputWhileRefreshing() {
this.fieldTargets.forEach(field => field.disabled = true )
}

loadingValueChanged() {
if (!this.hasLoadingClass) { return }
this.element.classList.toggle(...this.loadingClasses, this.loadingValue)
}

storeFieldValues() {
this.fieldTargets.forEach(field => {
let storeUpdate = {}
storeUpdate[field.name] = field.value
this.valuesStoreValue = Object.assign(this.valuesStoreValue, storeUpdate)
})
}

restoreFieldValues() {
this.fieldTargets.forEach(field => {
const storedValue = this.valuesStoreValue[field.name]
if (storedValue === undefined) { return }
field.value = storedValue
field.dispatchEvent(new Event('change')) // ensures cascading effects, including super-select validating against valid options
})

this.valuesStoreValue = {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ def show

# GET /account/scaffolding/absolutely_abstract/creative_concepts/:absolutely_abstract_creative_concept_id/completely_concrete/tangible_things/new
def new
# 🚅 skip this section when scaffolding.
@tangible_thing.address_value = Address.new
# 🚅 stop any skipping we're doing now.
end

# GET /account/scaffolding/completely_concrete/tangible_things/:id/edit
def edit
# 🚅 skip this section when scaffolding.
@tangible_thing.address_value ||= Address.new
# 🚅 stop any skipping we're doing now.
end

# POST /account/scaffolding/absolutely_abstract/creative_concepts/:absolutely_abstract_creative_concept_id/completely_concrete/tangible_things
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ def tangible_thing_params
# 🚅 skip this section when scaffolding.
multiple_button_values: [],
multiple_option_values: [],
address_value_attributes: [
:id,
:address_one,
:address_two,
:city,
:region_id,
:region_name,
:country_id,
:postal_code
],
multiple_super_select_values: []
# 🚅 stop any skipping we're doing now.
# 🚅 super scaffolding will insert new arrays above this line.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ class Scaffolding::CompletelyConcrete::TangibleThing < ApplicationRecord
# 🚅 add oauth providers above.

has_one_attached :file_field_value

has_one :team, through: :absolutely_abstract_creative_concept
has_one :address_value, class_name: "Address", as: :addressable
accepts_nested_attributes_for :address_value
# 🚅 add has_one associations above.

# 🚅 add scopes above.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<%= render 'shared/fields/super_select', method: :multiple_super_select_values, other_options: {search: true}, html_options: {multiple: true} %>
</div>
</div>

<%= render 'shared/fields/address_field', method: :address_value %>

<%= render 'shared/fields/text_area', method: :text_area_value %>
<%= render 'shared/fields/trix_editor', method: :action_text_value %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<%= render 'shared/attributes/email', attribute: :email_field_value %>
<%= render 'shared/attributes/text', attribute: :password_field_value %>
<%= render 'shared/attributes/phone_number', attribute: :phone_field_value %>
<%= render 'shared/attributes/address', attribute: :address_value %>
<%= render 'shared/attributes/option', attribute: :option_value %>
<%= render 'shared/attributes/options', attribute: :multiple_option_values %>
<%= render 'shared/attributes/option', attribute: :super_select_value %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ en:
_: &phone_field_value Phone Field Value
label: *phone_field_value
heading: *phone_field_value
address_value:
_: &address_value Address Value
label: *address_value
heading: *address_value
option_value:
_: &option_value Option Value
label: *option_value
Expand Down Expand Up @@ -215,6 +219,7 @@ en:
file_field_value: *file_field_value
password_field_value: *password_field_value
phone_field_value: *phone_field_value
address_value: *address_value
option_value: *option_value
multiple_option_values: *multiple_option_values
super_select_value: *super_select_value
Expand Down
Loading

0 comments on commit 4f79050

Please sign in to comment.