Module: TurboOverlay::Helpers::ViewHelper

Defined in:
lib/turbo_overlay/helpers/view_helper.rb

Instance Method Summary collapse

Instance Method Details

#drawer_button_to(name = nil, options = nil, html_options = nil, &block) ⇒ Object

‘button_to` counterpart to `drawer_link_to` — see `modal_button_to`. Accepts `position:`, `backdrop:`, `close:`, and `keep_overlay_open_on_redirect:`.



100
101
102
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 100

def drawer_button_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_button_to(:drawer, name, options, html_options, &block)
end

Inside a drawer, render a link styled as a “dismiss” trigger.



93
94
95
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 93

def drawer_dismiss_link_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_dismiss_link_to(:drawer, name, options, html_options, &block)
end

Build a link that opens its target as a drawer overlay. Same stacking and ‘overlay_id:` semantics as `modal_link_to`.

<%= drawer_link_to "Filters", filters_path %>

‘position:` overrides the configured drawer side for this one link (`:left`, `:right`, `:top`, `:bottom`). When omitted the drawer opens on the side set by `TurboOverlay.configuration.drawer.position`.

<%= drawer_link_to "Nav", nav_path, position: :left %>

‘backdrop: false` opens the drawer non-modally: no dimmed backdrop, the page stays scrollable, and text on the page remains selectable so users can copy/paste between the page and the drawer. Click outside the drawer is also ignored (no backdrop to click). ESC still closes.

<%= drawer_link_to "Inspector", inspect_path, backdrop: false %>

‘close: false` opens the drawer without the default close (“×”) button. ESC still closes; the backdrop is unaffected.



88
89
90
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 88

def drawer_link_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_link_to(:drawer, name, options, html_options, &block)
end

Decorate any ‘<a>` so the gem’s JS shows a hover-hint preview. ‘hint_link_to` is a thin wrapper around `link_to` that sets the two data attributes the JS reads. Compose freely with overlay helpers (`modal_link_to “Edit”, path, hint: true, hint_url: …`) or set the data attributes directly on any `link_to`.

<%= hint_link_to "User", user_path(@user) %>
<%= hint_link_to "User", user_path(@user), hint_url: hint_user_path(@user) %>

Without ‘hint_url:`, the gem extracts a `<template id=“…”>` from the page’s own response when Turbo prefetches it on hover. With ‘hint_url:`, the gem fetches the alternate URL with the `:hint` request variant on hover. Overlay links (`modal_link_to` etc.) are excluded from Turbo’s hover prefetch — provide ‘hint_url:` for them.

‘show_delay:` / `hide_delay:` (ms) override the configured `TurboOverlay.configuration.hint.show_delay_ms` / `hide_delay_ms` for this one link. Useful for dense lists (datatables, menus) where a longer show delay keeps hints from flickering during scroll/keyboard navigation, or for a single high-signal link that wants a near-zero delay.

<%= hint_link_to "User", user_path(@user), show_delay: 600 %>


168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 168

def hint_link_to(name = nil, options = nil, html_options = nil, &block)
  if block_given?
    html_options = options || {}
    options      = name
    options, html_options = _hint_normalize_link_args(options, html_options)
    link_to(options, html_options, &block)
  else
    html_options = (html_options || {}).dup
    options, html_options = _hint_normalize_link_args(options, html_options)
    link_to(name, options, html_options)
  end
end

Render a ‘button_to` form whose POST/DELETE/PATCH/PUT response opens a modal overlay. Use this when opening the modal is the result of a non-GET action — creating a record, deleting an item, kicking off a wizard. The form carries the same overlay data attributes a `modal_link_to` link would, so the server sees identical `X-Turbo-Overlay-*` request headers and wraps the response in the modal layout.

<%= modal_button_to "Delete", widget_path(@w), method: :delete %>
<%= modal_button_to "Start wizard", wizards_path, method: :post %>

Accepts the same overlay options as ‘modal_link_to` (`overlay_id:`, `close:`, `keep_overlay_open_on_redirect:`). `advance:` is not exposed — non-GET requests don’t push history.



60
61
62
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 60

def modal_button_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_button_to(:modal, name, options, html_options, &block)
end

Inside a modal, render a link styled as a “dismiss” trigger. Outside a modal, behaves like a normal ‘link_to`.

<%= modal_dismiss_link_to "Cancel", cancel_path %>


42
43
44
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 42

def modal_dismiss_link_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_dismiss_link_to(:modal, name, options, html_options, &block)
end

Build a link that opens its target as a modal overlay. The response is appended to the host-page stack container, so a modal opened from inside another modal stacks on top rather than replacing.

<%= modal_link_to "New User", new_user_path %>
<%= modal_link_to "Edit",     edit_user_path(@user),
                  overlay_id: "edit_user_#{@user.id}" %>

