Browse Source

Switch to pridepack, add setFormValues

Implement setFormValues for easy setting of fields.

Also migrated to pridepack because tsdx is already unmaintained.
master
TheoryOfNekomata 1 year ago
parent
commit
cc3aa1df91
22 changed files with 4033 additions and 7458 deletions
  1. +16
    -0
      .eslintrc
  2. +0
    -1
      .npmignore
  3. +4
    -18
      LICENSE
  4. +19
    -9
      README.md
  5. +3
    -1
      cypress.json
  6. +38
    -25
      cypress/integration/checkbox.test.ts
  7. +76
    -48
      cypress/integration/file.test.ts
  8. +117
    -34
      cypress/integration/misc.test.ts
  9. +23
    -17
      cypress/integration/select.test.ts
  10. +37
    -28
      cypress/integration/submitter.test.ts
  11. +91
    -38
      cypress/integration/text.test.ts
  12. +28
    -5
      cypress/utils/index.ts
  13. +46
    -6
      cypress/utils/jsdom-compat.ts
  14. +0
    -3
      jest.config.js
  15. +76
    -78
      package.json
  16. +3
    -0
      pridepack.json
  17. +529
    -197
      src/index.ts
  18. +3
    -0
      test-globals.js
  19. +20
    -0
      tsconfig.eslint.json
  20. +9
    -8
      tsconfig.json
  21. +16
    -0
      vitest.config.js
  22. +2879
    -6942
      yarn.lock

+ 16
- 0
.eslintrc View File

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

+ 0
- 1
.npmignore View File

@@ -4,7 +4,6 @@ src/
.editorconfig
.prettierrc
cypress.json
jest.config.js
publish.sh
tsconfig.json
yarn.lock

+ 4
- 18
LICENSE View File

@@ -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.

+ 19
- 9
README.md View File

@@ -2,7 +2,7 @@

_(read "form extra")_

Extract form values through the DOM.
Extract and set form values through the DOM.

## 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
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,
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=""`
attribute](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form))
* 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

@@ -68,23 +70,31 @@ For an example form:
Use the library as follows (code is in TypeScript, but can work with JavaScript as well):

```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');

// 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
const submitter = form.querySelector('[type="submit"][name="type"][value="client"]');

const values = getFormValues(form, { submitter });

const processResult = (result: Record<string, unknown>) => {
setFormValues(form, {
username: 'Username',
password: 'verylongsecret',
});
throw new Error('Not yet implemented.');
};

