Extract and set form values through the DOM—no frameworks required! https://github.com/TheoryOfNekomata/formxtra
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

886 rindas
24 KiB

  1. /**
  2. * Line ending.
  3. */
  4. export enum LineEnding {
  5. /**
  6. * Carriage return. Used for legacy macOS 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 valueWhenChecked = inputEl.getAttribute(ATTRIBUTE_VALUE);
  251. if (valueWhenChecked !== null) {
  252. // eslint-disable-next-line no-param-reassign
  253. inputEl.checked = (
  254. Array.isArray(value)
  255. ? value.includes(valueWhenChecked)
  256. : value === valueWhenChecked
  257. );
  258. return;
  259. }
  260. if (
  261. INPUT_CHECKBOX_FALSY_VALUES.includes(
  262. (value as string).toLowerCase() as typeof INPUT_CHECKBOX_FALSY_VALUES[0],
  263. )
  264. || !value
  265. ) {
  266. // eslint-disable-next-line no-param-reassign
  267. inputEl.checked = false;
  268. return;
  269. }
  270. if (
  271. INPUT_CHECKBOX_TRUTHY_VALUES.includes(
  272. (value as string).toLowerCase() as typeof INPUT_CHECKBOX_TRUTHY_VALUES[0],
  273. )
  274. || value === true
  275. || value === 1
  276. ) {
  277. // eslint-disable-next-line no-param-reassign
  278. inputEl.checked = true;
  279. }
  280. };
  281. /**
  282. * Value of the `type` attribute for `<input>` elements considered as file upload components.
  283. */
  284. const INPUT_TYPE_FILE = 'file' as const;
  285. /**
  286. * Type for an `<input type="file">` element.
  287. */
  288. export type HTMLInputFileElement = HTMLInputElement & { type: typeof INPUT_TYPE_FILE }
  289. /**
  290. * Options for getting an `<input type="file">` element field value.
  291. */
  292. type GetInputFileFieldValueOptions = {
  293. /**
  294. * Should we retrieve the `files` attribute of file inputs instead of the currently selected file
  295. * names?
  296. */
  297. getFileObjects?: true,
  298. }
  299. /**
  300. * Gets the value of an `<input type="file">` element.
  301. * @param inputEl - The element.
  302. * @param options - The options.
  303. * @returns Value of the input element.
  304. */
  305. const getInputFileFieldValue = (
  306. inputEl: HTMLInputFileElement,
  307. options = {} as GetInputFileFieldValueOptions,
  308. ) => {
  309. const { files } = inputEl;
  310. if ((files as unknown) === null) {
  311. return null;
  312. }
  313. if (options.getFileObjects) {
  314. return files;
  315. }
  316. const filesArray = Array.from(files as FileList);
  317. if (filesArray.length > 1) {
  318. return filesArray.map((f) => f.name);
  319. }
  320. return filesArray[0]?.name || '';
  321. };
  322. /**
  323. * Value of the `type` attribute for `<input>` elements considered as discrete number selectors.
  324. */
  325. const INPUT_TYPE_NUMBER = 'number' as const;
  326. /**
  327. * Type for an `<input type="number">` element.
  328. */
  329. export type HTMLInputNumberElement = HTMLInputElement & { type: typeof INPUT_TYPE_NUMBER }
  330. /**
  331. * Value of the `type` attribute for `<input>` elements considered as continuous number selectors.
  332. */
  333. const INPUT_TYPE_RANGE = 'range' as const;
  334. /**
  335. * Type for an `<input type="range">` element.
  336. */
  337. export type HTMLInputRangeElement = HTMLInputElement & { type: typeof INPUT_TYPE_RANGE }
  338. /**
  339. * Type for an `<input>` element that handles numeric values.
  340. */
  341. export type HTMLInputNumericElement = HTMLInputNumberElement | HTMLInputRangeElement;
  342. /**
  343. * Options for getting an `<input type="number">` element field value.
  344. */
  345. type GetInputNumberFieldValueOptions = {
  346. /**
  347. * Should we force values to be numeric?
  348. *
  349. * **Note:** Form values are retrieved to be strings by default, hence this option.
  350. */
  351. forceNumberValues?: true,
  352. }
  353. /**
  354. * Gets the value of an `<input type="number">` element.
  355. * @param inputEl - The element.
  356. * @param options - The options.
  357. * @returns Value of the input element.
  358. */
  359. const getInputNumericFieldValue = (
  360. inputEl: HTMLInputNumericElement,
  361. options = {} as GetInputNumberFieldValueOptions,
  362. ) => {
  363. if (options.forceNumberValues) {
  364. return inputEl.valueAsNumber;
  365. }
  366. return inputEl.value;
  367. };
  368. /**
  369. * Sets the value of an `<input type="number">` element.
  370. * @param inputEl - The element.
  371. * @param value - Value of the input element.
  372. */
  373. const setInputNumericFieldValue = (
  374. inputEl: HTMLInputNumericElement,
  375. value: unknown,
  376. ) => {
  377. // eslint-disable-next-line no-param-reassign
  378. inputEl.valueAsNumber = Number(value);
  379. };
  380. /**
  381. * Value of the `type` attribute for `<input>` elements considered as date pickers.
  382. */
  383. const INPUT_TYPE_DATE = 'date' as const;
  384. /**
  385. * Type for an `<input type="date">` element.
  386. */
  387. export type HTMLInputDateElement = HTMLInputElement & { type: typeof INPUT_TYPE_DATE }
  388. /**
  389. * Value of the `type` attribute for `<input>` elements considered as date and time pickers.
  390. */
  391. const INPUT_TYPE_DATETIME_LOCAL = 'datetime-local' as const;
  392. /**
  393. * Type for an `<input type="datetime-local">` element.
  394. */
  395. export type HTMLInputDateTimeLocalElement = HTMLInputElement & {
  396. type: typeof INPUT_TYPE_DATETIME_LOCAL,
  397. }
  398. /**
  399. * Type for an `<input>` element.that handles date values.
  400. */
  401. export type HTMLInputDateLikeElement = HTMLInputDateTimeLocalElement | HTMLInputDateElement
  402. /**
  403. * Options for getting a date-like `<input>` element field value.
  404. */
  405. type GetInputDateFieldValueOptions = {
  406. /**
  407. * Should we force values to be dates?
  408. * @note Form values are retrieved to be strings by default, hence this option.
  409. */
  410. forceDateValues?: true,
  411. };
  412. /**
  413. * Gets the value of an `<input type="date">` element.
  414. * @param inputEl - The element.
  415. * @param options - The options.
  416. * @returns Value of the input element.
  417. */
  418. const getInputDateLikeFieldValue = (
  419. inputEl: HTMLInputDateLikeElement,
  420. options = {} as GetInputDateFieldValueOptions,
  421. ) => {
  422. if (options.forceDateValues) {
  423. return inputEl.valueAsDate;
  424. }
  425. return inputEl.value;
  426. };
  427. /**
  428. * ISO format for dates.
  429. */
  430. const DATE_FORMAT_ISO = 'yyyy-MM-DD' as const;
  431. /**
  432. * Sets the value of an `<input type="date">` element.
  433. * @param inputEl - The element.
  434. * @param value - Value of the input element.
  435. */
  436. const setInputDateLikeFieldValue = (
  437. inputEl: HTMLInputDateLikeElement,
  438. value: unknown,
  439. ) => {
  440. if (inputEl.type.toLowerCase() === INPUT_TYPE_DATE) {
  441. // eslint-disable-next-line no-param-reassign
  442. inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0])
  443. .toISOString()
  444. .slice(0, DATE_FORMAT_ISO.length);
  445. return;
  446. }
  447. if (inputEl.type.toLowerCase() === INPUT_TYPE_DATETIME_LOCAL) {
  448. // eslint-disable-next-line no-param-reassign
  449. inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0])
  450. .toISOString()
  451. .slice(0, -1); // remove extra 'Z' suffix
  452. }
  453. };
  454. /**
  455. * Options for getting an `<input>` element field value.
  456. */
  457. type GetInputFieldValueOptions
  458. = GetInputCheckboxFieldValueOptions
  459. & GetInputFileFieldValueOptions
  460. & GetInputRadioFieldValueOptions
  461. & GetInputNumberFieldValueOptions
  462. & GetInputDateFieldValueOptions
  463. /**
  464. * Gets the value of an `<input>` element.
  465. * @param inputEl - The element.
  466. * @param options - The options.
  467. * @returns Value of the input element.
  468. */
  469. const getInputFieldValue = (
  470. inputEl: HTMLInputElement,
  471. options = {} as GetInputFieldValueOptions,
  472. ) => {
  473. switch (inputEl.type.toLowerCase()) {
  474. case INPUT_TYPE_CHECKBOX:
  475. return getInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, options);
  476. case INPUT_TYPE_RADIO:
  477. return getInputRadioFieldValue(inputEl as HTMLInputRadioElement, options);
  478. case INPUT_TYPE_FILE:
  479. return getInputFileFieldValue(inputEl as HTMLInputFileElement, options);
  480. case INPUT_TYPE_NUMBER:
  481. case INPUT_TYPE_RANGE:
  482. return getInputNumericFieldValue(inputEl as HTMLInputNumericElement, options);
  483. case INPUT_TYPE_DATE:
  484. case INPUT_TYPE_DATETIME_LOCAL:
  485. return getInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, options);
  486. default:
  487. break;
  488. }
  489. return inputEl.value;
  490. };
  491. /**
  492. * Sets the value of an `<input>` element.
  493. * @param inputEl - The element.
  494. * @param value - Value of the input element.
  495. * @param nthOfName - What order is this field in with respect to fields of the same name?
  496. * @param totalOfName - How many fields with the same name are in the form?
  497. * @note This function is a noop for `<input type="file">` because by design, file inputs are not
  498. * assignable programmatically.
  499. */
  500. const setInputFieldValue = (
  501. inputEl: HTMLInputElement,
  502. value: unknown,
  503. nthOfName: number,
  504. totalOfName: number,
  505. ) => {
  506. switch (inputEl.type.toLowerCase()) {
  507. case INPUT_TYPE_CHECKBOX:
  508. setInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, value);
  509. return;
  510. case INPUT_TYPE_RADIO:
  511. setInputRadioFieldValue(inputEl as HTMLInputRadioElement, value);
  512. return;
  513. case INPUT_TYPE_FILE:
  514. // We shouldn't tamper with file inputs! This will not have any implementation.
  515. return;
  516. case INPUT_TYPE_NUMBER:
  517. case INPUT_TYPE_RANGE:
  518. setInputNumericFieldValue(inputEl as HTMLInputNumericElement, value);
  519. return;
  520. case INPUT_TYPE_DATE:
  521. case INPUT_TYPE_DATETIME_LOCAL:
  522. setInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, value);
  523. return;
  524. default:
  525. break;
  526. }
  527. if (Array.isArray(value) && totalOfName > 1) {
  528. // eslint-disable-next-line no-param-reassign
  529. inputEl.value = value[nthOfName];
  530. return;
  531. }
  532. // eslint-disable-next-line no-param-reassign
  533. inputEl.value = value as string;
  534. };
  535. /**
  536. * Options for getting a field value.
  537. */
  538. type GetFieldValueOptions
  539. = GetTextAreaValueOptions
  540. & GetSelectValueOptions
  541. & GetInputFieldValueOptions
  542. /**
  543. * Types for elements with names (i.e. can be assigned the `name` attribute).
  544. */
  545. type HTMLElementWithName
  546. = (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement);
  547. /**
  548. * Gets the value of a field element.
  549. * @param el - The field element.
  550. * @param options - The options.
  551. * @returns Value of the field element.
  552. */
  553. export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => {
  554. switch (el.tagName) {
  555. case TAG_NAME_TEXTAREA:
  556. return getTextAreaFieldValue(el as HTMLTextAreaElement, options);
  557. case TAG_NAME_SELECT:
  558. return getSelectFieldValue(el as HTMLSelectElement, options);
  559. case TAG_NAME_INPUT:
  560. return getInputFieldValue(el as HTMLInputElement, options);
  561. default:
  562. break;
  563. }
  564. const fieldEl = el as HTMLElement & { value?: unknown };
  565. return fieldEl.value || null;
  566. };
  567. /**
  568. * Sets the value of a field element.
  569. * @param el - The field element.
  570. * @param value - Value of the field element.
  571. * @param nthOfName - What order is this field in with respect to fields of the same name?
  572. * @param totalOfName - How many fields with the same name are in the form?
  573. */
  574. const setFieldValue = (
  575. el: HTMLElement,
  576. value: unknown,
  577. nthOfName: number,
  578. totalOfName: number,
  579. ) => {
  580. switch (el.tagName) {
  581. case TAG_NAME_TEXTAREA:
  582. setTextAreaFieldValue(el as HTMLTextAreaElement, value);
  583. return;
  584. case TAG_NAME_SELECT:
  585. setSelectFieldValue(el as HTMLSelectElement, value);
  586. return;
  587. case TAG_NAME_INPUT:
  588. setInputFieldValue(el as HTMLInputElement, value, nthOfName, totalOfName);
  589. return;
  590. default:
  591. break;
  592. }
  593. const fieldEl = el as HTMLElement & { value?: unknown };
  594. fieldEl.value = value;
  595. };
  596. /**
  597. * Attribute name for the element's field name.
  598. */
  599. const ATTRIBUTE_NAME = 'name' as const;
  600. /**
  601. * Attribute name for the element's disabled status.
  602. */
  603. const ATTRIBUTE_DISABLED = 'disabled' as const;
  604. /**
  605. * Determines if an element is a named and enabled form field.
  606. * @param el - The element.
  607. * @returns Value determining if the element is a named and enabled form field.
  608. */
  609. export const isNamedEnabledFormFieldElement = (el: HTMLElement) => {
  610. if (!(ATTRIBUTE_NAME in el)) {
  611. return false;
  612. }
  613. if (typeof el[ATTRIBUTE_NAME] !== 'string') {
  614. return false;
  615. }
  616. const namedEl = el as unknown as HTMLElementWithName;
  617. return (
  618. el[ATTRIBUTE_NAME].length > 0
  619. && !(ATTRIBUTE_DISABLED in namedEl && Boolean(namedEl[ATTRIBUTE_DISABLED]))
  620. && isFormFieldElement(namedEl)
  621. );
  622. };
  623. /**
  624. * Options for getting form values.
  625. */
  626. type GetFormValuesOptions = GetFieldValueOptions & {
  627. /**
  628. * The element that triggered the submission of the form.
  629. */
  630. submitter?: HTMLElement,
  631. }
  632. /**
  633. * Tag name for the `<form>` element.
  634. */
  635. const TAG_NAME_FORM = 'FORM' as const;
  636. /**
  637. * Checks if the provided value is a valid form.
  638. * @param maybeForm - The value to check.
  639. * @param context - Context where this function is run, which are used for error messages.
  640. */
  641. const assertIsFormElement = (maybeForm: unknown, context: string) => {
  642. const formType = typeof maybeForm;
  643. if (formType !== 'object') {
  644. throw new TypeError(
  645. `Invalid form argument provided for ${context}(). The argument value ${String(maybeForm)} is of type "${formType}". Expected an HTML element.`,
  646. );
  647. }
  648. if (!maybeForm) {
  649. // Don't accept `null`.
  650. throw new TypeError(`No <form> element was provided for ${context}().`);
  651. }
  652. const element = maybeForm as HTMLElement;
  653. // We're not so strict when it comes to checking if the passed value for `maybeForm` is a
  654. // legitimate HTML element.
  655. if (element.tagName !== TAG_NAME_FORM) {
  656. throw new TypeError(
  657. `Invalid form argument provided for ${context}(). Expected <form>, got <${element.tagName.toLowerCase()}>.`,
  658. );
  659. }
  660. };
  661. /**
  662. * Filters the form elements that can be processed.
  663. * @param form - The form element.
  664. * @returns Array of key-value pairs for the field names and field elements.
  665. */
  666. const filterFieldElements = (form: HTMLFormElement) => {
  667. const formElements = form.elements as unknown as Record<string | number, HTMLElement>;
  668. const allFormFieldElements = Object.entries<HTMLElement>(formElements);
  669. return allFormFieldElements.filter(([k, el]) => (
  670. // We use the number-indexed elements because they are consistent to enumerate.
  671. !Number.isNaN(Number(k))
  672. // Only the enabled/read-only elements can be enumerated.
  673. && isNamedEnabledFormFieldElement(el)
  674. )) as [string, HTMLElementWithName][];
  675. };
  676. /**
  677. * Gets the values of all the fields within the form through accessing the DOM nodes.
  678. * @param form - The form.
  679. * @param options - The options.
  680. * @returns The form values.
  681. */
  682. export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValuesOptions) => {
  683. assertIsFormElement(form, 'getFormValues');
  684. const fieldElements = filterFieldElements(form);
  685. const fieldValues = fieldElements.reduce(
  686. (theFormValues, [, el]) => {
  687. const fieldValue = getFieldValue(el, options);
  688. if (fieldValue === null) {
  689. return theFormValues;
  690. }
  691. const { name: fieldName } = el;
  692. const { [fieldName]: oldFormValue = null } = theFormValues;
  693. if (oldFormValue === null) {
  694. return {
  695. ...theFormValues,
  696. [fieldName]: fieldValue,
  697. };
  698. }
  699. if (!Array.isArray(oldFormValue)) {
  700. return {
  701. ...theFormValues,
  702. [fieldName]: [oldFormValue, fieldValue],
  703. };
  704. }
  705. return {
  706. ...theFormValues,
  707. [fieldName]: [...oldFormValue, fieldValue],
  708. };
  709. },
  710. {} as Record<string, unknown>,
  711. );
  712. if (options.submitter as unknown as HTMLButtonElement) {
  713. const { submitter } = options as unknown as Pick<HTMLFormElement, 'submitter'>;
  714. if (submitter.name.length > 0) {
  715. return {
  716. ...fieldValues,
  717. [submitter.name]: submitter.value,
  718. };
  719. }
  720. }
  721. return fieldValues;
  722. };
  723. const normalizeValues = (values: unknown): Record<string, unknown | unknown[]> => {
  724. if (typeof values === 'string') {
  725. return Object.fromEntries(new URLSearchParams(values).entries());
  726. }
  727. if (values instanceof URLSearchParams) {
  728. return Object.fromEntries(values.entries());
  729. }
  730. if (Array.isArray(values)) {
  731. return Object.fromEntries(values);
  732. }
  733. return values as Record<string, unknown | unknown[]>;
  734. };
  735. /**
  736. * Sets the values of all the fields within the form through accessing the DOM nodes. Partial values
  737. * may be passed to set values only to certain form fields.
  738. * @param form - The form.
  739. * @param values - The form values.
  740. */
  741. export const setFormValues = (
  742. form: HTMLFormElement,
  743. values: ConstructorParameters<typeof URLSearchParams>[0] | Record<string, unknown>,
  744. ) => {
  745. assertIsFormElement(form, 'getFormValues');
  746. const valuesType = typeof values;
  747. if (!['string', 'object'].includes(valuesType)) {
  748. throw new TypeError(`Invalid values argument provided for setFormValues(). Expected "object" or "string", got ${valuesType}`);
  749. }
  750. if (!values) {
  751. return;
  752. }
  753. const fieldElements = filterFieldElements(form);
  754. const objectValues = normalizeValues(values);
  755. const count = fieldElements
  756. .filter(([, el]) => el.name in objectValues)
  757. .reduce(
  758. (currentCount, [, el]) => ({
  759. ...currentCount,
  760. [el.name]: (
  761. el.tagName === TAG_NAME_INPUT && el.type === INPUT_TYPE_RADIO
  762. ? 1
  763. : (
  764. typeof currentCount[el.name] === 'number'
  765. ? currentCount[el.name] + 1
  766. : 1
  767. )
  768. ),
  769. }),
  770. {} as Record<string, number>,
  771. );
  772. const counter = {} as Record<string, number>;
  773. fieldElements
  774. .filter(([, el]) => el.name in objectValues)
  775. .forEach(([, el]) => {
  776. counter[el.name] = typeof counter[el.name] === 'number' ? counter[el.name] + 1 : 0;
  777. setFieldValue(el, objectValues[el.name], counter[el.name], count[el.name]);
  778. });
  779. };
  780. /**
  781. * Gets the values of all the fields within the form through accessing the DOM nodes.
  782. * @deprecated Default import is deprecated. Use named export `getFormValues()` instead. This
  783. * default export is only for backwards compatibility.
  784. * @param args - The arguments.
  785. * @see getFormValues
  786. */
  787. export default (...args: Parameters<typeof getFormValues>) => {
  788. // eslint-disable-next-line no-console
  789. console.warn('Default import is deprecated. Use named export `getFormValues()` instead. This default export is only for backwards compatibility.');
  790. return getFormValues(...args);
  791. };