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
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,24 +484,36 @@ For richer layouts — a table, badges, compatibility columns, etc. — pass `to
) %>
```

The partial is rendered on the server for the initial selection, and the controller keeps it in sync client-side as the selection changes — no Turbo Stream round-trip. To opt in, mark the row container and provide a one-row `<template>` the controller clones per selected option:

- `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).

```erb
<%# app/views/alternatives/_tooltip.html.erb %>
<table>
<thead>
<tr><th>Code &amp; Name</th><th>Type</th></tr>
</thead>
<tbody>
<tbody data-advanced-select-tooltip-list>
<% selected_options.each do |option| %>
<tr>
<td><%= option.fetch(:display_label) %></td>
<td><%= option.fetch(:value) %></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>
</tr>
</template>
</table>
```

A custom `tooltip_partial` is rendered once on the server, so it is best for display-oriented content. If your selection changes on the client and the custom tooltip must reflect it live, re-render the field (for example via a Turbo Stream) after the change. The built-in `tooltip: true` list updates client-side automatically.
The initial server-rendered rows stay until the first change, then the controller rebuilds them from the template — so just like `tooltip: true`, the custom tooltip updates client-side automatically. A custom partial without a `<template>` still renders once and stays static, which is fine for display-only content.

### Add Mode

Expand Down
65 changes: 64 additions & 1 deletion app/javascript/advanced_select/advanced_select_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,19 @@ export default class extends Controller {
}

renderTooltip() {
if (!this.hasTooltipTarget || this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
if (!this.hasTooltipTarget) {
return
}

if (this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
this.renderCustomTooltip()
return
}

this.renderBuiltInTooltip()
}

renderBuiltInTooltip() {
const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
if (!list) {
return
Expand All @@ -544,6 +553,60 @@ export default class extends Controller {
}
}

renderCustomTooltip() {
const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
const template = this.customTooltipTemplate()

if (!list || !template) {
if (this.selectedValue.length === 0) {
this.hideTooltip(true)
}
return
}

list.replaceChildren(
...this.selectedValue.map((option) => this.customTooltipElement(template, option))
)

if (this.selectedValue.length === 0) {
this.hideTooltip(true)
}
}

customTooltipTemplate() {
if (this.customTooltipTemplateContent) {
return this.customTooltipTemplateContent
}

const template = this.tooltipTarget.querySelector("template[data-advanced-select-tooltip-template]")
if (!template) {
return null
}

this.customTooltipTemplateContent = template.content.cloneNode(true)
template.remove()
return this.customTooltipTemplateContent
}

customTooltipElement(template, option) {
const fragment = template.cloneNode(true)
fragment.querySelectorAll("[data-advanced-select-tooltip-field]").forEach((element) => {
element.textContent = this.tooltipFieldValue(option, element.dataset.advancedSelectTooltipField)
})

return fragment
}

tooltipFieldValue(option, field) {
if (field === "display_label" || field === "displayLabel") {
return this.displayLabel(option)
}

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

textElement(tagName, className, text) {
const element = document.createElement(tagName)
element.className = className
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
<th>Type</th>
</tr>
</thead>
<tbody>
<tbody data-advanced-select-tooltip-list>
<% selected_options.each do |option| %>
<tr>
<td><%= option.fetch(:display_label) %></td>
<td><%= option.fetch(:value) %></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>
</tr>
</template>
</table>
4 changes: 3 additions & 1 deletion test/helpers/advanced_select/helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,9 @@ class AdvancedSelectHelperTest < ActionView::TestCase
)

assert_selector fragment, "#report_items_tooltip[data-advanced-select-tooltip-custom]"
assert_no_selector fragment, "#report_items_tooltip [data-advanced-select-tooltip-list]"
assert_selector fragment, "#report_items_tooltip table.advanced-select-tooltip-table"
assert_selector fragment, "#report_items_tooltip template[data-advanced-select-tooltip-template]"
assert_no_selector fragment, "#report_items_tooltip ul"
end

test "omits the tooltip element when tooltip is disabled" do
Expand Down
30 changes: 30 additions & 0 deletions test/system/advanced_select_interaction_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,36 @@ class AdvancedSelectInteractionTest < ApplicationSystemTestCase
end
end

test "rebuilds the custom tooltip partial as the selection changes" do
visit root_path

find("#example_tooltip_partial_ids_trigger").click
find("#example_tooltip_partial_ids_options button", text: "ALT-003 – Antikor C").click
find("#example_tooltip_partial_ids_trigger").click

# Hover a different trigger first so re-hovering fires a fresh mouseenter.
find("#example_item_id_trigger").hover
find("#example_tooltip_partial_ids_trigger").hover

within "#example_tooltip_partial_ids_tooltip" do
assert_text "ALT-001 – Antikor A"
assert_text "ALT-003 – Antikor C"
assert_text "Eşdeğer"
end

find("#example_tooltip_partial_ids_trigger").click
find("#example_tooltip_partial_ids_options button", text: "ALT-001 – Antikor A").click
find("#example_tooltip_partial_ids_trigger").click

find("#example_item_id_trigger").hover
find("#example_tooltip_partial_ids_trigger").hover

within "#example_tooltip_partial_ids_tooltip" do
assert_no_text "ALT-001 – Antikor A"
assert_text "ALT-003 – Antikor C"
end
end

private

def assert_selected_option_check(select_id, text)
Expand Down
Loading