‘overlay_id:` is optional. When omitted the server generates a random id; supply your own when you want to close the overlay later from server code via `turbo_stream.overlay(:close, id: “…”)`.

‘close: false` opens the modal without the default close (“×”) button rendered by the chrome partial. Useful when the body provides its own dismiss controls (the confirm partial uses this internally). ESC and backdrop click still close.



34
35
36
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 34

def modal_link_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_link_to(:modal, name, options, html_options, &block)
end

#overlay_close(show = true) ⇒ Object

Toggle the chrome’s default close (“×”) button for the current overlay render. Defaults to on; call with ‘false` from inside an overlay view to suppress it:

<% overlay_close false %>

Precedence (highest first): partial local ‘close:` on `render “turbo_overlay/modal”`, this helper, the link option `close: false` (carried as a request header and exposed via `turbo_overlay_close?`), then the default `true`.



359
360
361
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 359

def overlay_close(show = true)
  @_overlay_close = show
end

#overlay_close?Boolean

Whether the chrome should render its default close button. Resolves the precedence described on ‘overlay_close`.

Returns:

  • (Boolean)


365
366
367
368
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 365

def overlay_close?
  return @_overlay_close != false if defined?(@_overlay_close)
  controller.turbo_overlay_close?
end

Set the overlay footer content.



345
346
347
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 345

def overlay_footer(value = nil, &block)
  content_for(:overlay_footer, value, &block)
end

#overlay_frame_tags(*_types) ⇒ Object

Deprecated. Aliased to ‘overlay_stack_tag` for one minor cycle. The previous frame-per-type model has been replaced by a single shared stack container.



291
292
293
294
295
296
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 291

def overlay_frame_tags(*_types)
  ActiveSupport::Deprecation.new("0.4", "turbo_overlay").warn(
    "overlay_frame_tags is deprecated; use overlay_stack_tag instead."
  )
  overlay_stack_tag
end

#overlay_response_wrapper(type, &block) ⇒ Object

Wraps the given block in the appropriate response primitive for the current overlay request:

  • Initial open (‘X-Turbo-Overlay` header): emits a `<turbo-stream action=“append” target=“<stack_id>”>` whose template contains a `<turbo-frame id=“<frame_id>”>` around the dialog.

  • Form re-render inside an open overlay (‘Turbo-Frame: turbo_overlay_<type>_<id>`): emits a `<turbo-stream action=“replace” method=“morph”>` targeting the open frame. Morphing preserves the `<dialog>` node identity (top-layer membership, popover anchor, stack registration, ESC / outside-click handlers, focus, and scroll position) and just updates the children to show the new markup — the error messages, the populated form fields. Plain frame replacement would tear the dialog down and re-attach a fresh one, which detaches popovers from their anchor and leaks document-level handlers.

Used by the modal/drawer layouts to keep them readable.



327
328
329
330
331
332
333
334
335
336
337
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 327

def overlay_response_wrapper(type, &block)
  frame_id = "turbo_overlay_#{type}_#{turbo_overlay_id}"
  frame_html = turbo_frame_tag(frame_id, class: "turbo-overlay-frame", &block)

  if controller.turbo_overlay_frame_re_render?
    turbo_stream.replace(frame_id, method: :morph) { frame_html }
  else
    stack_id = TurboOverlay.configuration.stack_id
    turbo_stream.append(stack_id) { frame_html }
  end
end

#overlay_stack_tagObject

