Implement setFormValues for easy setting of fields. Also migrated to pridepack because tsdx is already unmaintained.master
@@ -0,0 +1,16 @@ | |||||
{ | |||||
"root": true, | |||||
"extends": [ | |||||
"lxsmnsyc/typescript" | |||||
], | |||||
"rules": { | |||||
"@typescript-eslint/no-unsafe-assignment": "off", | |||||
"@typescript-eslint/no-unsafe-argument": "off", | |||||
"@typescript-eslint/no-unsafe-member-access": "off", | |||||
"@typescript-eslint/no-unsafe-return": "off", | |||||
"@typescript-eslint/no-unsafe-call": "off" | |||||
}, | |||||
"parserOptions": { | |||||
"project": "./tsconfig.eslint.json" | |||||
} | |||||
} |
@@ -4,7 +4,6 @@ src/ | |||||
.editorconfig | .editorconfig | ||||
.prettierrc | .prettierrc | ||||
cypress.json | cypress.json | ||||
jest.config.js | |||||
publish.sh | publish.sh | ||||
tsconfig.json | tsconfig.json | ||||
yarn.lock | yarn.lock |
@@ -1,21 +1,7 @@ | |||||
MIT License | |||||
MIT License Copyright (c) 2023 TheoryOfNekomata <allan.crisostomo@outlook.com> | |||||
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: | |||||
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 (including the next paragraph) shall be included in all copies or substantial portions of the Software. | |||||
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. | |||||
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. |
@@ -2,7 +2,7 @@ | |||||
_(read "form extra")_ | _(read "form extra")_ | ||||
Extract form values through the DOM. | |||||
Extract and set form values through the DOM. | |||||
## Motivation | ## Motivation | ||||
@@ -16,7 +16,9 @@ values to each field in the form. | |||||
Libraries made for extracting form values query field elements in the DOM, which is inefficient since they need to | Libraries made for extracting form values query field elements in the DOM, which is inefficient since they need to | ||||
traverse the DOM tree in some way, using methods such as `document.getElementsByTagName()` and | traverse the DOM tree in some way, using methods such as `document.getElementsByTagName()` and | ||||
`document.querySelector()`. | |||||
`document.querySelector()`. This is the same case with setting each form values for, say, prefilling values to save | |||||
time. It might be a simple improvement to the user experience, but the logic behind can be unwieldy as there may be | |||||
inconsistencies in setting up each field value depending on the form library being used. | |||||
Upon retrieving the field values somehow, some libraries attempt to duplicate the values of the fields as they change, | Upon retrieving the field values somehow, some libraries attempt to duplicate the values of the fields as they change, | ||||
for instance by attaching event listeners and storing the new values into some internal object or map. This is then | for instance by attaching event listeners and storing the new values into some internal object or map. This is then | ||||
@@ -25,15 +27,15 @@ where changes to the document are essential to establish functionality and impro | |||||
--- | --- | ||||
With `formxtra`, there is no need to traverse the DOM for individual fields to get their values, provided they are: | |||||
With `formxtra`, there is no need to traverse elements for individual fields to get and set their values, provided they are: | |||||
* Associated to the form (either as a descendant of the `<form>` element or [associated through the `form=""` | * Associated to the form (either as a descendant of the `<form>` element or [associated through the `form=""` | ||||
attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form)) | attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form)) | ||||
* Has a valid `name` | * Has a valid `name` | ||||
The values of these fields can be easily extracted, using the `form.elements` attribute built-in to the DOM. With this, | |||||
only the reference to the form is needed. The current state of the field elements is already stored in the DOM, waiting | |||||
to be accessed. | |||||
The values of these fields can be easily extracted and set, using the `form.elements` attribute built-in to the DOM. | |||||
With this, only the reference to the form is needed. The current state of the field elements is already stored in the | |||||
DOM, waiting to be accessed. | |||||
## Installation | ## Installation | ||||
@@ -68,23 +70,31 @@ For an example form: | |||||
Use the library as follows (code is in TypeScript, but can work with JavaScript as well): | Use the library as follows (code is in TypeScript, but can work with JavaScript as well): | ||||
```typescript | ```typescript | ||||
import getFormValues from '@theoryofnekomata/formxtra'; | |||||
// The default export is same with `getFormValues`, but it is recommended to use the named import for future-proofing! | |||||
import { getFormValues, setFormValues } from '@theoryofnekomata/formxtra'; | |||||
// This is the only query we need. On libraries like React, we can easily get form elements when we attach submit event | |||||
// listeners. | |||||
const form: HTMLFormElement = document.getElementById('form'); | const form: HTMLFormElement = document.getElementById('form'); | ||||
// optional, but just in case there are multiple submit buttons in the form, | |||||
// Optional, but just in case there are multiple submit buttons in the form, | |||||
// individual submitters can be considered | // individual submitters can be considered | ||||
const submitter = form.querySelector('[type="submit"][name="type"][value="client"]'); | const submitter = form.querySelector('[type="submit"][name="type"][value="client"]'); | ||||
const values = getFormValues(form, { submitter }); | const values = getFormValues(form, { submitter }); | ||||
const processResult = (result: Record<string, unknown>) => { | const processResult = (result: Record<string, unknown>) => { | ||||
setFormValues(form, { | |||||
username: 'Username', | |||||
password: 'verylongsecret', | |||||
}); | |||||
throw new Error('Not yet implemented.'); | throw new Error('Not yet implemented.'); | ||||
}; | }; | ||||
// Best use case is with event handlers | // Best use case is with event handlers | ||||
form.addEventListener('submit', async e => { | form.addEventListener('submit', async e => { | ||||
const { target: form, submitter } = e; | |||||
const { currentTarget: form, submitter } = e; | |||||
e.preventDefault(); | e.preventDefault(); | ||||
const values = getFormValues(form, { submitter }); | const values = getFormValues(form, { submitter }); | ||||
@@ -1 +1,3 @@ | |||||
{} | |||||
{ | |||||
"video": false | |||||
} |
@@ -1,4 +1,4 @@ | |||||
import getFormValues from '../../src' | |||||
import { getFormValues } from '../../src' | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('checkbox', () => { | describe('checkbox', () => { | ||||
@@ -23,27 +23,37 @@ describe('checkbox', () => { | |||||
`)) | `)) | ||||
it('should have no form values', () => { | it('should have no form values', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const values = getFormValues(form, {submitter}) | |||||
const before = utils.makeSearchParams(values).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(values['enabled']).toBeUndefined(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const values = getFormValues(form, { submitter }) | |||||
const before = utils.makeSearchParams(values) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(values['enabled']) | |||||
.toBeUndefined(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
'' | |||||
); | |||||
expectedStaticValue: '', | |||||
}); | |||||
}); | }); | ||||
it('should have false checked value', () => { | it('should have false checked value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const values = getFormValues(form, {submitter, booleanValuelessCheckbox: true }) | |||||
expect(values['enabled']).toBe(false); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const values = getFormValues(form, | |||||
{ | |||||
submitter, | |||||
booleanValuelessCheckbox: true | |||||
} | |||||
) | |||||
expect(values['enabled']) | |||||
.toBe(false); | |||||
} | } | ||||
); | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -68,15 +78,18 @@ describe('checkbox', () => { | |||||
`)) | `)) | ||||
it('should have single form value on a single field', () => { | it('should have single form value on a single field', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
'enabled=on' | |||||
); | |||||
expectedStaticValue: 'enabled=on', | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
}) | }) |
@@ -1,4 +1,4 @@ | |||||
import getFormValues from '../../src' | |||||
import { getFormValues } from '../../src' | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('file', () => { | describe('file', () => { | ||||
@@ -23,52 +23,65 @@ describe('file', () => { | |||||
`)) | `)) | ||||
it('should have no form values when no file is selected', () => { | it('should have no form values when no file is selected', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{} | |||||
); | |||||
expectedStaticValue: { | |||||
hello: '' | |||||
}, | |||||
}); | |||||
}) | }) | ||||
it('should have single form value when a file is selected', () => { | it('should have single form value when a file is selected', () => { | ||||
utils.test( | |||||
(cy: any) => { | |||||
utils.test({ | |||||
action: (cy: any) => { | |||||
cy | cy | ||||
.get('[name="hello"]') | .get('[name="hello"]') | ||||
.attachFile('uploads/data.json') | .attachFile('uploads/data.json') | ||||
return cy.get('[type="submit"]') | return cy.get('[type="submit"]') | ||||
}, | }, | ||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'data.json', | hello: 'data.json', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}) | }) | ||||
it('should retrieve the file list upon setting appropriate option', () => { | it('should retrieve the file list upon setting appropriate option', () => { | ||||
utils.test( | |||||
(cy: any) => { | |||||
utils.test({ | |||||
action: (cy: any) => { | |||||
cy | cy | ||||
.get('[name="hello"]') | .get('[name="hello"]') | ||||
.attachFile('uploads/data.json') | .attachFile('uploads/data.json') | ||||
return cy.get('[type="submit"]') | return cy.get('[type="submit"]') | ||||
}, | }, | ||||
(form: HTMLFormElement, submitter: any) => { | |||||
const formValues = getFormValues(form, {submitter, getFileObjects: true}) | |||||
expect(formValues.hello[0].name).toBe('data.json') | |||||
//expect(before).toEqual(after); | |||||
test: (form: HTMLFormElement, submitter: any) => { | |||||
const formValues = getFormValues(form, | |||||
{ | |||||
submitter, | |||||
getFileObjects: true | |||||
} | |||||
) | |||||
expect(formValues.hello[0].name) | |||||
.toBe('data.json') | |||||
}, | }, | ||||
); | |||||
}); | |||||
}) | }) | ||||
}) | }) | ||||
@@ -93,50 +106,65 @@ describe('file', () => { | |||||
`)) | `)) | ||||
it('should have no form values when no file is selected', () => { | it('should have no form values when no file is selected', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | |||||
expectedStaticValue: { | |||||
hello: '', | |||||
}, | }, | ||||
{} | |||||
); | |||||
}); | |||||
}) | }) | ||||
it('should have single form value when a file is selected', () => { | it('should have single form value when a file is selected', () => { | ||||
utils.test( | |||||
(cy: any) => { | |||||
utils.test({ | |||||
action: (cy: any) => { | |||||
cy | cy | ||||
.get('[name="hello"]') | .get('[name="hello"]') | ||||
.attachFile(['uploads/data.json', 'uploads/data2.json']) | .attachFile(['uploads/data.json', 'uploads/data2.json']) | ||||
return cy.get('[type="submit"]') | return cy.get('[type="submit"]') | ||||
}, | }, | ||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
'hello=data.json&hello=data2.json', | |||||
); | |||||
expectedStaticValue: 'hello=data.json&hello=data2.json', | |||||
}); | |||||
}) | }) | ||||
it('should retrieve the file list upon setting appropriate option', () => { | it('should retrieve the file list upon setting appropriate option', () => { | ||||
utils.test( | |||||
(cy: any) => { | |||||
utils.test({ | |||||
action: (cy: any) => { | |||||
cy | cy | ||||
.get('[name="hello"]') | .get('[name="hello"]') | ||||
.attachFile(['uploads/data.json', 'uploads/data2.json']) | .attachFile(['uploads/data.json', 'uploads/data2.json']) | ||||
return cy.get('[type="submit"]') | return cy.get('[type="submit"]') | ||||
}, | }, | ||||
(form: HTMLFormElement, submitter: any) => { | |||||
const formValues = getFormValues(form, {submitter, getFileObjects: true}) | |||||
expect(formValues.hello[0].name).toBe('data.json') | |||||
expect(formValues.hello[1].name).toBe('data2.json') | |||||
test: (form: HTMLFormElement, submitter: any) => { | |||||
const formValues = getFormValues(form, | |||||
{ | |||||
submitter, | |||||
getFileObjects: true | |||||
} | |||||
) | |||||
expect(formValues.hello[0].name) | |||||
.toBe('data.json') | |||||
expect(formValues.hello[1].name) | |||||
.toBe('data2.json') | |||||
}, | }, | ||||
); | |||||
}); | |||||
}) | }) | ||||
}) | }) | ||||
}) | }) |
@@ -1,4 +1,4 @@ | |||||
import getFormValues from '../../src' | |||||
import { getFormValues, setFormValues } from '../../src'; | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('misc', () => { | describe('misc', () => { | ||||
@@ -19,15 +19,18 @@ describe('misc', () => { | |||||
`)) | `)) | ||||
it('should have blank form value', () => { | it('should have blank form value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{} | |||||
); | |||||
expectedStaticValue: {}, | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -65,6 +68,9 @@ describe('misc', () => { | |||||
</label> | </label> | ||||
</div> | </div> | ||||
</fieldset> | </fieldset> | ||||
<div> | |||||
<input type="date" placeholder="Birthday" name="birthday" /> | |||||
</div> | |||||
<div> | <div> | ||||
<select name="civil_status"> | <select name="civil_status"> | ||||
<option value="">Select Civil Status</option> | <option value="">Select Civil Status</option> | ||||
@@ -80,12 +86,18 @@ describe('misc', () => { | |||||
New Registration | New Registration | ||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | |||||
<input type="datetime-local" placeholder="Appointment Date/Time" name="appointment_datetime" /> | |||||
</div> | |||||
<div> | <div> | ||||
<label> | <label> | ||||
<input type="checkbox" value="filipino" name="nationality" /> | <input type="checkbox" value="filipino" name="nationality" /> | ||||
Filipino | Filipino | ||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | |||||
<input type="number" placeholder="Gross Salary" name="gross" /> | |||||
</div> | |||||
<fieldset> | <fieldset> | ||||
<legend> | <legend> | ||||
Default Dependents | Default Dependents | ||||
@@ -109,6 +121,12 @@ describe('misc', () => { | |||||
<div> | <div> | ||||
<textarea name="notes" placeholder="Notes"></textarea> | <textarea name="notes" placeholder="Notes"></textarea> | ||||
</div> | </div> | ||||
<div> | |||||
<label> | |||||
Quality of Service | |||||
<input type="range" min="0" max="10" placeholder="Quality of Service" name="qos" /> | |||||
</label> | |||||
</div> | |||||
<div> | <div> | ||||
<button name="submit" value="Hello" type="submit">Hello</button> | <button name="submit" value="Hello" type="submit">Hello</button> | ||||
<button name="submit" value="Hi" type="submit">Hi</button> | <button name="submit" value="Hi" type="submit">Hi</button> | ||||
@@ -134,35 +152,100 @@ describe('misc', () => { | |||||
`)) | `)) | ||||
it('should have correct form values', () => { | it('should have correct form values', () => { | ||||
utils.test( | |||||
(cy) => { | |||||
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() | |||||
// Note: JSDOM is static for now | |||||
cy.get('button.dependents').click() | |||||
cy.get('.additional-dependent [name="dependent"][type="text"]').last().type('Juana') | |||||
cy.get('button.dependents').click() | |||||
cy.get('.additional-dependent [name="dependent"][type="text"]').last().type('Jane') | |||||
cy.get('button.dependents').click() | |||||
cy.get('.additional-dependent [name="dependent"][type="text"]').last().type('Josh') | |||||
utils.test({ | |||||
action: (cy) => { | |||||
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="birthday"]') | |||||
.type('1989-06-04') | |||||
cy.get('[name="civil_status"]') | |||||
.select('Married') | |||||
cy.get('[name="new_registration"]') | |||||
.check() | |||||
cy.get('[name="appointment_datetime"]') | |||||
.type('2001-09-11T06:09') | |||||
cy.get('[name="nationality"][value="filipino"]') | |||||
.check() | |||||
cy.get('[name="gross"]') | |||||
.type('131072') | |||||
cy.get('[name="dependent"][value="Jun"]') | |||||
.check() | |||||
cy.get('[name="notes"]').type('Test content\n\nNew line\n\nAnother line') | |||||
cy.get('button.dependents') | |||||
.click() | |||||
cy.get('.additional-dependent [name="dependent"][type="text"]') | |||||
.last() | |||||
.type('Juana') | |||||
cy.get('button.dependents') | |||||
.click() | |||||
cy.get('.additional-dependent [name="dependent"][type="text"]') | |||||
.last() | |||||
.type('Jane') | |||||
cy.get('button.dependents') | |||||
.click() | |||||
cy.get('.additional-dependent [name="dependent"][type="text"]') | |||||
.last() | |||||
.type('Josh') | |||||
cy.get('[name="qos"]') | |||||
.invoke('val', 10) | |||||
.trigger('change') | |||||
cy.get('[name="notes"]') | |||||
.type('Test content\n\nNew line\n\nAnother line') | |||||
return cy.get('[name="submit"][value="Hi"]') | return cy.get('[name="submit"][value="Hi"]') | ||||
}, | }, | ||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | |||||
expectedStaticValue: 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&birthday=1989-06-04&civil_status=married&new_registration=on&appointment_datetime=2001-09-11T06%3A09&nationality=filipino&gross=131072&dependent=Jun¬es=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=10&submit=Hi', | |||||
}); | |||||
}); | |||||
it('should have filled form values', () => { | |||||
utils.test({ | |||||
action: (cy) => cy.wait(3000).get('[name="submit"][value="Hi"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | |||||
preAction: (form: HTMLFormElement) => { | |||||
setFormValues(form, { | |||||
first_name: 'John', | |||||
middle_name: 'Marcelo', | |||||
last_name: 'Dela Cruz', | |||||
gender: 'm', | |||||
birthday: new Date('1989-06-04'), | |||||
civil_status: 'married', | |||||
new_registration: 'on', | |||||
appointment_datetime: new Date('2001-09-11T06:09:00'), | |||||
nationality: 'filipino', | |||||
gross: 131072, | |||||
dependent: 'Jun', | |||||
notes: `Test content | |||||
New line | |||||
Another line`, | |||||
qos: 10, | |||||
}); | |||||
}, | }, | ||||
'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&civil_status=married&new_registration=on&nationality=filipino&dependent=Jun¬es=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&submit=Hi', | |||||
); | |||||
expectedStaticValue: 'first_name=John&middle_name=Marcelo&last_name=Dela+Cruz&gender=m&birthday=1989-06-04&civil_status=married&new_registration=on&appointment_datetime=2001-09-11T06%3A09&nationality=filipino&gross=131072&dependent=Jun¬es=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=10&submit=Hi', | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
}) | }) |
@@ -1,4 +1,4 @@ | |||||
import getFormValues from '../../src' | |||||
import { getFormValues } from '../../src' | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('select', () => { | describe('select', () => { | ||||
@@ -28,15 +28,18 @@ describe('select', () => { | |||||
`)) | `)) | ||||
it('should have multiple form values on a single field', () => { | it('should have multiple form values on a single field', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
'hello=Bar&hello=Quux' | |||||
); | |||||
expectedStaticValue: 'hello=Bar&hello=Quux' | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -65,17 +68,20 @@ describe('select', () => { | |||||
`)) | `)) | ||||
it('should have single form value on a single field', () => { | it('should have single form value on a single field', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Baz', | hello: 'Baz', | ||||
} | } | ||||
); | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
}) | }) |
@@ -1,4 +1,4 @@ | |||||
import getFormValues from '../../src' | |||||
import { getFormValues } from '../../src' | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('submitter', () => { | describe('submitter', () => { | ||||
@@ -24,18 +24,21 @@ describe('submitter', () => { | |||||
`)) | `)) | ||||
it('should have double form values', () => { | it('should have double form values', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[name="action"][value="Foo"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[name="action"][value="Foo"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | hello: 'Hi', | ||||
action: 'Foo', | action: 'Foo', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -61,18 +64,21 @@ describe('submitter', () => { | |||||
`)) | `)) | ||||
it('should have double form values', () => { | it('should have double form values', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[name="action"][value="Bar"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[name="action"][value="Bar"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | hello: 'Hi', | ||||
action: 'Bar', | action: 'Bar', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -96,17 +102,20 @@ describe('submitter', () => { | |||||
`)) | `)) | ||||
it('should have single form value', () => { | it('should have single form value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | hello: 'Hi', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
}) | }) |
@@ -1,4 +1,4 @@ | |||||
import getFormValues from '../../src' | |||||
import { getFormValues, setFormValues } from '../../src'; | |||||
import * as utils from '../utils' | import * as utils from '../utils' | ||||
describe('text', () => { | describe('text', () => { | ||||
@@ -23,17 +23,20 @@ describe('text', () => { | |||||
`)) | `)) | ||||
it('should have single form value', () => { | it('should have single form value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | hello: 'Hi', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -61,15 +64,18 @@ describe('text', () => { | |||||
`)) | `)) | ||||
it('should have blank form value', () => { | it('should have blank form value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{} | |||||
); | |||||
expectedStaticValue: {}, | |||||
}); | |||||
}); | }); | ||||
}) | }) | ||||
@@ -94,19 +100,22 @@ describe('text', () => { | |||||
`)) | `)) | ||||
it('should have single form value', () => { | it('should have single form value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | hello: 'Hi', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}); | }); | ||||
}) | |||||
}); | |||||
describe('readonly', () => { | describe('readonly', () => { | ||||
beforeEach(utils.setup(` | beforeEach(utils.setup(` | ||||
@@ -132,17 +141,61 @@ describe('text', () => { | |||||
`)) | `)) | ||||
it('should have single form value', () => { | it('should have single form value', () => { | ||||
utils.test( | |||||
(cy: any) => cy.get('[type="submit"]'), | |||||
(form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, {submitter})).toString(); | |||||
const after = utils.makeSearchParams(search).toString(); | |||||
expect(before).toEqual(after); | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | }, | ||||
{ | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | hello: 'Hi', | ||||
} | |||||
); | |||||
}, | |||||
}); | |||||
}); | }); | ||||
}) | |||||
}); | |||||
describe('programmatical value setting', () => { | |||||
beforeEach(utils.setup(` | |||||
<!DOCTYPE html> | |||||
<html lang="en-PH"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<title>Text/Basic</title> | |||||
</head> | |||||
<body> | |||||
<form> | |||||
<label> | |||||
<span>Hello</span> | |||||
<input type="text" name="hello" /> | |||||
</label> | |||||
<button type="submit">Submit</button> | |||||
</form> | |||||
</body> | |||||
</html> | |||||
`)); | |||||
it('should have form values set', () => { | |||||
utils.test({ | |||||
action: (cy: any) => cy.get('[type="submit"]'), | |||||
preAction: (form: HTMLFormElement) => { | |||||
setFormValues(form, { hello: 'Hi', }) | |||||
}, | |||||
test: (form: HTMLFormElement, submitter: any, search: any) => { | |||||
const before = utils.makeSearchParams(getFormValues(form, { submitter })) | |||||
.toString(); | |||||
const after = utils.makeSearchParams(search) | |||||
.toString(); | |||||
expect(before) | |||||
.toEqual(after); | |||||
}, | |||||
expectedStaticValue: { | |||||
hello: 'Hi', | |||||
}, | |||||
}); | |||||
}); | |||||
}); | |||||
}) | }) |
@@ -4,7 +4,7 @@ import JSDOMDummyCypress from './jsdom-compat' | |||||
type ExpectedSearchValue = Record<string, string> | string | type ExpectedSearchValue = Record<string, string> | string | ||||
type RetrieveSubmitterFn = (wrapper: any) => any | |||||
type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any | |||||
type HTMLSubmitterElement = HTMLButtonElement | HTMLInputElement | type HTMLSubmitterElement = HTMLButtonElement | HTMLInputElement | ||||
@@ -18,13 +18,27 @@ export const setup = (template: string) => { | |||||
} | } | ||||
} | } | ||||
return () => { | return () => { | ||||
// @ts-ignore | |||||
window.document.open(undefined, undefined, undefined, true) | window.document.open(undefined, undefined, undefined, true) | ||||
window.document.write(template) | window.document.write(template) | ||||
window.document.close() | window.document.close() | ||||
} | } | ||||
} | } | ||||
export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, expectedValue?: ExpectedSearchValue) => { | |||||
type TestOptions = { | |||||
action: RetrieveSubmitterFn, | |||||
test: TestFn, | |||||
expectedStaticValue?: ExpectedSearchValue, | |||||
preAction?: Function, | |||||
} | |||||
export const test = (options: TestOptions) => { | |||||
const { | |||||
action: retrieveSubmitterFn, | |||||
test: testFn, | |||||
expectedStaticValue, | |||||
preAction, | |||||
} = options; | |||||
let form: HTMLFormElement | let form: HTMLFormElement | ||||
let submitter: HTMLButtonElement | HTMLInputElement | let submitter: HTMLButtonElement | HTMLInputElement | ||||
let r: any | let r: any | ||||
@@ -34,6 +48,10 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e | |||||
.get('form') | .get('form') | ||||
.then((formResult: any) => { | .then((formResult: any) => { | ||||
[form] = Array.from(formResult); | [form] = Array.from(formResult); | ||||
if (typeof preAction === 'function') { | |||||
preAction(form); | |||||
} | |||||
}) | }) | ||||
r = retrieveSubmitterFn(cy) | r = retrieveSubmitterFn(cy) | ||||
@@ -41,7 +59,7 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e | |||||
[submitter] = Array.from(submitterQueryEl as any[]) | [submitter] = Array.from(submitterQueryEl as any[]) | ||||
}) | }) | ||||
if (typeof expectedValue !== 'undefined') { | |||||
if (typeof expectedStaticValue !== 'undefined') { | |||||
r.click() | r.click() | ||||
cy | cy | ||||
.wait('@submitted') | .wait('@submitted') | ||||
@@ -61,10 +79,15 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e | |||||
.then((submitterQueryEl: any) => { | .then((submitterQueryEl: any) => { | ||||
[submitter] = Array.from(submitterQueryEl as any[]); | [submitter] = Array.from(submitterQueryEl as any[]); | ||||
[form] = Array.from(window.document.getElementsByTagName('form')) | [form] = Array.from(window.document.getElementsByTagName('form')) | ||||
testFn(form, submitter, expectedValue) | |||||
if (typeof preAction === 'function') { | |||||
preAction(form); | |||||
} | |||||
testFn(form, submitter, expectedStaticValue) | |||||
}) | }) | ||||
if (typeof expectedValue !== 'undefined') { | |||||
if (typeof expectedStaticValue !== 'undefined') { | |||||
r.click() | r.click() | ||||
} | } | ||||
} | } | ||||
@@ -1,7 +1,7 @@ | |||||
/// <reference types="node" /> | /// <reference types="node" /> | ||||
import { readFileSync } from 'fs' | |||||
import { join } from 'path' | |||||
import { readFileSync, statSync } from 'fs' | |||||
import { join, basename } from 'path' | |||||
class JSDOMJQuery { | class JSDOMJQuery { | ||||
private selectedElements: Node[] | private selectedElements: Node[] | ||||
@@ -16,6 +16,11 @@ class JSDOMJQuery { | |||||
el.value = s | el.value = s | ||||
return | return | ||||
} | } | ||||
if (el.type === 'datetime-local') { | |||||
el.value = new Date(`${s}:00.000Z`).toISOString().slice(0, 'yyyy-MM-DDTHH:mm'.length) | |||||
return | |||||
} | |||||
el.setAttribute('value', s) | el.setAttribute('value', s) | ||||
el.value = s | el.value = s | ||||
}) | }) | ||||
@@ -57,17 +62,52 @@ class JSDOMJQuery { | |||||
return this | return this | ||||
} | } | ||||
attachFile(filename: string) { | |||||
const contents = readFileSync(join('cypress', 'fixtures', filename)) | |||||
// TODO | |||||
contents.toString('binary') | |||||
invoke(key: string, value: unknown) { | |||||
if (key === 'val') { | |||||
this.selectedElements.forEach((el) => (el as unknown as Record<string, unknown>).valueAsNumber = value); | |||||
} | |||||
return this | return this | ||||
} | } | ||||
trigger(which: string) { | |||||
return this | |||||
} | |||||
attachFile(filename: string | string[]) { | |||||
const { File, FileList } = window; | |||||
const theFilenames = Array.isArray(filename) ? filename : [filename]; | |||||
const theFiles = theFilenames.map((f) => { | |||||
const filePath = join('cypress', 'fixtures', f); | |||||
const { mtimeMs: lastModified } = statSync(filePath); | |||||
const contents = readFileSync(filePath); | |||||
return new File( | |||||
[contents], | |||||
basename(filePath), | |||||
{ | |||||
lastModified, | |||||
type: '', | |||||
}, | |||||
); | |||||
}); | |||||
(theFiles as unknown as Record<string, unknown>).__proto__ = Object.create(FileList.prototype); | |||||
this.selectedElements.forEach((el) => { | |||||
Object.defineProperty(el, 'files', { | |||||
value: theFiles, | |||||
writable: false, | |||||
}) | |||||
}); | |||||
return this; | |||||
} | |||||
} | } | ||||
export default class JSDOMDummyCypress { | export default class JSDOMDummyCypress { | ||||
private currentElement = window.document; | private currentElement = window.document; | ||||
wait(time: number) { | |||||
return this; | |||||
} | |||||
get(q: string) { | get(q: string) { | ||||
return new JSDOMJQuery(this.currentElement.querySelectorAll(q)); | return new JSDOMJQuery(this.currentElement.querySelectorAll(q)); | ||||
} | } | ||||
@@ -1,3 +0,0 @@ | |||||
module.exports = { | |||||
testEnvironment: 'jsdom', | |||||
}; |
@@ -1,80 +1,78 @@ | |||||
{ | { | ||||
"version": "0.2.3", | |||||
"license": "MIT", | |||||
"main": "dist/index.js", | |||||
"typings": "dist/index.d.ts", | |||||
"files": [ | |||||
"dist" | |||||
], | |||||
"publishing": { | |||||
"github": { | |||||
"repository": "https://github.com/TheoryOfNekomata/formxtra.git", | |||||
"publishConfig": { | |||||
"registry": "https://npm.pkg.github.com" | |||||
} | |||||
}, | |||||
"master": { | |||||
"repository": "https://code.modal.sh/TheoryOfNekomata/formxtra.git", | |||||
"publishConfig": { | |||||
"registry": "https://js.pack.modal.sh" | |||||
} | |||||
}, | |||||
"npm": { | |||||
"publishConfig": { | |||||
"registry": "https://registry.npmjs.com" | |||||
} | |||||
} | |||||
}, | |||||
"engines": { | |||||
"node": ">=10" | |||||
}, | |||||
"scripts": { | |||||
"start": "tsdx watch", | |||||
"build": "tsdx build", | |||||
"test:jsdom": "tsdx test", | |||||
"test:dom": "cypress open", | |||||
"lint": "tsdx lint", | |||||
"prepare": "tsdx build", | |||||
"size": "size-limit", | |||||
"analyze": "size-limit --why" | |||||
}, | |||||
"peerDependencies": {}, | |||||
"husky": { | |||||
"hooks": { | |||||
"pre-commit": "tsdx lint" | |||||
} | |||||
}, | |||||
"name": "@theoryofnekomata/formxtra", | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"description": "Extract form values through the DOM.", | |||||
"module": "dist/get-form-values.esm.js", | |||||
"keywords": [ | |||||
"form", | |||||
"value", | |||||
"utility" | |||||
], | |||||
"homepage": "https://code.modal.sh/TheoryOfNekomata/formxtra", | |||||
"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", | |||||
"@types/jsdom": "^16.2.10", | |||||
"cypress": "^7.2.0", | |||||
"cypress-file-upload": "^5.0.7", | |||||
"cypress-jest-adapter": "^0.1.1", | |||||
"husky": "^6.0.0", | |||||
"jsdom": "^16.5.3", | |||||
"size-limit": "^4.10.2", | |||||
"tsdx": "^0.14.1", | |||||
"tslib": "^2.2.0", | |||||
"typescript": "^4.2.4" | |||||
} | |||||
"name": "@theoryofnekomata/formxtra", | |||||
"version": "1.0.0", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"publishing": { | |||||
"github": { | |||||
"repository": "https://github.com/TheoryOfNekomata/formxtra.git", | |||||
"publishConfig": { | |||||
"registry": "https://npm.pkg.github.com" | |||||
} | |||||
}, | |||||
"master": { | |||||
"repository": "https://code.modal.sh/TheoryOfNekomata/formxtra.git", | |||||
"publishConfig": { | |||||
"registry": "https://js.pack.modal.sh" | |||||
} | |||||
}, | |||||
"npm": { | |||||
"publishConfig": { | |||||
"registry": "https://registry.npmjs.com" | |||||
} | |||||
} | |||||
}, | |||||
"engines": { | |||||
"node": ">=10" | |||||
}, | |||||
"license": "MIT", | |||||
"keywords": [ | |||||
"form", | |||||
"value", | |||||
"utility" | |||||
], | |||||
"devDependencies": { | |||||
"@types/jsdom": "^21.1.0", | |||||
"@types/node": "^18.14.1", | |||||
"cypress": "^7.2.0", | |||||
"cypress-file-upload": "^5.0.7", | |||||
"cypress-jest-adapter": "^0.1.1", | |||||
"eslint": "^8.35.0", | |||||
"eslint-config-lxsmnsyc": "^0.4.8", | |||||
"husky": "^6.0.0", | |||||
"jsdom": "^21.1.0", | |||||
"pridepack": "2.4.1", | |||||
"tslib": "^2.5.0", | |||||
"typescript": "^4.9.5", | |||||
"vitest": "^0.29.2" | |||||
}, | |||||
"scripts": { | |||||
"prepublishOnly": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"lint": "pridepack lint", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"start": "pridepack start", | |||||
"dev": "pridepack dev", | |||||
"test:jsdom": "vitest", | |||||
"test:cypress": "cypress run", | |||||
"test:cpanel": "cypress open" | |||||
}, | |||||
"private": false, | |||||
"description": "Extract and set form values through the DOM.", | |||||
"repository": { | |||||
"url": "https://code.modal.sh/TheoryOfNekomata/formxtra.git", | |||||
"type": "git" | |||||
}, | |||||
"homepage": "https://code.modal.sh/TheoryOfNekomata/formxtra", | |||||
"bugs": { | |||||
"url": "https://code.modal.sh/TheoryOfNekomata/formxtra/issues" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"publishConfig": { | |||||
"access": "public" | |||||
} | |||||
} | } |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2018" | |||||
} |
@@ -2,48 +2,50 @@ | |||||
* Line ending. | * Line ending. | ||||
*/ | */ | ||||
export enum LineEnding { | export enum LineEnding { | ||||
/** | |||||
* Carriage return. | |||||
*/ | |||||
CR = '\r', | |||||
/** | |||||
* Line feed. | |||||
*/ | |||||
LF = '\n', | |||||
/** | |||||
* Carriage return/line feed combination. | |||||
*/ | |||||
CRLF = '\r\n', | |||||
/** | |||||
* Carriage return. Used for legacy Mac OS systems. | |||||
*/ | |||||
CR = '\r', | |||||
/** | |||||
* Line feed. Used for Linux/*NIX systems as well as newer macOS systems. | |||||
*/ | |||||
LF = '\n', | |||||
/** | |||||
* Carriage return/line feed combination. Used for Windows systems. | |||||
*/ | |||||
CRLF = '\r\n', | |||||
} | } | ||||
type PlaceholderObject = Record<string, unknown> | |||||
/** | /** | ||||
* Checks if an element can hold a field value. | * Checks if an element can hold a field value. | ||||
* @param el - The element. | * @param el - The element. | ||||
*/ | */ | ||||
export const isFormFieldElement = (el: HTMLElement) => { | export const isFormFieldElement = (el: HTMLElement) => { | ||||
const { tagName } = el | |||||
if (['SELECT', 'TEXTAREA'].includes(tagName)) { | |||||
return true | |||||
} | |||||
if (tagName !== 'INPUT') { | |||||
return false | |||||
} | |||||
const inputEl = el as HTMLInputElement | |||||
const { type } = inputEl | |||||
if (type === 'submit' || type === 'reset') { | |||||
return false | |||||
} | |||||
return Boolean(inputEl.name) | |||||
} | |||||
const { tagName } = el; | |||||
if (['SELECT', 'TEXTAREA'].includes(tagName)) { | |||||
return true; | |||||
} | |||||
if (tagName !== 'INPUT') { | |||||
return false; | |||||
} | |||||
const inputEl = el as HTMLInputElement; | |||||
const { type } = inputEl; | |||||
if (type === 'submit' || type === 'reset') { | |||||
return false; | |||||
} | |||||
return Boolean(inputEl.name); | |||||
}; | |||||
/** | /** | ||||
* Options for getting a `<textarea>` element field value. | * Options for getting a `<textarea>` element field value. | ||||
*/ | */ | ||||
type GetTextAreaValueOptions = { | type GetTextAreaValueOptions = { | ||||
/** | |||||
* Line ending used for the element's value. | |||||
*/ | |||||
lineEndings?: LineEnding, | |||||
/** | |||||
* Line ending used for the element's value. | |||||
*/ | |||||
lineEndings?: LineEnding, | |||||
} | } | ||||
/** | /** | ||||
@@ -52,15 +54,31 @@ type GetTextAreaValueOptions = { | |||||
* @param options - The options. | * @param options - The options. | ||||
* @returns Value of the textarea element. | * @returns Value of the textarea element. | ||||
*/ | */ | ||||
const getTextAreaFieldValue = (textareaEl: HTMLTextAreaElement, options = {} as GetTextAreaValueOptions) => { | |||||
const { lineEndings = LineEnding.CRLF, } = options | |||||
return textareaEl.value.replace(/\n/g, lineEndings) | |||||
} | |||||
const getTextAreaFieldValue = ( | |||||
textareaEl: HTMLTextAreaElement, | |||||
options = {} as GetTextAreaValueOptions, | |||||
) => { | |||||
const { lineEndings = LineEnding.CRLF } = options; | |||||
return textareaEl.value.replace(/\n/g, lineEndings); | |||||
}; | |||||
/** | |||||
* Sets the value of a `<textarea>` element. | |||||
* @param textareaEl - The element. | |||||
* @param value - Value of the textarea element. | |||||
*/ | |||||
const setTextAreaFieldValue = ( | |||||
textareaEl: HTMLTextAreaElement, | |||||
value: unknown, | |||||
) => { | |||||
// eslint-disable-next-line no-param-reassign | |||||
textareaEl.value = value as string; | |||||
}; | |||||
/** | /** | ||||
* Options for getting a `<select>` element field value. | * Options for getting a `<select>` element field value. | ||||
*/ | */ | ||||
type GetSelectValueOptions = {} | |||||
type GetSelectValueOptions = PlaceholderObject | |||||
/** | /** | ||||
* Gets the value of a `<select>` element. | * Gets the value of a `<select>` element. | ||||
@@ -68,15 +86,37 @@ type GetSelectValueOptions = {} | |||||
* @param options - The options. | * @param options - The options. | ||||
* @returns Value of the select element. | * @returns Value of the select element. | ||||
*/ | */ | ||||
const getSelectFieldValue = (selectEl: HTMLSelectElement, options = {} as GetSelectValueOptions) => { | |||||
if (selectEl.multiple) { | |||||
return Array.from(selectEl.options).filter(o => o.selected).map(o => o.value) | |||||
} | |||||
if (typeof options !== 'object' || options === null) { | |||||
throw new Error('Invalid options.') | |||||
} | |||||
return selectEl.value | |||||
} | |||||
const getSelectFieldValue = ( | |||||
selectEl: HTMLSelectElement, | |||||
options = {} as GetSelectValueOptions, | |||||
) => { | |||||
if (selectEl.multiple) { | |||||
return Array.from(selectEl.options).filter((o) => o.selected).map((o) => o.value); | |||||
} | |||||
if (typeof options !== 'object' || options === null) { | |||||
throw new Error('Invalid options.'); | |||||
} | |||||
return selectEl.value; | |||||
}; | |||||
/** | |||||
* Sets the value of a `<select>` element. | |||||
* @param selectEl - The element. | |||||
* @param value - Value of the select element. | |||||
*/ | |||||
const setSelectFieldValue = (selectEl: HTMLSelectElement, value: unknown) => { | |||||
Array.from(selectEl.options) | |||||
.filter((o) => { | |||||
if (Array.isArray(value)) { | |||||
return (value as string[]).includes(o.value); | |||||
} | |||||
return o.value === value; | |||||
}) | |||||
.forEach((el) => { | |||||
// eslint-disable-next-line no-param-reassign | |||||
el.selected = true; | |||||
}); | |||||
}; | |||||
/** | /** | ||||
* Type for an `<input type="radio">` element. | * Type for an `<input type="radio">` element. | ||||
@@ -86,7 +126,7 @@ export type HTMLInputRadioElement = HTMLInputElement & { type: 'radio' } | |||||
/** | /** | ||||
* Options for getting an `<input type="radio">` element field value. | * Options for getting an `<input type="radio">` element field value. | ||||
*/ | */ | ||||
type GetInputRadioFieldValueOptions = {} | |||||
type GetInputRadioFieldValueOptions = PlaceholderObject | |||||
/** | /** | ||||
* Gets the value of an `<input type="radio">` element. | * Gets the value of an `<input type="radio">` element. | ||||
@@ -94,15 +134,32 @@ type GetInputRadioFieldValueOptions = {} | |||||
* @param options - The options. | * @param options - The options. | ||||
* @returns Value of the input element. | * @returns Value of the input element. | ||||
*/ | */ | ||||
const getInputRadioFieldValue = (inputEl: HTMLInputRadioElement, options = {} as GetInputRadioFieldValueOptions) => { | |||||
if (inputEl.checked) { | |||||
return inputEl.value | |||||
} | |||||
if (typeof options !== 'object' || options === null) { | |||||
throw new Error('Invalid options.') | |||||
} | |||||
return null | |||||
} | |||||
const getInputRadioFieldValue = ( | |||||
inputEl: HTMLInputRadioElement, | |||||
options = {} as GetInputRadioFieldValueOptions, | |||||
) => { | |||||
if (inputEl.checked) { | |||||
return inputEl.value; | |||||
} | |||||
if (typeof options !== 'object' || options === null) { | |||||
throw new Error('Invalid options.'); | |||||
} | |||||
return null; | |||||
}; | |||||
/** | |||||
* Sets the value of an `<input type="radio">` element. | |||||
* @param inputEl - The element. | |||||
* @param value - Value of the input element. | |||||
*/ | |||||
const setInputRadioFieldValue = ( | |||||
inputEl: HTMLInputRadioElement, | |||||
value: unknown, | |||||
) => { | |||||
const checkedValue = inputEl.getAttribute('value'); | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.checked = checkedValue === value; | |||||
}; | |||||
/** | /** | ||||
* Type for an `<input type="checkbox">` element. | * Type for an `<input type="checkbox">` element. | ||||
@@ -113,13 +170,13 @@ export type HTMLInputCheckboxElement = HTMLInputElement & { type: 'checkbox' } | |||||
* Options for getting an `<input type="checkbox">` element field value. | * Options for getting an `<input type="checkbox">` element field value. | ||||
*/ | */ | ||||
type GetInputCheckboxFieldValueOptions = { | type GetInputCheckboxFieldValueOptions = { | ||||
/** | |||||
* Should we consider the `checked` attribute of checkboxes with no `value` attributes instead of the default value | |||||
* "on" when checked? | |||||
* | |||||
* This forces the field to get the `false` value when unchecked. | |||||
*/ | |||||
booleanValuelessCheckbox?: true, | |||||
/** | |||||
* Should we consider the `checked` attribute of checkboxes with no `value` attributes instead of | |||||
* the default value "on" when checked? | |||||
* | |||||
* This forces the field to get the `false` value when unchecked. | |||||
*/ | |||||
booleanValuelessCheckbox?: true, | |||||
} | } | ||||
/** | /** | ||||
@@ -129,24 +186,65 @@ type GetInputCheckboxFieldValueOptions = { | |||||
* @returns Value of the input element. | * @returns Value of the input element. | ||||
*/ | */ | ||||
const getInputCheckboxFieldValue = ( | const getInputCheckboxFieldValue = ( | ||||
inputEl: HTMLInputCheckboxElement, | |||||
options = {} as GetInputCheckboxFieldValueOptions | |||||
inputEl: HTMLInputCheckboxElement, | |||||
options = {} as GetInputCheckboxFieldValueOptions, | |||||
) => { | ) => { | ||||
const checkedValue = inputEl.getAttribute('value') | |||||
if (checkedValue !== null) { | |||||
if (inputEl.checked) { | |||||
return inputEl.value | |||||
} | |||||
return null | |||||
} | |||||
if (options.booleanValuelessCheckbox) { | |||||
return inputEl.checked | |||||
} | |||||
if (inputEl.checked) { | |||||
return 'on' | |||||
} | |||||
return null | |||||
} | |||||
const checkedValue = inputEl.getAttribute('value'); | |||||
if (checkedValue !== null) { | |||||
if (inputEl.checked) { | |||||
return inputEl.value; | |||||
} | |||||
return null; | |||||
} | |||||
if (options.booleanValuelessCheckbox) { | |||||
return inputEl.checked; | |||||
} | |||||
if (inputEl.checked) { | |||||
return 'on'; | |||||
} | |||||
return null; | |||||
}; | |||||
/** | |||||
* String values resolvable to an unchecked checkbox state. | |||||
*/ | |||||
const INPUT_CHECKBOX_FALSY_VALUES = ['false', 'off', 'no', '0', '']; | |||||
/** | |||||
* String values resolvable to a checked checkbox state. | |||||
*/ | |||||
const INPUT_CHECKBOX_TRUTHY_VALUES = ['true', 'on', 'yes', '1']; | |||||
/** | |||||
* Sets the value of an `<input type="checkbox">` element. | |||||
* @param inputEl - The element. | |||||
* @param value - Value of the input element. | |||||
*/ | |||||
const setInputCheckboxFieldValue = ( | |||||
inputEl: HTMLInputCheckboxElement, | |||||
value: unknown, | |||||
) => { | |||||
const checkedValue = inputEl.getAttribute('value'); | |||||
if (checkedValue !== null) { | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.checked = value === checkedValue; | |||||
return; | |||||
} | |||||
if (INPUT_CHECKBOX_FALSY_VALUES.includes((value as string).toLowerCase()) || !value) { | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.checked = false; | |||||
return; | |||||
} | |||||
if (INPUT_CHECKBOX_TRUTHY_VALUES.includes((value as string).toLowerCase()) | |||||
|| value === true | |||||
|| value === 1 | |||||
) { | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.checked = true; | |||||
} | |||||
}; | |||||
/** | /** | ||||
* Type for an `<input type="file">` element. | * Type for an `<input type="file">` element. | ||||
@@ -157,10 +255,11 @@ export type HTMLInputFileElement = HTMLInputElement & { type: 'file' } | |||||
* Options for getting an `<input type="file">` element field value. | * Options for getting an `<input type="file">` element field value. | ||||
*/ | */ | ||||
type GetInputFileFieldValueOptions = { | type GetInputFileFieldValueOptions = { | ||||
/** | |||||
* Should we retrieve the `files` attribute of file inputs instead of the currently selected file names? | |||||
*/ | |||||
getFileObjects?: true, | |||||
/** | |||||
* Should we retrieve the `files` attribute of file inputs instead of the currently selected file | |||||
* names? | |||||
*/ | |||||
getFileObjects?: true, | |||||
} | } | ||||
/** | /** | ||||
@@ -169,28 +268,156 @@ type GetInputFileFieldValueOptions = { | |||||
* @param options - The options. | * @param options - The options. | ||||
* @returns Value of the input element. | * @returns Value of the input element. | ||||
*/ | */ | ||||
const getInputFileFieldValue = (inputEl: HTMLInputFileElement, options = {} as GetInputFileFieldValueOptions) => { | |||||
const { files } = inputEl | |||||
if ((files as unknown) === null) { | |||||
return null | |||||
} | |||||
if (options.getFileObjects) { | |||||
return files | |||||
} | |||||
const filesArray = Array.from(files as FileList) | |||||
if (filesArray.length > 1) { | |||||
return filesArray.map(f => f.name) | |||||
} | |||||
return filesArray[0]?.name || '' | |||||
const getInputFileFieldValue = ( | |||||
inputEl: HTMLInputFileElement, | |||||
options = {} as GetInputFileFieldValueOptions, | |||||
) => { | |||||
const { files } = inputEl; | |||||
if ((files as unknown) === null) { | |||||
return null; | |||||
} | |||||
if (options.getFileObjects) { | |||||
return files; | |||||
} | |||||
const filesArray = Array.from(files as FileList); | |||||
if (filesArray.length > 1) { | |||||
return filesArray.map((f) => f.name); | |||||
} | |||||
return filesArray[0]?.name || ''; | |||||
}; | |||||
/** | |||||
* Type for an `<input type="number">` element. | |||||
*/ | |||||
export type HTMLInputNumberElement = HTMLInputElement & { type: 'number' } | |||||
/** | |||||
* Type for an `<input type="range">` element. | |||||
*/ | |||||
export type HTMLInputRangeElement = HTMLInputElement & { type: 'range' } | |||||
/** | |||||
* Type for an `<input` element that handles numeric values. | |||||
*/ | |||||
export type HTMLInputNumericElement = HTMLInputNumberElement | HTMLInputRangeElement; | |||||
/** | |||||
* Options for getting an `<input type="number">` element field value. | |||||
*/ | |||||
type GetInputNumberFieldValueOptions = { | |||||
/** | |||||
* Should we force values to be numeric? | |||||
* @note Form values are retrieved to be strings by default, hence this option. | |||||
*/ | |||||
forceNumberValues?: true, | |||||
} | } | ||||
/** | |||||
* Gets the value of an `<input type="number">` element. | |||||
* @param inputEl - The element. | |||||
* @param options - The options. | |||||
* @returns Value of the input element. | |||||
*/ | |||||
const getInputNumericFieldValue = ( | |||||
inputEl: HTMLInputNumericElement, | |||||
options = {} as GetInputNumberFieldValueOptions, | |||||
) => { | |||||
if (options.forceNumberValues) { | |||||
return inputEl.valueAsNumber; | |||||
} | |||||
return inputEl.value; | |||||
}; | |||||
/** | |||||
* Sets the value of an `<input type="number">` element. | |||||
* @param inputEl - The element. | |||||
* @param value - Value of the input element. | |||||
*/ | |||||
const setInputNumericFieldValue = ( | |||||
inputEl: HTMLInputNumericElement, | |||||
value: unknown, | |||||
) => { | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.valueAsNumber = Number(value); | |||||
}; | |||||
/** | |||||
* Type for an `<input type="date">` element. | |||||
*/ | |||||
export type HTMLInputDateElement = HTMLInputElement & { type: 'date' } | |||||
/** | |||||
* Type for an `<input type="datetime-local">` element. | |||||
*/ | |||||
export type HTMLInputDateTimeLocalElement = HTMLInputElement & { type: 'datetime-local' } | |||||
/** | |||||
* Type for an `<input>` element.that handles date values. | |||||
*/ | |||||
export type HTMLInputDateLikeElement = HTMLInputDateTimeLocalElement | HTMLInputDateElement | |||||
/** | |||||
* Options for getting a date-like `<input>` element field value. | |||||
*/ | |||||
type GetInputDateFieldValueOptions = { | |||||
/** | |||||
* Should we force values to be dates? | |||||
* @note Form values are retrieved to be strings by default, hence this option. | |||||
*/ | |||||
forceDateValues?: true, | |||||
}; | |||||
/** | |||||
* Gets the value of an `<input type="date">` element. | |||||
* @param inputEl - The element. | |||||
* @param options - The options. | |||||
* @returns Value of the input element. | |||||
*/ | |||||
const getInputDateLikeFieldValue = ( | |||||
inputEl: HTMLInputDateLikeElement, | |||||
options = {} as GetInputDateFieldValueOptions, | |||||
) => { | |||||
if (options.forceDateValues) { | |||||
return inputEl.valueAsDate; | |||||
} | |||||
return inputEl.value; | |||||
}; | |||||
/** | |||||
* Sets the value of an `<input type="date">` element. | |||||
* @param inputEl - The element. | |||||
* @param value - Value of the input element. | |||||
*/ | |||||
const setInputDateLikeFieldValue = ( | |||||
inputEl: HTMLInputDateLikeElement, | |||||
value: unknown, | |||||
) => { | |||||
if (inputEl.type.toLowerCase() === 'date') { | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0]) | |||||
.toISOString() | |||||
.slice(0, 'yyyy-MM-DD'.length); | |||||
return; | |||||
} | |||||
if (inputEl.type.toLowerCase() === 'datetime-local') { | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.value = new Date(value as ConstructorParameters<typeof Date>[0]) | |||||
.toISOString() | |||||
.slice(0, -1); // remove extra 'Z' suffix | |||||
} | |||||
// inputEl.valueAsDate = new Date(value as ConstructorParameters<typeof Date>[0]); | |||||
}; | |||||
/** | /** | ||||
* Options for getting an `<input>` element field value. | * Options for getting an `<input>` element field value. | ||||
*/ | */ | ||||
type GetInputFieldValueOptions | type GetInputFieldValueOptions | ||||
= GetInputCheckboxFieldValueOptions | |||||
& GetInputFileFieldValueOptions | |||||
& GetInputRadioFieldValueOptions | |||||
= GetInputCheckboxFieldValueOptions | |||||
& GetInputFileFieldValueOptions | |||||
& GetInputRadioFieldValueOptions | |||||
& GetInputNumberFieldValueOptions | |||||
& GetInputDateFieldValueOptions | |||||
/** | /** | ||||
* Gets the value of an `<input>` element. | * Gets the value of an `<input>` element. | ||||
@@ -198,27 +425,77 @@ type GetInputFieldValueOptions | |||||
* @param options - The options. | * @param options - The options. | ||||
* @returns Value of the input element. | * @returns Value of the input element. | ||||
*/ | */ | ||||
const getInputFieldValue = (inputEl: HTMLInputElement, options = {} as GetInputFieldValueOptions) => { | |||||
switch (inputEl.type.toLowerCase()) { | |||||
case 'checkbox': | |||||
return getInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, options) | |||||
case 'radio': | |||||
return getInputRadioFieldValue(inputEl as HTMLInputRadioElement, options) | |||||
case 'file': | |||||
return getInputFileFieldValue(inputEl as HTMLInputFileElement, options) | |||||
default: | |||||
break | |||||
} | |||||
return inputEl.value | |||||
} | |||||
const getInputFieldValue = ( | |||||
inputEl: HTMLInputElement, | |||||
options = {} as GetInputFieldValueOptions, | |||||
) => { | |||||
switch (inputEl.type.toLowerCase()) { | |||||
case 'checkbox': | |||||
return getInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, options); | |||||
case 'radio': | |||||
return getInputRadioFieldValue(inputEl as HTMLInputRadioElement, options); | |||||
case 'file': | |||||
return getInputFileFieldValue(inputEl as HTMLInputFileElement, options); | |||||
case 'number': | |||||
case 'range': | |||||
return getInputNumericFieldValue(inputEl as HTMLInputNumericElement, options); | |||||
case 'date': | |||||
case 'datetime-local': | |||||
return getInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, options); | |||||
// TODO week and month | |||||
default: | |||||
break; | |||||
} | |||||
return inputEl.value; | |||||
}; | |||||
/** | |||||
* Sets the value of an `<input>` element. | |||||
* @param inputEl - The element. | |||||
* @param value - Value of the input element. | |||||
* @note This function is a noop for `<input type="file">` because by design, file inputs are not | |||||
* assignable programmatically. | |||||
*/ | |||||
const setInputFieldValue = ( | |||||
inputEl: HTMLInputElement, | |||||
value: unknown, | |||||
) => { | |||||
switch (inputEl.type.toLowerCase()) { | |||||
case 'checkbox': | |||||
setInputCheckboxFieldValue(inputEl as HTMLInputCheckboxElement, value); | |||||
return; | |||||
case 'radio': | |||||
setInputRadioFieldValue(inputEl as HTMLInputRadioElement, value); | |||||
return; | |||||
case 'file': | |||||
// We shouldn't tamper with file inputs! This will not have any implementation. | |||||
return; | |||||
case 'number': | |||||
case 'range': | |||||
// eslint-disable-next-line no-param-reassign | |||||
setInputNumericFieldValue(inputEl as HTMLInputNumericElement, value); | |||||
return; | |||||
case 'date': | |||||
case 'datetime-local': | |||||
setInputDateLikeFieldValue(inputEl as HTMLInputDateLikeElement, value); | |||||
return; | |||||
default: | |||||
break; | |||||
} | |||||
// eslint-disable-next-line no-param-reassign | |||||
inputEl.value = value as string; | |||||
}; | |||||
/** | /** | ||||
* Options for getting a field value. | * Options for getting a field value. | ||||
*/ | */ | ||||
type GetFieldValueOptions | type GetFieldValueOptions | ||||
= GetTextAreaValueOptions | |||||
& GetSelectValueOptions | |||||
& GetInputFieldValueOptions | |||||
= GetTextAreaValueOptions | |||||
& GetSelectValueOptions | |||||
& GetInputFieldValueOptions | |||||
type HTMLElementWithName | |||||
= (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement); | |||||
/** | /** | ||||
* Gets the value of a field element. | * Gets the value of a field element. | ||||
@@ -227,20 +504,44 @@ type GetFieldValueOptions | |||||
* @returns Value of the field element. | * @returns Value of the field element. | ||||
*/ | */ | ||||
export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => { | export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOptions) => { | ||||
switch (el.tagName.toUpperCase()) { | |||||
case 'TEXTAREA': | |||||
return getTextAreaFieldValue(el as HTMLTextAreaElement, options) | |||||
case 'SELECT': | |||||
return getSelectFieldValue(el as HTMLSelectElement, options) | |||||
case 'INPUT': | |||||
return getInputFieldValue(el as HTMLInputElement, options) | |||||
default: | |||||
break | |||||
} | |||||
const fieldEl = el as HTMLElement & { value?: unknown } | |||||
return fieldEl.value || null | |||||
} | |||||
switch (el.tagName.toLowerCase()) { | |||||
case 'textarea': | |||||
return getTextAreaFieldValue(el as HTMLTextAreaElement, options); | |||||
case 'select': | |||||
return getSelectFieldValue(el as HTMLSelectElement, options); | |||||
case 'input': | |||||
return getInputFieldValue(el as HTMLInputElement, options); | |||||
default: | |||||
break; | |||||
} | |||||
const fieldEl = el as HTMLElement & { value?: unknown }; | |||||
return fieldEl.value || null; | |||||
}; | |||||
/** | |||||
* Sets the value of a field element. | |||||
* @param el - The field element. | |||||
* @param value - Value of the field element. | |||||
*/ | |||||
const setFieldValue = (el: HTMLElement, value: unknown) => { | |||||
switch (el.tagName.toLowerCase()) { | |||||
case 'textarea': | |||||
setTextAreaFieldValue(el as HTMLTextAreaElement, value); | |||||
return; | |||||
case 'select': | |||||
setSelectFieldValue(el as HTMLSelectElement, value); | |||||
return; | |||||
case 'input': | |||||
setInputFieldValue(el as HTMLInputElement, value); | |||||
return; | |||||
default: | |||||
break; | |||||
} | |||||
const fieldEl = el as HTMLElement & { value?: unknown }; | |||||
fieldEl.value = value; | |||||
}; | |||||
/** | /** | ||||
* Determines if an element is a named and enabled form field. | * Determines if an element is a named and enabled form field. | ||||
@@ -248,25 +549,28 @@ export const getFieldValue = (el: HTMLElement, options = {} as GetFieldValueOpti | |||||
* @returns Value determining if the element is a named and enabled form field. | * @returns Value determining if the element is a named and enabled form field. | ||||
*/ | */ | ||||
export const isNamedEnabledFormFieldElement = (el: HTMLElement) => { | export const isNamedEnabledFormFieldElement = (el: HTMLElement) => { | ||||
if (typeof el['name'] !== 'string') { | |||||
return false | |||||
} | |||||
const namedEl = el as HTMLElement & { name: string, disabled: unknown } | |||||
return ( | |||||
namedEl.name.length > 0 | |||||
&& !('disabled' in namedEl && Boolean(namedEl.disabled)) | |||||
&& isFormFieldElement(namedEl) | |||||
) | |||||
} | |||||
if (!('name' in el)) { | |||||
return false; | |||||
} | |||||
if (typeof el.name !== 'string') { | |||||
return false; | |||||
} | |||||
const namedEl = el as unknown as HTMLElementWithName; | |||||
return ( | |||||
el.name.length > 0 | |||||
&& !('disabled' in namedEl && Boolean(namedEl.disabled)) | |||||
&& isFormFieldElement(namedEl) | |||||
); | |||||
}; | |||||
/** | /** | ||||
* Options for getting form values. | * Options for getting form values. | ||||
*/ | */ | ||||
type GetFormValuesOptions = GetFieldValueOptions & { | type GetFormValuesOptions = GetFieldValueOptions & { | ||||
/** | |||||
* The element that triggered the submission of the form. | |||||
*/ | |||||
submitter?: HTMLElement, | |||||
/** | |||||
* The element that triggered the submission of the form. | |||||
*/ | |||||
submitter?: HTMLElement, | |||||
} | } | ||||
/** | /** | ||||
@@ -275,59 +579,87 @@ type GetFormValuesOptions = GetFieldValueOptions & { | |||||
* @param options - The options. | * @param options - The options. | ||||
* @returns The form values. | * @returns The form values. | ||||
*/ | */ | ||||
const getFormValues = (form: HTMLFormElement, options = {} as GetFormValuesOptions) => { | |||||
if (!form) { | |||||
throw new TypeError('Invalid form element.') | |||||
} | |||||
const formElements = form.elements as unknown as Record<string | number, HTMLElement> | |||||
const allFormFieldElements = Object.entries<HTMLElement>(formElements) | |||||
const indexedNamedEnabledFormFieldElements = allFormFieldElements.filter(([k, el]) => ( | |||||
!isNaN(Number(k)) | |||||
&& isNamedEnabledFormFieldElement(el) | |||||
)) | |||||
const fieldValues = indexedNamedEnabledFormFieldElements.reduce( | |||||
(theFormValues, [,el]) => { | |||||
const fieldValue = getFieldValue(el, options) | |||||
if (fieldValue === null) { | |||||
return theFormValues | |||||
} | |||||
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(options.submitter as unknown)) { | |||||
const submitter = options.submitter as HTMLElement & { name: string, value: unknown } | |||||
if (submitter.name.length > 0) { | |||||
return { | |||||
...fieldValues, | |||||
[submitter.name]: submitter.value, | |||||
} | |||||
} | |||||
} | |||||
return fieldValues | |||||
} | |||||
export const getFormValues = (form: HTMLFormElement, options = {} as GetFormValuesOptions) => { | |||||
if (!form) { | |||||
throw new TypeError('Invalid form element.'); | |||||
} | |||||
const formElements = form.elements as unknown as Record<string | number, HTMLElement>; | |||||
const allFormFieldElements = Object.entries<HTMLElement>(formElements); | |||||
const indexedNamedEnabledFormFieldElements = allFormFieldElements.filter(([k, el]) => ( | |||||
!Number.isNaN(Number(k)) | |||||
&& isNamedEnabledFormFieldElement(el) | |||||
)) as [string, HTMLElementWithName][]; | |||||
const fieldValues = indexedNamedEnabledFormFieldElements.reduce( | |||||
(theFormValues, [, el]) => { | |||||
const fieldValue = getFieldValue(el, options); | |||||
if (fieldValue === null) { | |||||
return theFormValues; | |||||
} | |||||
const { name: fieldName } = el; | |||||
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 Record<string, unknown>, | |||||
); | |||||
if (options.submitter as unknown as HTMLButtonElement) { | |||||
const { submitter } = options as unknown as Pick<HTMLFormElement, 'submitter'>; | |||||
if (submitter.name.length > 0) { | |||||
return { | |||||
...fieldValues, | |||||
[submitter.name]: submitter.value, | |||||
}; | |||||
} | |||||
} | |||||
return fieldValues; | |||||
}; | |||||
/** | |||||
* Sets the values of all the fields within the form through accessing the DOM nodes. | |||||
* @param form - The form. | |||||
* @param values - The form values. | |||||
*/ | |||||
export const setFormValues = ( | |||||
form: HTMLFormElement, | |||||
values: ConstructorParameters<typeof URLSearchParams>[0] | Record<string, unknown>, | |||||
) => { | |||||
if (!form) { | |||||
throw new TypeError('Invalid form element.'); | |||||
} | |||||
const objectValues = new URLSearchParams(values as unknown as string | Record<string, string>); | |||||
const formElements = form.elements as unknown as Record<string | number, HTMLElement>; | |||||
const allFormFieldElements = Object.entries<HTMLElement>(formElements); | |||||
const indexedNamedEnabledFormFieldElements = allFormFieldElements.filter(([k, el]) => ( | |||||
!Number.isNaN(Number(k)) | |||||
&& isNamedEnabledFormFieldElement(el) | |||||
)) as [string, HTMLElementWithName][]; | |||||
indexedNamedEnabledFormFieldElements | |||||
.filter(([, el]) => objectValues.has(el.name)) | |||||
.forEach(([, el]) => { | |||||
// eslint-disable-next-line no-param-reassign | |||||
setFieldValue(el, objectValues.get(el.name)); | |||||
}); | |||||
}; | |||||
export default getFormValues | |||||
// Deprecated. Use named export instead. This default export is only for compatibility. | |||||
export default getFormValues; |
@@ -0,0 +1,3 @@ | |||||
export const setup = () => { | |||||
process.env.TZ = 'UTC' | |||||
} |
@@ -0,0 +1,20 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["ESNext", "DOM"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"baseUrl": ".", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"esModuleInterop": true, | |||||
"target": "es2018" | |||||
} | |||||
} |
@@ -1,20 +1,21 @@ | |||||
{ | { | ||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | "include": ["src", "types"], | ||||
"compilerOptions": { | "compilerOptions": { | ||||
"module": "esnext", | |||||
"lib": ["dom", "esnext"], | |||||
"types": ["vitest/globals"], | |||||
"module": "ESNext", | |||||
"lib": ["ESNext", "DOM"], | |||||
"importHelpers": true, | "importHelpers": true, | ||||
"declaration": true, | "declaration": true, | ||||
"sourceMap": true, | "sourceMap": true, | ||||
"strict": false, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"baseUrl": ".", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | "noUnusedLocals": true, | ||||
"noUnusedParameters": true, | "noUnusedParameters": true, | ||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | "moduleResolution": "node", | ||||
"esModuleInterop": true, | "esModuleInterop": true, | ||||
"skipLibCheck": true, | |||||
"forceConsistentCasingInFileNames": true, | |||||
"noEmit": true | |||||
"target": "es2018" | |||||
} | } | ||||
} | } |
@@ -0,0 +1,16 @@ | |||||
import { defineConfig } from 'vitest/config' | |||||
export default defineConfig({ | |||||
test: { | |||||
include: ['**/cypress/**/*.test.ts'], | |||||
exclude: [ | |||||
'**/node_modules/**', | |||||
'**/dist/**', | |||||
'**/.{idea,git,cache,output,temp}/**', | |||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', | |||||
], | |||||
globals: true, | |||||
environment: 'jsdom', | |||||
globalSetup: './test-globals.js', | |||||
}, | |||||
}) |