// Best use case is with event handlers
form.addEventListener('submit', async e => {
const { target: form, submitter } = e;
const { currentTarget: form, submitter } = e;
e.preventDefault();

const values = getFormValues(form, { submitter });


+ 3
- 1
cypress.json View File

@@ -1 +1,3 @@
{}
{
"video": false
}

+ 38
- 25
cypress/integration/checkbox.test.ts View File

@@ -1,4 +1,4 @@
import getFormValues from '../../src'
import { getFormValues } from '../../src'
import * as utils from '../utils'

describe('checkbox', () => {
@@ -23,27 +23,37 @@ describe('checkbox', () => {
`))

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', () => {
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', () => {
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',
});
});
})
})

+ 76
- 48
cypress/integration/file.test.ts View File

@@ -1,4 +1,4 @@
import getFormValues from '../../src'
import { getFormValues } from '../../src'
import * as utils from '../utils'

describe('file', () => {
@@ -23,52 +23,65 @@ describe('file', () => {
`))

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', () => {
utils.test(
(cy: any) => {
utils.test({
action: (cy: any) => {
cy
.get('[name="hello"]')
.attachFile('uploads/data.json')

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',
}
);
},
});
})

it('should retrieve the file list upon setting appropriate option', () => {
utils.test(
(cy: any) => {
utils.test({
action: (cy: any) => {
cy
.get('[name="hello"]')
.attachFile('uploads/data.json')

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', () => {
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', () => {
utils.test(
(cy: any) => {
utils.test({
action: (cy: any) => {
cy
.get('[name="hello"]')
.attachFile(['uploads/data.json', 'uploads/data2.json'])

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', () => {
utils.test(
(cy: any) => {
utils.test({
action: (cy: any) => {
cy
.get('[name="hello"]')
.attachFile(['uploads/data.json', 'uploads/data2.json'])

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')
},
);
});
})
})
})

+ 117
- 34
cypress/integration/misc.test.ts View File

@@ -1,4 +1,4 @@
import getFormValues from '../../src'
import { getFormValues, setFormValues } from '../../src';
import * as utils from '../utils'

describe('misc', () => {
@@ -19,15 +19,18 @@ describe('misc', () => {
`))

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>
</div>
</fieldset>
<div>
<input type="date" placeholder="Birthday" name="birthday" />
</div>
<div>
<select name="civil_status">
<option value="">Select Civil Status</option>
@@ -80,12 +86,18 @@ describe('misc', () => {
New Registration
</label>
</div>
<div>
<input type="datetime-local" placeholder="Appointment Date/Time" name="appointment_datetime" />
</div>
<div>
<label>
<input type="checkbox" value="filipino" name="nationality" />
Filipino
</label>
</div>
<div>
<input type="number" placeholder="Gross Salary" name="gross" />
</div>
<fieldset>
<legend>
Default Dependents
@@ -109,6 +121,12 @@ describe('misc', () => {
<div>
<textarea name="notes" placeholder="Notes"></textarea>
</div>
<div>
<label>
Quality of Service
<input type="range" min="0" max="10" placeholder="Quality of Service" name="qos" />
</label>
</div>
<div>
<button name="submit" value="Hello" type="submit">Hello</button>
<button name="submit" value="Hi" type="submit">Hi</button>
@@ -134,35 +152,100 @@ describe('misc', () => {
`))

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"]')
},
(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&notes=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&notes=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&notes=Test+content%0D%0A%0D%0ANew+line%0D%0A%0D%0AAnother+line&qos=10&submit=Hi',
});
});
})
})

+ 23
- 17
cypress/integration/select.test.ts View File

@@ -1,4 +1,4 @@
import getFormValues from '../../src'
import { getFormValues } from '../../src'
import * as utils from '../utils'

describe('select', () => {
@@ -28,15 +28,18 @@ describe('select', () => {
`))

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', () => {
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',
}
);
});
});
})
})

+ 37
- 28
cypress/integration/submitter.test.ts View File

@@ -1,4 +1,4 @@
import getFormValues from '../../src'
import { getFormValues } from '../../src'
import * as utils from '../utils'

describe('submitter', () => {
@@ -24,18 +24,21 @@ describe('submitter', () => {
`))

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',
action: 'Foo',
}
);
},
});
});
})

@@ -61,18 +64,21 @@ describe('submitter', () => {
`))

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',
action: 'Bar',
}
);
},
});
});
})

@@ -96,17 +102,20 @@ describe('submitter', () => {
`))

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',
}
);
},
});
});
})
})

+ 91
- 38
cypress/integration/text.test.ts View File

@@ -1,4 +1,4 @@
import getFormValues from '../../src'
import { getFormValues, setFormValues } from '../../src';
import * as utils from '../utils'

describe('text', () => {
@@ -23,17 +23,20 @@ describe('text', () => {
`))

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',
}
);
},
});
});
})

@@ -61,15 +64,18 @@ describe('text', () => {
`))

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', () => {
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',
}
);
},
});
});
})
});

describe('readonly', () => {
beforeEach(utils.setup(`
@@ -132,17 +141,61 @@ describe('text', () => {
`))

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',
}
);
},
});
});
})
});

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',
},
});
});
});
})

+ 28
- 5
cypress/utils/index.ts View File

@@ -4,7 +4,7 @@ import JSDOMDummyCypress from './jsdom-compat'

type ExpectedSearchValue = Record<string, string> | string

type RetrieveSubmitterFn = (wrapper: any) => any
type RetrieveSubmitterFn = (wrapper: typeof cy | JSDOMDummyCypress) => any

type HTMLSubmitterElement = HTMLButtonElement | HTMLInputElement

@@ -18,13 +18,27 @@ export const setup = (template: string) => {
}
}
return () => {
// @ts-ignore
window.document.open(undefined, undefined, undefined, true)
window.document.write(template)
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 submitter: HTMLButtonElement | HTMLInputElement
let r: any
@@ -34,6 +48,10 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e
.get('form')
.then((formResult: any) => {
[form] = Array.from(formResult);

if (typeof preAction === 'function') {
preAction(form);
}
})

r = retrieveSubmitterFn(cy)
@@ -41,7 +59,7 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e
[submitter] = Array.from(submitterQueryEl as any[])
})

