Skip to content

Commit 2d36f16

Browse files
Copilotcodingjoe
andcommitted
Implement disconnectedCallback and prevent duplicate inputs
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
1 parent 5715ff8 commit 2d36f16

File tree

2 files changed

+83
-11
lines changed

2 files changed

+83
-11
lines changed

s3file/static/s3file/js/s3file.js

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,59 @@ export class S3FileInput extends globalThis.HTMLElement {
3434
}
3535

3636
connectedCallback() {
37+
// Prevent creating duplicate inputs if already connected
38+
if (this._fileInput) return
39+
3740
// Create a hidden file input for the file picker functionality
3841
this._fileInput = document.createElement("input")
3942
this._fileInput.type = "file"
4043

4144
// Sync attributes to hidden input
4245
this._syncAttributesToHiddenInput()
4346

44-
// Listen for file selection on hidden input
45-
this._fileInput.addEventListener("change", () => {
47+
// Create bound handler references for cleanup
48+
this._changeHandler = () => {
4649
this._files = this._fileInput.files
4750
this.dispatchEvent(new Event("change", { bubbles: true }))
4851
this.changeHandler()
49-
})
52+
}
53+
this._boundFormDataHandler = this.fromDataHandler.bind(this)
54+
this._boundSubmitHandler = this.submitHandler.bind(this)
55+
this._boundUploadHandler = this.uploadHandler.bind(this)
56+
57+
// Listen for file selection on hidden input
58+
this._fileInput.addEventListener("change", this._changeHandler)
5059

5160
// Append elements
5261
this.appendChild(this._fileInput)
5362

5463
// Setup form event listeners
55-
this.form?.addEventListener("formdata", this.fromDataHandler.bind(this))
56-
this.form?.addEventListener("submit", this.submitHandler.bind(this), {
64+
this.form?.addEventListener("formdata", this._boundFormDataHandler)
65+
this.form?.addEventListener("submit", this._boundSubmitHandler, {
5766
once: true,
5867
})
59-
this.form?.addEventListener("upload", this.uploadHandler.bind(this))
68+
this.form?.addEventListener("upload", this._boundUploadHandler)
69+
}
70+
71+
disconnectedCallback() {
72+
// Clean up event listeners
73+
if (this._fileInput) {
74+
this._fileInput.removeEventListener("change", this._changeHandler)
75+
}
76+
77+
if (this.form) {
78+
this.form.removeEventListener("formdata", this._boundFormDataHandler)
79+
this.form.removeEventListener("submit", this._boundSubmitHandler)
80+
this.form.removeEventListener("upload", this._boundUploadHandler)
81+
}
82+
83+
// Remove the file input from DOM
84+
if (this._fileInput && this._fileInput.parentNode) {
85+
this._fileInput.parentNode.removeChild(this._fileInput)
86+
}
87+
88+
// Clear reference to allow reconnection
89+
this._fileInput = null
6090
}
6191

6292
/**
@@ -198,12 +228,17 @@ export class S3FileInput extends globalThis.HTMLElement {
198228
changeHandler() {
199229
this.keys = []
200230
this.upload = null
201-
try {
202-
this.form?.removeEventListener("submit", this.submitHandler.bind(this))
203-
} catch (error) {
204-
console.debug(error)
231+
// Remove previous submit handler and add a new one
232+
if (this._boundSubmitHandler) {
233+
try {
234+
this.form?.removeEventListener("submit", this._boundSubmitHandler)
235+
} catch (error) {
236+
console.debug(error)
237+
}
205238
}
206-
this.form?.addEventListener("submit", this.submitHandler.bind(this), {
239+
// Create a new bound handler for this change
240+
this._boundSubmitHandler = this.submitHandler.bind(this)
241+
this.form?.addEventListener("submit", this._boundSubmitHandler, {
207242
once: true,
208243
})
209244
}

tests/__tests__/s3file.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,43 @@ describe("S3FileInput", () => {
4141
assert(input._fileInput.type === "file")
4242
})
4343

44+
test("disconnectedCallback", () => {
45+
const form = document.createElement("form")
46+
document.body.appendChild(form)
47+
const input = new s3file.S3FileInput()
48+
form.addEventListener = mock.fn(form.addEventListener)
49+
form.appendChild(input)
50+
const fileInput = input._fileInput
51+
assert(fileInput !== null)
52+
// Mock removeEventListener to verify cleanup
53+
form.removeEventListener = mock.fn(form.removeEventListener)
54+
fileInput.removeEventListener = mock.fn(fileInput.removeEventListener)
55+
// Manually call disconnectedCallback since jsdom doesn't trigger it
56+
input.disconnectedCallback()
57+
assert(form.removeEventListener.mock.calls.length === 3)
58+
assert(fileInput.removeEventListener.mock.calls.length === 1)
59+
assert(fileInput.parentNode === null)
60+
assert(input._fileInput === null)
61+
})
62+
63+
test("connectedCallback prevents duplicate inputs on reconnection", () => {
64+
const form = document.createElement("form")
65+
document.body.appendChild(form)
66+
const input = new s3file.S3FileInput()
67+
// First connection
68+
form.appendChild(input)
69+
assert(input._fileInput !== null)
70+
assert(input.querySelectorAll('input[type="file"]').length === 1)
71+
// Disconnect and clear state
72+
input.disconnectedCallback()
73+
// Reconnect - should create a new input since old one was cleaned up
74+
input.connectedCallback()
75+
assert(input._fileInput !== null)
76+
// Check only one input element exists after reconnection
77+
const inputElements = input.querySelectorAll('input[type="file"]')
78+
assert(inputElements.length === 1)
79+
})
80+
4481
test("changeHandler", () => {
4582
const form = document.createElement("form")
4683
const input = new s3file.S3FileInput()

0 commit comments

Comments
 (0)