├── .gitignore ├── tsconfig.json ├── tslint.json ├── .travis.yml ├── package.json ├── README.md ├── test └── main.test.ts └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor 2 | *.sublime* 3 | .vscode 4 | 5 | # OSX 6 | *.DS_Store 7 | 8 | # NPM 9 | node_modules 10 | npm-debug.log 11 | 12 | # Build 13 | dist 14 | 15 | # Coverage 16 | coverage 17 | .nyc_output 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "declaration": true 7 | }, 8 | "include": [ 9 | "./src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-config-prettier" 6 | ], 7 | "jsRules": {}, 8 | "rules": {}, 9 | "rulesDirectory": [] 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Faster builds on setup when not using sudo. 2 | sudo: false 3 | 4 | # Cache nodemodules for faster builds 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | language: node_js 10 | 11 | node_js: 12 | - '8' 13 | - '4' 14 | 15 | script: npm run test-ci 16 | after_success: npm run coveralls 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-form", 3 | "description": "Utility to easily parse a form in the browser.", 4 | "version": "4.0.5", 5 | "author": "Dylan Piercey ", 6 | "bugs": { 7 | "url": "https://github.com/DylanPiercey/parse-form/issues" 8 | }, 9 | "dependencies": { 10 | "q-set": "^2.0.8" 11 | }, 12 | "devDependencies": { 13 | "@types/mocha": "^2.2.43", 14 | "@types/node": "^8.0.40", 15 | "coveralls": "^3.0.0", 16 | "husky": "^0.14.3", 17 | "jsdom": "^9.8.3", 18 | "jsdom-global": "^2.1.0", 19 | "lint-staged": "^4.2.3", 20 | "mocha": "^4.0.1", 21 | "nyc": "^11.2.1", 22 | "prettier": "^1.7.4", 23 | "ts-node": "^3.3.0", 24 | "tslint": "^5.7.0", 25 | "tslint-config-prettier": "^1.5.0", 26 | "typescript": "^2.5.3" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "homepage": "https://github.com/DylanPiercey/parse-form", 32 | "keywords": [ 33 | "form", 34 | "json", 35 | "parse" 36 | ], 37 | "license": "MIT", 38 | "lint-staged": { 39 | "*.ts": [ 40 | "prettier --write", 41 | "tslint -t codeFrame -c tslint.json", 42 | "git add" 43 | ] 44 | }, 45 | "main": "dist/index.js", 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/DylanPiercey/parse-form" 49 | }, 50 | "scripts": { 51 | "build": "tsc", 52 | "coveralls": "cat coverage/lcov.info | coveralls", 53 | "format-all": "find {src,test} -name '*.ts' | xargs prettier --write", 54 | "mocha": "mocha -r ts-node/register -r jsdom-global/register ./test/**/*.test.ts", 55 | "precommit": "lint-staged && npm test && npm run build", 56 | "test": "nyc --extension=.ts --include=src/**/*.ts --reporter=lcov --reporter=text-summary npm run mocha", 57 | "test-ci": "nyc --extension=.ts --include=src/**/*.ts --reporter=lcovonly --reporter=text npm run mocha" 58 | }, 59 | "types": "dist/index.d.ts" 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | Parse-Form 5 |
6 | 7 | 8 | 9 | API Stability 10 | 11 | 12 | 13 | TypeScript 14 | 15 | 16 | 17 | Styled with prettier 18 | 19 | 20 | 21 | Build status 22 | 23 | 24 | 25 | Test Coverage 26 | 27 | 28 | 29 | NPM Version 30 | 31 | 32 | 33 | Downloads 34 | 35 | 36 | 37 | Browser Bundle Size 38 | 39 |

