diff --git a/docs/ja/reference/object/toFormData.md b/docs/ja/reference/object/toFormData.md new file mode 100644 index 000000000..8a78b385c --- /dev/null +++ b/docs/ja/reference/object/toFormData.md @@ -0,0 +1,107 @@ +# toFormData + +オブジェクトを `FormData` インスタンスに変換します。この関数は、オブジェクト内の各キーと値のペアを再帰的に処理し、それを `FormData` インスタンスに追加します。ネストされたオブジェクトや配列、ファイル、およびさまざまなJavaScriptデータ型をサポートしており、複雑なデータ構造に対応できるように構成オプションでのカスタマイズが可能です。 + +- **ディープ変換**: ネストされたオブジェクトや配列を再帰的に `FormData` フォーマットに変換します。 +- **ファイルサポート**: `File` および `Blob` オブジェクトを自動的に処理します。 +- **型変換**: `boolean`、`BigInt`、`Date` などの一般的なJavaScriptデータ型を `FormData` に適した文字列表現に変換します。 +- **構成オプション**: 配列、boolean、null値、ネストされたオブジェクトの処理方法をカスタマイズするためのオプションを提供します。 + +## シグネチャ + +```typescript +function toFormData({ + data: Record, + formData = new FormData(), + parentKey?: string, + config = formDataOptions +}): FormData; +``` + +### パラメータ + +- `data` (`Record`): `FormData` に変換されるオブジェクト。プリミティブ型、配列、オブジェクト、`File`、`Blob` などの一般的なデータ型をサポートします。 +- `formData` (`FormData`): データを追加するための既存の `FormData` インスタンスです。指定しない場合、新しい `FormData` インスタンスが作成されます。 +- `parentKey` (`string`): ネストされたオブジェクトや配列を処理するためのオプションのキー。再帰処理中に内部的に使用されます。 +- `config` (`object`): `FormData` 変換をカスタマイズするための構成オブジェクトです。 + +### 構成オプション + +- `allowEmptyArrays` (`boolean`): `true` の場合、空の配列が空文字列として `FormData` に追加されます。デフォルトは `false` です。 +- `convertBooleansToIntegers` (`boolean`): `true` の場合、boolean 値 (`true`/`false`) が `'1'` および `'0'` に変換されます。デフォルトは `false` です。 +- `ignoreNullValues` (`boolean`): `true` の場合、`null` 値が `FormData` から省略されます。デフォルトは `false` です。 +- `noArrayNotationForFiles` (`boolean`): `true` の場合、ファイルの配列は `[]` 表記なしで `FormData` に追加されます。デフォルトは `false` です。 +- `noArrayNotationForNonFiles` (`boolean`): `true` の場合、ファイル以外の配列が `[]` 表記なしで `FormData` に追加されます。デフォルトは `false` です。 +- `useDotNotationForObjects` (`boolean`): `true` の場合、ネストされたオブジェクトプロパティは、ブラケット表記(例: `parent[child]`)ではなくドット表記(例: `parent.child`)で表されます。デフォルトは `false` です。 + +### 戻り値 + +(`FormData`): オブジェクトのキーと値のペアで埋められた `FormData` インスタンスです。 + +### データ型のサポート + +この関数は、さまざまなJavaScriptデータ型を処理します: + +- `undefined`: スキップされ、`FormData` にエントリは作成されません。 +- `null`: 空の文字列 (`''`) が追加されるか、`ignoreNullValues` に基づいて無視されます。 +- `boolean`: `'true'` または `'false'` に変換されます。または、`convertBooleansToIntegers` が `true` の場合は `'1'` または `'0'` に変換されます。 +- `BigInt`: 文字列に変換されます。 +- `Date`: ISO 文字列に変換されます。 +- `File` / `Blob`: そのまま追加されます。 +- `Array`: 構成に基づき、インデックスまたは省略された `[]` 表記で再帰的に処理されます。 +- `Object`: 構成に基づき、ドット表記またはブラケット表記のネストされたキーで再帰的に処理されます。 + +## 使用例 + +### 基本的な使用例と構成 + +```typescript +const obj = { name: 'John', age: 30, preferences: { color: 'blue', food: 'pizza' } }; +const formData = toFormData({ data: obj, config: { useDotNotationForObjects: true } }); +// formDataには次が含まれます: +// "name" -> "John" +// "age" -> "30" +// "preferences.color" -> "blue" +// "preferences.food" -> "pizza" +``` + +### ファイルと空の配列の処理 + +```typescript +const file = new File(['file content'], 'file.txt', { type: 'text/plain' }); +const obj = { name: 'John', profilePicture: file, tags: [] }; +const formData = toFormData({ data: obj, config: { allowEmptyArrays: true } }); +// formDataには次が含まれます: +// "name" -> "John" +// "profilePicture" -> file +// "tags" -> "" +``` + +### boolean の変換と null 値の無視 + +```typescript +const obj = { isActive: true, age: null }; +const formData = toFormData({ data: obj, config: { convertBooleansToIntegers: true, ignoreNullValues: true } }); +// formDataには次が含まれます: +// "isActive" -> "1" +// (null の "age" エントリは省略されます) +``` + +### ネストされたオブジェクトと配列 + +```typescript +const obj = { + name: 'Alice', + hobbies: ['reading', 'gaming'], + address: { + street: '123 Main St', + city: 'Wonderland', + }, +}; +const formData = toFormData({ data: obj, config: { noArrayNotationForNonFiles: true } }); +// formDataには次が含まれます: +// "name" -> "Alice" +// "hobbies" -> ["reading", "gaming"] // 非ファイル配列に配列表記がありません +// "address[street]" -> "123 Main St" +// "address[city]" -> "Wonderland" +``` diff --git a/docs/ko/reference/object/toFormData.md b/docs/ko/reference/object/toFormData.md new file mode 100644 index 000000000..3dede40fc --- /dev/null +++ b/docs/ko/reference/object/toFormData.md @@ -0,0 +1,107 @@ +# toFormData + +객체를 `FormData` 인스턴스로 변환합니다. 이 함수는 객체 내의 각 키-값 쌍을 재귀적으로 처리하여 `FormData` 인스턴스에 추가합니다. 중첩된 객체와 배열, 파일 및 다양한 JavaScript 데이터 유형을 지원하며, 구성 옵션을 통해 복잡한 데이터 구조에 대해 사용자 정의 처리가 가능합니다. + +- **깊은 변환**: 중첩된 객체와 배열을 재귀적으로 `FormData` 형식으로 변환합니다. +- **파일 지원**: `File` 및 `Blob` 객체를 자동으로 처리합니다. +- **유형 변환**: `boolean`, `BigInt`, `Date` 등의 일반적인 JavaScript 데이터 유형을 `FormData`의 문자열 표현으로 변환합니다. +- **구성 가능**: 배열, boolean, null 값, 중첩 객체 처리 방법을 사용자 지정할 수 있는 옵션을 제공합니다. + +## 시그니처 + +```typescript +function toFormData({ + data: Record, + formData = new FormData(), + parentKey?: string, + config = formDataOptions +}): FormData; +``` + +### 매개변수 + +- `data` (`Record`): `FormData`로 변환할 객체입니다. 기본 유형, 배열, 객체, `File`, `Blob` 등 다양한 유형을 지원합니다. +- `formData` (`FormData`): 데이터를 추가할 기존 `FormData` 인스턴스입니다. 제공되지 않으면 새로운 `FormData` 인스턴스가 생성됩니다. +- `parentKey` (`string`): 중첩된 객체 및 배열을 처리하는 데 사용되는 선택적 키입니다. 재귀 처리 중에 내부적으로 사용됩니다. +- `config` (`object`): `FormData` 변환을 사용자 지정할 수 있는 구성 객체입니다. + +### 구성 옵션 + +- `allowEmptyArrays` (`boolean`): `true`인 경우, 빈 배열이 빈 문자열로 `FormData`에 추가됩니다. 기본값은 `false`입니다. +- `convertBooleansToIntegers` (`boolean`): `true`인 경우, boolean 값(`true`/`false`)이 `'1'` 및 `'0'`으로 변환됩니다. 기본값은 `false`입니다. +- `ignoreNullValues` (`boolean`): `true`인 경우, `null` 값이 `FormData`에 추가되지 않습니다. 기본값은 `false`입니다. +- `noArrayNotationForFiles` (`boolean`): `true`인 경우, 파일 배열이 `[]` 표기 없이 `FormData`에 추가됩니다. 기본값은 `false`입니다. +- `noArrayNotationForNonFiles` (`boolean`): `true`인 경우, 파일이 아닌 배열이 `[]` 표기 없이 `FormData`에 추가됩니다. 기본값은 `false`입니다. +- `useDotNotationForObjects` (`boolean`): `true`인 경우, 중첩된 객체 속성이 괄호 표기(`parent[child]`) 대신 점 표기(`parent.child`)를 사용합니다. 기본값은 `false`입니다. + +### 반환값 + +(`FormData`): 객체의 키-값 쌍이 포함된 `FormData` 인스턴스입니다. + +### 데이터 유형 지원 + +이 함수는 다양한 JavaScript 데이터 유형을 처리합니다: + +- `undefined`: 무시되며, `FormData`에 항목이 생성되지 않습니다. +- `null`: 빈 문자열 (`''`)을 추가하거나 `ignoreNullValues` 설정에 따라 무시됩니다. +- `boolean`: `'true'` 또는 `'false'`로 변환되며, `convertBooleansToIntegers`가 `true`일 경우 `'1'` 또는 `'0'`으로 변환됩니다. +- `BigInt`: 문자열로 변환됩니다. +- `Date`: ISO 문자열로 변환됩니다. +- `File` / `Blob`: 그대로 추가됩니다. +- `Array`: 구성에 따라 인덱스 표기 또는 생략된 `[]` 표기로 재귀 처리됩니다. +- `Object`: 구성에 따라 점 표기 또는 괄호 표기의 중첩 키로 재귀 처리됩니다. + +## 사용 예시 + +### 기본 사용 예시와 구성 + +```typescript +const obj = { name: 'John', age: 30, preferences: { color: 'blue', food: 'pizza' } }; +const formData = toFormData({ data: obj, config: { useDotNotationForObjects: true } }); +// formData에는 다음이 포함됩니다: +// "name" -> "John" +// "age" -> "30" +// "preferences.color" -> "blue" +// "preferences.food" -> "pizza" +``` + +### 파일과 빈 배열 처리 + +```typescript +const file = new File(['file content'], 'file.txt', { type: 'text/plain' }); +const obj = { name: 'John', profilePicture: file, tags: [] }; +const formData = toFormData({ data: obj, config: { allowEmptyArrays: true } }); +// formData에는 다음이 포함됩니다: +// "name" -> "John" +// "profilePicture" -> file +// "tags" -> "" +``` + +### boolean 변환 및 null 값 무시 + +```typescript +const obj = { isActive: true, age: null }; +const formData = toFormData({ data: obj, config: { convertBooleansToIntegers: true, ignoreNullValues: true } }); +// formData에는 다음이 포함됩니다: +// "isActive" -> "1" +// ("age" 항목 없음, null 값 무시됨) +``` + +### 중첩 객체와 배열 처리 + +```typescript +const obj = { + name: 'Alice', + hobbies: ['reading', 'gaming'], + address: { + street: '123 Main St', + city: 'Wonderland', + }, +}; +const formData = toFormData({ data: obj, config: { noArrayNotationForNonFiles: true } }); +// formData에는 다음이 포함됩니다: +// "name" -> "Alice" +// "hobbies" -> ["reading", "gaming"] // 비파일 배열의 경우 인덱스 생략 +// "address[street]" -> "123 Main St" +// "address[city]" -> "Wonderland" +``` diff --git a/docs/reference/object/toFormData.md b/docs/reference/object/toFormData.md new file mode 100644 index 000000000..2c88063a9 --- /dev/null +++ b/docs/reference/object/toFormData.md @@ -0,0 +1,107 @@ +# toFormData + +Converts an object into a `FormData` instance, allowing customization through a configuration object. This function recursively processes each key-value pair in an object, appending them to the `FormData` instance. It supports nested objects, arrays, files, and various JavaScript data types, making it suitable for handling complex data structures. Configuration options control how different data types and structures are represented in the `FormData`. + +- **Deep Conversion**: Recursively converts nested objects and arrays into `FormData` format. +- **Supports Files**: Automatically handles `File` and `Blob` objects. +- **Type Conversion**: Converts common JavaScript types like `boolean`, `BigInt`, `Date`, and more into their appropriate string representations for `FormData`. +- **Configurable Behavior**: Provides options to customize how arrays, booleans, null values, and nested objects are handled. + +## Signature + +```typescript +function toFormData({ + data: Record, + formData = new FormData(), + parentKey?: string, + config = formDataOptions +}): FormData; +``` + +### Parameters + +- `data` (`Record`): The object to be converted into `FormData`. Supports primitives, arrays, objects, `File`, `Blob`, and other common data types. +- `formData` (`FormData`): An optional existing `FormData` instance to append the data to. If not provided, a new `FormData` instance is created. +- `parentKey` (`string`): An optional key to handle nested objects and arrays. Used internally during recursion. +- `config` (`object`): Configuration object with options to customize the `FormData` conversion. + +### Configuration Options + +- `allowEmptyArrays` (`boolean`): When `true`, empty arrays are added to the `FormData` as empty strings. Default is `false`. +- `convertBooleansToIntegers` (`boolean`): When `true`, boolean values (`true`/`false`) are converted to `'1'` and `'0'`. Default is `false`. +- `ignoreNullValues` (`boolean`): When `true`, `null` values are omitted from the `FormData`. Default is `false`. +- `noArrayNotationForFiles` (`boolean`): When `true`, file arrays are added to `FormData` without the `[]` notation. Default is `false`. +- `noArrayNotationForNonFiles` (`boolean`): When `true`, non-file arrays are added to `FormData` without the `[]` notation. Default is `false`. +- `useDotNotationForObjects` (`boolean`): When `true`, nested object properties use dot notation (e.g., `parent.child`) instead of bracket notation (e.g., `parent[child]`). Default is `false`. + +### Returns + +(`FormData`): A `FormData` instance populated with the object's key-value pairs. + +### Data Type Support + +This function handles various JavaScript data types: + +- `undefined`: Skipped, no entry is created in `FormData`. +- `null`: Appends an empty string (`''`) or is ignored based on `ignoreNullValues`. +- `boolean`: Converted to `'true'` or `'false'` or to `'1'`/`'0'` if `convertBooleansToIntegers` is `true`. +- `BigInt`: Converted to a `string`. +- `Date`: Converted to an ISO `string`. +- `File` / `Blob`: Appended as-is. +- `Array`: Recursively processed, with or without `[]` notation based on configuration. +- `Object`: Recursively processed with dot or bracket notation based on configuration. + +## Examples + +### Basic Usage with Configuration + +```typescript +const obj = { name: 'John', age: 30, preferences: { color: 'blue', food: 'pizza' } }; +const formData = toFormData({ data: obj, config: { useDotNotationForObjects: true } }); +// formData will contain: +// "name" -> "John" +// "age" -> "30" +// "preferences.color" -> "blue" +// "preferences.food" -> "pizza" +``` + +### Handling Files and Empty Arrays + +```typescript +const file = new File(['file content'], 'file.txt', { type: 'text/plain' }); +const obj = { name: 'John', profilePicture: file, tags: [] }; +const formData = toFormData({ data: obj, config: { allowEmptyArrays: true } }); +// formData will contain: +// "name" -> "John" +// "profilePicture" -> file +// "tags" -> "" +``` + +### Converting Booleans and Ignoring Null Values + +```typescript +const obj = { isActive: true, age: null }; +const formData = toFormData({ data: obj, config: { convertBooleansToIntegers: true, ignoreNullValues: true } }); +// formData will contain: +// "isActive" -> "1" +// (No "age" entry, as null values are ignored) +``` + +### Nested Objects and Arrays + +```typescript +const obj = { + name: 'Alice', + hobbies: ['reading', 'gaming'], + address: { + street: '123 Main St', + city: 'Wonderland', + }, +}; +const formData = toFormData({ data: obj, config: { noArrayNotationForNonFiles: true } }); +// formData will contain: +// "name" -> "Alice" +// "hobbies" -> ["reading", "gaming"] // No array notation for non-files +// "address[street]" -> "123 Main St" +// "address[city]" -> "Wonderland" +``` diff --git a/docs/zh_hans/reference/object/toFormData.md b/docs/zh_hans/reference/object/toFormData.md new file mode 100644 index 000000000..87fc129e5 --- /dev/null +++ b/docs/zh_hans/reference/object/toFormData.md @@ -0,0 +1,107 @@ +# toFormData + +将一个对象转换为 `FormData` 实例,该实例用于在 Web 表单中传输键值对。此函数递归处理对象中的每个键值对,并将其附加到 `FormData` 实例中。它支持嵌套对象、数组、文件和各种 JavaScript 数据类型,并且可以通过配置选项灵活地处理复杂的数据结构。 + +- **深度转换**:递归将嵌套对象和数组转换为 `FormData` 格式。 +- **支持文件**:自动处理 `File` 和 `Blob` 对象。 +- **类型转换**:将常见的 JavaScript 数据类型(如 `boolean`、`BigInt`、`Date` 等)转换为 `FormData` 的字符串表示形式。 +- **可配置行为**:提供选项以自定义数组、布尔值、空值和嵌套对象的处理方式。 + +## 签名 + +```typescript +function toFormData({ + data: Record, + formData = new FormData(), + parentKey?: string, + config = formDataOptions +}): FormData; +``` + +### 参数 + +- `data` (`Record`): 要转换为 `FormData` 的对象。支持基本类型、数组、对象、`File`、`Blob` 和其他常见的数据类型。 +- `formData` (`FormData`): 可选的现有 `FormData` 实例,用于将数据附加到其中。如果未提供,则创建新的 `FormData` 实例。 +- `parentKey` (`string`): 处理嵌套对象和数组的可选键。在递归处理中内部使用。 +- `config` (`object`): 自定义 `FormData` 转换行为的配置对象。 + +### 配置选项 + +- `allowEmptyArrays` (`boolean`): 当为 `true` 时,将空数组添加为空字符串。默认值为 `false`。 +- `convertBooleansToIntegers` (`boolean`): 当为 `true` 时,将布尔值(`true` / `false`)转换为 `'1'` 和 `'0'`。默认值为 `false`。 +- `ignoreNullValues` (`boolean`): 当为 `true` 时,`null` 值将被忽略,不添加到 `FormData` 中。默认值为 `false`。 +- `noArrayNotationForFiles` (`boolean`): 当为 `true` 时,文件数组将不使用 `[]` 表记。默认值为 `false`。 +- `noArrayNotationForNonFiles` (`boolean`): 当为 `true` 时,非文件数组将不使用 `[]` 表记。默认值为 `false`。 +- `useDotNotationForObjects` (`boolean`): 当为 `true` 时,嵌套对象属性使用点表示法(例如:`parent.child`)而不是括号表示法(例如:`parent[child]`)。默认值为 `false`。 + +### 返回值 + +(`FormData`): 填充了对象的键值对的 `FormData` 实例。 + +### 数据类型支持 + +该函数可以处理多种 JavaScript 数据类型: + +- `undefined`: 跳过,不会在 `FormData` 中创建条目。 +- `null`: 添加空字符串 (`''`) 或根据 `ignoreNullValues` 配置忽略。 +- `boolean`: 转换为 `'true'` 或 `'false'`,如果 `convertBooleansToIntegers` 为 `true`,则转换为 `'1'` 或 `'0'`。 +- `BigInt`: 转换为字符串。 +- `Date`: 转换为 ISO 字符串。 +- `File` / `Blob`: 直接附加。 +- `Array`: 根据配置,使用索引或无 `[]` 表记递归处理。 +- `Object`: 根据配置,使用点表示法或括号表示法递归处理。 + +## 使用示例 + +### 基本用法和配置 + +```typescript +const obj = { name: 'John', age: 30, preferences: { color: 'blue', food: 'pizza' } }; +const formData = toFormData({ data: obj, config: { useDotNotationForObjects: true } }); +// formData 包含以下内容: +// "name" -> "John" +// "age" -> "30" +// "preferences.color" -> "blue" +// "preferences.food" -> "pizza" +``` + +### 处理文件和空数组 + +```typescript +const file = new File(['file content'], 'file.txt', { type: 'text/plain' }); +const obj = { name: 'John', profilePicture: file, tags: [] }; +const formData = toFormData({ data: obj, config: { allowEmptyArrays: true } }); +// formData 包含以下内容: +// "name" -> "John" +// "profilePicture" -> file +// "tags" -> "" +``` + +### 布尔值转换和忽略空值 + +```typescript +const obj = { isActive: true, age: null }; +const formData = toFormData({ data: obj, config: { convertBooleansToIntegers: true, ignoreNullValues: true } }); +// formData 包含以下内容: +// "isActive" -> "1" +// (没有 "age" 条目,因为忽略了 null 值) +``` + +### 处理嵌套对象和数组 + +```typescript +const obj = { + name: 'Alice', + hobbies: ['reading', 'gaming'], + address: { + street: '123 Main St', + city: 'Wonderland', + }, +}; +const formData = toFormData({ data: obj, config: { noArrayNotationForNonFiles: true } }); +// formData 包含以下内容: +// "name" -> "Alice" +// "hobbies" -> ["reading", "gaming"] // 非文件数组不使用数组表记 +// "address[street]" -> "123 Main St" +// "address[city]" -> "Wonderland" +``` diff --git a/src/object/toFormData.spec.ts b/src/object/toFormData.spec.ts new file mode 100644 index 000000000..b0487c3b3 --- /dev/null +++ b/src/object/toFormData.spec.ts @@ -0,0 +1,476 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { toFormData } from './toFormData'; + +describe('toFormData', () => { + beforeAll(() => { + vi.spyOn(FormData.prototype, 'append'); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('should not append when value is undefined', () => { + const object = { + foo: undefined, + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).not.toHaveBeenCalled(); + expect(formData).toEqual(new FormData()); + expect(formData.get('foo')).toBeNull(); + }); + it('should append empty string when value is null', () => { + const object = { + foo: null, + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('foo', ''); + expect(formData.get('foo')).toBe(''); + }); + it('should append boolean as string', () => { + const object = { + foo: true, + bar: false, + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(2); + expect(formData.append).toHaveBeenNthCalledWith(1, 'foo', 'true'); + expect(formData.append).toHaveBeenNthCalledWith(2, 'bar', 'false'); + }); + it('should append array values', () => { + const object = { + foo: [1, 2, 3], + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(3); + expect(formData.append).toHaveBeenNthCalledWith(1, 'foo[0]', '1'); + expect(formData.append).toHaveBeenNthCalledWith(2, 'foo[1]', '2'); + expect(formData.append).toHaveBeenNthCalledWith(3, 'foo[2]', '3'); + expect(formData.get('foo[0]')).toBe('1'); + expect(formData.get('foo[1]')).toBe('2'); + expect(formData.get('foo[2]')).toBe('3'); + }); + it('should append date as ISO string', () => { + const object = { + date: new Date(), + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('date', object.date.toISOString()); + expect(formData.get('date')).toBe(object.date.toISOString()); + }); + it('should append nested object', () => { + const object = { + foo: { + bar: 'baz', + }, + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('foo[bar]', 'baz'); + expect(formData.get('foo[bar]')).toBe('baz'); + }); + it('should append nested array', () => { + const object = { + foo: [ + { + bar: 'baz', + }, + ], + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('foo[0][bar]', 'baz'); + expect(formData.get('foo[0][bar]')).toBe('baz'); + }); + it('should append blob as is', () => { + const object = { + blob: new Blob(['content'], { type: 'text/plain' }), + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('blob', object.blob); + expect(formData.get('blob')).toBeInstanceOf(Blob); + }); + it('should append file as is', () => { + if (typeof File === 'undefined') { + return; + } + const object = { + file: new File(['content'], 'file.txt', { type: 'text/plain' }), + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('file', object.file); + expect(formData.get('file')).toBeInstanceOf(File); + }); + it('should append all types of values', () => { + if (typeof File === 'undefined') { + return; + } + const object = { + foo: undefined, + bar: null, + baz: true, + qux: [1, 2, 3], + quux: new Date(), + corge: { + grault: 'garply', + }, + waldo: [ + { + fred: 'plugh', + }, + ], + xyzzy: new Blob(['content'], { type: 'text/plain' }), + thud: new File(['content'], 'file.txt', { type: 'text/plain' }), + }; + + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(10); + expect(formData.get('foo')).toBeNull(); + expect(formData.get('bar')).toBe(''); + expect(formData.get('baz')).toBe('true'); + expect(formData.get('qux[0]')).toBe('1'); + expect(formData.get('qux[1]')).toBe('2'); + expect(formData.get('qux[2]')).toBe('3'); + expect(formData.get('quux')).toBe(object.quux.toISOString()); + expect(formData.get('corge[grault]')).toBe('garply'); + expect(formData.get('waldo[0][fred]')).toBe('plugh'); + expect(formData.get('xyzzy')).toBeInstanceOf(Blob); + expect(formData.get('thud')).toBeInstanceOf(File); + }); + it('should append float values', () => { + const object = { + foo: 1.23, + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('foo', '1.23'); + expect(formData.get('foo')).toBe('1.23'); + }); + it('should append empty object', () => { + const formData = toFormData({ + data: {}, + }); + expect(formData.append).not.toHaveBeenCalled(); + expect(formData).toEqual(new FormData()); + }); + it('should append empty array', () => { + const formData = toFormData({ + data: [], + }); + expect(formData.append).not.toHaveBeenCalled(); + expect(formData).toEqual(new FormData()); + }); + it('should append empty string object', () => { + const object = { + foo: '', + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('foo', ''); + expect(formData.get('foo')).toBe(''); + }); + it('should not append empty empty object', () => { + const object = { + foo: {}, + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).not.toHaveBeenCalled(); + expect(formData).toEqual(new FormData()); + }); + it('should append nested arrays', () => { + const object = { + foo: [ + [1, 2], + [3, 4], + ], + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(4); + expect(formData.append).toHaveBeenNthCalledWith(1, 'foo[0][0]', '1'); + expect(formData.append).toHaveBeenNthCalledWith(2, 'foo[0][1]', '2'); + expect(formData.append).toHaveBeenNthCalledWith(3, 'foo[1][0]', '3'); + expect(formData.append).toHaveBeenNthCalledWith(4, 'foo[1][1]', '4'); + expect(formData.get('foo[0][0]')).toBe('1'); + expect(formData.get('foo[0][1]')).toBe('2'); + expect(formData.get('foo[1][0]')).toBe('3'); + expect(formData.get('foo[1][1]')).toBe('4'); + }); + it('should append BigInt as string', () => { + const object = { + foo: BigInt(123456789012345678901234567890n), + }; + const formData = toFormData({ + data: object, + }); + expect(formData.append).toHaveBeenCalledTimes(1); + expect(formData.append).toHaveBeenCalledWith('foo', object.foo.toString()); + expect(formData.get('foo')).toBe(object.foo.toString()); + }); + it('should throw error when serializing symbol', () => { + const object = { + foo: Symbol('foo'), + }; + expect(() => toFormData({ data: object })).toThrow(TypeError); + }); + it('should ignore null values when ignoreNullValues is true', () => { + const data = { name: 'John', age: null, city: 'New York' }; + const formData = toFormData({ + data, + config: { ignoreNullValues: true }, + }); + expect(formData.has('name')).toBe(true); + expect(formData.get('name')).toBe('John'); + expect(formData.has('age')).toBe(false); + expect(formData.has('city')).toBe(true); + expect(formData.get('city')).toBe('New York'); + }); + + it('should include null values when ignoreNullValues is false', () => { + const data = { name: 'John', age: null, city: 'New York' }; + const formData = toFormData({ + data, + config: { ignoreNullValues: false }, + }); + expect(formData.has('name')).toBe(true); + expect(formData.get('name')).toBe('John'); + expect(formData.has('age')).toBe(true); + expect(formData.get('age')).toBe(''); + expect(formData.has('city')).toBe(true); + expect(formData.get('city')).toBe('New York'); + }); + it('should convert boolean values to integers when convertBooleansToIntegers is true', () => { + const data = { isActive: true, isAdmin: false }; + const formData = toFormData({ + data, + config: { convertBooleansToIntegers: true }, + }); + expect(formData.has('isActive')).toBe(true); + expect(formData.get('isActive')).toBe('1'); + expect(formData.has('isAdmin')).toBe(true); + expect(formData.get('isAdmin')).toBe('0'); + }); + it('should keep boolean values as strings when convertBooleansToIntegers is false', () => { + const data = { isActive: true, isAdmin: false }; + const formData = toFormData({ + data, + config: { convertBooleansToIntegers: false }, + }); + expect(formData.has('isActive')).toBe(true); + expect(formData.get('isActive')).toBe('true'); + expect(formData.has('isAdmin')).toBe(true); + expect(formData.get('isAdmin')).toBe('false'); + }); + it('should ignore empty arrays when allowEmptyArrays is false', () => { + const data = { items: [] }; + const formData = toFormData({ + data, + config: { allowEmptyArrays: false }, + }); + expect(formData.has('items')).toBe(false); + }); + it('should include empty arrays when allowEmptyArrays is true', () => { + const data = { items: [] }; + const formData = toFormData({ + data, + config: { allowEmptyArrays: true }, + }); + expect(formData.has('items')).toBe(true); + expect(formData.get('items')).toBe(''); + }); + it('should include array notation for non-file elements when noArrayNotationForNonFiles is false', () => { + const data = { tags: ['tag1', 'tag2'] }; + const formData = toFormData({ + data, + config: { noArrayNotationForNonFiles: false }, + }); + expect(formData.has('tags[0]')).toBe(true); + expect(formData.get('tags[0]')).toBe('tag1'); + expect(formData.has('tags[1]')).toBe(true); + expect(formData.get('tags[1]')).toBe('tag2'); + }); + it('should omit array notation for non-file elements when noArrayNotationForNonFiles is true', () => { + const data = { tags: ['tag1', 'tag2'] }; + const formData = toFormData({ + data, + config: { noArrayNotationForNonFiles: true }, + }); + expect(formData.has('tags')).toBe(true); + expect(formData.getAll('tags')).toEqual(['tag1', 'tag2']); + }); + it('should include array notation for file elements when noArrayNotationForFiles is false', () => { + if (typeof File === 'undefined') { + return; + } + const file1 = new File(['content'], 'file1.txt'); + const file2 = new File(['content'], 'file2.txt'); + const data = { files: [file1, file2] }; + const formData = toFormData({ + data, + config: { noArrayNotationForFiles: false }, + }); + expect(formData.has('files[0]')).toBe(true); + expect(formData.get('files[0]')).toBe(file1); + expect(formData.has('files[1]')).toBe(true); + expect(formData.get('files[1]')).toBe(file2); + }); + it('should omit array notation for file elements when noArrayNotationForFiles is true', () => { + if (typeof File === 'undefined') { + return; + } + const file1 = new File(['content'], 'file1.txt'); + const file2 = new File(['content'], 'file2.txt'); + const data = { files: [file1, file2] }; + const formData = toFormData({ + data, + config: { noArrayNotationForFiles: true }, + }); + expect(formData.has('files')).toBe(true); + expect(formData.getAll('files')).toEqual([file1, file2]); + }); + it('should use bracket notation for nested objects when useDotNotationForObjects is false', () => { + const data = { user: { name: 'Alice', age: 30 } }; + const formData = toFormData({ + data, + config: { useDotNotationForObjects: false }, + }); + expect(formData.has('user[name]')).toBe(true); + expect(formData.get('user[name]')).toBe('Alice'); + expect(formData.has('user[age]')).toBe(true); + expect(formData.get('user[age]')).toBe('30'); + }); + it('should use dot notation for nested objects when useDotNotationForObjects is true', () => { + const data = { user: { name: 'Alice', age: 30 } }; + const formData = toFormData({ + data, + config: { useDotNotationForObjects: true }, + }); + expect(formData.has('user.name')).toBe(true); + expect(formData.get('user.name')).toBe('Alice'); + expect(formData.has('user.age')).toBe(true); + expect(formData.get('user.age')).toBe('30'); + }); + it('should ignore null values and convert booleans to integers', () => { + const data = { name: 'Alice', isActive: true, age: null }; + const formData = toFormData({ + data, + config: { ignoreNullValues: true, convertBooleansToIntegers: true }, + }); + expect(formData.has('name')).toBe(true); + expect(formData.get('name')).toBe('Alice'); + expect(formData.has('isActive')).toBe(true); + expect(formData.get('isActive')).toBe('1'); + expect(formData.has('age')).toBe(false); + }); + it('should ignore null values, convert booleans to integers, and omit array notation for files', () => { + if (typeof File === 'undefined') { + return; + } + const data = { name: 'Alice', isActive: false, documents: [new File(['doc'], 'doc1.txt')] }; + const formData = toFormData({ + data, + config: { ignoreNullValues: true, convertBooleansToIntegers: true, noArrayNotationForFiles: true }, + }); + expect(formData.has('name')).toBe(true); + expect(formData.get('name')).toBe('Alice'); + expect(formData.has('isActive')).toBe(true); + expect(formData.get('isActive')).toBe('0'); + expect(formData.has('documents')).toBe(true); + expect(formData.getAll('documents').length).toBe(1); + expect(formData.getAll('documents')[0] instanceof File).toBe(true); + }); + it('should ignore null values, convert booleans to integers, and use dot notation for objects', () => { + const data = { user: { name: 'Alice', isActive: true, age: null }, roles: ['admin', 'user'] }; + const formData = toFormData({ + data, + config: { ignoreNullValues: true, convertBooleansToIntegers: true, useDotNotationForObjects: true }, + }); + expect(formData.has('user.name')).toBe(true); + expect(formData.get('user.name')).toBe('Alice'); + expect(formData.has('user.isActive')).toBe(true); + expect(formData.get('user.isActive')).toBe('1'); + expect(formData.has('user.age')).toBe(false); + expect(formData.has('roles[0]')).toBe(true); + expect(formData.get('roles[0]')).toBe('admin'); + expect(formData.has('roles[1]')).toBe(true); + expect(formData.get('roles[1]')).toBe('user'); + }); + it('should allow empty arrays, omit indices in keys, no array notation for non-files, and use dot notation', () => { + const data = { categories: [], user: { name: 'Alice', hobbies: ['reading', 'coding'] } }; + const formData = toFormData({ + data, + config: { + allowEmptyArrays: true, + noArrayNotationForNonFiles: true, + useDotNotationForObjects: true, + }, + }); + expect(formData.has('categories')).toBe(true); + expect(formData.get('categories')).toBe(''); + expect(formData.has('user.name')).toBe(true); + expect(formData.get('user.name')).toBe('Alice'); + expect(formData.has('user.hobbies')).toBe(true); + expect(formData.getAll('user.hobbies')).toEqual(['reading', 'coding']); + }); + it('should ignore null values, convert booleans to integers, allow empty arrays, omit indices in keys, and no array notation for files', () => { + if (typeof File === 'undefined') { + return; + } + const file1 = new File(['content'], 'file1.txt'); + const data = { + isAdmin: false, + settings: { notifications: true, theme: null }, + files: [file1], + tags: [], + }; + const formData = toFormData({ + data, + config: { + ignoreNullValues: true, + convertBooleansToIntegers: true, + allowEmptyArrays: true, + noArrayNotationForFiles: true, + useDotNotationForObjects: true, + }, + }); + expect(formData.has('isAdmin')).toBe(true); + expect(formData.get('isAdmin')).toBe('0'); + expect(formData.has('settings.notifications')).toBe(true); + expect(formData.get('settings.notifications')).toBe('1'); + expect(formData.has('settings.theme')).toBe(false); + expect(formData.has('tags')).toBe(true); + expect(formData.get('tags')).toBe(''); + expect(formData.has('files')).toBe(true); + expect(formData.getAll('files').length).toBe(1); + expect(formData.getAll('files')[0] instanceof File).toBe(true); + }); +}); diff --git a/src/object/toFormData.ts b/src/object/toFormData.ts new file mode 100644 index 000000000..efeaa8b95 --- /dev/null +++ b/src/object/toFormData.ts @@ -0,0 +1,109 @@ +import { isArray } from '../compat'; +import { isBlob, isBoolean, isDate, isFile, isNull, isSymbol, isUndefined } from '../predicate'; + +const formDataOptions = { + allowEmptyArrays: false, + convertBooleansToIntegers: false, + ignoreNullValues: false, + noArrayNotationForFiles: false, + noArrayNotationForNonFiles: false, + useDotNotationForObjects: false, +}; + +/** + * Converts an object into a FormData instance with optional configuration. + * + * This function recursively converts an object into FormData, handling key-value pairs, nested objects, + * and arrays based on the provided configuration options. + * + * @param {Record} data - The object to be converted into FormData. + * @param {FormData} [formData=new FormData()] - An optional existing FormData instance to append data to. + * @param {string} [parentKey] - An optional parent key for nested structures. + * @param {object} [config=formDataOptions] - Configuration object that customizes FormData conversion behavior. + * + * Configuration options: + * - `allowEmptyArrays` (boolean): When `true`, empty arrays are added to the FormData as empty strings. Default is `false`. + * - `convertBooleansToIntegers` (boolean): When `true`, boolean values (`true`/`false`) are converted to `'1'` and `'0'`. Default is `false`. + * - `ignoreNullValues` (boolean): When `true`, `null` values are omitted from the FormData. Default is `false`. + * - `noArrayNotationForFiles` (boolean): When `true`, file arrays are added to FormData without the `[]` notation. Default is `false`. + * - `noArrayNotationForNonFiles` (boolean): When `true`, non-file arrays are added to FormData without the `[]` notation. Default is `false`. + * - `useDotNotationForObjects` (boolean): When `true`, nested object properties use dot notation (e.g., `parent.child`) instead of bracket notation (e.g., `parent[child]`). Default is `false`. + * + * @returns {FormData} A populated FormData instance. + * + * @throws {TypeError} If an unsupported data type (such as `Symbol`) is encountered. + */ +export function toFormData({ + data, + parentKey, + formData = new FormData(), + config = formDataOptions, +}: { + data: Record; + parentKey?: string; + formData?: FormData; + config?: Partial; +}): FormData { + if (isUndefined(data)) { + return formData; + } + if (isNull(data)) { + if (config.ignoreNullValues) { + return formData; + } + if (parentKey) { + formData.append(parentKey, ''); + } + } else if (isBoolean(data) && parentKey) { + const value = config.convertBooleansToIntegers ? (data ? '1' : '0') : String(data); + formData.append(parentKey, value); + } else if (isArray(data) && data.length === 0) { + if (config.allowEmptyArrays && parentKey) { + formData.append(parentKey, ''); + } + } else if (isArray(data)) { + data.forEach((item, index) => { + let key = parentKey ? `${parentKey}[${index}]` : String(index); + if ( + (config.noArrayNotationForNonFiles && !isFile(item) && !isBlob(item)) || + (config.noArrayNotationForFiles && (isFile(item) || isBlob(item))) + ) { + key = parentKey!; + } + toFormData({ + data: item, + formData, + parentKey: key, + config, + }); + }); + } else if (isDate(data) && parentKey) { + formData.append(parentKey, data.toISOString()); + } else if ((isFile(data) || isBlob(data)) && parentKey) { + formData.append(parentKey, data); + } else if (typeof data === 'bigint' && parentKey) { + formData.append(parentKey, (data as bigint).toString()); + } else if (isSymbol(data)) { + throw new TypeError('Cannot serialize a symbol to FormData'); + } else if (typeof data === 'object' && !isBlob(data)) { + for (const key in data) { + const value = data[key]; + const formKey = parentKey + ? config.useDotNotationForObjects + ? `${parentKey}.${key}` + : `${parentKey}[${key}]` + : key; + toFormData({ + data: value, + formData, + parentKey: formKey, + config, + }); + } + } else if (parentKey) { + formData.append(parentKey, String(data)); + } else { + throw new TypeError(`Unsupported data type: ${typeof data}`); + } + return formData; +}