Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
advanced_select (0.1.6)
advanced_select (0.1.7)
actionview (>= 7.1)
railties (>= 7.1)
stimulus-rails (>= 1.3)
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ AdvancedSelect does not provide query objects, model concerns, authorization log
Add the gem to the host Rails app:

```ruby
gem "advanced_select", "~> 0.1.5"
gem "advanced_select", "~> 0.1.7"
```

Run the installer:
Expand Down Expand Up @@ -488,26 +488,28 @@ The partial is rendered on the server for the initial selection, and the control

- `data-advanced-select-tooltip-list` on the element that holds the rows (the same hook the built-in list uses).
- `<template data-advanced-select-tooltip-template>` containing the markup for a single selected row.
- `data-advanced-select-tooltip-field="…"` on the elements inside the template that should be filled in. Available fields are `id`, `value`, `label`, and `display_label`; values are written with `textContent` (text only, no markup).
- `data-advanced-select-tooltip-field="…"` on the elements inside the template that should be filled in. Available fields include `id`, `value`, `label`, `display_label`, extra option keys, and nested paths such as `test.name`; values are written with `textContent` (text only, no markup).

```erb
<%# app/views/alternatives/_tooltip.html.erb %>
<table>
<thead>
<tr><th>Code &amp; Name</th><th>Type</th></tr>
<tr><th>Code &amp; Name</th><th>Type</th><th>Test</th></tr>
</thead>
<tbody data-advanced-select-tooltip-list>
<% selected_options.each do |option| %>
<tr>
<td><%= option.fetch(:display_label) %></td>
<td><%= option.fetch(:value) %></td>
<td><%= option.dig(:test, :name) %></td>
</tr>
<% end %>
</tbody>
<template data-advanced-select-tooltip-template>
<tr>
<td data-advanced-select-tooltip-field="display_label"></td>
<td data-advanced-select-tooltip-field="value"></td>
<td data-advanced-select-tooltip-field="test.name"></td>
</tr>
</template>
</table>
Expand Down
111 changes: 85 additions & 26 deletions app/javascript/advanced_select/advanced_select_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ export default class extends Controller {
this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
this.selectedValue = this.selectedValue.map((option) => ({
...option,
id: option.id.toString(),
displayLabel: option.displayLabel || option.label
}))
this.selectedValue = this.selectedValue.map((option) => this.normalizeSelectedOption(option))
this.close = this.close.bind(this)
this.renderOptionsState()
this.setupEagerDependentFields()
Expand Down Expand Up @@ -109,7 +105,7 @@ export default class extends Controller {

choose(event) {
event.preventDefault()
this.selectOption(event.params.value, event.params.label, event.params.submitValue, { displayLabel: event.params.displayLabel })
this.selectOptionFromElement(event.currentTarget)
}

add(event) {
Expand Down Expand Up @@ -281,17 +277,28 @@ export default class extends Controller {
}

selectOption(value, label, submitValue = value, { displayLabel = label, refreshOptions = this.multipleValue } = {}) {
value = value.toString()
submitValue = submitValue.toString()
this.selectOptionData(
this.normalizeSelectedOption({ id: value, value: submitValue, label, displayLabel }),
{ refreshOptions }
)
}

selectOptionFromElement(element, { refreshOptions = this.multipleValue } = {}) {
this.selectOptionData(this.optionData(element), { refreshOptions })
}

selectOptionData(option, { refreshOptions = this.multipleValue } = {}) {
const selectedOption = this.normalizeSelectedOption(option)
const value = selectedOption.id

if (this.multipleValue) {
if (this.selectedValue.some((option) => option.id === value)) {
this.selectedValue = this.selectedValue.filter((option) => option.id !== value)
} else {
this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }, ...this.selectedValue]
this.selectedValue = [selectedOption, ...this.selectedValue]
}
} else {
this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }]
this.selectedValue = [selectedOption]
}

this.renderSelection()
Expand All @@ -304,6 +311,51 @@ export default class extends Controller {
}
}

optionData(element) {
const data = this.parseOptionData(element.dataset.advancedSelectOptionParam)
const value = element.dataset.advancedSelectValueParam
const submitValue = element.dataset.advancedSelectSubmitValueParam || value
const label = element.dataset.advancedSelectLabelParam
const displayLabel = element.dataset.advancedSelectDisplayLabelParam || label

return {
...data,
id: value,
value: submitValue,
label,
displayLabel
}
}

parseOptionData(json) {
if (!json) {
return {}
}

try {
const data = JSON.parse(json)
return data && typeof data === "object" && !Array.isArray(data) ? data : {}
} catch (_error) {
return {}
}
}

normalizeSelectedOption(option) {
const id = option.id.toString()
const value = (option.value || id).toString()
const label = (option.label || option.displayLabel || option.display_label || value).toString()
const displayLabel = (option.displayLabel || option.display_label || label).toString()

return {
...option,
id,
value,
label,
displayLabel,
display_label: displayLabel
}
}

renderSelection() {
this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
this.summaryTarget.replaceChildren(...this.selectionElements)
Expand Down Expand Up @@ -383,13 +435,7 @@ export default class extends Controller {
return
}

const option = options[0]
this.selectOption(
option.dataset.advancedSelectValueParam,
option.dataset.advancedSelectLabelParam,
option.dataset.advancedSelectSubmitValueParam || option.dataset.advancedSelectValueParam,
{ displayLabel: option.dataset.advancedSelectDisplayLabelParam, refreshOptions: false }
)
this.selectOptionFromElement(options[0], { refreshOptions: false })
}

chooseActiveOption() {
Expand All @@ -401,12 +447,7 @@ export default class extends Controller {
this.activeOption.dataset.advancedSelectDisplayLabelParam
)
} else {
this.selectOption(
this.activeOption.dataset.advancedSelectValueParam,
this.activeOption.dataset.advancedSelectLabelParam,
this.activeOption.dataset.advancedSelectSubmitValueParam || this.activeOption.dataset.advancedSelectValueParam,
{ displayLabel: this.activeOption.dataset.advancedSelectDisplayLabelParam }
)
this.selectOptionFromElement(this.activeOption)
}
}

Expand Down Expand Up @@ -602,11 +643,29 @@ export default class extends Controller {
return this.displayLabel(option)
}

const camelizedField = field.replace(/_([a-z])/g, (_match, character) => character.toUpperCase())
const value = option[field] ?? option[camelizedField]
const value = this.optionFieldValue(option, field)
return value === undefined || value === null ? "" : value.toString()
}

optionFieldValue(option, field) {
const directValue = option[field] ?? option[this.camelize(field)]
if (directValue !== undefined) {
return directValue
}

return field.split(".").reduce((value, segment) => {
if (value === undefined || value === null) {
return undefined
}

return value[segment] ?? value[this.camelize(segment)]
}, option)
}

camelize(value) {
return value.replace(/_([a-z])/g, (_match, character) => character.toUpperCase())
}

textElement(tagName, className, text) {
const element = document.createElement(tagName)
element.className = className
Expand Down Expand Up @@ -642,4 +701,4 @@ export default class extends Controller {
get expanded() {
return this.triggerTarget.getAttribute("aria-expanded") === "true"
}
}
}
16 changes: 13 additions & 3 deletions lib/advanced_select/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ def advanced_select_state_class(class_map, key)

def advanced_select_selected_options(selected)
advanced_select_array(selected).map do |option|
{
option.to_h.merge(
id: option.fetch(:id).to_s,
value: advanced_select_option_value(option),
label: advanced_select_option_label(option),
display_label: advanced_select_option_display_label(option)
}
)
end
end

Expand All @@ -79,7 +79,8 @@ def advanced_select_option_tag(option, selected_options, option_content_partial,
advanced_select_value_param: option.fetch(:id),
advanced_select_submit_value_param: advanced_select_option_value(option),
advanced_select_label_param: advanced_select_option_label(option),
advanced_select_display_label_param: advanced_select_option_display_label(option)
advanced_select_display_label_param: advanced_select_option_display_label(option),
advanced_select_option_param: advanced_select_option_payload(option)
}
) do
safe_join([
Expand Down Expand Up @@ -170,6 +171,15 @@ def advanced_select_option_description(option)
option[:description].to_s
end

def advanced_select_option_payload(option)
option.to_h.merge(
id: option.fetch(:id).to_s,
value: advanced_select_option_value(option),
label: advanced_select_option_label(option),
display_label: advanced_select_option_display_label(option)
).to_json
end

def advanced_select_display_label(label)
label.to_s.split(" > ").last
end
Expand Down
2 changes: 1 addition & 1 deletion lib/advanced_select/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module AdvancedSelect
VERSION = "0.1.6"
VERSION = "0.1.7"
end
Loading
Loading