Skip to content

Commit

Permalink
feat: New Boolean Group Options functionality
Browse files Browse the repository at this point in the history
As an engineer, I'd like to easily configure a boolean group.  This PR
makes the following changes:

- automatic field detection for inline and embedded `embeds_one` groups
  now work.  (It was broken for inline and did not exist for embedded)
- Boolean Groups can now define their options with the same
  functionality as Select fields (ignoring groups)
- Boolean Group UI was updated for the changes ensuring that order
  matters.
  • Loading branch information
staylorwr committed Jun 8, 2021
1 parent c674da7 commit f79e867
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 158 deletions.
12 changes: 6 additions & 6 deletions assets/src/components/Detail/BooleanGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
>
<li
v-for="option in value"
:key="option.name"
:key="option.value"
class="mb-1 text-gray-darkest"
>
<span
:class="{ 'bg-success': option.checked, 'bg-danger': !option.checked }"
class="inline-block rounded-full w-2 h-2 mr-1"
/>
<span>{{ option.label }}</span>
<span>{{ option.key }}</span>
</li>
</ul>
<span v-else>{{ field.options.no_value || "No Data" }}</span>
Expand Down Expand Up @@ -54,11 +54,11 @@ export default {
created () {
this.field.value = this.field.value || {};
const options = this.field.options.group_options || {};
this.value = Object.keys(options).map(name => {
this.value = options.map(option => {
return {
name: name,
label: options[name],
checked: this.field.value[name] || false
key: option.key,
value: option.value,
checked: this.field.value[option.value] || false
};
});
},
Expand Down
25 changes: 11 additions & 14 deletions assets/src/components/Form/BooleanGroup.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
<template>
<default-field
:field="field"
:errors="errors"
>
<default-field :field="field" :errors="errors">
<template slot="field">
<checkbox-with-label
v-for="option in value"
:key="option.name"
:key="option.value"
class="mt-2"
:name="option.name"
:name="option.key"
:checked="option.checked"
@change="toggle($event, option)"
>
{{ option.label }}
{{ option.key }}
</checkbox-with-label>
</template>
</default-field>
Expand All @@ -34,7 +31,7 @@ export default {
*/
finalPayload () {
return _(this.value)
.map(o => [ o.name, o.checked ])
.map((o) => [ o.value, o.checked ])
.fromPairs()
.value();
},
Expand All @@ -47,11 +44,11 @@ export default {
setInitialValue () {
this.field.value = this.field.value || {};
const options = this.field.options.group_options || {};
this.value = Object.keys(options).map(name => {
this.value = options.map((option) => {
return {
name: name,
label: options[name],
checked: this.field.value[name] || false
key: option.key,
value: option.value,
checked: this.field.value[option.value] || false,
};
});
},
Expand All @@ -68,10 +65,10 @@ export default {
* Toggle the option's value.
*/
toggle (event, option) {
const firstOption = this.value.find(o => o.name == option.name);
const firstOption = this.value.find((o) => o.value == option.value);
if (!firstOption) {
this.value[option.name] = event.target.checked;
this.value[option.value] = event.target.checked;
} else {
firstOption.checked = event.target.checked;
}
Expand Down
12 changes: 6 additions & 6 deletions assets/src/components/Index/BooleanGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
>
<li
v-for="option in value"
:key="option.name"
:key="option.key"
class="mb-1"
>
<span
Expand All @@ -24,7 +24,7 @@
width="20"
height="20"
/>
<span class="ml-1">{{ option.label }}</span>
<span class="ml-1">{{ option.key }}</span>
</span>
</li>
</ul>
Expand Down Expand Up @@ -54,11 +54,11 @@ export default {
created () {
this.field.value = this.field.value || {};
const options = this.field.options.group_options || {};
this.value = Object.keys(options).map(name => {
this.value = options.map(option => {
return {
name: name,
label: options[name],
checked: this.field.value[name] || false
key: option.key,
value: option.value,
checked: this.field.value[option.value] || false
};
});
}
Expand Down
68 changes: 68 additions & 0 deletions lib/ex_teal/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,72 @@ defmodule ExTeal.Field do

def help_text(%Field{options: options} = f, text),
do: %{f | options: Map.put_new(options, :help_text, text)}

@doc """
Define the options for the select field. The function accepts a list of
options that are either strings or maps with of `value` and `label` keys. If
the list members are strings, the value will be used for both the value and label
of the `<option>` element it represents.
`options` are expected to be an enumerable which will be used to generate
each respective `option`. The enumerable may have:
* keyword lists - each keyword list is expected to have the keys `:key` and
`:value`. Additional keys such as `:disabled` may be given to customize
the option
* two-item tuples - where the first element is an atom, string or integer to
be used as the option label and the second element is an atom, string or
integer to be used as the option value
* atom, string or integer - which will be used as both label and value for
the generated select
## Optgroups
If `options` is a map or keyword list where the firs element is a string, atom,
or integer and the second element is a list or a map, it is assumed the key
will be wrapped in an `<optgroup>` and teh value will be used to generate
`<options>` nested under the group. This functionality is only handled in the UI for
select fields, boolean groups will not respond.
This functionality is equivalent to `Phoenix.HTML.Form.select/3`
"""
def transform_options(options) do
Enum.into(options, [], fn
{option_key, option_value} ->
option(option_key, option_value, [])

options_list when is_list(options_list) ->
{option_key, options_list} = Keyword.pop(options_list, :key)

option_key ||
raise ArgumentError,
"expected :key key when building <option> from keyword list: #{
inspect(options_list)
}"

{option_value, options_list} = Keyword.pop(options_list, :value)

option_value ||
raise ArgumentError,
"expected :value key when building option from keyword list: #{
inspect(options_list)
}"

option(option_key, option_value, options_list)

option_value ->
option(option_value, option_value, [])
end)
end

defp option(group_label, group_values, [])
when is_list(group_values) or is_map(group_values) do
%{group: group_label, options: transform_options(group_values)}
end

defp option(key, value, extra) do
%{value: value, key: key, disabled: Keyword.get(extra, :disabled, false)}
end
end
39 changes: 23 additions & 16 deletions lib/ex_teal/fields/boolean_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ defmodule ExTeal.Fields.BooleanGroup do
{:embed, embedded} ->
parameterize_an_embed(field, embedded)

{:parameterized, Ecto.Embedded, %Ecto.Embedded{cardinality: :one}} ->
field
{:parameterized, Ecto.Embedded, %Ecto.Embedded{cardinality: :one} = embedded} ->
parameterize_an_embedded(field, embedded)
end
end

Expand All @@ -103,9 +103,9 @@ defmodule ExTeal.Fields.BooleanGroup do
@doc """
Add the available options to manage in the boolean group
"""
@spec options(Field.t(), map()) :: Field.t()
@spec options(Field.t(), any()) :: Field.t()
def options(field, options) do
opts = Map.merge(options, %{group_options: options})
opts = Map.merge(field.options, %{group_options: ExTeal.Field.transform_options(options)})
%{field | options: opts}
end

Expand All @@ -118,17 +118,24 @@ defmodule ExTeal.Fields.BooleanGroup do
end

defp parameterize_an_embed(field, embedded_schema) do
fields = embedded_schema.related.__schema__(:fields)
group_options = Map.get(field.options, :group_options, %{})

opts =
(fields -- [:id])
|> Enum.into(%{}, fn f ->
field_string = Atom.to_string(f)
{field_string, field_string}
end)

opts = Map.merge(opts, group_options)
options(field, opts)
case Map.fetch(field.options, :group_options) do
{:ok, _options} ->
field

_ ->
fields = embedded_schema.related.__schema__(:fields)
options(field, fields -- [:id])
end
end

defp parameterize_an_embedded(field, embedded) do
case Map.fetch(field.options, :group_options) do
{:ok, _options} ->
field

_ ->
embedded_fields = embedded.related.__schema__(:fields) -- [:id]
options(field, embedded_fields)
end
end
end
49 changes: 6 additions & 43 deletions lib/ex_teal/fields/select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule ExTeal.Fields.Select do
"""

use ExTeal.Field
alias ExTeal.Field

def component, do: "select"

Expand Down Expand Up @@ -42,7 +43,10 @@ defmodule ExTeal.Fields.Select do
This functionality is equivalent to `Phoenix.HTML.Form.select/3`
"""
def options(field, options) do
%{field | options: Map.put_new(field.options, :field_options, transform_options(options))}
%{
field
| options: Map.put_new(field.options, :field_options, Field.transform_options(options))
}
end

@doc """
Expand All @@ -59,47 +63,6 @@ defmodule ExTeal.Fields.Select do
%{field | options: Map.put_new(field.options, :searchable, true)}
end

@doc """
Returns the options to be used inside of a select.
"""
def transform_options(options) do
Enum.into(options, [], fn
{option_key, option_value} ->
option(option_key, option_value, [])

options_list when is_list(options_list) ->
{option_key, options_list} = Keyword.pop(options_list, :key)

option_key ||
raise ArgumentError,
"expected :key key when building <option> from keyword list: #{
inspect(options_list)
}"

{option_value, options_list} = Keyword.pop(options_list, :value)

option_value ||
raise ArgumentError,
"expected :value key when building option from keyword list: #{
inspect(options_list)
}"

option(option_key, option_value, options_list)

option_value ->
option(option_value, option_value, [])
end)
end

defp option(group_label, group_values, [])
when is_list(group_values) or is_map(group_values) do
%{group: group_label, options: transform_options(group_values)}
end

defp option(key, value, extra) do
%{value: value, key: key, disabled: Keyword.get(extra, :disabled, false)}
end

def with_options(field, options) when is_map(options) do
IO.warn("with_options/2 is depreciated. See `ExTeal.Fields.Select.options/2`")
options(field, options)
Expand Down Expand Up @@ -148,7 +111,7 @@ defmodule ExTeal.Fields.Select do
def apply_options_for(%Field{options: options} = field, model, _type) do
if field_represents_an_enum?(field, model) and Map.fetch(options, :field_options) == :error do
{:parameterized, Ecto.Enum, details} = schema_field_type(field, model)
enum_options = transform_options(details.values)
enum_options = Field.transform_options(details.values)
%{field | options: Map.put(field.options, :field_options, enum_options)}
else
field
Expand Down
Loading

0 comments on commit f79e867

Please sign in to comment.