@@ -1,103 +0,0 @@ | |||
# 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,6 @@ | |||
{ | |||
"fixturesFolder": "test/fixtures", | |||
"integrationFolder": "test/integrations", | |||
"pluginsFile": "test/plugins/index.ts", | |||
"supportFile": "test/support/index.ts" | |||
} |
@@ -0,0 +1,8 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "es5", | |||
"lib": ["es5", "dom"], | |||
"types": ["cypress"] | |||
}, | |||
"include": ["**/*.ts"] | |||
} |
@@ -15,6 +15,7 @@ | |||
"build": "tsdx build", | |||
"test": "tsdx test", | |||
"lint": "tsdx lint", | |||
"e2e": "cypress open", | |||
"prepare": "tsdx build", | |||
"size": "size-limit", | |||
"analyze": "size-limit --why" | |||
@@ -47,6 +48,7 @@ | |||
"devDependencies": { | |||
"@size-limit/preset-small-lib": "^4.10.2", | |||
"@types/jsdom": "^16.2.10", | |||
"cypress": "^7.2.0", | |||
"husky": "^6.0.0", | |||
"jsdom": "^16.5.3", | |||
"size-limit": "^4.10.2", | |||
@@ -1,4 +1,4 @@ | |||
import * as fixtures from '../test/fixtures' | |||
import * as fixtures from '../test/utils' | |||
import getFormValues from '.' | |||
describe('blank template', () => { | |||
@@ -109,20 +109,37 @@ const getFieldValue = (el: FieldNode, submitter?: HTMLSubmitterElement) => { | |||
const fieldEl = el as HTMLFieldElement | |||
const tagName = fieldEl.tagName | |||
const type = fieldEl.type | |||
if (tagName === 'TEXTAREA') { | |||
return fieldEl.value.replace(/\n/g, '\r\n') | |||
} | |||
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 | |||
if (tagName === 'INPUT') { | |||
switch (type) { | |||
case 'checkbox': | |||
const checkboxEl = fieldEl as HTMLInputElement | |||
const checkedValue = checkboxEl.getAttribute('value') | |||
if (checkedValue !== null) { | |||
if (checkboxEl.checked) { | |||
return checkboxEl.value | |||
} | |||
return null | |||
} | |||
return 'on' // default value | |||
case 'radio': | |||
const radioEl = fieldEl as HTMLInputElement | |||
if (radioEl.checked) { | |||
return radioEl.value | |||
} | |||
return null | |||
default: | |||
break | |||
} | |||
return inputFieldEl.checked | |||
} | |||
return fieldEl.value | |||
@@ -154,15 +171,39 @@ const getFormValues = (form: HTMLFormElement, submitter?: HTMLSubmitterElement) | |||
const formFieldElements = allFormFieldElements.filter(([, el]) => isValidFormField(el)) | |||
const fieldValues = formFieldElements.reduce( | |||
(theFormValues, [,el]) => { | |||
const inputEl = el as HTMLInputElement | |||
if (inputEl.tagName === 'INPUT' && inputEl.type === 'radio' && !inputEl.checked) { | |||
return theFormValues | |||
} | |||
const fieldValue = getFieldValue(el, submitter) | |||
if (fieldValue === null) { | |||
return theFormValues | |||
} | |||
return { | |||
[el['name'] as string]: fieldValue, | |||
} | |||
const fieldName = el['name'] as string; | |||
// const { [fieldName]: oldFormValue = null } = theFormValues; | |||
// if (oldFormValue === null) { | |||
return { | |||
...theFormValues, | |||
[fieldName]: fieldValue, | |||
} | |||
// } | |||
// | |||
// if (!Array.isArray(oldFormValue)) { | |||
// return { | |||
// ...theFormValues, | |||
// [fieldName]: [oldFormValue, fieldValue], | |||
// } | |||
// } | |||
// | |||
// return { | |||
// ...theFormValues, | |||
// [fieldName]: [...oldFormValue, fieldValue], | |||
// } | |||
}, | |||
{} | |||
{} as any | |||
) | |||
if (Boolean(submitter as unknown)) { | |||
return { | |||
@@ -80,5 +80,19 @@ | |||
</div> | |||
</form> | |||
</article> | |||
<script> | |||
Array.from(document.getElementsByClassName('dependents')).forEach(d => { | |||
d.addEventListener('click', e => { | |||
const container = document.createElement('div') | |||
const input = document.createElement('input') | |||
input.name = 'dependent' | |||
input.type = 'text' | |||
input.placeholder = 'Dependent' | |||
container.classList.add('additional-dependent') | |||
container.appendChild(input) | |||
e.target.parentElement.parentElement.insertBefore(container, e.target.parentElement) | |||
}) | |||
}) | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,29 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('blank template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/blank.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/blank.html' }).as('submitted'); | |||
}) | |||
it('should have blank form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.get('form') | |||
.then((formResult) => { | |||
const [form] = Array.from(formResult); | |||
beforeValues = getFormValues(form); | |||
form.submit(); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
console.log(beforeValues) | |||
const before = new URLSearchParams(beforeValues).toString(); | |||
const after = new URLSearchParams(search).toString(); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}); | |||
}) |
@@ -0,0 +1,49 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('single input template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/default.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/default.html' }).as('submitted'); | |||
}) | |||
it('should have a single form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.then(() => { | |||
cy.get('[name="first_name"]').type('John') | |||
cy.get('[name="middle_name"]').type('Marcelo') | |||
cy.get('[name="last_name"]').type('Dela Cruz') | |||
cy.get('[name="gender"][value="m"]').check() | |||
cy.get('[name="civil_status"]').select('Married') | |||
cy.get('[name="new_registration"]').check() | |||
cy.get('[name="nationality"][value="filipino"]').check() | |||
cy.get('[name="dependent"][value="Jun"]').check() | |||
// cy.get('button.dependents').click() | |||
// cy.get('.additional-dependent [name="dependent"][type="text"]').eq(0).type('Juana') | |||
// cy.get('button.dependents').click() | |||
// cy.get('.additional-dependent [name="dependent"][type="text"]').eq(1).type('Jane') | |||
// cy.get('button.dependents').click() | |||
// cy.get('.additional-dependent [name="dependent"][type="text"]').eq(2).type('Josh') | |||
cy.get('[name="notes"]').type('Test content\n\nNew line\n\nAnother line').as('filled') | |||
}) | |||
.get('form') | |||
.then((theForm) => { | |||
cy | |||
.get('[name="submit"][value="Hi"]') | |||
.then((submitterEl) => { | |||
const [submitter] = Array.from(submitterEl) as HTMLButtonElement[]; | |||
beforeValues = getFormValues(theForm[0], submitter); | |||
submitterEl.trigger('click'); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
const before = JSON.stringify(new URLSearchParams(beforeValues).toString().split('&')); | |||
const after = JSON.stringify(new URLSearchParams(search).toString().split('&')); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}) | |||
}) | |||
}); |
@@ -0,0 +1,28 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('single input template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/single-disabled-input.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-disabled-input.html' }).as('submitted'); | |||
}) | |||
it('should have a single form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.get('form') | |||
.then((formResult) => { | |||
const [form] = Array.from(formResult); | |||
beforeValues = getFormValues(form); | |||
form.submit(); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
const before = new URLSearchParams(beforeValues).toString(); | |||
const after = new URLSearchParams(search).toString(); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}) | |||
}); |
@@ -0,0 +1,28 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('single input template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/single-input-with-double-input-submitters.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-input-with-double-input-submitters.html' }).as('submitted'); | |||
}) | |||
it('should have a single form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.get('form') | |||
.then((formResult) => { | |||
const [form] = Array.from(formResult); | |||
beforeValues = getFormValues(form); | |||
form.submit(); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
const before = new URLSearchParams(beforeValues).toString(); | |||
const after = new URLSearchParams(search).toString(); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}) | |||
}); |
@@ -0,0 +1,28 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('single input template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/single-input-with-double-submitters.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-input-with-double-submitters.html' }).as('submitted'); | |||
}) | |||
it('should have a single form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.get('form') | |||
.then((formResult) => { | |||
const [form] = Array.from(formResult); | |||
beforeValues = getFormValues(form); | |||
form.submit(); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
const before = new URLSearchParams(beforeValues).toString(); | |||
const after = new URLSearchParams(search).toString(); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}) | |||
}); |
@@ -0,0 +1,28 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('single input template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/single-input.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-input.html' }).as('submitted'); | |||
}) | |||
it('should have a single form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.get('form') | |||
.then((formResult) => { | |||
const [form] = Array.from(formResult); | |||
beforeValues = getFormValues(form); | |||
form.submit(); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
const before = new URLSearchParams(beforeValues).toString(); | |||
const after = new URLSearchParams(search).toString(); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}) | |||
}); |
@@ -0,0 +1,28 @@ | |||
/// <reference types="cypress" /> | |||
import getFormValues from '../../src' | |||
describe('single input template', () => { | |||
beforeEach(() => { | |||
cy.intercept({ url: '/' }, { fixture: 'templates/single-readonly-input.html' }); | |||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-readonly-input.html' }).as('submitted'); | |||
}) | |||
it('should have a single form value', () => { | |||
let beforeValues; | |||
cy | |||
.visit('/') | |||
.get('form') | |||
.then((formResult) => { | |||
const [form] = Array.from(formResult); | |||
beforeValues = getFormValues(form); | |||
form.submit(); | |||
cy.wait('@submitted') | |||
cy.location('search').then(search => { | |||
const before = new URLSearchParams(beforeValues).toString(); | |||
const after = new URLSearchParams(search).toString(); | |||
expect(before).to.equal(after); | |||
}) | |||
}) | |||
}) | |||
}); |
@@ -0,0 +1,22 @@ | |||
/// <reference types="cypress" /> | |||
// *********************************************************** | |||
// This example plugins/index.ts can be used to load plugins | |||
// | |||
// You can change the location of this file or turn off loading | |||
// the plugins file with the 'pluginsFile' configuration option. | |||
// | |||
// You can read more here: | |||
// https://on.cypress.io/plugins-guide | |||
// *********************************************************** | |||
// This function is called when a project is opened or re-opened (e.g. due to | |||
// the project's config changing) | |||
/** | |||
* @type {Cypress.PluginConfig} | |||
*/ | |||
// eslint-disable-next-line no-unused-vars | |||
module.exports = (on, config) => { | |||
// `on` is used to hook into various events Cypress emits | |||
// `config` is the resolved Cypress config | |||
} |
@@ -0,0 +1,25 @@ | |||
// *********************************************** | |||
// This example commands.js shows you how to | |||
// create various custom commands and overwrite | |||
// existing commands. | |||
// | |||
// For more comprehensive examples of custom | |||
// commands please read more here: | |||
// https://on.cypress.io/custom-commands | |||
// *********************************************** | |||
// | |||
// | |||
// -- This is a parent command -- | |||
// Cypress.Commands.add('login', (email, password) => { ... }) | |||
// | |||
// | |||
// -- This is a child command -- | |||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) | |||
// | |||
// | |||
// -- This is a dual command -- | |||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) | |||
// | |||
// | |||
// -- This will overwrite an existing command -- | |||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) |
@@ -0,0 +1,20 @@ | |||
// *********************************************************** | |||
// This example support/index.js is processed and | |||
// loaded automatically before your test files. | |||
// | |||
// This is a great place to put global configuration and | |||
// behavior that modifies Cypress. | |||
// | |||
// You can change the location of this file or turn off | |||
// automatically serving support files with the | |||
// 'supportFile' configuration option. | |||
// | |||
// You can read more here: | |||
// https://on.cypress.io/configuration | |||
// *********************************************************** | |||
// Import commands.ts using ES2015 syntax: | |||
import './commands' | |||
// Alternatively you can use CommonJS syntax: | |||
// require('./commands') |