40 | 41 | Utility convert a form to a javascript object in the way that a browser might. 42 | Supports files, and every type of native input. 43 | 44 | # Installation 45 | 46 | ```console 47 | npm install parse-form 48 | ``` 49 | 50 | # Example 51 | 52 | ```html 53 |
54 | 55 | 56 | 57 |
58 | ``` 59 | 60 | ```javascript 61 | import { parse } from "parse-form"; 62 | 63 | const form = document.getElementById("my-form"); 64 | parse(form); 65 | /** 66 | * { 67 | * body: { a: { b: { c: "hello world" } } }, 68 | * files: { myFile: [...] } 69 | * } 70 | */ 71 | ``` 72 | 73 | # API 74 | 75 | `parse(form: HTMLFormElement, shallow: boolean): { body: object, files: object }` 76 | * Parses a form into a javascript object. 77 | * If `shallow` is true then nested keys such as a[b][c] won't be expanded. 78 | 79 | ### Contributions 80 | 81 | * Use `npm test` to build and run tests. 82 | 83 | Please feel free to create a PR! 84 | -------------------------------------------------------------------------------- /test/main.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { parse } from "../src"; 3 | 4 | describe("Parse Form", () => { 5 | it("should error on non forms", () => { 6 | assert.throws(() => parse(document.body as any)); 7 | }); 8 | 9 | it("should parse all inputs on a form", () => { 10 | document.body.innerHTML = ` 11 |
12 | 13 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 |
38 | `; 39 | 40 | document.getElementById("submit").focus(); 41 | 42 | // Deep parse. 43 | assert.deepEqual( 44 | parse(document.getElementById("form") as HTMLFormElement).body, 45 | { 46 | field1: "text-area text", 47 | field2: "2", 48 | field3: ["3"], 49 | field5: "true", 50 | field6: "2", 51 | field7: "input text", 52 | field8: ["1", "3"], 53 | field9: "1", 54 | nested: { 55 | field10: "input text" 56 | } 57 | } 58 | ); 59 | 60 | // Shallow parse. 61 | assert.deepEqual( 62 | parse(document.getElementById("form") as HTMLFormElement, true).body, 63 | { 64 | field1: "text-area text", 65 | field2: "2", 66 | field3: ["3"], 67 | field5: "true", 68 | field6: "2", 69 | field7: "input text", 70 | field8: ["1", "3"], 71 | field9: "1", 72 | "nested[field10]": "input text" 73 | } 74 | ); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { deep as setDeep, shallow as setShallow } from "q-set"; 2 | const validTags = { 3 | BUTTON: true, 4 | INPUT: true, 5 | SELECT: true, 6 | TEXTAREA: true 7 | }; 8 | 9 | /** 10 | * @description 11 | * Serialize a html form as JS object. 12 | * 13 | * @param form The html form to parse. 14 | * @param shallow If true, nested properties such as "a[b]" will not be resolved. 15 | */ 16 | export function parse(form: HTMLFormElement, shallow?: boolean) { 17 | if (!form || !(form instanceof HTMLFormElement)) { 18 | throw new Error("Can only parse form elements."); 19 | } 20 | 21 | const { enctype, elements } = form; 22 | const set = shallow ? setShallow : setDeep; 23 | const isMultiPart = enctype === "multipart/form-data"; 24 | const body: { [x: string]: string | string[] } = {}; 25 | /* istanbul ignore next */ 26 | 27 | const files = isMultiPart ? {} : undefined; 28 | 29 | for (const el of elements as any) { 30 | const { name } = el; 31 | // Check if this el should be serialized. 32 | if (el.disabled || !(name && validTags[el.nodeName])) { 33 | continue; 34 | } 35 | 36 | switch (el.type) { 37 | case "submit": 38 | // We check if the submit button is active 39 | // otherwise all type=submit buttons would be serialized. 40 | if (el === getActiveElement()) { 41 | set(body, name, el.value); 42 | } 43 | break; 44 | case "checkbox": 45 | case "radio": 46 | if (el.checked) { 47 | set(body, name, el.value); 48 | } 49 | break; 50 | case "select-one": 51 | if (el.selectedIndex >= 0) { 52 | set(body, name, el.options[el.selectedIndex].value); 53 | } 54 | break; 55 | case "select-multiple": 56 | const selected: string[] = []; 57 | for (const option of el.options) { 58 | if (option && option.selected) { 59 | selected.push(option.value); 60 | } 61 | } 62 | 63 | set(body, name, selected); 64 | break; 65 | case "file": 66 | /* istanbul ignore next */ 67 | if (isMultiPart && el.files) { 68 | for (const file of el.files) { 69 | set(files, name, file); 70 | } 71 | } 72 | break; 73 | default: 74 | set(body, name, el.value); 75 | } 76 | } 77 | 78 | return { body, files }; 79 | } 80 | 81 | /** 82 | * Tracks which button submitted a form last. 83 | * This is a patch for safari which does not properly focus the clicked button. 84 | */ 85 | let clickTarget: HTMLButtonElement = null; 86 | /* istanbul ignore next */ 87 | window.addEventListener("click", (e: MouseEvent) => { 88 | // Ignore canceled events, modified clicks, and right clicks. 89 | if ( 90 | e.defaultPrevented || 91 | e.metaKey || 92 | e.ctrlKey || 93 | e.shiftKey || 94 | e.button !== 0 95 | ) { 96 | return; 97 | } 98 | 99 | let el = e.target as HTMLButtonElement; 100 | // Find an