hotwire_combobox

repository·main·Indexed 20 days ago

https://github.com/josefarias/hotwire_combobox

An accessible autocomplete combobox implementation for Ruby on Rails applications using Hotwire (Turbo and Stimulus). It features support for async loading via Turbo Streams, multiselect chips with customizable templates, and a mixin-based Stimulus controller architecture. The library follows APG combobox pattern guidelines and includes a responsive dialog mode for small viewports.

Tokens
4.1K
Snippets
12
Records
23
Agent score
71%

What's inside hotwire_combobox

  1. Accessibility implementation details

    main

    HotwireCombobox follows the APG combobox pattern guidelines with the following specific implementation exceptions:

    1. Wrap-around selection: Pressing Up Arrow on the first option selects the last option. Pressing Down Arrow on the last option selects the first option. In paginated comboboxes, this refers to the currently available options.
    2. Unlabeled comboboxes: The library allows for unlabeled comboboxes; the responsibility for providing a label lies with the implementing user.
    3. Multiselect support: Since official APG guidelines for multiselect comboboxes are currently unavailable, the library uses custom mechanisms like live regions to announce multi-selections.
  2. Configure JS for Importmaps

    main

    If your application uses importmaps, most setups will work automatically if you are using eagerLoadControllersFrom or lazyLoadControllersFrom in app/javascript/controllers/index.js.

    If you need to manually register the controller, modify app/javascript/controllers/application.js as follows:

    import { Application } from "@hotwired/stimulus"
    const application = Application.start()
    
    // Add the following two lines:
    import HwComboboxController from "controllers/hw_combobox_controller"
    application.register("hw-combobox", HwComboboxController)
    
    export { application }
  3. Configure JS for JS Bundling (esbuild, rollup, etc.)

    main

    If you are using a JS bundler, you must install the JavaScript portion via npm or yarn and manually register the Stimulus controller.

    Note: You must keep the version of the npm package and the Ruby gem in sync.

    yarn add @josefarias/hotwire_combobox
    # OR
    npm install @josefarias/hotwire_combobox

    Then, register the controller in app/javascript/controllers/application.js:

    import { Application } from "@hotwired/stimulus"
    const application = Application.start()
    
    // Add the following two lines:
    import HwComboboxController from "@josefarias/hotwire_combobox"
    application.register("hw-combobox", HwComboboxController)
    
    export { application }
  4. How HwComboboxController handles expansion and collapse

    main

    The controller manages the visibility of the options list through the expanded value. When the expanded value changes, the expandedValueChanged lifecycle method is triggered, which calls either _expand() or _collapse().

    Expansion Modes

    Depending on the viewport size and configuration, the controller uses one of two modes:

    1. Inline Mode: The listbox target's hidden attribute is set to false to show the list below the input.
    2. Dialog Mode: If smallViewportMaxWidth is exceeded, the controller moves artifacts to a dialog element, calls dialogTarget.showModal(), and prevents body scroll to provide a mobile-friendly experience.

    Collapse and Closing

    Closing the combobox can be triggered by:

    • Manual toggling.
    • Clicking outside the mainWrapper (unless the click is on a dialog dismisser).
    • Focusing outside the element.
    • Pressing the Escape key.

    When closing, the controller may perform a _lockInSelection() to ensure the current query or selection is finalized.

  5. How HwComboboxController handles validity

    main

    The controller implements validation logic via the Combobox.Validity concern. It synchronizes the HTML5 required attribute from the comboboxTarget to ensure proper form submission behavior.

    Validation Workflow

    1. Required Attribute: The controller tracks if the comboboxTarget is required. It uses a custom dataset attribute hwComboboxRequiredByAuthor to distinguish between the original requirement and the dynamically toggled required attribute used for browser validation.
    2. Marking Invalid: If the value is invalid (e.g., a required field is empty), the controller:
      • Adds the invalidClass to the combobox.
      • Sets aria-invalid="true".
      • Sets aria-errormessage with a default string: "Please select a valid option for {name}".
    3. Marking Valid: When the value becomes valid, the controller removes the invalidClass, aria-invalid, and aria-errormessage attributes.
  6. How Hotwire Combobox mixins work

    main

    The hotwire_combobox library is built using a mixin pattern for Stimulus controllers. Instead of a single monolithic class, functionality is divided into discrete, reusable modules (mixins) that are applied to a base Stimulus controller using a Concerns helper function.

    Key mixins include:

    • Actors: Manages the relationship between the combobox input and the listbox.
    • Autocomplete: Handles inline or list-based text completion.
    • Filtering: Manages sync/async filtering of options based on user input.
    • Multiselect: Provides chip-based selection for multiple values.
    • Navigation: Handles keyboard interactions (Arrow keys, Home, End, etc.).
    • Events: Dispatches custom DOM events like hw-combobox:selection or hw-combobox:removal.
    • Dialog: Manages the transition of the combobox to a full-screen dialog on small viewports.
  7. Configure Multiselect Chips with Templates

    main

    When using Multiselect mode, you can render selection chips using a client-side template. This allows you to customize the look of the chips and inject dynamic data using a placeholder syntax.

    1. Define a <template> element inside your combobox container.
    2. Add the attribute data-hw-combobox-chip-template to the template.
    3. Use the {{key}} syntax to inject data. The keys correspond to attributes on your option elements prefixed with data-chip-.

    Example Template:

    <template data-hw-combobox-chip-template>
      <div class="chip">
        <span>{{display}}</span>
        <button data-action="click->combobox#removeChip">×</button>
        <small>{{category}}</small>
      </div>
    </template>

    If an option element has data-chip-category="Work", the {{category}} placeholder will be replaced with "Work".

  8. Implement Async Loading for Combobox

    main

    To enable asynchronous searching (e.g., fetching results from a server via Turbo Streams), configure the AsyncLoading mixin by providing an async-src value to your controller.

    When a user types, the controller will perform a GET request to the URL provided in async-src. The request includes the following query parameters:

    • q: The current full query.
    • input_type: The type of input event (e.g., insertText).
    • for_id: The data-async-id of the combobox.
    • callback_id: A unique ID to track the request.

    The server should respond with a text/vnd.turbo-stream.html response to update the listbox options.

  9. Configure HwComboboxController values

    main

    The HwComboboxController uses Stimulus values to configure its behavior. When implementing the controller in your Rails application, you can set the following values via data attributes on the element containing the controller:

    ValueTypeDescription
    asyncSrcStringThe source URL for asynchronous loading
    autocompletableAttributeStringAttribute used for autocomplete
    autocompleteStringAutocomplete mode
    debounceIntervalNumberDelay in milliseconds for debouncing input
    expandedBooleanInitial expanded state
    filterableAttributeStringAttribute used for filtering
    nameWhenNewStringThe name to use when a new (non-preselected) value is entered
    originalNameStringThe original name of the field
    prefilledChipsArrayInitial chips for multiselect
    prefilledDisplayStringThe display value to use if prefilled
    selectionChipSrcStringSource for selection chips
    smallViewportMaxWidthStringMaximum width to trigger dialog mode for small viewports