Extract and set form values through the DOM—no frameworks required! https://github.com/TheoryOfNekomata/formxtra
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

index.ts 22 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. /**
  2. * Line ending.
  3. */
  4. export enum LineEnding {
  5. /**
  6. * Carriage return. Used for legacy Mac OS systems.
  7. */
  8. CR = '\r',
  9. /**
  10. * Line feed. Used for Linux/*NIX systems as well as newer macOS systems.
  11. */
  12. LF = '\n',
  13. /**
  14. * Carriage return/line feed combination. Used for Windows systems.
  15. */
  16. CRLF = '\r\n',
  17. }
  18. /**
  19. * Type for a placeholder object value.
  20. */
  21. type PlaceholderObject = Record<string, unknown>
  22. /**
  23. * Tag name for the `<input>` element.
  24. */
  25. const TAG_NAME_INPUT = 'INPUT' as const;
  26. /**
  27. * Tag name for the `<textarea>` element.
  28. */
  29. const TAG_NAME_TEXTAREA = 'TEXTAREA' as const;
  30. /**
  31. * Tag name for the `<select>` element.
  32. */
  33. const TAG_NAME_SELECT = 'SELECT' as const;
  34. /**
  35. * Tag names for valid form field elements of any configuration.
  36. */
  37. const FORM_FIELD_ELEMENT_TAG_NAMES = [TAG_NAME_SELECT, TAG_NAME_TEXTAREA] as const;
  38. /**
  39. * Types for button-like `<input>` elements that are not considered as a form field.
  40. */
  41. const FORM_FIELD_INPUT_EXCLUDED_TYPES = ['submit', 'reset'] as const;
  42. /**
  43. * Checks if an element can hold a custom (user-inputted) field value.
  44. * @param el - The element.
  45. */
  46. export const isFormFieldElement = (el: HTMLElement) => {
  47. const { tagName } = el;
  48. if (FORM_FIELD_ELEMENT_TAG_NAMES.includes(tagName as typeof FORM_FIELD_ELEMENT_TAG_NAMES[0])) {
  49. return true;
  50. }
  51. if (tagName !== TAG_NAME_INPUT) {
  52. return false;
  53. }
  54. const inputEl = el as HTMLInputElement;
  55. const { type } = inputEl;
  56. if (FORM_FIELD_INPUT_EXCLUDED_TYPES.includes(
  57. type.toLowerCase() as typeof FORM_FIELD_INPUT_EXCLUDED_TYPES[0],
  58. )) {
  59. return false;
  60. }
  61. return Boolean(inputEl.name);
  62. };
  63. /**
  64. * Options for getting a `<textarea>` element field value.
  65. */
  66. type GetTextAreaValueOptions = {
  67. /**
  68. * Line ending used for the element's value.
  69. */
  70. lineEndings?: LineEnding,
  71. }
  72. /**
  73. * Gets the value of a `<textarea>` element.
  74. * @param textareaEl - The element.
  75. * @param options - The options.
  76. * @returns Value of the textarea element.
  77. */
  78. const getTextAreaFieldValue = (
  79. textareaEl: HTMLTextAreaElement,
  80. options = {} as GetTextAreaValueOptions,
  81. ) => {
  82. const { lineEndings = LineEnding.CRLF } = options;
  83. return textareaEl.value.replace(/\n/g, lineEndings);
  84. };
  85. /**
  86. * Sets the value of a `<textarea>` element.
  87. * @param textareaEl - The element.
  88. * @param value - Value of the textarea element.
  89. */
  90. const setTextAreaFieldValue = (
  91. textareaEl: HTMLTextAreaElement,
  92. value: unknown,
  93. ) => {
  94. // eslint-disable-next-line no-param-reassign
  95. textareaEl.value = value as string;
  96. };
  97. /**
  98. * Options for getting a `<select>` element field value.
  99. */
  100. type GetSelectValueOptions = PlaceholderObject
  101. /**
  102. * Gets the value of a `<select>` element.
  103. * @param selectEl - The element.
  104. * @param options - The options.
  105. * @returns Value of the select element.
  106. */
  107. const getSelectFieldValue = (
  108. selectEl: HTMLSelectElement,
  109. options = {} as GetSelectValueOptions,
  110. ) => {
  111. if (selectEl.multiple) {
  112. return Array.from(selectEl.options).filter((o) => o.selected).map((o) => o.value);
  113. }
  114. if (typeof options !== 'object' || options === null) {
  115. throw new TypeError('Invalid options for getSelectFieldValue().');
  116. }
  117. return selectEl.value;
  118. };
  119. /**
  120. * Sets the value of a `<select>` element.
  121. * @param selectEl - The element.
  122. * @param value - Value of the select element.
  123. */
  124. const setSelectFieldValue = (selectEl: HTMLSelectElement, value: unknown) => {
  125. Array.from(selectEl.options)
  126. .filter((o) => {
  127. if (Array.isArray(value)) {
  128. return (value as string[]).includes(o.value);
  129. }
  130. return o.value === value;
  131. })
  132. .forEach((el) => {
  133. // eslint-disable-next-line no-param-reassign
  134. el.selected = true;
  135. });
  136. };
  137. /**
  138. * Attribute name for the element's value.
  139. */
  140. const ATTRIBUTE_VALUE = 'value' as const;
  141. /**
  142. * Value of the `type` attribute for `<input>` elements considered as radio buttons.
  143. */
  144. const INPUT_TYPE_RADIO = 'radio' as const;
  145. /**
  146. * Type for an `<input type="radio">` element.
  147. */
  148. export type HTMLInputRadioElement = HTMLInputElement & { type: typeof INPUT_TYPE_RADIO }
  149. /**
  150. * Options for getting an `<input type="radio">` element field value.
  151. */
  152. type GetInputRadioFieldValueOptions = PlaceholderObject
  153. /**
  154. * Gets the value of an `<input type="radio">` element.
  155. * @param inputEl - The element.
  156. * @param options - The options.
  157. * @returns Value of the input element.
  158. */
  159. const getInputRadioFieldValue = (
  160. inputEl: HTMLInputRadioElement,
  161. options = {} as GetInputRadioFieldValueOptions,
  162. ) => {
  163. if (inputEl.checked) {
  164. return inputEl.value;
  165. }
  166. if (typeof options !== 'object' || options === null) {
  167. throw new TypeError('Invalid options for getInputRadioFieldValue().');
  168. }
  169. return null;
  170. };
  171. /**
  172. * Sets the value of an `<input type="radio">` element.
  173. * @param inputEl - The element.
  174. * @param value - Value of the input element.
  175. */
  176. const setInputRadioFieldValue = (
  177. inputEl: HTMLInputRadioElement,
  178. value: unknown,
  179. ) => {
  180. const valueWhenChecked = inputEl.getAttribute(ATTRIBUTE_VALUE);
  181. // eslint-disable-next-line no-param-reassign
  182. inputEl.checked = valueWhenChecked === value;
  183. };
  184. /**
  185. * Value of the `type` attribute for `<input>` elements considered as checkboxes.
  186. */
  187. const INPUT_TYPE_CHECKBOX = 'checkbox' as const;
  188. /**
  189. * Type for an `<input type="checkbox">` element.
  190. */
  191. export type HTMLInputCheckboxElement = HTMLInputElement & { type: typeof INPUT_TYPE_CHECKBOX }
  192. /**
  193. * Options for getting an `<input type="checkbox">` element field value.
  194. */
  195. type GetInputCheckboxFieldValueOptions = {
  196. /**
  197. * Should we consider the `checked` attribute of checkboxes with no `value` attributes instead of
  198. * the default value "on" when checked?
  199. *
  200. * This forces the field to get the `false` value when unchecked.
  201. */
  202. booleanValuelessCheckbox?: true,
  203. }
  204. /**
  205. * String values resolvable to an unchecked checkbox state.
  206. */
  207. const INPUT_CHECKBOX_FALSY_VALUES = ['false', 'off', 'no', '0', ''] as const;
  208. /**
  209. * Default value of the `<input type="checkbox">` when it is checked.
  210. */
  211. const INPUT_CHECKBOX_DEFAULT_CHECKED_VALUE = 'on' as const;
  212. /**
  213. * String values resolvable to a checked checkbox state.
  214. */
  215. const INPUT_CHECKBOX_TRUTHY_VALUES = ['true', INPUT_CHECKBOX_DEFAULT_CHECKED_VALUE, 'yes', '1'] as const;
  216. /**
  217. * Gets the value of an `<input type="checkbox">` element.
  218. * @param inputEl - The element.
  219. * @param options - The options.
  220. * @returns Value of the input element.
  221. */
  222. const getInputCheckboxFieldValue = (
  223. inputEl: HTMLInputCheckboxElement,
  224. options = {} as GetInputCheckboxFieldValueOptions,
  225. ) => {
  226. const checkedValue = inputEl.getAttribute(ATTRIBUTE_VALUE);
  227. if (checkedValue !== null) {
  228. if (inputEl.checked) {
  229. return inputEl.value;
  230. }
  231. return null;
  232. }
  233. if (options.booleanValuelessCheckbox) {
  234. return inputEl.checked;
  235. }
  236. if (inputEl.checked) {
  237. return INPUT_CHECKBOX_DEFAULT_CHECKED_VALUE;
  238. }
  239. return null;
  240. };
  241. /**
  242. * Sets the value of an `<input type="checkbox">` element.
  243. * @param inputEl - The element.
  244. * @param value - Value of the input element.
  245. */
  246. const setInputCheckboxFieldValue = (
  247. inputEl: HTMLInputCheckboxElement,
  248. value: unknown,
  249. ) => {
  250. const checkedValue = inputEl.getAttribute(ATTRIBUTE_VALUE);
  251. if (checkedValue !== null) {
  252. // eslint-disable-next-line no-param-reassign
  253. inputEl.checked = value === checkedValue;
  254. return;
  255. }
  256. if (
  257. INPUT_CHECKBOX_FALSY_VALUES.includes(
  258. (value as string).toLowerCase() as typeof INPUT_CHECKBOX_FALSY_VALUES[0],
  259. )
  260. || !value
  261. ) {
  262. // eslint-disable-next-line no-param-reassign
  263. inputEl.checked = false;
  264. return;
  265. }
  266. if (
  267. INPUT_CHECKBOX_TRUTHY_VALUES.includes(
  268. (value as string).toLowerCase() as typeof INPUT_CHECKBOX_TRUTHY_VALUES[0],
  269. )
  270. || value === true
  271. || value === 1
  272. ) {
  273. // eslint-disable-next-line no-param-reassign
  274. inputEl.checked = true;
  275. }
  276. };
  277. /**
  278. * Value of the `type` attribute for `<input>` elements considered as file upload components.
  279. */
  280. const INPUT_TYPE_FILE = 'file' as const;
  281. /**
  282. * Type for an `<input type="file">` element.
  283. */
  284. export type HTMLInputFileElement = HTMLInputElement & { type: typeof INPUT_TYPE_FILE }
  285. /**
  286. * Options for getting an `<input type="file">` element field value.
  287. */
  288. type GetInputFileFieldValueOptions = {
  289. /**
  290. * Should we retrieve the `files` attribute of file inputs instead of the currently selected file
  291. * names?
  292. */
  293. getFileObjects?: true,
  294. }
  295. /**
  296. * Gets the value of an `<input type="file">` element.
  297. * @param inputEl - The element.
  298. * @param options - The options.
  299. * @returns Value of the input element.
  300. */
  301. const getInputFileFieldValue = (
  302. inputEl: HTMLInputFileElement,
  303. options = {} as GetInputFileFieldValueOptions,
  304. ) => {
  305. const { files } = inputEl;
  306. if ((files as unknown) === null) {
  307. return null;
  308. }
  309. if (options.getFileObjects) {
  310. return files;
  311. }
  312. const filesArray = Array.from(files as FileList);
  313. if (filesArray.length > 1) {
  314. return filesArray.map((f) => f.name);
  315. }
  316. return filesArray[0]?.name || '';
  317. };
  318. /**
  319. * Value of the `type` attribute for `<input>` elements considered as discrete number selectors.
  320. */
  321. const INPUT_TYPE_NUMBER = 'number' as const;
  322. /**
  323. * Type for an `<input type="number">` element.
  324. */
  325. export type HTMLInputNumberElement = HTMLInputElement & { type: typeof INPUT_TYPE_NUMBER }
  326. /**
  327. * Value of the `type` attribute for `<input>` elements considered as continuous number selectors.
  328. */
  329. const INPUT_TYPE_RANGE = 'range' as const;
  330. /**
  331. * Type for an `<input type="range">` element.
  332. */
  333. export type HTMLInputRangeElement = HTMLInputElement & { type: typeof INPUT_TYPE_RANGE }
  334. /**
  335. * Type for an `<input>` element that handles numeric values.
  336. */
  337. export type HTMLInputNumericElement = HTMLInputNumberElement | HTMLInputRangeElement;
  338. /**
  339. * Options for getting an `<input type="number">` element field value.
  340. */
  341. type GetInputNumberFieldValueOptions = {
  342. /**
  343. * Should we force values to be numeric?
  344. *
  345. * **Note:** Form values are retrieved to be strings by default, hence this option.
  346. */
  347. forceNumberValues?: true,
  348. }
  349. /**
  350. * Gets the value of an `<input type="number">` element.
  351. * @param inputEl - The element.
  352. * @param options - The options.
  353. * @returns Value of the input element.
  354. */
  355. const getInputNumericFieldValue = (
  356. inputEl: HTMLInputNumericElement,
  357. options = {} as GetInputNumberFieldValueOptions,
  358. ) => {
  359. if (options.forceNumberValues) {
  360. return inputEl.valueAsNumber;
  361. }
  362. return inputEl.value;
  363. };
  364. /**
  365. * Sets the value of an `<input type="number">` element.
  366. * @param inputEl - The element.
  367. * @param value - Value of the input element.
  368. */
  369. const setInputNumericFieldValue = (
  370. inputEl: HTMLInputNumericElement,
  371. value: unknown,
  372. ) => {
  373. // eslint-disable-next-line no-param-reassign
  374. inputEl.valueAsNumber = Number(value);
  375. };
  376. /**
  377. * Value of the `type` attribute for `<input>` elements considered as date pickers.
  378. */
  379. const INPUT_TYPE_DATE = 'date' as const;
  380. /**
  381. * Type for an `<input type="date">` element.
  382. */
  383. export type HTMLInputDateElement = HTMLInputElement & { type: typeof INPUT_TYPE_DATE }
  384. /**
  385. * Value of the `type` attribute for `<input>` elements considered as date and time pickers.
  386. */
  387. const INPUT_TYPE_DATETIME_LOCAL = 'datetime-local' as const;
  388. /**
  389. * Type for an `<input type="datetime-local">` element.
  390. */
  391. export type HTMLInputDateTimeLocalElement = HTMLInputElement & {
  392. type: typeof INPUT_TYPE_DATETIME_LOCAL,
  393. }
  394. /**
  395. * Type for an `<input>` element.that handles date values.
  396. */
  397. export type HTMLInputDateLikeElement = HTMLInputDateTimeLocalElement | HTMLInputDateElement
  398. /**
  399. * Options for getting a date-like `<input>` element field value.
  400. */
  401. type GetInputDateFieldValueOptions = {
  402. /**
  403. * Should we force values to be dates?
  404. * @note Form values are retrieved to be strings by default, hence this option.
  405. */
  406. forceDateValues?: true,
  407. };
  408. /**
  409. * Gets the value of an `<input type="date">` element.
  410. * @param inputEl - The element.
  411. * @param options - The options.
  412. * @returns Value of the input element.
  413. */
  414. const getInputDateLikeFieldValue = (
  415. inputEl: HTMLInputDateLikeElement,
  416. options = {} as GetInputDateFieldValueOptions,
  417. ) => {
  418. if (options.forceDateValues) {
  419. return inputEl.valueAsDate;
  420. }
  421. return inputEl.value;
  422. };
  423. /**
  424. * ISO format for dates.
  425. */
  426. const DATE_FORMAT_ISO = 'yyyy-MM-DD' as const;
  427. /**
  428. * Sets the value of an `<input type="date">` element.
  429. * @param inputEl - The element.
  430. * @param value - Value of the input element.
  431. */
  432. const setInputDateLikeFieldValue = (
  433. inputEl: HTMLInputDateLikeElement,
  434. value: unknown,
  435. ) => {
  436. if (inputEl.type.toLowerCase() === INPUT_TYPE_DATE) {
  437. // eslint-disable-next-line no-param-reassign
  438. inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0])
  439. .toISOString()
  440. .slice(0, DATE_FORMAT_ISO.length);
  441. return;
  442. }
  443. if (inputEl.type.toLowerCase() === INPUT_TYPE_DATETIME_LOCAL) {
  444. // eslint-disable-next-line no-param-reassign
  445. inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0])
  446. .toISOString()
  447. .slice(0, -1); // remove extra 'Z' suffix
  448. }
  449. };
  450. /**
  451. * Options for getting an `<input>` element field value.
  452. */
  453. type GetInputFieldValueOptions
  454. = GetInputCheckboxFieldValueOptions
  455. & GetInputFileFieldValueOptions
  456. & GetInputRadioFieldValueOptions
  457. & GetInputNumberFieldValueOptions
  458. & GetInputDateFieldValueOptions
  459. /**
  460. * Gets the value of an `<input>` element.
  461. * @param inputEl - The element.
  462. * @param options - The options.
  463. * @returns Value of the input element.
  464. */
  465. const getInputFieldValue = (
  466. inputEl: HTMLInputElement,
  467. options = {} as GetInputFieldValueOptions,
  468. ) => {
  469. switch (inputEl.type.toLowerCase()) {
  470. case INPUT_TYPE_CHECKBOX:
  471. return getInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, options);
  472. case INPUT_TYPE_RADIO:
  473. return getInputRadioFieldValue(inputEl as HTMLInputRadioElement, options);
  474. case INPUT_TYPE_FILE:
  475. return getInputFileFieldValue(inputEl as HTMLInputFileElement, options);
  476. case INPUT_TYPE_NUMBER:
  477. case INPUT_TYPE_RANGE:
  478. return getInputNumericFieldValue(inputEl as HTMLInputNumericElement, options);
  479. case INPUT_TYPE_DATE:
  480. case INPUT_TYPE_DATETIME_LOCAL:
  481. return getInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, options);
  482. default:
  483. break;
  484. }
  485. return inputEl.value;
  486. };
  487. /**
  488. * Sets the value of an `<input>` element.
  489. * @param inputEl - The element.
  490. * @param value - Value of the input element.
  491. * @note This function is a noop for `<input type="file">` because by design, file inputs are not
  492. * assignable programmatically.
  493. */
  494. const setInputFieldValue = (
  495. inputEl: HTMLInputElement,
  496. value: unknown,
  497. ) => {
  498. switch (inputEl.type.toLowerCase()) {
  499. case INPUT_TYPE_CHECKBOX:
  500. setInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, value);
  501. return;
  502. case INPUT_TYPE_RADIO:
  503. setInputRadioFieldValue(inputEl as HTMLInputRadioElement, value);
  504. return;
  505. case INPUT_TYPE_FILE:
  506. // We shouldn't tamper with file inputs! This will not have any implementation.
  507. return;
  508. case INPUT_TYPE_NUMBER:
  509. case INPUT_TYPE_RANGE:
  510. setInputNumericFieldValue(inputEl as HTMLInputNumericElement, value);
  511. return;
  512. case INPUT_TYPE_DATE:
  513. case INPUT_TYPE_DATETIME_LOCAL:
  514. setInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, value);
  515. return;
  516. default:
  517. break;
  518. }
  519. // eslint-disable-next-line no-param-reassign
  520. inputEl.value = value as string;
  521. };
  522. /**
  523. * Options for getting a field value.
  524. */
  525. type GetFieldValueOptions
  526. = GetTextAreaValueOptions
  527. & GetSelectValueOptions
  528. & GetInputFieldValueOptions
  529. /**
  530. * Types for elements with names (i.e. can be assigned the `name` attribute).
  531. */
  532. type HTMLElementWithName
  533. = (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement);
  534. /**
  535. * Gets the value of a field element.
  536. * @param el - The field element.
  537. * @param options - The options.
  538. * @returns Value of the field element.
  539. */
  540. export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => {
  541. switch (el.tagName) {
  542. case TAG_NAME_TEXTAREA:
  543. return getTextAreaFieldValue(el as HTMLTextAreaElement, options);
  544. case TAG_NAME_SELECT:
  545. return getSelectFieldValue(el as HTMLSelectElement, options);
  546. case TAG_NAME_INPUT:
  547. return getInputFieldValue(el as HTMLInputElement, options);
  548. default:
  549. break;
  550. }
  551. const fieldEl = el as HTMLElement & { value?: unknown };
  552. return fieldEl.value || null;
  553. };
  554. /**
  555. * Sets the value of a field element.
  556. * @param el - The field element.
  557. * @param value - Value of the field element.
  558. */
  559. const setFieldValue = (el: HTMLElement, value: unknown) => {
  560. switch (el.tagName) {
  561. case TAG_NAME_TEXTAREA:
  562. setTextAreaFieldValue(el as HTMLTextAreaElement, value);
  563. return;
  564. case TAG_NAME_SELECT:
  565. setSelectFieldValue(el as HTMLSelectElement, value);
  566. return;
  567. case TAG_NAME_INPUT:
  568. setInputFieldValue(el as HTMLInputElement, value);
  569. return;
  570. default:
  571. break;
  572. }
  573. const fieldEl = el as HTMLElement & { value?: unknown };
  574. fieldEl.value = value;
  575. };
  576. /**
  577. * Attribute name for the element's field name.
  578. */
  579. const ATTRIBUTE_NAME = 'name' as const;
  580. /**
  581. * Attribute name for the element's disabled status.
  582. */
  583. const ATTRIBUTE_DISABLED = 'disabled' as const;
  584. /**
  585. * Determines if an element is a named and enabled form field.
  586. * @param el - The element.
  587. * @returns Value determining if the element is a named and enabled form field.
  588. */
  589. export const isNamedEnabledFormFieldElement = (el: HTMLElement) => {
  590. if (!(ATTRIBUTE_NAME in el)) {
  591. return false;
  592. }
  593. if (typeof el[ATTRIBUTE_NAME] !== 'string') {
  594. return false;
  595. }
  596. const namedEl = el as unknown as HTMLElementWithName;
  597. return (
  598. el[ATTRIBUTE_NAME].length > 0
  599. && !(ATTRIBUTE_DISABLED in namedEl && Boolean(namedEl[ATTRIBUTE_DISABLED]))
  600. && isFormFieldElement(namedEl)
  601. );
  602. };
  603. /**
  604. * Options for getting form values.
  605. */
  606. type GetFormValuesOptions = GetFieldValueOptions & {
  607. /**
  608. * The element that triggered the submission of the form.
  609. */
  610. submitter?: HTMLElement,
  611. }
  612. /**
  613. * Tag name for the `<form>` element.
  614. */
  615. const TAG_NAME_FORM = 'FORM' as const;
  616. /**
  617. * Checks if the provided value is a valid form.
  618. * @param maybeForm - The value to check.
  619. * @param context - Context where this function is run, which are used for error messages.
  620. */
  621. const assertIsFormElement = (maybeForm: unknown, context: string) => {
  622. const formType = typeof maybeForm;
  623. if (formType !== 'object') {
  624. throw new TypeError(
  625. `Invalid form argument provided for ${context}(). The argument value ${String(maybeForm)} is of type "${formType}". Expected an HTML element.`,
  626. );
  627. }
  628. if (!maybeForm) {
  629. // Don't accept `null`.
  630. throw new TypeError(`No <form> element was provided for ${context}().`);
  631. }
  632. const element = maybeForm as HTMLElement;
  633. // We're not so strict when it comes to checking if the passed value for `maybeForm` is a
  634. // legitimate HTML element.
  635. if (element.tagName !== TAG_NAME_FORM) {
  636. throw new TypeError(
  637. `Invalid form argument provided for ${context}(). Expected <form>, got <${element.tagName.toLowerCase()}>.`,
  638. );
  639. }
  640. };
  641. /**
  642. * Filters the form elements that can be processed.
  643. * @param form - The form element.
  644. * @returns Array of key-value pairs for the field names and field elements.
  645. */
  646. const filterFieldElements = (form: HTMLFormElement) => {
  647. const formElements = form.elements as unknown as Record<string | number, HTMLElement>;
  648. const allFormFieldElements = Object.entries<HTMLElement>(formElements);
  649. return allFormFieldElements.filter(([k, el]) => (
  650. // We use the number-indexed elements because they are consistent to enumerate.
  651. !Number.isNaN(Number(k))
  652. // Only the enabled/read-only elements can be enumerated.
  653. && isNamedEnabledFormFieldElement(el)
  654. )) as [string, HTMLElementWithName][];
  655. };
  656. /**
  657. * Gets the values of all the fields within the form through accessing the DOM nodes.
  658. * @param form - The form.
  659. * @param options - The options.
  660. * @returns The form values.
  661. */
  662. export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValuesOptions) => {
  663. assertIsFormElement(form, 'getFormValues');
  664. const fieldElements = filterFieldElements(form);
  665. const fieldValues = fieldElements.reduce(
  666. (theFormValues, [, el]) => {
  667. const fieldValue = getFieldValue(el, options);
  668. if (fieldValue === null) {
  669. return theFormValues;
  670. }
  671. const { name: fieldName } = el;
  672. const { [fieldName]: oldFormValue = null } = theFormValues;
  673. if (oldFormValue === null) {
  674. return {
  675. ...theFormValues,
  676. [fieldName]: fieldValue,
  677. };
  678. }
  679. if (!Array.isArray(oldFormValue)) {
  680. return {
  681. ...theFormValues,
  682. [fieldName]: [oldFormValue, fieldValue],
  683. };
  684. }
  685. return {
  686. ...theFormValues,
  687. [fieldName]: [...oldFormValue, fieldValue],
  688. };
  689. },
  690. {} as Record<string, unknown>,
  691. );
  692. if (options.submitter as unknown as HTMLButtonElement) {
  693. const { submitter } = options as unknown as Pick<HTMLFormElement, 'submitter'>;
  694. if (submitter.name.length > 0) {
  695. return {
  696. ...fieldValues,
  697. [submitter.name]: submitter.value,
  698. };
  699. }
  700. }
  701. return fieldValues;
  702. };
  703. /**
  704. * Sets the values of all the fields within the form through accessing the DOM nodes.
  705. * @param form - The form.
  706. * @param values - The form values.
  707. */
  708. export const setFormValues = (
  709. form: HTMLFormElement,
  710. values: ConstructorParameters<typeof URLSearchParams>[0] | Record<string, unknown>,
  711. ) => {
  712. assertIsFormElement(form, 'getFormValues');
  713. const valuesType = typeof values;
  714. if (!['string', 'object'].includes(valuesType)) {
  715. throw new TypeError(`Invalid values argument provided for setFormValues(). Expected "object" or "string", got ${valuesType}`);
  716. }
  717. if (!values) {
  718. return;
  719. }
  720. const fieldElements = filterFieldElements(form);
  721. const objectValues = new URLSearchParams(values as unknown as string | Record<string, string>);
  722. fieldElements
  723. .filter(([, el]) => objectValues.has(el.name))
  724. .forEach(([, el]) => {
  725. setFieldValue(el, objectValues.get(el.name));
  726. });
  727. };
  728. /**
  729. * Gets the values of all the fields within the form through accessing the DOM nodes.
  730. * @deprecated Default import is deprecated. Use named export `getFormValues()` instead. This
  731. * default export is only for backwards compatibility.
  732. * @param args - The arguments.
  733. * @see getFormValues
  734. */
  735. export default (...args: Parameters<typeof getFormValues>) => {
  736. // eslint-disable-next-line no-console
  737. console.warn('Default import is deprecated. Use named export `getFormValues()` instead. This default export is only for backwards compatibility.');
  738. return getFormValues(...args);
  739. };