Skip to content

Commit 1649ea4

Browse files
authored
Merge pull request #3841 from AlchemyCMS/fix-custom-elements-cloneable
Fix custom elements setup and teardown
2 parents ebe1a66 + e62fe70 commit 1649ea4

30 files changed

Lines changed: 303 additions & 211 deletions

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ bun run test
4242
bun run build
4343

4444
# Build individual components
45-
bun run build:js # Rollup JavaScript bundling
45+
bun run build:admin # Bundle admin JavaScript (app/javascript/alchemy_admin/**)
46+
bun run build:js # Bundle vendored dependencies (sortablejs, shoelace, tinymce, etc.)
4647
bun run build:css # Sass compilation
4748
bun run handlebars:compile # Compile Handlebars templates
4849
bun run build:icons # Generate icon sprite

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ class AutoSubmit extends HTMLElement {
44
connectedCallback() {
55
// Still using jQuery here, because select2 does not emit
66
// the event from the original select element.
7-
$(this).on("change", function (event) {
8-
// We need to dispatch a submit event, so that Turbo that listens
9-
// to it submits the search form us.
10-
const submitEvent = new Event("submit", {
11-
bubbles: true,
12-
cancelable: true
13-
})
14-
event.target.form.dispatchEvent(submitEvent)
15-
return false
7+
$(this).on("change", this.#onChange)
8+
}
9+
10+
disconnectedCallback() {
11+
$(this).off("change", this.#onChange)
12+
}
13+
14+
#onChange = (event) => {
15+
// We need to dispatch a submit event, so that Turbo that listens
16+
// to it submits the search form us.
17+
const submitEvent = new Event("submit", {
18+
bubbles: true,
19+
cancelable: true
1620
})
21+
event.target.form.dispatchEvent(submitEvent)
22+
return false
1723
}
1824
}
1925

app/javascript/alchemy_admin/components/clipboard_button.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ import "clipboard"
22
import { growl } from "alchemy_admin/growler"
33

44
class ClipboardButton extends HTMLElement {
5-
constructor() {
6-
super()
7-
8-
this.innerHTML = `
9-
<alchemy-icon name="clipboard"></alchemy-icon>
10-
`
5+
connectedCallback() {
6+
this.innerHTML = '<alchemy-icon name="clipboard"></alchemy-icon>'
117

128
this.clipboard = new ClipboardJS(this, {
139
text: () => {

app/javascript/alchemy_admin/components/color_select.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ const formatItem = (object) => {
1414
}
1515

1616
class ColorSelect extends HTMLElement {
17+
#select2 = null
18+
1719
connectedCallback() {
1820
if (this.select) {
1921
this.#initializeSelect2()
20-
$(this.select).on("change", (event) =>
21-
this.#toggleColorPicker(event.val === "custom_color")
22-
)
22+
this.#select2.on("change", this.#onSelectChange)
2323
} else {
2424
this.colorInput?.addEventListener("input", this)
2525
this.textInput?.addEventListener("input", this)
@@ -41,6 +41,15 @@ class ColorSelect extends HTMLElement {
4141
disconnectedCallback() {
4242
this.colorInput?.removeEventListener("input", this)
4343
this.textInput?.removeEventListener("input", this)
44+
if (this.#select2) {
45+
this.#select2.off("change", this.#onSelectChange)
46+
this.#select2.select2("destroy")
47+
this.#select2 = null
48+
}
49+
}
50+
51+
#onSelectChange = (event) => {
52+
this.#toggleColorPicker(event.val === "custom_color")
4453
}
4554

4655
#initializeSelect2() {
@@ -50,7 +59,7 @@ class ColorSelect extends HTMLElement {
5059
formatResult: formatItem,
5160
formatSelection: formatItem
5261
}
53-
$(this.select).select2(options)
62+
this.#select2 = $(this.select).select2(options)
5463
}
5564

5665
#toggleColorPicker(enabled = true) {

app/javascript/alchemy_admin/components/dialog_link.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Dialog } from "alchemy_admin/dialog"
22

33
export class DialogLink extends HTMLAnchorElement {
4-
constructor() {
5-
super()
4+
connectedCallback() {
65
this.addEventListener("click", this)
76
}
87

8+
disconnectedCallback() {
9+
this.removeEventListener("click", this)
10+
}
11+
912
handleEvent(evt) {
1013
if (!this.disabled) {
1114
this.openDialog()

app/javascript/alchemy_admin/components/element_editor.js

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ export function dispatchPageDirtyEvent(data) {
1515
}
1616

1717
export class ElementEditor extends HTMLElement {
18-
constructor() {
19-
super()
18+
#form = null
19+
#header = null
20+
#toggleButton = null
21+
22+
connectedCallback() {
23+
// The placeholder while be being dragged is empty.
24+
if (this.classList.contains("ui-sortable-placeholder")) {
25+
return
26+
}
2027

2128
// Add event listeners
2229
this.addEventListener("click", this)
@@ -27,24 +34,16 @@ export class ElementEditor extends HTMLElement {
2734

2835
// Dirty observer still needs to be jQuery
2936
// in order to support select2.
30-
$(this.form).on("change", this.onChange)
37+
this.#form = this.form
38+
if (this.#form) {
39+
$(this.#form).on("change", this.onChange)
40+
}
3141

32-
this.header?.addEventListener("dblclick", () => {
33-
this.toggle()
34-
})
35-
this.toggleButton?.addEventListener("click", (evt) => {
36-
const elementEditor = evt.target.closest("alchemy-element-editor")
37-
if (elementEditor === this) {
38-
this.toggle()
39-
}
40-
})
41-
}
42+
this.#header = this.header
43+
this.#header?.addEventListener("dblclick", this.#onHeaderDblclick)
4244

43-
connectedCallback() {
44-
// The placeholder while be being dragged is empty.
45-
if (this.classList.contains("ui-sortable-placeholder")) {
46-
return
47-
}
45+
this.#toggleButton = this.toggleButton
46+
this.#toggleButton?.addEventListener("click", this.#onToggleClick)
4847

4948
// When newly created, focus the element and refresh the preview
5049
if (this.hasAttribute("created")) {
@@ -56,6 +55,20 @@ export class ElementEditor extends HTMLElement {
5655
}
5756
}
5857

58+
disconnectedCallback() {
59+
this.removeEventListener("click", this)
60+
this.removeEventListener("alchemy:element-update-title", this)
61+
this.removeEventListener("ajax:complete", this)
62+
if (this.#form) {
63+
$(this.#form).off("change", this.onChange)
64+
this.#form = null
65+
}
66+
this.#header?.removeEventListener("dblclick", this.#onHeaderDblclick)
67+
this.#header = null
68+
this.#toggleButton?.removeEventListener("click", this.#onToggleClick)
69+
this.#toggleButton = null
70+
}
71+
5972
handleEvent(event) {
6073
switch (event.type) {
6174
case "click":
@@ -79,15 +92,15 @@ export class ElementEditor extends HTMLElement {
7992
}
8093
}
8194

82-
onChange(event) {
95+
onChange = (event) => {
8396
const target = event.target
8497
// SortableJS fires a native change event :/
8598
// and we do not want to set the element editor dirty
8699
// when this happens
87100
if (target.classList.contains("nested-elements")) {
88101
return
89102
}
90-
this.closest("alchemy-element-editor").setDirty(target)
103+
this.setDirty(target)
91104
event.stopPropagation()
92105
return false
93106
}
@@ -576,6 +589,17 @@ export class ElementEditor extends HTMLElement {
576589
get previewWindow() {
577590
return document.getElementById("alchemy_preview_window")
578591
}
592+
593+
#onHeaderDblclick = () => {
594+
this.toggle()
595+
}
596+
597+
#onToggleClick = (evt) => {
598+
const elementEditor = evt.target.closest("alchemy-element-editor")
599+
if (elementEditor === this) {
600+
this.toggle()
601+
}
602+
}
579603
}
580604

581605
customElements.define("alchemy-element-editor", ElementEditor)

app/javascript/alchemy_admin/components/element_editor/delete_element_button.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import { openConfirmDialog } from "alchemy_admin/confirm_dialog"
66
import { dispatchPageDirtyEvent } from "alchemy_admin/components/element_editor"
77

88
export class DeleteElementButton extends HTMLElement {
9-
constructor() {
10-
super()
9+
connectedCallback() {
1110
this.button?.addEventListener("click", this)
1211
}
1312

13+
disconnectedCallback() {
14+
this.button?.removeEventListener("click", this)
15+
}
16+
1417
async handleEvent() {
1518
const confirmed = await openConfirmDialog(this.message)
1619
if (confirmed) {

app/javascript/alchemy_admin/components/element_select.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ const formatItem = (icon, text, hint) => {
2121
}
2222

2323
class ElementSelect extends HTMLElement {
24-
constructor() {
25-
super()
26-
}
24+
#select2 = null
2725

2826
connectedCallback() {
2927
const results = this.options
@@ -48,7 +46,12 @@ class ElementSelect extends HTMLElement {
4846
formatSelection,
4947
placeholder: this.placeholder
5048
}
51-
$(this.inputField).select2(options)
49+
this.#select2 = $(this.inputField).select2(options)
50+
}
51+
52+
disconnectedCallback() {
53+
this.#select2?.select2("destroy")
54+
this.#select2 = null
5255
}
5356

5457
get options() {

0 commit comments

Comments
 (0)