Skip to content

Commit efbc812

Browse files
committed
refactor(RemoteSelect): Extend HTMLElement directly
Dropping the AlchemyHTMLElement base class avoids the constructor-time reading of innerHTML and attribute processing, which violated the Web Components spec and contributed to the SortableJS drag-clone crash. Attributes are now read through getters, Select2 is initialized in connectedCallback with an isConnected guard after the async locale import, and the instance is destroyed in disconnectedCallback.
1 parent bb9a9d9 commit efbc812

3 files changed

Lines changed: 55 additions & 19 deletions

File tree

app/assets/builds/alchemy/alchemy_admin.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/builds/alchemy/alchemy_admin.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/javascript/alchemy_admin/components/remote_select.js

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
1-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
21
import { setupSelectLocale } from "alchemy_admin/i18n"
32

43
export function hightlightTerm(name, term) {
54
return name.replace(new RegExp(term, "gi"), (match) => `<em>${match}</em>`)
65
}
76

8-
export class RemoteSelect extends AlchemyHTMLElement {
9-
static properties = {
10-
allowClear: { default: false },
11-
selection: { default: undefined },
12-
placeholder: { default: "" },
13-
queryParams: { default: "{}" },
14-
url: { default: "" }
15-
}
7+
export class RemoteSelect extends HTMLElement {
8+
#select2 = null
169

17-
async connected() {
10+
async connectedCallback() {
1811
await setupSelectLocale()
12+
// Bail out if the element was disconnected while the locale was loading.
13+
// Otherwise Select2 would leak onto a detached input.
14+
if (!this.isConnected) return
1915

2016
this.input.classList.add("alchemy_selectbox")
2117

22-
$(this.input)
18+
this.#select2 = $(this.input)
2319
.select2(this.select2Config)
24-
.on("select2-open", (evt) => {
25-
this.onOpen(evt)
26-
})
27-
.on("change", (evt) => {
28-
this.onChange(evt)
29-
})
20+
.on("select2-open", this.#onOpen)
21+
.on("change", this.#onChange)
3022
}
3123

24+
disconnectedCallback() {
25+
if (this.#select2) {
26+
this.#select2.off("select2-open", this.#onOpen)
27+
this.#select2.off("change", this.#onChange)
28+
this.#select2.select2("destroy")
29+
this.#select2 = null
30+
}
31+
}
32+
33+
#onOpen = (evt) => this.onOpen(evt)
34+
#onChange = (evt) => this.onChange(evt)
35+
3236
/**
3337
* Optional on change handler called by Select2.
3438
* @param {Event} event
@@ -54,6 +58,38 @@ export class RemoteSelect extends AlchemyHTMLElement {
5458
}, 100)
5559
}
5660

61+
/**
62+
* Dispatches a custom event with given name, namespaced under `Alchemy.`.
63+
* Subclasses may call this to emit their own events.
64+
* @param {string} name The name of the custom event
65+
* @param {object} detail Optional event details
66+
*/
67+
dispatchCustomEvent(name, detail = {}) {
68+
this.dispatchEvent(
69+
new CustomEvent(`Alchemy.${name}`, { bubbles: true, detail })
70+
)
71+
}
72+
73+
get allowClear() {
74+
return this.hasAttribute("allow-clear")
75+
}
76+
77+
get selection() {
78+
return this.getAttribute("selection")
79+
}
80+
81+
get placeholder() {
82+
return this.getAttribute("placeholder") ?? ""
83+
}
84+
85+
get queryParams() {
86+
return this.getAttribute("query-params") ?? "{}"
87+
}
88+
89+
get url() {
90+
return this.getAttribute("url") ?? ""
91+
}
92+
5793
get input() {
5894
return this.getElementsByTagName("input")[0]
5995
}

0 commit comments

Comments
 (0)