@@ -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, | |||
} | |||
} |