Skip to content

Commit

Permalink
feat: Select refactor (#43)
Browse files Browse the repository at this point in the history
This commit includes a major update to the `Select` field by introducing
`options/2`, `searchable/1` and depreciating `with_options/2` and
`display_using_labels/1`.

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

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 equivalent to `Phoenix.HTML.Form.select/3`

At times it's convenient to be able to search or filter the list of options in a
select field.  You can enable this by calling `Select.searchable` on the field:

```
Select.make(:type) |> Select.options(~w(foo bar)) |> Select.searchable()
```

When using this field, Teal will display an input field which allows you to filter
the list based on it's key.

`with_options/2` and `display_using_labels/1` are now depreciated and emit a
warning when called.
  • Loading branch information
staylorwr committed Nov 18, 2020
1 parent b1a3abd commit 667dbd9
Show file tree
Hide file tree
Showing 17 changed files with 391 additions and 213 deletions.
30 changes: 14 additions & 16 deletions assets/src/components/Controls/SelectControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@
v-on="inputListeners"
>
<slot />
<template v-for="(gOptions, group) in groupedOptions">
<template v-for="option in options">
<optgroup
v-if="group"
:key="group"
:label="group"
v-if="option.group"
:key="option.group"
:label="option.group"
>
<option
v-for="option in gOptions"
:key="option.value"
v-bind="attrsFor(option)"
v-for="o in option.options"
:key="o.key"
v-bind="attrsFor(o)"
>
{{ labelFor(option) }}
{{ labelFor(o) }}
</option>
</optgroup>
<template v-else>
<option
v-for="option in gOptions"
:key="option.id"
:key="option.value"
v-bind="attrsFor(option)"
>
{{ labelFor(option) }}
Expand All @@ -34,7 +33,6 @@
</template>

<script>
import groupBy from 'lodash/groupBy';
import assign from 'lodash/assign';
export default {
props: {
Expand All @@ -59,12 +57,12 @@ export default {
},
computed: {
groupedOptions () {
return groupBy(this.options, option => option.group || '');
},
inputListeners () {
return assign({}, this.$listeners, {
change: event => {
this.$emit('input', event.target.value);
this.$emit('change', event);
},
input: event => {
this.$emit('input', event.target.value);
},
Expand All @@ -84,7 +82,7 @@ export default {
{ value: option[this.valueKey] },
this.selected !== void 0 ? { selected: this.selected == option[this.valueKey] } : {}
);
},
}
},
};
</script>
9 changes: 7 additions & 2 deletions assets/src/components/DropdownMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
>
<div
:style="styles"
class="z-40 overflow-hidden bg-white border shadow"
class="z-40 overflow-x-hidden overflow-y-scroll bg-white border shadow"
>
<slot />
</div>
Expand All @@ -28,6 +28,10 @@ export default {
default: 120,
type: [ Number, String ]
},
maxHeight: {
default: 200,
type: [ Number, String ]
},
override: {
type: String,
default: null
Expand All @@ -47,7 +51,8 @@ export default {
},
styles () {
return {
width: `${this.width}px`
width: `${this.width}px`,
'max-height': `${this.maxHeight}px`
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/FieldFilters/FieldFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
/>
<button
:class="{
'hidden group-hover:block rounded-r border border-danger text-danger text-xs h-8 w-8 bg-gray-lightest': true
'hidden group-hover:flex items-center justify-center rounded-r border border-danger text-danger text-xs h-8 w-8 bg-gray-lightest': true
}"
@click="deleteFilter"
>
Expand Down
107 changes: 79 additions & 28 deletions assets/src/components/Form/Select.vue
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
<template>
<default-field :field="field">
<template slot="field">
<select
:id="field.attribute"
v-model="value"
:class="errorClasses"
class="w-full form-control form-select"
<search-input
v-if="isSearchable"
:value="selectedOption"
:data="filteredOptions"
track-by="value"
class="w-full"
@input="performSearch"
@selected="selectOption"
>
<option
value=""
selected
disabled
<div
v-if="selectedOption"
slot="default"
class="flex items-center"
>
Choose an Option
</option>

<option
v-for="(label, key) in field.options"
:key="key"
:value="key"
:selected="key == value"
{{ selectedOption.key }}
</div>
<div
slot="option"
slot-scope="{ option, selected }"
class="flex items-center text-sm font-semibold leading-5 text-gray-darkest"
:class="{ 'text-gray': selected }"
>
{{ label }}
</option>
</select>

<p
v-if="hasError"
class="my-2 text-danger"
>
{{ firstError }}
</p>
{{ option.key }}
</div>
</search-input>
<select-control
v-else
:selected="value"
class="w-full form-control form-select"
:options="field.options.field_options"
label="key"
value-key="value"
@change="handleChange"
/>
</template>
</default-field>
</template>
Expand All @@ -41,10 +45,57 @@ import { FormField, HandlesValidationErrors } from 'ex-teal-js';
export default {
mixins: [ HandlesValidationErrors, FormField ],
data: () => ({
selectedOption: '',
search: ''
}),
computed: {
placeholder () {
return this.field.options.placeholder || 'Choose an option';
},
isSearchable () {
return this.field.options.searchable;
},
filteredOptions () {
return this.field.options.field_options.filter(option => {
return (
option.key.toLowerCase().indexOf(this.search.toLowerCase()) > -1
);
});
}
},
created () {
if (this.field.value && this.isSearchable) {
this.selectedOption = this.field.options.field_options.find(
v => v.value == this.field.value
);
}
},
methods: {
fill (formData) {
formData.append(this.field.attribute, this.value);
},
/**
* Handle the selection change event.
*/
handleChange (e) {
this.value = e.target.value;
},
performSearch (event) {
this.search = event;
},
selectOption (option) {
this.selectedOption = option;
this.value = option.value;
}
}
};
</script>
</script>
2 changes: 1 addition & 1 deletion assets/src/mixins/InteractsWithTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const hexToRgb = function (hex) {
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
};

const InteractsWithTheme = {
methods: {
Expand Down
54 changes: 15 additions & 39 deletions lib/ex_teal/field_filter/select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ defmodule ExTeal.FieldFilter.Select do

use ExTeal.FieldFilter
import Ecto.Query
alias ExTeal.Fields.Select

@impl true
def operators(field) do
field
|> Map.get(:options, [])
|> Enum.map(fn {_value, label} ->
%{"op" => label, "no_operand" => true}
|> Select.all_options_for()
|> Enum.map(fn %{value: value} ->
%{"op" => value, "no_operand" => true}
end)
end

Expand All @@ -26,50 +27,25 @@ defmodule ExTeal.FieldFilter.Select do

option =
field
|> Map.get(:options, [])
|> Enum.find(fn {_k, label} -> label == op end)
|> Select.all_options_for()
|> Enum.find(fn %{value: value} -> value == op end)

case option do
{k, _} ->
where(query, [q], field(q, ^field_name) == ^k)
%{key: key} ->
where(query, [q], field(q, ^field_name) == ^key)

_ ->
query
end
end

def filter(query, %{"operator" => "!=", "operand" => val}, field_name, _)
when val != "" and not is_nil(val) do
where(query, [q], field(q, ^field_name) != ^val)
end

def filter(query, %{"operator" => ">", "operand" => val}, field_name, _)
when val != "" and not is_nil(val) do
where(query, [q], field(q, ^field_name) > ^val)
end

def filter(query, %{"operator" => ">=", "operand" => val}, field_name, _)
when val != "" and not is_nil(val) do
where(query, [q], field(q, ^field_name) >= ^val)
end

def filter(query, %{"operator" => "<", "operand" => val}, field_name, _)
when val != "" and not is_nil(val) do
where(query, [q], field(q, ^field_name) < ^val)
end

def filter(query, %{"operator" => "<=", "operand" => val}, field_name, _)
when val != "" and not is_nil(val) do
where(query, [q], field(q, ^field_name) <= ^val)
end
def filter(query, op, field_name, resource) do
IO.warn(
"Unmatched select field filter for resource: #{resource.title} on field #{field_name} with params: #{
inspect(op)
}"
)

def filter(query, %{"operator" => "is empty"}, field_name, _) do
where(query, [q], is_nil(field(q, ^field_name)))
query
end

def filter(query, %{"operator" => "not empty"}, field_name, _) do
where(query, [q], not is_nil(field(q, ^field_name)))
end

def filter(query, _, _, _), do: query
end
Loading

0 comments on commit 667dbd9

Please sign in to comment.