if (typeof expectedValue !== 'undefined') {
if (typeof expectedStaticValue !== 'undefined') {
r.click()
cy
.wait('@submitted')
@@ -61,10 +79,15 @@ export const test = (retrieveSubmitterFn: RetrieveSubmitterFn, testFn: TestFn, e
.then((submitterQueryEl: any) => {
[submitter] = Array.from(submitterQueryEl as any[]);
[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()
}
}


+ 46
- 6
cypress/utils/jsdom-compat.ts View File

@@ -1,7 +1,7 @@
/// <reference types="node" />

import { readFileSync } from 'fs'
import { join } from 'path'
import { readFileSync, statSync } from 'fs'
import { join, basename } from 'path'

class JSDOMJQuery {
private selectedElements: Node[]
@@ -16,6 +16,11 @@ class JSDOMJQuery {
el.value = s
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.value = s
})
@@ -57,17 +62,52 @@ class JSDOMJQuery {
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
}

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 {
private currentElement = window.document;

wait(time: number) {
return this;
}

get(q: string) {
return new JSDOMJQuery(this.currentElement.querySelectorAll(q));
}


+ 0
- 3
jest.config.js View File

@@ -1,3 +0,0 @@
module.exports = {
testEnvironment: 'jsdom',
};

+ 76
- 78
package.json View File

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

+ 3
- 0
pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 529
- 197
src/index.ts View File

@@ -2,48 +2,50 @@
* Line ending.
*/
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.
* @param el - The element.
*/
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.
*/
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.
* @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.
*/
type GetSelectValueOptions = {}
type GetSelectValueOptions = PlaceholderObject

/**
* Gets the value of a `<select>` element.
@@ -68,15 +86,37 @@ type GetSelectValueOptions = {}
* @param options - The options.
* @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.
@@ -86,7 +126,7 @@ export type HTMLInputRadioElement = HTMLInputElement & { type: 'radio' }
/**
* Options for getting an `<input type="radio">` element field value.
*/
type GetInputRadioFieldValueOptions = {}
type GetInputRadioFieldValueOptions = PlaceholderObject

/**
* Gets the value of an `<input type="radio">` element.
@@ -94,15 +134,32 @@ type GetInputRadioFieldValueOptions = {}
* @param options - The options.
* @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.
@@ -113,13 +170,13 @@ export type HTMLInputCheckboxElement = HTMLInputElement & { type: 'checkbox' }
* Options for getting an `<input type="checkbox">` element field value.
*/
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.
*/
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.
@@ -157,10 +255,11 @@ export type HTMLInputFileElement = HTMLInputElement & { type: 'file' }
* Options for getting an `<input type="file">` element field value.
*/
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.
* @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.
*/
type GetInputFieldValueOptions
= GetInputCheckboxFieldValueOptions
& GetInputFileFieldValueOptions
& GetInputRadioFieldValueOptions
= GetInputCheckboxFieldValueOptions
& GetInputFileFieldValueOptions
& GetInputRadioFieldValueOptions
& GetInputNumberFieldValueOptions
& GetInputDateFieldValueOptions

/**
* Gets the value of an `<input>` element.
@@ -198,27 +425,77 @@ type GetInputFieldValueOptions
* @param options - The options.
* @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.
*/
type GetFieldValueOptions
= GetTextAreaValueOptions
& GetSelectValueOptions
& GetInputFieldValueOptions
= GetTextAreaValueOptions
& GetSelectValueOptions
& GetInputFieldValueOptions

type HTMLElementWithName
= (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement);

/**
* Gets the value of a field element.
@@ -227,20 +504,44 @@ type GetFieldValueOptions
* @returns Value of the field element.
*/
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.
@@ -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.
*/
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.
*/
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.
* @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;

+ 3
- 0
test-globals.js View File

@@ -0,0 +1,3 @@
export const setup = () => {
process.env.TZ = 'UTC'
}

+ 20
- 0
tsconfig.eslint.json View File

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

+ 9
- 8
tsconfig.json View File

@@ -1,20 +1,21 @@
{
"exclude": ["node_modules"],
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"types": ["vitest/globals"],
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"strict": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true
"target": "es2018"
}
}

+ 16
- 0
vitest.config.js View File

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

+ 2879
- 6942
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save