@@ -0,0 +1,4 @@ | |||||
*.log | |||||
.DS_Store | |||||
node_modules | |||||
dist |
@@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2021 TheoryOfNekomata | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@@ -0,0 +1,103 @@ | |||||
# TSDX User Guide | |||||
Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. | |||||
> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. | |||||
> If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) | |||||
## Commands | |||||
TSDX scaffolds your new library inside `/src`. | |||||
To run TSDX, use: | |||||
```bash | |||||
npm start # or yarn start | |||||
``` | |||||
This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. | |||||
To do a one-off build, use `npm run build` or `yarn build`. | |||||
To run tests, use `npm test` or `yarn test`. | |||||
## Configuration | |||||
Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. | |||||
### Jest | |||||
Jest tests are set up to run with `npm test` or `yarn test`. | |||||
### Bundle Analysis | |||||
[`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`. | |||||
#### Setup Files | |||||
This is the folder structure we set up for you: | |||||
```txt | |||||
/src | |||||
index.tsx # EDIT THIS | |||||
/test | |||||
blah.test.tsx # EDIT THIS | |||||
.gitignore | |||||
package.json | |||||
README.md # EDIT THIS | |||||
tsconfig.json | |||||
``` | |||||
### Rollup | |||||
TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. | |||||
### TypeScript | |||||
`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. | |||||
## Continuous Integration | |||||
### GitHub Actions | |||||
Two actions are added by default: | |||||
- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix | |||||
- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) | |||||
## Optimizations | |||||
Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: | |||||
```js | |||||
// ./types/index.d.ts | |||||
declare var __DEV__: boolean; | |||||
// inside your code... | |||||
if (__DEV__) { | |||||
console.log('foo'); | |||||
} | |||||
``` | |||||
You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. | |||||
## Module Formats | |||||
CJS, ESModules, and UMD module formats are supported. | |||||
The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. | |||||
## Named Exports | |||||
Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. | |||||
## Including Styles | |||||
There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. | |||||
For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. | |||||
## Publishing to NPM | |||||
We recommend using [np](https://github.com/sindresorhus/np). |
@@ -0,0 +1,55 @@ | |||||
{ | |||||
"version": "0.1.0", | |||||
"license": "MIT", | |||||
"main": "dist/index.js", | |||||
"typings": "dist/index.d.ts", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=10" | |||||
}, | |||||
"scripts": { | |||||
"start": "tsdx watch", | |||||
"build": "tsdx build", | |||||
"test": "tsdx test", | |||||
"lint": "tsdx lint", | |||||
"prepare": "tsdx build", | |||||
"size": "size-limit", | |||||
"analyze": "size-limit --why" | |||||
}, | |||||
"peerDependencies": {}, | |||||
"husky": { | |||||
"hooks": { | |||||
"pre-commit": "tsdx lint" | |||||
} | |||||
}, | |||||
"prettier": { | |||||
"printWidth": 80, | |||||
"semi": true, | |||||
"singleQuote": true, | |||||
"trailingComma": "es5" | |||||
}, | |||||
"name": "get-form-values", | |||||
"author": "TheoryOfNekomata", | |||||
"module": "dist/get-form-values.esm.js", | |||||
"size-limit": [ | |||||
{ | |||||
"path": "dist/get-form-values.cjs.production.min.js", | |||||
"limit": "10 KB" | |||||
}, | |||||
{ | |||||
"path": "dist/get-form-values.esm.js", | |||||
"limit": "10 KB" | |||||
} | |||||
], | |||||
"devDependencies": { | |||||
"@size-limit/preset-small-lib": "^4.10.2", | |||||
"husky": "^6.0.0", | |||||
"size-limit": "^4.10.2", | |||||
"tsdx": "^0.14.1", | |||||
"tslib": "^2.2.0", | |||||
"typescript": "^4.2.4" | |||||
} | |||||
} |
@@ -0,0 +1,159 @@ | |||||
type HTMLFieldElement | |||||
= HTMLInputElement | |||||
| HTMLButtonElement | |||||
| HTMLSelectElement | |||||
| HTMLTextAreaElement | |||||
type FieldNode | |||||
= RadioNodeList | |||||
| HTMLFieldElement | |||||
type HTMLSubmitterElement | |||||
= HTMLButtonElement | |||||
| HTMLInputElement | |||||
const isFormFieldElement = (el: FieldNode) => { | |||||
if ((el as unknown) instanceof RadioNodeList) { | |||||
return true | |||||
} | |||||
const htmlEl = el as HTMLElement | |||||
const tagName = htmlEl.tagName.toLowerCase() | |||||
if (['SELECT', 'TEXTAREA', 'BUTTON'].includes(tagName)) { | |||||
return true | |||||
} | |||||
if (tagName !== 'INPUT') { | |||||
return false | |||||
} | |||||
const inputEl = htmlEl as HTMLInputElement | |||||
const type = inputEl.type.toLowerCase() | |||||
const checkedValue = inputEl.getAttribute('value') | |||||
if (type === 'checkbox' && checkedValue !== null) { | |||||
return inputEl.checked | |||||
} | |||||
return Boolean(inputEl.name) | |||||
} | |||||
const isValidFieldNode = (submitter?: HTMLSubmitterElement) => (fieldNode: Node) => { | |||||
const fieldEl = fieldNode as HTMLElement | |||||
const fieldElTagName = fieldEl.tagName | |||||
if (fieldElTagName === 'BUTTON' && Boolean(submitter as HTMLSubmitterElement)) { | |||||
const buttonEl = fieldEl as HTMLButtonElement | |||||
if (buttonEl.type === 'reset' || buttonEl.type === 'button') { | |||||
return false | |||||
} | |||||
return ( | |||||
buttonEl.name === submitter!.name | |||||
&& buttonEl.value === submitter!.value | |||||
) | |||||
} | |||||
if (fieldElTagName === 'INPUT') { | |||||
const inputEl = fieldEl as HTMLInputElement | |||||
if (inputEl.type === 'radio') { | |||||
return inputEl.checked | |||||
} | |||||
if (inputEl.type === 'submit' && Boolean(submitter as HTMLSubmitterElement)) { | |||||
return ( | |||||
inputEl.name === submitter!.name | |||||
&& inputEl.value === submitter!.value | |||||
) | |||||
} | |||||
if (inputEl.type === 'reset' || inputEl.type === 'button') { | |||||
return false | |||||
} | |||||
} | |||||
return true | |||||
} | |||||
const getRadioNodeListResolvedValue = (radioNodeList: RadioNodeList, submitter?: HTMLSubmitterElement) => { | |||||
const isValid = isValidFieldNode(submitter) | |||||
const validFieldElements: Node[] = Array.from(radioNodeList).filter(isValid) | |||||
if (validFieldElements.length > 1) { | |||||
return validFieldElements.map((fieldNode: Node) => (fieldNode as HTMLFieldElement).value) | |||||
} | |||||
if (validFieldElements.length > 0) { | |||||
const [validFieldElement] = (validFieldElements as HTMLFieldElement[]) | |||||
if (validFieldElement) { | |||||
return validFieldElement.value | |||||
} | |||||
} | |||||
return null | |||||
} | |||||
/** | |||||
* Gets the value of a field element. | |||||
* @param el - The field element node. | |||||
* @param submitter - The element which triggered the enclosing form's submit event, if said form is submitted. | |||||
*/ | |||||
const getFieldValue = (el: FieldNode, submitter?: HTMLSubmitterElement) => { | |||||
if ((el as unknown) instanceof RadioNodeList) { | |||||
return getRadioNodeListResolvedValue(el as RadioNodeList, submitter) | |||||
} | |||||
const fieldEl = el as HTMLFieldElement | |||||
const tagName = fieldEl.tagName | |||||
const type = fieldEl.type.toLowerCase() | |||||
if (tagName === 'SELECT' && fieldEl.value === '') { | |||||
return null | |||||
} | |||||
if (tagName === 'INPUT' && type === 'CHECKBOX') { | |||||
const inputFieldEl = fieldEl as HTMLInputElement | |||||
const checkedValue = inputFieldEl.getAttribute('value') | |||||
if (checkedValue !== null) { | |||||
if (inputFieldEl.checked) { | |||||
return inputFieldEl.value | |||||
} | |||||
return null | |||||
} | |||||
return inputFieldEl.checked | |||||
} | |||||
return fieldEl.value | |||||
} | |||||
/** | |||||
* Returns only named form field elements. | |||||
* @param key - The key from the `form.elements` object. | |||||
* @param el - The element | |||||
*/ | |||||
const isValidFormField = (key: string, el: FieldNode) => { | |||||
return ( | |||||
isNaN(Number(key)) | |||||
&& isFormFieldElement(el) | |||||
) | |||||
} | |||||
// TODO handle disabled/readonly fields | |||||
/** | |||||
* Gets the values of all the fields within the form through accessing the DOM nodes. | |||||
* @param form - The form element. | |||||
* @param submitter - The element which triggered the form's submit event. | |||||
*/ | |||||
const getFormValues = (form: HTMLFormElement, submitter?: HTMLSubmitterElement) => { | |||||
const formElements = form.elements as unknown as Record<string | number, FieldNode> | |||||
const allFormFieldElements = Object.entries<FieldNode>(formElements) | |||||
const formFieldElements = allFormFieldElements.filter(([key, el]) => isValidFormField(key, el)) | |||||
return formFieldElements.reduce( | |||||
(theFormValues, [name, el]) => { | |||||
const fieldValue = getFieldValue(el, submitter) | |||||
if (fieldValue === null) { | |||||
return theFormValues | |||||
} | |||||
return { | |||||
[name]: fieldValue, | |||||
} | |||||
}, | |||||
{} | |||||
) | |||||
} | |||||
export default getFormValues |
@@ -0,0 +1,35 @@ | |||||
{ | |||||
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "esnext", | |||||
"lib": ["dom", "esnext"], | |||||
"importHelpers": true, | |||||
// output .d.ts declaration files for consumers | |||||
"declaration": true, | |||||
// output .js.map sourcemap files for consumers | |||||
"sourceMap": true, | |||||
// match output dir to input dir. e.g. dist/index instead of dist/src/index | |||||
"rootDir": "./src", | |||||
// stricter type-checking for stronger correctness. Recommended by TS | |||||
"strict": true, | |||||
// linter checks for common issues | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
// use Node's module resolution algorithm, instead of the legacy TS one | |||||
"moduleResolution": "node", | |||||
// transpile JSX to React.createElement | |||||
"jsx": "react", | |||||
// interop between ESM and CJS modules. Recommended by TS | |||||
"esModuleInterop": true, | |||||
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS | |||||
"skipLibCheck": true, | |||||
// error out if import and file system have a casing mismatch. Recommended by TS | |||||
"forceConsistentCasingInFileNames": true, | |||||
// `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` | |||||
"noEmit": true, | |||||
} | |||||
} |