Skip to content

Commit

Permalink
Create callback queue to run callbacks in order (#191)
Browse files Browse the repository at this point in the history
* Create callback queue to run callbacks in order

* _runCallbackAfterEndOfOptionsStream -> _runCallback
  • Loading branch information
josefarias committed Jul 30, 2024
1 parent 6e541e1 commit 4bfb641
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 10 deletions.
40 changes: 32 additions & 8 deletions app/assets/javascripts/controllers/hw_combobox_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Combobox from "hw_combobox/models/combobox"
import { Concerns, sleep } from "hw_combobox/helpers"
import { nextRepaint } from "hw_combobox/helpers"
import { Controller } from "@hotwired/stimulus"

window.HOTWIRE_COMBOBOX_STREAM_DELAY = 0 // ms, for testing purposes
Expand All @@ -10,6 +11,7 @@ const concerns = [
Combobox.Announcements,
Combobox.AsyncLoading,
Combobox.Autocomplete,
Combobox.Callbacks,
Combobox.Dialog,
Combobox.Events,
Combobox.Filtering,
Expand Down Expand Up @@ -61,6 +63,7 @@ export default class HwComboboxController extends Concerns(...concerns) {
initialize() {
this._initializeActors()
this._initializeFiltering()
this._initializeCallbacks()
}

connect() {
Expand All @@ -87,18 +90,39 @@ export default class HwComboboxController extends Concerns(...concerns) {
}

async endOfOptionsStreamTargetConnected(element) {
const inputType = element.dataset.inputType
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY
if (element.dataset.callbackId) {
this._runCallback(element)
} else {
this._preselectSingle()
}
}

async _runCallback(element) {
const callbackId = element.dataset.callbackId

this._resetMultiselectionMarks()
if (this._callbackAttemptsExceeded(callbackId)) {
this._dequeueCallback(callbackId)
return
} else {
this._recordCallbackAttempt(callbackId)
}

if (this._isNextCallback(callbackId)) {
const inputType = element.dataset.inputType
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY

if (inputType === "hw:multiselectSync") {
this.openByFocusing()
} else if (inputType && inputType !== "hw:lockInSelection") {
if (delay) await sleep(delay)
this._selectOnQuery(inputType)
this._dequeueCallback(callbackId)
this._resetMultiselectionMarks()

if (inputType === "hw:multiselectSync") {
this.openByFocusing()
} else if (inputType !== "hw:lockInSelection") {
this._selectOnQuery(inputType)
}
} else {
this._preselectSingle()
await nextRepaint()
this._runCallback(element)
}
}

Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/hw_combobox/models/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "hw_combobox/models/combobox/actors"
import "hw_combobox/models/combobox/announcements"
import "hw_combobox/models/combobox/async_loading"
import "hw_combobox/models/combobox/autocomplete"
import "hw_combobox/models/combobox/callbacks"
import "hw_combobox/models/combobox/dialog"
import "hw_combobox/models/combobox/events"
import "hw_combobox/models/combobox/filtering"
Expand Down
45 changes: 45 additions & 0 deletions app/assets/javascripts/hw_combobox/models/combobox/callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Combobox from "hw_combobox/models/combobox/base"

const MAX_CALLBACK_ATTEMPTS = 3

Combobox.Callbacks = Base => class extends Base {
_initializeCallbacks() {
this.callbackQueue = []
this.callbackExecutionAttempts = {}
}

_enqueueCallback() {
const callbackId = crypto.randomUUID()
this.callbackQueue.push(callbackId)
return callbackId
}

_isNextCallback(callbackId) {
return this._nextCallback === callbackId
}

_callbackAttemptsExceeded(callbackId) {
return this._callbackAttempts(callbackId) > MAX_CALLBACK_ATTEMPTS
}

_callbackAttempts(callbackId) {
return this.callbackExecutionAttempts[callbackId] || 0
}

_recordCallbackAttempt(callbackId) {
this.callbackExecutionAttempts[callbackId] = this._callbackAttempts(callbackId) + 1
}

_dequeueCallback(callbackId) {
this.callbackQueue = this.callbackQueue.filter(id => id !== callbackId)
this._forgetCallbackExecutionAttempts(callbackId)
}

_forgetCallbackExecutionAttempts(callbackId) {
delete this.callbackExecutionAttempts[callbackId]
}

get _nextCallback() {
return this.callbackQueue[0]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Combobox.Filtering = Base => class extends Base {
const query = {
q: this._fullQuery,
input_type: inputType,
for_id: this.element.dataset.asyncId
for_id: this.element.dataset.asyncId,
callback_id: this._enqueueCallback()
}

await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
Expand Down
2 changes: 1 addition & 1 deletion app/views/hotwire_combobox/_pagination.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%# locals: (for_id:, src:) -%>
<%= tag.li id: hw_pagination_frame_wrapper_id(for_id), class: "hw_combobox__pagination__wrapper",
data: { hw_combobox_target: "endOfOptionsStream", input_type: params[:input_type] },
data: { hw_combobox_target: "endOfOptionsStream", input_type: params[:input_type], callback_id: params[:callback_id] },
aria: { hidden: true } do %>
<%= turbo_frame_tag hw_pagination_frame_id(for_id), src: src, loading: :lazy %>
<% end %>
91 changes: 91 additions & 0 deletions test/system/hotwire_combobox_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,102 @@ class HotwireComboboxTest < ApplicationSystemTestCase

open_combobox "#state-field"
type_in_combobox "#state-field", "is"
assert_selected_option_with text: "Illinois"
click_away
assert_closed_combobox
assert_combobox_display_and_value "#state-field", "Illinois", "IL"

open_combobox "#state-field"
"Illinois".chars.each { type_in_combobox "#state-field", :arrow_right }
type_in_combobox "#state-field", :backspace
assert_combobox_display_and_value "#state-field", "Illinoi", nil
assert_no_visible_selected_option
click_away
assert_closed_combobox
assert_combobox_display_and_value "#state-field", "Illinois", "IL"

on_small_screen do
visit html_options_path

open_combobox "#state-field"

within "dialog" do
type_in_combobox "#state-field-hw-dialog-combobox", "il"
assert_selected_option_with text: "Illinois"
assert_combobox_display "#state-field-hw-dialog-combobox", "Illinois"
end

click_away
assert_closed_combobox
assert_combobox_display_and_value "#state-field", "Illinois", "IL"

open_combobox "#state-field"

within "dialog" do
"Illinois".chars.each { type_in_combobox "#state-field-hw-dialog-combobox", :arrow_right }
type_in_combobox "#state-field-hw-dialog-combobox", :backspace
assert_combobox_display "#state-field-hw-dialog-combobox", "Illinoi"
assert_no_visible_selected_option
end

click_away
assert_closed_combobox
assert_combobox_display_and_value "#state-field", "Illinois", "IL"
end

open_combobox "#state-field"
assert_options_with count: 1
end

test "clicking away locks in the current selection for async comboboxes" do
visit async_path

open_combobox "#movie-field"
type_in_combobox "#movie-field", "wh"
assert_selected_option_with text: "Whiplash"
click_away
assert_closed_combobox
assert_combobox_display_and_value "#movie-field", "Whiplash", movies(:whiplash).id

open_combobox "#movie-field"
"Whiplash".chars.each { type_in_combobox "#movie-field", :arrow_right }
type_in_combobox "#movie-field", :backspace
assert_combobox_display_and_value "#movie-field", "Whiplas", nil
assert_no_visible_selected_option
click_away
assert_closed_combobox
assert_combobox_display_and_value "#movie-field", "Whiplash", movies(:whiplash).id

on_small_screen do
visit async_path

open_combobox "#movie-field"

within "dialog" do
type_in_combobox "#movie-field-hw-dialog-combobox", "wh"
assert_selected_option_with text: "Whiplash"
assert_combobox_display "#movie-field-hw-dialog-combobox", "Whiplash"
end

click_away
assert_closed_combobox
assert_combobox_display_and_value "#movie-field", "Whiplash", movies(:whiplash).id

open_combobox "#movie-field"

within "dialog" do
"Whiplash".chars.each { type_in_combobox "#movie-field-hw-dialog-combobox", :arrow_right }
type_in_combobox "#movie-field-hw-dialog-combobox", :backspace
assert_combobox_display "#movie-field-hw-dialog-combobox", "Whiplas"
assert_no_visible_selected_option
end

click_away
assert_closed_combobox
assert_combobox_display_and_value "#movie-field", "Whiplash", movies(:whiplash).id
end

open_combobox "#movie-field"
assert_options_with count: 1
end

Expand Down

0 comments on commit 4bfb641

Please sign in to comment.