Skip to content

Commit 476aa7b

Browse files
committed
Update Turnstile JS controller to work multiple times on a page
1 parent ec25cda commit 476aa7b

1 file changed

Lines changed: 31 additions & 34 deletions

File tree

app/javascript/controllers/rpi_turnstile/turnstile_controller.js

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,20 @@ export default class extends Controller {
77
static sourceUrl = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
88
static callbackFunctionName = '__turnstileLoadedCallback'
99

10-
loadingState = undefined
11-
loadingPromise = {
12-
resolve: () => {},
13-
reject: () => {}
14-
}
15-
widgetId = undefined
16-
17-
initialize () {
18-
this.loadingState = typeof window.turnstile !== 'undefined' ? 'ready' : 'unloaded'
10+
// Shared across all instances so that multiple widgets on the same page
11+
// only ever inject one script tag and all resolve together.
12+
static loadingState = typeof window.turnstile !== 'undefined' ? 'ready' : 'unloaded'
13+
static pendingResolvers = []
14+
static pendingRejectors = []
1915

20-
// This defines a global function that can be called by Turnstile once it
21-
// has been loaded and ready. The callbackFunctionName name is used in the
22-
// URL of the script in the loadTurnstile() function.
23-
window[this.constructor.callbackFunctionName] = () => {
24-
this.loadingPromise.resolve()
25-
this.loadingState = 'ready'
26-
delete window[this.constructor.callbackFunctionName]
27-
}
28-
}
16+
widgetId = undefined
2917

3018
connect () {
3119
if (!this.hasOptionsValue) return
3220

3321
// This call waits for the promise returned by loadTurnstile to resolve
3422
// before trying to call render().
35-
this.loadTurnstile()
23+
this.constructor.loadTurnstile()
3624
.then(() => { this.render() })
3725
.catch((e) => console.log(e))
3826
}
@@ -52,37 +40,46 @@ export default class extends Controller {
5240
this.widgetId = window.turnstile.render(this.containerTarget, this.optionsValue)
5341
}
5442

55-
// This returns a promise that resolves when the loading has completed, or
56-
// rejects if an error is raised during loading.
57-
loadTurnstile () {
43+
// Static so that all instances share a single script load. Returns a promise
44+
// that resolves when Turnstile is ready, or rejects if the script fails to load.
45+
static loadTurnstile () {
46+
if (this.loadingState === 'ready') {
47+
return Promise.resolve()
48+
}
49+
5850
if (this.loadingState === 'unloaded') {
5951
this.loadingState = 'loading'
6052

53+
// This defines a global function that Turnstile calls once it is ready.
54+
// The name is embedded in the script URL via the onload parameter.
6155
// See https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget
62-
const url = `${this.constructor.sourceUrl}?onload=${this.constructor.callbackFunctionName}&render=explicit`
56+
window[this.callbackFunctionName] = () => {
57+
this.loadingState = 'ready'
58+
delete window[this.callbackFunctionName]
59+
this.pendingResolvers.splice(0).forEach(resolve => resolve())
60+
this.pendingRejectors.splice(0)
61+
}
62+
63+
const url = `${this.sourceUrl}?onload=${this.callbackFunctionName}&render=explicit`
6364
const script = document.createElement('script')
6465
script.src = url
6566
script.async = true
6667
script.defer = true
6768

6869
script.addEventListener('error', () => {
69-
this.loadingPromise.reject('Failed to load Turnstile.')
70-
delete window[this.constructor.callbackFunctionName]
70+
this.loadingState = 'unloaded'
71+
delete window[this.callbackFunctionName]
72+
this.pendingRejectors.splice(0).forEach(reject => reject('Failed to load Turnstile.'))
73+
this.pendingResolvers.splice(0)
7174
})
7275

7376
document.head.appendChild(script)
7477
}
7578

76-
// Return a promise that we can resolve when the callback function is
77-
// called by the Turnstile JS when it is ready.
79+
// Loading is already in progress — queue this caller alongside any others.
7880
return new Promise((resolve, reject) => {
79-
this.loadingPromise = { resolve, reject }
80-
81-
// If turnstile is already defined, resolve immediately.
82-
if (this.loadingState === 'ready') {
83-
resolve()
84-
delete window[this.constructor.callbackFunctionName]
85-
}
81+
this.pendingResolvers.push(resolve)
82+
this.pendingRejectors.push(reject)
8683
})
8784
}
8885
}

0 commit comments

Comments
 (0)