Emit the receiving stack container for overlays. Drop this in your application layout (typically just before ‘</body>`) once.

<%= overlay_stack_tag %>

When the host app has confirm chrome partials in ‘app/views/turbo_overlay/`, emits sibling `<template>` elements per variant the JS confirm hook clones from:

<template id="turbo_overlay_confirm_modal_template">…</template>
<template id="turbo_overlay_confirm_popover_template">…</template>

Partial resolution per variant prefers ‘_confirm.html+<variant>.erb`, then falls back to a shared `_confirm.html.erb`. The shared partial — if it’s the only one present — is rendered once for both modal and popover styles so apps that don’t want chrome-specific variants can ship a single file.

The same shape applies to the loading partials:

<template id="turbo_overlay_loading_modal_template">…</template>
<template id="turbo_overlay_loading_drawer_template">…</template>
<template id="turbo_overlay_loading_popover_template">…</template>
<template id="turbo_overlay_loading_hint_template">…</template>

rendered from ‘_loading.html+<type>.erb` with `_loading.html.erb` as the shared fallback. Cloned by the JS at click time to give users immediate feedback while the real response is in flight.

The configured default confirm style is exposed as a data attribute on the stack container so the JS can read it without a separate config plumbing pass.



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 216

def overlay_stack_tag
  stack_id      = TurboOverlay.configuration.stack_id
  confirm_style = TurboOverlay.configuration.confirm.style.to_s
  hint_cfg      = TurboOverlay.configuration.hint

  data_attrs = {
    controller: "turbo-overlay-stack",
    "turbo-overlay-confirm-style": confirm_style,
    "turbo-overlay-hint-show-delay": hint_cfg.show_delay_ms,
    "turbo-overlay-hint-hide-delay": hint_cfg.hide_delay_ms,
    "turbo-overlay-advance-modal":  TurboOverlay.configuration.modal.advance.to_s,
    "turbo-overlay-advance-drawer": TurboOverlay.configuration.drawer.advance.to_s
  }

  allowed_click_outside = TurboOverlay.configuration.allowed_click_outside_selectors
  if allowed_click_outside.any?
    data_attrs["turbo-overlay-stack-allowed-click-outside-selectors-value"] =
      allowed_click_outside.to_json
  end

  stack = (:div, "".html_safe,
    id: stack_id,
    class: "turbo-overlay-stack",
    data: data_attrs)

  return stack unless respond_to?(:lookup_context) && lookup_context

  parts = [stack]

  [:modal, :popover].each do |variant|
    rendered = _render_overlay_chrome_partial(
      "turbo_overlay/confirm", variant,
      chrome: variant, locals: { close: false }
    )
    next unless rendered
    parts << (:template, rendered,
      id: "turbo_overlay_confirm_#{variant}_template")
  end

  [:modal, :drawer, :popover, :hint].each do |variant|
    rendered = _render_overlay_chrome_partial(
      "turbo_overlay/loading", variant,
      chrome: variant, locals: { loading: true, close: false }
    )
    next unless rendered
    parts << (:template, rendered,
      id: "turbo_overlay_loading_#{variant}_template")
  end

  # On a hintable request, render the action's `+hint.erb`
  # variant template if one exists. Gated on
  # `_overlay_hintable_request?` so a regular page render
  # doesn't pay the cost.
  body = _turbo_overlay_resolved_hint_body
  if body
    hint_body = if lookup_context.exists?("turbo_overlay/hint", [], true)
      # Render the partial as a layout so the user's body lands
      # at `<%= yield %>`. Same pattern used by the modal/drawer
      # /popover layouts.
      render(layout: "turbo_overlay/hint") { body }
    else
      # No chrome partial in the app yet; emit the body unwrapped
      # so the JS still has something to extract.
      body
    end
    parts << (:template, hint_body, id: "turbo-overlay-hint")
  end

  return stack if parts.size == 1
  safe_join(parts)
end

#overlay_title(value = nil, &block) ⇒ Object

Set the overlay header title.



340
341
342
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 340

def overlay_title(value = nil, &block)
  content_for(:overlay_title, value, &block)
end

#popover_button_to(name = nil, options = nil, html_options = nil, &block) ⇒ Object

‘button_to` counterpart to `popover_link_to` — see `modal_button_to`. Accepts `position:`, `align:`, `offset:`, `close:`. The popover anchors to the rendered form (which wraps the button), so positioning works the same as for a `popover_link_to` link.



138
139
140
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 138

def popover_button_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_button_to(:popover, name, options, html_options, &block)
end

Inside a popover, render a link styled as a “dismiss” trigger.



129
130
131
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 129

def popover_dismiss_link_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_dismiss_link_to(:popover, name, options, html_options, &block)
end

Build a link that opens its target as a popover overlay anchored to the clicked link. Same stacking and ‘overlay_id:` semantics as `modal_link_to`.

<%= popover_link_to "Edit", edit_user_path(@user) %>

‘position:` overrides the configured side (`:top`, `:bottom`, `:left`, `:right`). `align:` overrides the cross-axis alignment (`:start`, `:center`, `:end`). `offset:` overrides the pixel gap between trigger and popover.

<%= popover_link_to "Info", info_path,
                    position: :top, align: :center, offset: 8 %>

Popovers are non-modal (no backdrop, page stays interactive) and dismiss on outside click or ESC. Opening a second popover automatically closes any other open popover; modals and drawers still stack on top.



124
125
126
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 124

def popover_link_to(name = nil, options = nil, html_options = nil, &block)
  _overlay_link_to(:popover, name, options, html_options, &block)
end

#turbo_overlay_frame_id(type = nil) ⇒ Object

The DOM id of the per-overlay turbo-frame for the current request: ‘turbo_overlay_<type>_<id>`. Used by overlay layouts to tag the wrapping frame.



301
302
303
304
305
# File 'lib/turbo_overlay/helpers/view_helper.rb', line 301

def turbo_overlay_frame_id(type = nil)
  type ||= controller.turbo_overlay_type
  return nil unless type && turbo_overlay_id
  "turbo_overlay_#{type}_#{turbo_overlay_id}"
end