@csedl/hotwire-svelte-helpers
v7.0.0
Published
Hotwire + Svelte helpers for Rails: Stimulus floating dropdowns/toolips + Svelte global panels/modals + RTurbo-friendly utilities. Build together with the rubygem svelte-on-rails and its npm-package.
Readme
Hotwire Svelte Helpers
Overview
Helpers for building overlays — such as dropdowns, tooltips, and modals — with Floating UI, Stimulus, and Svelte in a Hotwire/Turbo environment.
Includes flexible Svelte form components that work with the svelte-on-rails gem and can be imported directly from the
package or copied into your project.
A small toolkit for building more interactive Rails apps with Hotwire and Svelte.
Links
Installation
npm install @csedl/hotwire-svelte-helpersSetup
import {HotwireSvelteHelpers} from "@csedl/hotwire-svelte-helpers/setup"
HotwireSvelteHelpers.debug = true
HotwireSvelteHelpers.initializeOverlays()Core Concepts
Overlay helpers
The core idea is to have few helpers for creating overlays, such as dropdowns, tooltips or modals with stimulus and
svelte. Among others, theese are initializeDropdown, openDropdownPanel and closePanel.
Next, on the package included are stimulus controllers which are built on top of theese helpers and Svelte components that using the same.
Important: When creating or initializing the dropdown, always call the initialize function before attaching a
listener to the panel's close event. The initialize function adds a close event listener to the panel that executes
the onPanelClose function. If your custom close function destroys the panel, this has to be done after the
onPanelClose is fired by the close event.
The #overlays-box Container
Svelte dropdown panels are mounted into #overlays-box, a shared overlay container appended directly to
document.body. If #overlays-box does not exist, it is created automatically. This keeps overlays at the root level
to avoid most z-index and clipping issues, while keeping the DOM cleaner than mounting panels directly into body.
This aligns to the behaviour of the stimulus side
Stimulus Usage Philosophy
Stimulus, in this context is used mainly for its initializers, namely the connect and disconnect events.
For modifying the DOM we do not, or only minimal use Stimulus, because of 2 reasons:
- Stimulus can only badly handle the initial status of HTML elements, because this would only be done by the
connectevent. This would mean that you always would see a unpleasant blinking effect when the page is loaded. Or you would write double-logic: Backend code for the initial status and Stimulus code for the actual DOM manipulation. - Writing interactive elements with Stimulus leads to spaghetti code.
For solving theese problems we wrote the svelte-on-rails gem.
Svelte
There are some default components included, which can be used to build Svelte Dropdowns. Check the online example app for more details.
Components
Dropdowns
Basic Example
<div data-controller="hotwire-svelte-helpers-dropdown" data-panel-id="dropdown-panel-3h5k7l4">
Button
</div>
<div id="dropdown-panel-3h5k7l4" class="dropdownSvelte-panel-example-class" style="display: none;">
... any content
</div>Behavior
- When the button is clicked, it toggles the
displaystyle on the panel and places the panel using floating-ui. - Adds the close-panel functionality to a
onClickevent listener to Elements that have adata-closeattribute set, which is considered as the close button. - When the panel is open, the
has-open-panelclass is added to the button, otherwise it is removed. - Adds functionality to close all panels when clicking outside a panel.
- When a
data-srcattribute is given to the panel, on opening the panel, it fires a xhr request and replaces the content (fetched by selector:.contentwithin the panel-element) by the response. - This all works with stacked panels too (panel in panel).
Close on Click Outside
When a dropdownSvelte is open, it closes when clicking outside a panel.
This behaviour can be stopped by:
- The clicked element or its parent elements has the
data-dropdownSvelte-persist(not:data-dropdownSvelte-persist="false") attribute. - the event has the attribute
event.detail.dataDropdownPersistset to true.
Positioning Options / Arrow
- If there is an element with ID
arrowinside the dropdownSvelte panel, it is treated as described on floating-ui. - The
data-placementattribute on the panel can be used to control positioning, see floating-ui/placements.
Events
Events on the button element:
place-panelplaces the panel, and, if present, the arrow element, byfloating-ui.
Events the panel element:
closecloses the panel.place-melike place-panel on the button.
Event Triggers on the button element:
before-open-panelafter-close-panel
Event Triggers on the panel element:
before-open
Modals
<div class="fmodal-button" data-controller="hotwire-svelte-helpers-modal" data-panel-id="modal-overlay-449a3aee">
Modal-Button
</div>
<div class="modal-overlay" id="modal-overlay-449a3aee">
<div class="fetch-from-server modal-panel">
... any content
</div>
</div>Tooltips
<span data-controller="hotwire-svelte-helpers-tooltip" data-panel-id="tooltip-123" data-delay="0.2">
Text-with-tooltip
</span>
<div id="tooltip-123" class="tooltip-panel" style="display: none;">
<div id="arrow"></div>
... any content
</div>makes a tooltip.
It adds the class has-open-panel to the tooltip label while the tooltip is visible.
data-src attribute is working similar to dropdownSvelte.
During normal use, hovering over the tooltip label causes the tooltip to appear and disappear once the mouse leaves the label.
Clicking on the tooltip label keeps the tooltip open, even when the mouse leaves the label. Clicking outside the tooltip label closes it.
Svelte Templates and Form Components
Included are some Svelte components that are ready to use from the package, see example app.
Theese are mainly:
DropdownButton.svelteDropdownPanel.svelteTooltip.svelteModalButton.svelteModal.svelte
and:
FormInput.svelte- This corresponds to the #to_svelte helper:
- Handles Validation Errors up from a rails model
- Generates selects for enums with translated labels
- This corresponds to the #to_svelte helper:
Repositioning Panels in Scrollable Containers
If the panels are rendered to a different location than the button (see z-index on rails-app), within a scrollable (e.g.) container, the button would scroll away from the panel. For such cases, add this both data-attributes to the scrollable element:
<div data-controller="csedl-place-dropdownSvelte-panels" data-on="scroll" data-run-after="500"
style="overflow: scroll;">
...
</div>Now, on scrolling, it searches for all dropdownSvelte-buttons (by class-name has-open-panel) and triggers the
place-panel event there.
Options
data-on Attribute:
scrolltriggered byscrollEvent of the given element.resize-observertriggered by ResizeObserver on the given element.
data-run-after Attribute:
- Milliseconds as number.
This is only relevant if you have things like css transition enabled, so that after the above resize events are fired,
subsequent events are needed. It will fire the place-panel after the last resize/scroll event within the given time.
Tip Turn console-debug-log on (see configs) and check how events are working.
Explanation
What these helpers mainly do is to find all the dropdowns by the has-open-panel class and fire the place-panel
event. But within the helper, things like performance optimisation are done: it searches once and places the panels
multiple times.
CleanMount
Interaction with (Svelte) components can lead to orphaned instances. This can happen when the unmount event does not happen, for example when the underlaying DOM element disappears which can happen in a Hotwire Environemnt.
For this here is a double security. CleanMount () adds the instance to a global store and then executes mount ().
import {cleanMount} from "@csedl/hotwire-svelte-helpers";
cleanMount(AnySvelteComponent, {target: cleanMountDemoTag, ...});Next, you must add something like
import {unmountAllDetached} from '@csedl/hotwire-svelte-helpers'
document.addEventListener('turbo:render', () => {
unmountAllDetached()
})to your application.js. This will check for detached instances and unmount them.
Testing
When adding system tests, it is important not to click the button until the event listener has been added.
In this package, the stimulus controllers add the class stimulus-connected to the button elements after the connect
method of stimulus has passed.
Playwright Example
For example, to test a dropdown button using Playwright, you can do the following:
page.wait_for_selector(".dropdown-button.stimulus-connected").clickThis aligns with the .hydrated class of the svelte-on-rails gem.
License
MIT
