Browse Source

Refactor code

Make error messages more user-friendly. Also remove magic constants
within functions.
master
TheoryOfNekomata 1 year ago
parent
commit
fdcdf1ddf3
5 changed files with 249 additions and 90 deletions
  1. +1
    -1
      README.md
  2. +5
    -2
      cypress/integration/misc.test.ts
  3. +2
    -0
      cypress/integration/text.test.ts
  4. +1
    -1
      package.json
  5. +240
    -86
      src/index.ts

+ 1
- 1
README.md View File

@@ -121,4 +121,4 @@ form.addEventListener('submit', async e => {


## Tests ## Tests


The library has been tested on the static DOM using JSDOM and Jest, and the real dynamic DOM using Cypress.
The library has been tested on the static DOM using JSDOM, and the real dynamic DOM using Cypress.

+ 5
- 2
cypress/integration/misc.test.ts View File

@@ -251,6 +251,7 @@ describe('misc', () => {
.toString(); .toString();
const after = utils.makeSearchParams(search) const after = utils.makeSearchParams(search)
.toString(); .toString();

expect(before) expect(before)
.toEqual(after); .toEqual(after);
}, },
@@ -280,5 +281,7 @@ Another line`,
expectedStaticValue: 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&birthday=1989-06-04&civil_status=married&new_registration=on&last_appointment_datetime=2001-09-11T06%3A09&new_appointment_week=2001-W51&start_month=2002-03&nationality=filipino&gross=131072&dependent=Jun&notes=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=9.5&submit=Hi', expectedStaticValue: 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&birthday=1989-06-04&civil_status=married&new_registration=on&last_appointment_datetime=2001-09-11T06%3A09&new_appointment_week=2001-W51&start_month=2002-03&nationality=filipino&gross=131072&dependent=Jun&notes=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=9.5&submit=Hi',
}); });
}); });
})
})
});

// TODO implement tests for multiple values
});

+ 2
- 0
cypress/integration/text.test.ts View File

@@ -198,4 +198,6 @@ describe('text', () => {
}); });
}); });
}); });

// TODO implement textarea tests
}) })

+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
{ {
"name": "@theoryofnekomata/formxtra", "name": "@theoryofnekomata/formxtra",
"version": "1.0.0",
"version": "1.0.1",
"files": [ "files": [
"dist", "dist",
"src" "src"


+ 240
- 86
src/index.ts View File

@@ -16,23 +16,53 @@ export enum LineEnding {
CRLF = '\r\n', CRLF = '\r\n',
} }


/**
* Type for a placeholder object value.
*/
type PlaceholderObject = Record<string, unknown> type PlaceholderObject = Record<string, unknown>


/** /**
* Checks if an element can hold a field value.
* Tag name for the `<input>` element.
*/
const TAG_NAME_INPUT = 'INPUT' as const;

/**
* Tag name for the `<textarea>` element.
*/
const TAG_NAME_TEXTAREA = 'TEXTAREA' as const;

/**
* Tag name for the `<select>` element.
*/
const TAG_NAME_SELECT = 'SELECT' as const;

/**
* Tag names for valid form field elements of any configuration.
*/
const FORM_FIELD_ELEMENT_TAG_NAMES = [TAG_NAME_SELECT, TAG_NAME_TEXTAREA] as const;

/**
* Types for button-like `<input>` elements that are not considered as a form field.
*/
const FORM_FIELD_INPUT_EXCLUDED_TYPES = ['submit', 'reset'] as const;

/**
* Checks if an element can hold a custom (user-inputted) field value.
* @param el - The element. * @param el - The element.
*/ */
export const isFormFieldElement = (el: HTMLElement) => { export const isFormFieldElement = (el: HTMLElement) => {
const { tagName } = el; const { tagName } = el;
if (['SELECT', 'TEXTAREA'].includes(tagName)) {
if (FORM_FIELD_ELEMENT_TAG_NAMES.includes(tagName as typeof FORM_FIELD_ELEMENT_TAG_NAMES[0])) {
return true; return true;
} }
if (tagName !== 'INPUT') {
if (tagName !== TAG_NAME_INPUT) {
return false; return false;
} }
const inputEl = el as HTMLInputElement; const inputEl = el as HTMLInputElement;
const { type } = inputEl; const { type } = inputEl;
if (type === 'submit' || type === 'reset') {
if (FORM_FIELD_INPUT_EXCLUDED_TYPES.includes(
type.toLowerCase() as typeof FORM_FIELD_INPUT_EXCLUDED_TYPES[0],
)) {
return false; return false;
} }
return Boolean(inputEl.name); return Boolean(inputEl.name);
@@ -94,7 +124,7 @@ const getSelectFieldValue = (
return Array.from(selectEl.options).filter((o) => o.selected).map((o) => o.value); return Array.from(selectEl.options).filter((o) => o.selected).map((o) => o.value);
} }
if (typeof options !== 'object' || options === null) { if (typeof options !== 'object' || options === null) {
throw new Error('Invalid options.');
throw new TypeError('Invalid options for getSelectFieldValue().');
} }
return selectEl.value; return selectEl.value;
}; };
@@ -118,10 +148,20 @@ const setSelectFieldValue = (selectEl: HTMLSelectElement, value: unknown) => {
}); });
}; };


/**
* Attribute name for the element's value.
*/
const ATTRIBUTE_VALUE = 'value' as const;

/**
* Value of the `type` attribute for `<input>` elements considered as radio buttons.
*/
const INPUT_TYPE_RADIO = 'radio' as const;

/** /**
* Type for an `<input type="radio">` element. * Type for an `<input type="radio">` element.
*/ */
export type HTMLInputRadioElement = HTMLInputElement & { type: 'radio' }
export type HTMLInputRadioElement = HTMLInputElement & { type: typeof INPUT_TYPE_RADIO }


/** /**
* Options for getting an `<input type="radio">` element field value. * Options for getting an `<input type="radio">` element field value.
@@ -142,7 +182,7 @@ const getInputRadioFieldValue = (
return inputEl.value; return inputEl.value;
} }
if (typeof options !== 'object' || options === null) { if (typeof options !== 'object' || options === null) {
throw new Error('Invalid options.');
throw new TypeError('Invalid options for getInputRadioFieldValue().');
} }
return null; return null;
}; };
@@ -156,15 +196,20 @@ const setInputRadioFieldValue = (
inputEl: HTMLInputRadioElement, inputEl: HTMLInputRadioElement,
value: unknown, value: unknown,
) => { ) => {
const checkedValue = inputEl.getAttribute('value');
const valueWhenChecked = inputEl.getAttribute(ATTRIBUTE_VALUE);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
inputEl.checked = checkedValue === value;
inputEl.checked = valueWhenChecked === value;
}; };


/**
* Value of the `type` attribute for `<input>` elements considered as checkboxes.
*/
const INPUT_TYPE_CHECKBOX = 'checkbox' as const;

/** /**
* Type for an `<input type="checkbox">` element. * Type for an `<input type="checkbox">` element.
*/ */
export type HTMLInputCheckboxElement = HTMLInputElement & { type: 'checkbox' }
export type HTMLInputCheckboxElement = HTMLInputElement & { type: typeof INPUT_TYPE_CHECKBOX }


/** /**
* Options for getting an `<input type="checkbox">` element field value. * Options for getting an `<input type="checkbox">` element field value.
@@ -179,6 +224,21 @@ type GetInputCheckboxFieldValueOptions = {
booleanValuelessCheckbox?: true, booleanValuelessCheckbox?: true,
} }


/**
* String values resolvable to an unchecked checkbox state.
*/
const INPUT_CHECKBOX_FALSY_VALUES = ['false', 'off', 'no', '0', ''] as const;

/**
* Default value of the `<input type="checkbox">` when it is checked.
*/
const INPUT_CHECKBOX_DEFAULT_CHECKED_VALUE = 'on' as const;

/**
* String values resolvable to a checked checkbox state.
*/
const INPUT_CHECKBOX_TRUTHY_VALUES = ['true', INPUT_CHECKBOX_DEFAULT_CHECKED_VALUE, 'yes', '1'] as const;

/** /**
* Gets the value of an `<input type="checkbox">` element. * Gets the value of an `<input type="checkbox">` element.
* @param inputEl - The element. * @param inputEl - The element.
@@ -189,7 +249,7 @@ const getInputCheckboxFieldValue = (
inputEl: HTMLInputCheckboxElement, inputEl: HTMLInputCheckboxElement,
options = {} as GetInputCheckboxFieldValueOptions, options = {} as GetInputCheckboxFieldValueOptions,
) => { ) => {
const checkedValue = inputEl.getAttribute('value');
const checkedValue = inputEl.getAttribute(ATTRIBUTE_VALUE);
if (checkedValue !== null) { if (checkedValue !== null) {
if (inputEl.checked) { if (inputEl.checked) {
return inputEl.value; return inputEl.value;
@@ -200,21 +260,11 @@ const getInputCheckboxFieldValue = (
return inputEl.checked; return inputEl.checked;
} }
if (inputEl.checked) { if (inputEl.checked) {
return 'on';
return INPUT_CHECKBOX_DEFAULT_CHECKED_VALUE;
} }
return null; return null;
}; };


/**
* String values resolvable to an unchecked checkbox state.
*/
const INPUT_CHECKBOX_FALSY_VALUES = ['false', 'off', 'no', '0', ''];

/**
* String values resolvable to a checked checkbox state.
*/
const INPUT_CHECKBOX_TRUTHY_VALUES = ['true', 'on', 'yes', '1'];

/** /**
* Sets the value of an `<input type="checkbox">` element. * Sets the value of an `<input type="checkbox">` element.
* @param inputEl - The element. * @param inputEl - The element.
@@ -224,20 +274,28 @@ const setInputCheckboxFieldValue = (
inputEl: HTMLInputCheckboxElement, inputEl: HTMLInputCheckboxElement,
value: unknown, value: unknown,
) => { ) => {
const checkedValue = inputEl.getAttribute('value');
const checkedValue = inputEl.getAttribute(ATTRIBUTE_VALUE);
if (checkedValue !== null) { if (checkedValue !== null) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
inputEl.checked = value === checkedValue; inputEl.checked = value === checkedValue;
return; return;
} }


if (INPUT_CHECKBOX_FALSY_VALUES.includes((value as string).toLowerCase()) || !value) {
if (
INPUT_CHECKBOX_FALSY_VALUES.includes(
(value as string).toLowerCase() as typeof INPUT_CHECKBOX_FALSY_VALUES[0],
)
|| !value
) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
inputEl.checked = false; inputEl.checked = false;
return; return;
} }


if (INPUT_CHECKBOX_TRUTHY_VALUES.includes((value as string).toLowerCase())
if (
INPUT_CHECKBOX_TRUTHY_VALUES.includes(
(value as string).toLowerCase() as typeof INPUT_CHECKBOX_TRUTHY_VALUES[0],
)
|| value === true || value === true
|| value === 1 || value === 1
) { ) {
@@ -246,10 +304,15 @@ const setInputCheckboxFieldValue = (
} }
}; };


/**
* Value of the `type` attribute for `<input>` elements considered as file upload components.
*/
const INPUT_TYPE_FILE = 'file' as const;

/** /**
* Type for an `<input type="file">` element. * Type for an `<input type="file">` element.
*/ */
export type HTMLInputFileElement = HTMLInputElement & { type: 'file' }
export type HTMLInputFileElement = HTMLInputElement & { type: typeof INPUT_TYPE_FILE }


/** /**
* Options for getting an `<input type="file">` element field value. * Options for getting an `<input type="file">` element field value.
@@ -286,18 +349,28 @@ const getInputFileFieldValue = (
return filesArray[0]?.name || ''; return filesArray[0]?.name || '';
}; };


/**
* Value of the `type` attribute for `<input>` elements considered as discrete number selectors.
*/
const INPUT_TYPE_NUMBER = 'number' as const;

/** /**
* Type for an `<input type="number">` element. * Type for an `<input type="number">` element.
*/ */
export type HTMLInputNumberElement = HTMLInputElement & { type: 'number' }
export type HTMLInputNumberElement = HTMLInputElement & { type: typeof INPUT_TYPE_NUMBER }

/**
* Value of the `type` attribute for `<input>` elements considered as continuous number selectors.
*/
const INPUT_TYPE_RANGE = 'range' as const;


/** /**
* Type for an `<input type="range">` element. * Type for an `<input type="range">` element.
*/ */
export type HTMLInputRangeElement = HTMLInputElement & { type: 'range' }
export type HTMLInputRangeElement = HTMLInputElement & { type: typeof INPUT_TYPE_RANGE }


/** /**
* Type for an `<input` element that handles numeric values.
* Type for an `<input>` element that handles numeric values.
*/ */
export type HTMLInputNumericElement = HTMLInputNumberElement | HTMLInputRangeElement; export type HTMLInputNumericElement = HTMLInputNumberElement | HTMLInputRangeElement;


@@ -307,7 +380,8 @@ export type HTMLInputNumericElement = HTMLInputNumberElement | HTMLInputRangeEle
type GetInputNumberFieldValueOptions = { type GetInputNumberFieldValueOptions = {
/** /**
* Should we force values to be numeric? * Should we force values to be numeric?
* @note Form values are retrieved to be strings by default, hence this option.
*
* **Note:** Form values are retrieved to be strings by default, hence this option.
*/ */
forceNumberValues?: true, forceNumberValues?: true,
} }
@@ -341,15 +415,27 @@ const setInputNumericFieldValue = (
inputEl.valueAsNumber = Number(value); inputEl.valueAsNumber = Number(value);
}; };


/**
* Value of the `type` attribute for `<input>` elements considered as date pickers.
*/
const INPUT_TYPE_DATE = 'date' as const;

/** /**
* Type for an `<input type="date">` element. * Type for an `<input type="date">` element.
*/ */
export type HTMLInputDateElement = HTMLInputElement & { type: 'date' }
export type HTMLInputDateElement = HTMLInputElement & { type: typeof INPUT_TYPE_DATE }

/**
* Value of the `type` attribute for `<input>` elements considered as date and time pickers.
*/
const INPUT_TYPE_DATETIME_LOCAL = 'datetime-local' as const;


/** /**
* Type for an `<input type="datetime-local">` element. * Type for an `<input type="datetime-local">` element.
*/ */
export type HTMLInputDateTimeLocalElement = HTMLInputElement & { type: 'datetime-local' }
export type HTMLInputDateTimeLocalElement = HTMLInputElement & {
type: typeof INPUT_TYPE_DATETIME_LOCAL,
}


/** /**
* Type for an `<input>` element.that handles date values. * Type for an `<input>` element.that handles date values.
@@ -383,6 +469,11 @@ const getInputDateLikeFieldValue = (
return inputEl.value; return inputEl.value;
}; };


/**
* ISO format for dates.
*/
const DATE_FORMAT_ISO = 'yyyy-MM-DD' as const;

/** /**
* Sets the value of an `<input type="date">` element. * Sets the value of an `<input type="date">` element.
* @param inputEl - The element. * @param inputEl - The element.
@@ -392,21 +483,20 @@ const setInputDateLikeFieldValue = (
inputEl: HTMLInputDateLikeElement, inputEl: HTMLInputDateLikeElement,
value: unknown, value: unknown,
) => { ) => {
if (inputEl.type.toLowerCase() === 'date') {
if (inputEl.type.toLowerCase() === INPUT_TYPE_DATE) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0]) inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0])
.toISOString() .toISOString()
.slice(0, 'yyyy-MM-DD'.length);
.slice(0, DATE_FORMAT_ISO.length);
return; return;
} }


if (inputEl.type.toLowerCase() === 'datetime-local') {
if (inputEl.type.toLowerCase() === INPUT_TYPE_DATETIME_LOCAL) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0]) inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0])
.toISOString() .toISOString()
.slice(0, -1); // remove extra 'Z' suffix .slice(0, -1); // remove extra 'Z' suffix
} }
// inputEl.valueAsDate = new Date(value as ConstructorParameters<typeof Date>[0]);
}; };


/** /**
@@ -430,19 +520,18 @@ const getInputFieldValue = (
options = {} as GetInputFieldValueOptions, options = {} as GetInputFieldValueOptions,
) => { ) => {
switch (inputEl.type.toLowerCase()) { switch (inputEl.type.toLowerCase()) {
case 'checkbox':
case INPUT_TYPE_CHECKBOX:
return getInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, options); return getInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, options);
case 'radio':
case INPUT_TYPE_RADIO:
return getInputRadioFieldValue(inputEl as HTMLInputRadioElement, options); return getInputRadioFieldValue(inputEl as HTMLInputRadioElement, options);
case 'file':
case INPUT_TYPE_FILE:
return getInputFileFieldValue(inputEl as HTMLInputFileElement, options); return getInputFileFieldValue(inputEl as HTMLInputFileElement, options);
case 'number':
case 'range':
case INPUT_TYPE_NUMBER:
case INPUT_TYPE_RANGE:
return getInputNumericFieldValue(inputEl as HTMLInputNumericElement, options); return getInputNumericFieldValue(inputEl as HTMLInputNumericElement, options);
case 'date':
case 'datetime-local':
case INPUT_TYPE_DATE:
case INPUT_TYPE_DATETIME_LOCAL:
return getInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, options); return getInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, options);
// TODO week and month
default: default:
break; break;
} }
@@ -461,22 +550,21 @@ const setInputFieldValue = (
value: unknown, value: unknown,
) => { ) => {
switch (inputEl.type.toLowerCase()) { switch (inputEl.type.toLowerCase()) {
case 'checkbox':
case INPUT_TYPE_CHECKBOX:
setInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, value); setInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, value);
return; return;
case 'radio':
case INPUT_TYPE_RADIO:
setInputRadioFieldValue(inputEl as HTMLInputRadioElement, value); setInputRadioFieldValue(inputEl as HTMLInputRadioElement, value);
return; return;
case 'file':
case INPUT_TYPE_FILE:
// We shouldn't tamper with file inputs! This will not have any implementation. // We shouldn't tamper with file inputs! This will not have any implementation.
return; return;
case 'number':
case 'range':
// eslint-disable-next-line no-param-reassign
case INPUT_TYPE_NUMBER:
case INPUT_TYPE_RANGE:
setInputNumericFieldValue(inputEl as HTMLInputNumericElement, value); setInputNumericFieldValue(inputEl as HTMLInputNumericElement, value);
return; return;
case 'date':
case 'datetime-local':
case INPUT_TYPE_DATE:
case INPUT_TYPE_DATETIME_LOCAL:
setInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, value); setInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, value);
return; return;
default: default:
@@ -494,6 +582,9 @@ type GetFieldValueOptions
& GetSelectValueOptions & GetSelectValueOptions
& GetInputFieldValueOptions & GetInputFieldValueOptions


/**
* Types for elements with names (i.e. can be assigned the `name` attribute).
*/
type HTMLElementWithName type HTMLElementWithName
= (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement); = (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement);


@@ -504,12 +595,12 @@ type HTMLElementWithName
* @returns Value of the field element. * @returns Value of the field element.
*/ */
export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => { export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => {
switch (el.tagName.toLowerCase()) {
case 'textarea':
switch (el.tagName) {
case TAG_NAME_TEXTAREA:
return getTextAreaFieldValue(el as HTMLTextAreaElement, options); return getTextAreaFieldValue(el as HTMLTextAreaElement, options);
case 'select':
case TAG_NAME_SELECT:
return getSelectFieldValue(el as HTMLSelectElement, options); return getSelectFieldValue(el as HTMLSelectElement, options);
case 'input':
case TAG_NAME_INPUT:
return getInputFieldValue(el as HTMLInputElement, options); return getInputFieldValue(el as HTMLInputElement, options);
default: default:
break; break;
@@ -525,14 +616,14 @@ export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOpti
* @param value - Value of the field element. * @param value - Value of the field element.
*/ */
const setFieldValue = (el: HTMLElement, value: unknown) => { const setFieldValue = (el: HTMLElement, value: unknown) => {
switch (el.tagName.toLowerCase()) {
case 'textarea':
switch (el.tagName) {
case TAG_NAME_TEXTAREA:
setTextAreaFieldValue(el as HTMLTextAreaElement, value); setTextAreaFieldValue(el as HTMLTextAreaElement, value);
return; return;
case 'select':
case TAG_NAME_SELECT:
setSelectFieldValue(el as HTMLSelectElement, value); setSelectFieldValue(el as HTMLSelectElement, value);
return; return;
case 'input':
case TAG_NAME_INPUT:
setInputFieldValue(el as HTMLInputElement, value); setInputFieldValue(el as HTMLInputElement, value);
return; return;
default: default:
@@ -543,22 +634,32 @@ const setFieldValue = (el: HTMLElement, value: unknown) => {
fieldEl.value = value; fieldEl.value = value;
}; };


/**
* Attribute name for the element's field name.
*/
const ATTRIBUTE_NAME = 'name' as const;

/**
* Attribute name for the element's disabled status.
*/
const ATTRIBUTE_DISABLED = 'disabled' as const;

/** /**
* Determines if an element is a named and enabled form field. * Determines if an element is a named and enabled form field.
* @param el - The element. * @param el - The element.
* @returns Value determining if the element is a named and enabled form field. * @returns Value determining if the element is a named and enabled form field.
*/ */
export const isNamedEnabledFormFieldElement = (el: HTMLElement) => { export const isNamedEnabledFormFieldElement = (el: HTMLElement) => {
if (!('name' in el)) {
if (!(ATTRIBUTE_NAME in el)) {
return false; return false;
} }
if (typeof el.name !== 'string') {
if (typeof el[ATTRIBUTE_NAME] !== 'string') {
return false; return false;
} }
const namedEl = el as unknown as HTMLElementWithName; const namedEl = el as unknown as HTMLElementWithName;
return ( return (
el.name.length > 0
&& !('disabled' in namedEl && Boolean(namedEl.disabled))
el[ATTRIBUTE_NAME].length > 0
&& !(ATTRIBUTE_DISABLED in namedEl && Boolean(namedEl[ATTRIBUTE_DISABLED]))
&& isFormFieldElement(namedEl) && isFormFieldElement(namedEl)
); );
}; };
@@ -574,22 +675,67 @@ type GetFormValuesOptions = GetFieldValueOptions & {
} }


/** /**
* Gets the values of all the fields within the form through accessing the DOM nodes.
* @param form - The form.
* @param options - The options.
* @returns The form values.
* Tag name for the `<form>` element.
*/ */
export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValuesOptions) => {
if (!form) {
throw new TypeError('Invalid form element.');
const TAG_NAME_FORM = 'FORM' as const;

/**
* Checks if the provided value is a valid form.
* @param maybeForm - The value to check.
* @param context - Context where this function is run, which are used for error messages.
*/
const assertIsFormElement = (maybeForm: unknown, context: string) => {
const formType = typeof maybeForm;
if (formType !== 'object') {
throw new TypeError(
`Invalid form argument provided for ${context}(). The argument value ${String(maybeForm)} is of type "${formType}". Expected an HTML element.`,
);
}

if (!maybeForm) {
// Don't accept `null`.
throw new TypeError(`No <form> element was provided for ${context}().`);
}

const element = maybeForm as HTMLElement;
// We're not so strict when it comes to checking if the passed value for `maybeForm` is a
// legitimate HTML element.

if (element.tagName !== TAG_NAME_FORM) {
throw new TypeError(
`Invalid form argument provided for ${context}(). Expected <form>, got <${element.tagName.toLowerCase()}>.`,
);
} }
};

/**
* Filters the form elements that can be processed.
* @param form - The form element.
* @returns Array of key-value pairs for the field names and field elements.
*/
const filterFieldElements = (form: HTMLFormElement) => {
const formElements = form.elements as unknown as Record<string | number, HTMLElement>; const formElements = form.elements as unknown as Record<string | number, HTMLElement>;
const allFormFieldElements = Object.entries<HTMLElement>(formElements); const allFormFieldElements = Object.entries<HTMLElement>(formElements);
const indexedNamedEnabledFormFieldElements = allFormFieldElements.filter(([k, el]) => (
return allFormFieldElements.filter(([k, el]) => (
// We use the number-indexed elements because they are consistent to enumerate.
!Number.isNaN(Number(k)) !Number.isNaN(Number(k))

// Only the enabled/read-only elements can be enumerated.
&& isNamedEnabledFormFieldElement(el) && isNamedEnabledFormFieldElement(el)
)) as [string, HTMLElementWithName][]; )) as [string, HTMLElementWithName][];
const fieldValues = indexedNamedEnabledFormFieldElements.reduce(
};

/**
* Gets the values of all the fields within the form through accessing the DOM nodes.
* @param form - The form.
* @param options - The options.
* @returns The form values.
*/
export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValuesOptions) => {
assertIsFormElement(form, 'getFormValues');

const fieldElements = filterFieldElements(form);
const fieldValues = fieldElements.reduce(
(theFormValues, [, el]) => { (theFormValues, [, el]) => {
const fieldValue = getFieldValue(el, options); const fieldValue = getFieldValue(el, options);
if (fieldValue === null) { if (fieldValue === null) {
@@ -643,27 +789,35 @@ export const setFormValues = (
form: HTMLFormElement, form: HTMLFormElement,
values: ConstructorParameters<typeof URLSearchParams>[0] | Record<string, unknown>, values: ConstructorParameters<typeof URLSearchParams>[0] | Record<string, unknown>,
) => { ) => {
if (!form) {
throw new TypeError('Invalid form element.');
assertIsFormElement(form, 'getFormValues');

const valuesType = typeof values;
if (!['string', 'object'].includes(valuesType)) {
throw new TypeError(`Invalid values argument provided for setFormValues(). Expected "object" or "string", got ${valuesType}`);
}

if (!values) {
return;
} }

const fieldElements = filterFieldElements(form);
const objectValues = new URLSearchParams(values as unknown as string | Record<string, string>); const objectValues = new URLSearchParams(values as unknown as string | Record<string, string>);
const formElements = form.elements as unknown as Record<string | number, HTMLElement>;
const allFormFieldElements = Object.entries<HTMLElement>(formElements);
const indexedNamedEnabledFormFieldElements = allFormFieldElements.filter(([k, el]) => (
!Number.isNaN(Number(k))
&& isNamedEnabledFormFieldElement(el)
)) as [string, HTMLElementWithName][];
indexedNamedEnabledFormFieldElements
fieldElements
.filter(([, el]) => objectValues.has(el.name)) .filter(([, el]) => objectValues.has(el.name))
.forEach(([, el]) => { .forEach(([, el]) => {
// eslint-disable-next-line no-param-reassign
setFieldValue(el, objectValues.get(el.name)); setFieldValue(el, objectValues.get(el.name));
}); });
}; };


// Default import is deprecated. Use named export instead. This default export is only for
// compatibility.
/**
* Gets the values of all the fields within the form through accessing the DOM nodes.
* @deprecated Default import is deprecated. Use named export `getFormValues()` instead. This
* default export is only for backwards compatibility.
* @param args - The arguments.
* @see getFormValues
*/
export default (...args: Parameters<typeof getFormValues>) => { export default (...args: Parameters<typeof getFormValues>) => {
console.warn('Default import is deprecated. Use named export instead. This default export is only for compatibility.');
// eslint-disable-next-line no-console
console.warn('Default import is deprecated. Use named export `getFormValues()` instead. This default export is only for backwards compatibility.');
return getFormValues(...args); return getFormValues(...args);
}; };

Loading…
Cancel
Save