Skip to content

Commit ebe1a66

Browse files
authored
Merge pull request #3846 from AlchemyCMS/remove-alchemy-html-element
Remove AlchemyHTMLElement
2 parents dde947e + 830b02e commit ebe1a66

15 files changed

Lines changed: 328 additions & 579 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ The admin interface uses a **hybrid architecture**:
188188

189189
**Modern Web Components** (preferred for new features):
190190
- Custom elements in `app/javascript/alchemy_admin/components/`
191-
- Base class: `AlchemyHTMLElement` (extends `HTMLElement`)
191+
- Extend `HTMLElement` directly; do all DOM work in `connectedCallback`
192192
- Examples: `alchemy-sitemap`, `alchemy-element-editor`, `alchemy-datepicker`
193193
- Vanilla JavaScript (no framework dependency)
194194

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/alchemy_html_element.js

Lines changed: 0 additions & 129 deletions
This file was deleted.
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
/**
22
* Show the character counter below input fields and textareas
33
*/
4-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
54
import { translate } from "alchemy_admin/i18n"
65

7-
class CharCounter extends AlchemyHTMLElement {
8-
static properties = {
9-
maxChars: { default: 60 }
10-
}
11-
connected() {
6+
class CharCounter extends HTMLElement {
7+
connectedCallback() {
128
this.translation = translate("allowed_chars", this.maxChars)
139
this.formField = this.getFormField()
1410

1511
if (this.formField) {
1612
this.createDisplayElement()
1713
this.countCharacters()
18-
this.formField.addEventListener("keyup", () => this.countCharacters()) // add arrow function to get a implicit this - binding
14+
this.formField.addEventListener("keyup", this)
1915
}
2016
}
2117

18+
disconnectedCallback() {
19+
this.formField?.removeEventListener("keyup", this)
20+
}
21+
22+
handleEvent(event) {
23+
if (event.type === "keyup") this.countCharacters()
24+
}
25+
2226
getFormField() {
2327
const formFields = this.querySelectorAll("input, textarea")
2428
return formFields.length > 0 ? formFields[0] : undefined
2529
}
2630

2731
createDisplayElement() {
32+
this.display = this.querySelector(":scope > .alchemy-char-counter")
33+
if (this.display) return
2834
this.display = document.createElement("small")
2935
this.display.className = "alchemy-char-counter"
3036
this.formField.after(this.display)
@@ -35,6 +41,10 @@ class CharCounter extends AlchemyHTMLElement {
3541
this.display.textContent = `${charLength} ${this.translation}`
3642
this.display.classList.toggle("too-long", charLength > this.maxChars)
3743
}
44+
45+
get maxChars() {
46+
return this.getAttribute("max-chars") ?? 60
47+
}
3848
}
3949

4050
customElements.define("alchemy-char-counter", CharCounter)

app/javascript/alchemy_admin/components/datepicker.js

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
1-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
21
import { translate, currentLocale } from "alchemy_admin/i18n"
32
import flatpickr from "flatpickr"
43

54
const locale = currentLocale()
65

7-
class Datepicker extends AlchemyHTMLElement {
8-
static properties = {
9-
inputType: { default: "date" }
10-
}
11-
12-
constructor() {
13-
super()
14-
this.flatpickr = undefined
15-
}
16-
6+
class Datepicker extends HTMLElement {
177
// Load the locales for flatpickr before setting it up.
18-
async connected() {
8+
async connectedCallback() {
199
// English is the default locale for flatpickr, so we don't need to load it
2010
if (locale !== "en") {
2111
await import(`flatpickr/${locale}.js`)
2212
}
13+
// Bail out if the element was disconnected while the locale was loading.
14+
// Otherwise flatpickr would leak a calendar onto a detached input.
15+
if (!this.isConnected) return
2316

2417
this.flatpickr = flatpickr(this.inputField, this.flatpickrOptions)
2518
}
2619

27-
disconnected() {
28-
this.flatpickr.destroy()
20+
disconnectedCallback() {
21+
this.flatpickr?.destroy()
2922
}
3023

3124
get flatpickrOptions() {
@@ -56,6 +49,10 @@ class Datepicker extends AlchemyHTMLElement {
5649
get inputField() {
5750
return this.querySelector("input")
5851
}
52+
53+
get inputType() {
54+
return this.getAttribute("input-type") || "date"
55+
}
5956
}
6057

6158
customElements.define("alchemy-datepicker", Datepicker)

app/javascript/alchemy_admin/components/overlay.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
2-
3-
class Overlay extends AlchemyHTMLElement {
4-
render() {
5-
return `
1+
class Overlay extends HTMLElement {
2+
connectedCallback() {
3+
this.innerHTML = `
64
<alchemy-spinner></alchemy-spinner>
75
<div id="overlay_text_box">
8-
<span id="overlay_text">${this.getAttribute("text")}</span>
6+
<span id="overlay_text">${this.getAttribute("text") ?? ""}</span>
97
</div>
10-
`
8+
`
119
}
1210

1311
set show(value) {

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
}
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
2-
3-
class Spinner extends AlchemyHTMLElement {
4-
static properties = {
5-
size: { default: "medium" },
6-
color: { default: "currentColor" }
7-
}
8-
9-
render() {
1+
class Spinner extends HTMLElement {
2+
connectedCallback() {
103
this.className = `spinner spinner--${this.size}`
11-
12-
return `
4+
this.innerHTML = `
135
<svg width="100%" viewBox="0 0 28 28" style="--spinner-color: ${this.color}">
146
<path
157
class="hex1"
@@ -26,6 +18,14 @@ class Spinner extends AlchemyHTMLElement {
2618
</svg>
2719
`
2820
}
21+
22+
get size() {
23+
return this.getAttribute("size") || "medium"
24+
}
25+
26+
get color() {
27+
return this.getAttribute("color") || "currentColor"
28+
}
2929
}
3030

3131
customElements.define("alchemy-spinner", Spinner)

0 commit comments

Comments
 (0)