Packages
phoenix_kit
1.7.49
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/shop/web/option_state.ex
defmodule PhoenixKit.Modules.Shop.Web.OptionState do
@moduledoc """
Encapsulates option-related state for product form.
This module manages all option-related data in a single struct,
replacing the multiple assigns previously used in product_form.ex:
- `new_value_inputs` -> `state.new_inputs`
- `selected_option_values` -> `state.selected`
- `original_option_values` -> `state.available`
- `metadata["_price_modifiers"]` -> `state.modifiers`
- `option_schema` -> `state.schema`
## Usage
# Initialize state from product and schema
state = OptionState.new(product, option_schema)
# Toggle a value selection
state = OptionState.toggle_value(state, "size", "M", ["S", "M", "L"])
# Add a new custom value
state = OptionState.add_value(state, "size", "XL")
# Remove a value
state = OptionState.remove_value(state, "size", "XL")
# Update a price modifier
state = OptionState.update_modifier(state, "size", "M", "5.00")
# Convert back to metadata for saving
metadata = OptionState.to_metadata(state)
"""
defstruct [
# Merged global + category option schema
schema: [],
# All available values per option key (original + manually added)
available: %{},
# Currently selected values per option key
selected: %{},
# Price modifiers per option/value (string format)
modifiers: %{},
# Temporary input field values for "add value" inputs
new_inputs: %{}
]
@type t :: %__MODULE__{
schema: list(map()),
available: %{String.t() => list(String.t())},
selected: %{String.t() => list(String.t())},
modifiers: %{String.t() => %{String.t() => String.t()}},
new_inputs: %{String.t() => String.t()}
}
@doc """
Creates a new OptionState from a product and option schema.
## Examples
product = %Product{metadata: %{"_option_values" => %{"size" => ["M", "L"]}}}
schema = [%{"key" => "size", "type" => "select", "options" => ["S", "M", "L"]}]
state = OptionState.new(product, schema)
# => %OptionState{
# schema: [...],
# available: %{"size" => ["S", "M", "L"]},
# selected: %{"size" => ["M", "L"]},
# modifiers: %{},
# new_inputs: %{}
# }
"""
def new(product, option_schema) when is_list(option_schema) do
metadata = (product && product.metadata) || %{}
option_values = Map.get(metadata, "_option_values", %{})
price_modifiers = Map.get(metadata, "_price_modifiers", %{})
# Build available values from option_values (imported/saved)
available = option_values
# Selected = saved option values (if present) or all available
selected = option_values
%__MODULE__{
schema: option_schema,
available: available,
selected: selected,
modifiers: normalize_modifiers(price_modifiers),
new_inputs: %{}
}
end
def new(nil, option_schema), do: new(%{metadata: %{}}, option_schema)
@doc """
Toggles a value selection on/off.
If the value is selected, it will be deselected. If not selected, it will be selected.
The `all_values` parameter is used to determine when all values are selected
(in which case the key is removed from selected map).
## Examples
state = OptionState.toggle_value(state, "size", "M", ["S", "M", "L"])
"""
def toggle_value(%__MODULE__{} = state, option_key, value, all_values)
when is_binary(option_key) and is_binary(value) and is_list(all_values) do
current = Map.get(state.selected, option_key, all_values)
updated =
if value in current do
Enum.reject(current, &(&1 == value))
else
current ++ [value]
end
# Normalize selection state
new_selected =
cond do
# None selected - keep explicit empty list
updated == [] ->
Map.put(state.selected, option_key, [])
# All selected - remove key to indicate "all"
Enum.sort(updated) == Enum.sort(all_values) ->
Map.delete(state.selected, option_key)
# Partial selection
true ->
Map.put(state.selected, option_key, updated)
end
%{state | selected: new_selected}
end
@doc """
Adds a new value to an option.
The value is added to both `available` and `selected` maps.
Returns `{:ok, state}` or `{:error, reason}`.
## Examples
{:ok, state} = OptionState.add_value(state, "size", "XL")
{:error, "already exists"} = OptionState.add_value(state, "size", "M")
"""
def add_value(%__MODULE__{} = state, option_key, value)
when is_binary(option_key) and is_binary(value) do
value = String.trim(value)
if value == "" do
{:error, "value cannot be empty"}
else
# Get all existing values (schema + available)
schema_values = get_schema_values(state.schema, option_key)
available_values = Map.get(state.available, option_key, [])
all_existing = Enum.uniq(schema_values ++ available_values)
if value in all_existing do
{:error, "value '#{value}' already exists"}
else
# Add to available
new_available =
Map.update(state.available, option_key, [value], fn existing ->
existing ++ [value]
end)
# Add to selected (new value is selected by default)
current_selected = Map.get(state.selected, option_key, all_existing)
new_selected = Map.put(state.selected, option_key, current_selected ++ [value])
# Clear input
new_inputs = Map.put(state.new_inputs, option_key, "")
{:ok, %{state | available: new_available, selected: new_selected, new_inputs: new_inputs}}
end
end
end
@doc """
Removes a value from an option.
Removes from `available`, `selected`, and any associated modifiers.
## Examples
state = OptionState.remove_value(state, "size", "XL")
"""
def remove_value(%__MODULE__{} = state, option_key, value)
when is_binary(option_key) and is_binary(value) do
# Remove from available
new_available =
case Map.get(state.available, option_key) do
nil ->
state.available
values ->
updated = Enum.reject(values, &(&1 == value))
if updated == [] do
Map.delete(state.available, option_key)
else
Map.put(state.available, option_key, updated)
end
end
# Remove from selected
new_selected =
case Map.get(state.selected, option_key) do
nil ->
state.selected
values ->
updated = Enum.reject(values, &(&1 == value))
if updated == [] do
Map.delete(state.selected, option_key)
else
Map.put(state.selected, option_key, updated)
end
end
# Remove modifier
new_modifiers = remove_modifier(state.modifiers, option_key, value)
%{state | available: new_available, selected: new_selected, modifiers: new_modifiers}
end
@doc """
Updates a price modifier for a specific option value.
## Examples
state = OptionState.update_modifier(state, "size", "M", "5.00")
"""
def update_modifier(%__MODULE__{} = state, option_key, value, modifier_value)
when is_binary(option_key) and is_binary(value) do
new_modifiers =
if modifier_value == nil or modifier_value == "" or modifier_value == "0" do
remove_modifier(state.modifiers, option_key, value)
else
option_mods = Map.get(state.modifiers, option_key, %{})
option_mods = Map.put(option_mods, value, modifier_value)
Map.put(state.modifiers, option_key, option_mods)
end
%{state | modifiers: new_modifiers}
end
@doc """
Updates the new value input for an option key.
## Examples
state = OptionState.update_new_input(state, "size", "XL")
"""
def update_new_input(%__MODULE__{} = state, option_key, value)
when is_binary(option_key) do
%{state | new_inputs: Map.put(state.new_inputs, option_key, value || "")}
end
@doc """
Converts the state back to a metadata map for saving.
Returns a map with `_option_values` and `_price_modifiers` keys.
Empty maps are omitted.
## Examples
state = %OptionState{
selected: %{"size" => ["M", "L"]},
modifiers: %{"size" => %{"M" => "5.00"}}
}
OptionState.to_metadata(state)
# => %{
# "_option_values" => %{"size" => ["M", "L"]},
# "_price_modifiers" => %{"size" => %{"M" => "5.00"}}
# }
"""
def to_metadata(%__MODULE__{} = state) do
metadata = %{}
# Add _option_values if present
metadata =
if state.available != %{} do
Map.put(metadata, "_option_values", state.available)
else
metadata
end
# Add _price_modifiers if present
metadata =
if state.modifiers != %{} do
Map.put(metadata, "_price_modifiers", state.modifiers)
else
metadata
end
metadata
end
@doc """
Checks if a value is currently selected for an option.
## Examples
OptionState.value_selected?(state, "size", "M", ["S", "M", "L"])
# => true
"""
def value_selected?(%__MODULE__{} = state, option_key, value, all_values) do
case Map.get(state.selected, option_key) do
nil -> value in all_values
selected -> value in selected
end
end
@doc """
Gets all values available for an option (schema + custom added).
## Examples
OptionState.get_all_values(state, "size")
# => ["S", "M", "L", "XL"]
"""
def get_all_values(%__MODULE__{} = state, option_key) do
schema_values = get_schema_values(state.schema, option_key)
custom_values = Map.get(state.available, option_key, [])
Enum.uniq(schema_values ++ custom_values)
end
@doc """
Gets selected values for an option (or all if not explicitly set).
## Examples
OptionState.get_selected_values(state, "size", ["S", "M", "L"])
# => ["M", "L"]
"""
def get_selected_values(%__MODULE__{} = state, option_key, all_values) do
Map.get(state.selected, option_key, all_values)
end
@doc """
Gets the modifier value for an option/value pair.
## Examples
OptionState.get_modifier(state, "size", "M")
# => "5.00"
"""
def get_modifier(%__MODULE__{} = state, option_key, value) do
get_in(state.modifiers, [option_key, value])
end
@doc """
Checks if option has custom selection (not all values selected).
## Examples
OptionState.has_custom_selection?(state, "size")
# => true
"""
def has_custom_selection?(%__MODULE__{} = state, option_key) do
Map.has_key?(state.selected, option_key)
end
@doc """
Adds a completely new option with an initial value.
Returns `{:ok, state}` or `{:error, reason}`.
## Examples
{:ok, state} = OptionState.add_new_option(state, "material", "Wood")
"""
def add_new_option(%__MODULE__{} = state, option_key, value)
when is_binary(option_key) and is_binary(value) do
key = option_key |> String.trim() |> String.downcase() |> String.replace(~r/\s+/, "_")
value = String.trim(value)
cond do
key == "" or value == "" ->
{:error, "option key and value are required"}
# Check if value already exists in this option
value in get_all_values(state, key) ->
{:error, "value '#{value}' already exists in '#{key}'"}
# Check if this is adding to existing option
Map.has_key?(state.available, key) or
Enum.any?(state.schema, &(&1["key"] == key)) ->
# Add value to existing option
add_value(state, key, value)
# New option entirely
true ->
new_available = Map.put(state.available, key, [value])
new_selected = Map.put(state.selected, key, [value])
{:ok, %{state | available: new_available, selected: new_selected}}
end
end
# Private helpers
defp get_schema_values(schema, option_key) do
case Enum.find(schema, &(&1["key"] == option_key)) do
nil -> []
opt -> opt["options"] || []
end
end
defp remove_modifier(modifiers, option_key, value) do
case Map.get(modifiers, option_key) do
nil ->
modifiers
option_mods ->
updated = Map.delete(option_mods, value)
if updated == %{} do
Map.delete(modifiers, option_key)
else
Map.put(modifiers, option_key, updated)
end
end
end
# Normalize modifiers to ensure all values are strings
defp normalize_modifiers(modifiers) when is_map(modifiers) do
Enum.map(modifiers, fn {key, values} when is_map(values) ->
normalized_values =
Enum.map(values, fn
{k, v} when is_binary(v) -> {k, v}
{k, %{"value" => v}} when is_binary(v) -> {k, v}
{k, _} -> {k, "0"}
end)
|> Map.new()
{key, normalized_values}
end)
|> Map.new()
end
defp normalize_modifiers(_), do: %{}
end