hydrate-form.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import classNames from 'classnames'
  2. import { FetchError, postJSON } from '../../infrastructure/fetch-json'
  3. import { validateCaptchaV2 } from './captcha'
  4. import inputValidator from './input-validator'
  5. import { disableElement, enableElement } from '../utils/disableElement'
  6. // Form helper(s) to handle:
  7. // - Attaching to the relevant form elements
  8. // - Listening for submit event
  9. // - Validating captcha
  10. // - Sending fetch request
  11. // - Redirect handling
  12. // - Showing errors
  13. // - Disabled state
  14. function formSubmitHelper(formEl) {
  15. formEl.addEventListener('submit', async e => {
  16. e.preventDefault()
  17. formEl.dispatchEvent(new Event('pending'))
  18. const messageBag = []
  19. try {
  20. const captchaResponse = await validateCaptcha(formEl)
  21. const data = await sendFormRequest(formEl, captchaResponse)
  22. formEl.dispatchEvent(new Event('sent'))
  23. // Handle redirects
  24. if (data.redir) {
  25. window.location = data.redir
  26. return
  27. }
  28. // Show a success message (e.g. used on 2FA page)
  29. if (data.message) {
  30. messageBag.push({
  31. type: 'message',
  32. text: data.message.text || data.message,
  33. })
  34. }
  35. } catch (error) {
  36. let text = error.message
  37. if (error instanceof FetchError) {
  38. text = error.getUserFacingMessage()
  39. }
  40. messageBag.push({
  41. type: 'error',
  42. key: error.data?.message?.key,
  43. text,
  44. })
  45. } finally {
  46. showMessages(formEl, messageBag)
  47. formEl.dispatchEvent(new Event('idle'))
  48. }
  49. })
  50. }
  51. async function validateCaptcha(formEl) {
  52. let captchaResponse
  53. if (formEl.hasAttribute('captcha')) {
  54. captchaResponse = await validateCaptchaV2()
  55. }
  56. return captchaResponse
  57. }
  58. async function sendFormRequest(formEl, captchaResponse) {
  59. const formData = new FormData(formEl)
  60. if (captchaResponse) {
  61. formData.set('g-recaptcha-response', captchaResponse)
  62. }
  63. const body = Object.fromEntries(formData.entries())
  64. const url = formEl.getAttribute('action')
  65. return postJSON(url, { body })
  66. }
  67. function showMessages(formEl, messageBag) {
  68. const messagesEl = formEl.querySelector('[data-ol-form-messages]')
  69. if (!messagesEl) return
  70. // Clear content
  71. messagesEl.textContent = ''
  72. formEl.querySelectorAll('[data-ol-custom-form-message]').forEach(el => {
  73. el.hidden = true
  74. })
  75. // Render messages
  76. messageBag.forEach(message => {
  77. if (message.key) {
  78. formEl
  79. .querySelectorAll(`[data-ol-custom-form-message="${message.key}"]`)
  80. .forEach(el => {
  81. el.hidden = false
  82. })
  83. return
  84. }
  85. const messageEl = document.createElement('div')
  86. messageEl.className = classNames('alert', {
  87. 'alert-danger': message.type === 'error',
  88. 'alert-success': message.type !== 'error',
  89. })
  90. messageEl.textContent = message.text
  91. messageEl.setAttribute('aria-live', 'assertive')
  92. messageEl.setAttribute(
  93. 'role',
  94. message.type === 'error' ? 'alert' : 'status'
  95. )
  96. messagesEl.append(messageEl)
  97. })
  98. }
  99. export function inflightHelper(el) {
  100. const disabledInflight = el.querySelectorAll('[data-ol-disabled-inflight]')
  101. const showWhenNotInflight = el.querySelectorAll('[data-ol-inflight="idle"]')
  102. const showWhenInflight = el.querySelectorAll('[data-ol-inflight="pending"]')
  103. el.addEventListener('pending', () => {
  104. disabledInflight.forEach(disableElement)
  105. toggleDisplay(showWhenNotInflight, showWhenInflight)
  106. })
  107. el.addEventListener('idle', () => {
  108. disabledInflight.forEach(enableElement)
  109. toggleDisplay(showWhenInflight, showWhenNotInflight)
  110. })
  111. }
  112. function formSentHelper(el) {
  113. const showWhenPending = el.querySelectorAll('[data-ol-not-sent]')
  114. const showWhenDone = el.querySelectorAll('[data-ol-sent]')
  115. if (showWhenDone.length === 0) return
  116. el.addEventListener('sent', () => {
  117. toggleDisplay(showWhenPending, showWhenDone)
  118. })
  119. }
  120. function formValidationHelper(el) {
  121. el.querySelectorAll('input').forEach(inputEl => {
  122. if (
  123. inputEl.willValidate &&
  124. !inputEl.hasAttribute('data-ol-no-custom-form-validation-messages')
  125. ) {
  126. inputValidator(inputEl)
  127. }
  128. })
  129. }
  130. function formAutoSubmitHelper(el) {
  131. if (el.hasAttribute('data-ol-auto-submit')) {
  132. setTimeout(() => {
  133. el.querySelector('[type="submit"]').click()
  134. }, 0)
  135. }
  136. }
  137. export function toggleDisplay(hide, show) {
  138. hide.forEach(el => {
  139. el.hidden = true
  140. })
  141. show.forEach(el => {
  142. el.hidden = false
  143. })
  144. }
  145. function hydrateAsyncForm(el) {
  146. formSubmitHelper(el)
  147. inflightHelper(el)
  148. formSentHelper(el)
  149. formValidationHelper(el)
  150. formAutoSubmitHelper(el)
  151. }
  152. function hydrateRegularForm(el) {
  153. inflightHelper(el)
  154. formValidationHelper(el)
  155. el.addEventListener('submit', () => {
  156. el.dispatchEvent(new Event('pending'))
  157. })
  158. formAutoSubmitHelper(el)
  159. }
  160. document.querySelectorAll(`[data-ol-async-form]`).forEach(hydrateAsyncForm)
  161. document.querySelectorAll(`[data-ol-regular-form]`).forEach(hydrateRegularForm)