@thinknimble/tn-forms
Version:
Utilities for building front-end forms.
454 lines (434 loc) • 17.7 kB
text/typescript
import * as assert from 'assert'
import { Equals, Expect } from './type-utils';
import { FormArray, FormField, Form } from '../src/forms'
import { GetFormFieldNames, IFormArray, IFormField } from '../src/interfaces'
import {
EmailValidator,
MaxDateValidator,
MinDateValidator,
MinLengthValidator,
MustMatchValidator,
RequiredValidator,
TrueFalseValidator,
DynamicMinDateValidator,
UrlValidator,
} from '../src/validators'
import { ExtractFormFields } from '../src'
type IUserAddressForm = {
street: IFormField
city: IFormField
apartmentNumber: IFormField
}
type IUserForm = {
firstName: IFormField<string>
password: IFormField
confirmPassword: IFormField
dob: IFormField
email: IFormField
address: IFormArray<IUserAddressForm>
websiteUrl: IFormField<string>
}
type IFormNoAddress = {
address: FormArray<IUserAddressForm>
}
type ICrossFieldForm = {
usersName: IFormField
confirmName: IFormField<string>
}
type TCrossFieldForm = ICrossFieldForm & CrossFieldForm
type TUserForm = UserForm & IUserForm
type TUserAddressForm = UserAddressForm & IUserAddressForm
type TFormNoAddressInstance = IFormNoAddress & FormNoAddressInstance
class CrossFieldForm extends Form<ICrossFieldForm> {
static dynamicFormValidators = {
confirmName: [new MustMatchValidator({ matcher: 'usersName' })],
}
static usersName = new FormField()
static confirmName = new FormField()
}
class UserAddressForm extends Form<IUserAddressForm> {
static street = new FormField({ validators: [], value: 'this' })
static city = new FormField({
validators: [new MinLengthValidator({ minLength: 5 })],
})
static apartmentNumber = new FormField<string | null>({ value: null })
}
class FormNoAddressInstance extends Form<IFormNoAddress> {
static address = new FormArray<IUserAddressForm>({
name: 'address',
groups: [],
FormClass: FormNoAddressInstance,
})
}
const firstNamePlaceholder = 'Input your first name...'
const firstNameLabel = 'First name'
class UserForm extends Form<IUserForm> {
static firstName = new FormField({
validators: [new MinLengthValidator({ minLength: 5 })],
placeholder: firstNamePlaceholder,
label: firstNameLabel,
})
static email = new FormField({ validators: [new EmailValidator()] })
static password = new FormField({ validators: [new RequiredValidator()] })
static confirmPassword = new FormField({
validators: [new MinLengthValidator({ minLength: 5 })],
})
static dob = new FormField({
validators: [
new MinDateValidator({ min: new Date('10/20/2022') }),
new MaxDateValidator({ max: new Date('10/18/2026') }),
],
})
static address = new FormArray<IUserAddressForm>({
name: 'address',
groups: [new UserAddressForm()],
})
static websiteUrl = new FormField({
name: 'websiteUrl',
value: '',
validators: [
new UrlValidator({
code: 'invalidLink',
message: 'Invalid LInk',
isRequired: false,
}),
],
})
}
describe('Forms', () => {
describe('# Fields', () => {
it('should create a new field', () => {
let formField: IFormField = new FormField()
assert.equal(formField instanceof FormField, true)
})
})
describe('# Form setup', () => {
const userForm = new UserForm({ firstName: 'pari' }) as TUserForm
it('should have as many fields as static properties defined', () => {
assert.equal(userForm.fields.length, 7)
})
it('should have fields with keys of firstName', () => {
assert.equal(
userForm.fields.map((f: IFormField | IFormArray<any>) => f?.name).includes('firstName'),
true,
)
})
it('should set the default value of the userForm firstname to pari set on the instance creation', () => {
assert.equal(userForm.field['firstName'].value, 'pari')
})
it('should set firstName, password and confirm password to have validators (ignoring address for now)', () => {
userForm.fields.forEach((field) => {
if (field instanceof FormField) {
assert.equal((userForm.fields[0] as FormField).validators.length, 1)
assert.equal((userForm.fields[1] as FormField).validators.length, 1)
assert.equal((userForm.fields[2] as FormField).validators.length, 1)
}
})
})
it('should indicate that firstname has a validator', () => {
assert.equal(userForm.field['firstName'].validators.length, 1)
assert.equal(userForm.firstName.validators.length, 1)
})
it('should have a form array for address', () => {
assert.equal(userForm.address.groups.length, 1)
})
it('should have a validator for address', () => {
assert.equal(userForm.address.groups[0]!.field['city'].validators.length, 1)
})
it('should have a validator for address', () => {
assert.equal((userForm.address.groups[0] as TUserAddressForm).city.validators.length, 1)
})
it('should have 2 user address entries', () => {
userForm.address.add(new UserAddressForm())
assert.equal(userForm.address.groups.length, 2)
})
it('should set two different forms that do not share values', () => {
let userFormOriginal = new UserForm() as TUserForm
const originalFormAddresses = userFormOriginal.address.groups as TUserAddressForm[]
originalFormAddresses[0]!.street.value = 'testingoriginal'
originalFormAddresses[0]!.city.value = 'testingoriginal'
let userFormSecond = new UserForm() as TUserForm
const secondUserFormAddresses = userFormSecond.address.groups as TUserAddressForm[]
secondUserFormAddresses[0]!.street.value = 'testing'
assert.notEqual(
originalFormAddresses[0]!.street.value,
secondUserFormAddresses[0]!.street.value,
)
})
it('should create a form using the factory method', () => {
const userFormFact = UserForm.create() as TUserForm
assert.equal(userFormFact.fields.length, 7)
})
it('should load all the form array values and create a new instance for each using the FormClass type', () => {
const values = {
address: [{ street: 'testswdf', city: 'asdasdasdasd', apartmentNumber: '4B' }],
}
let testForm = new FormNoAddressInstance(values) as TFormNoAddressInstance
assert.equal(testForm.address.groups.length, 1)
})
it('should replicate the form exactly', () => {
const values = {
usersName: 'test',
confrimName: 'tests',
}
let testForm = new CrossFieldForm() as TCrossFieldForm
testForm.usersName.value = 'testing123'
testForm.confirmName.value = 'tiinngngngn'
testForm.validate()
assert.equal(testForm.confirmName.errors.length, 1)
let duplicateForm = testForm.replicate() as TCrossFieldForm
assert.equal(duplicateForm.confirmName.errors.length, 1)
testForm.confirmName.value = 'testing123'
testForm.validate()
assert.equal(testForm.confirmName.errors.length, 0)
assert.notEqual(testForm.confirmName.value, duplicateForm.confirmName.value)
assert.equal(duplicateForm.confirmName.errors.length, 1)
duplicateForm.validate()
assert.equal(duplicateForm.confirmName.errors.length, 1)
duplicateForm.confirmName.value = 'testing123'
duplicateForm.validate()
assert.equal(duplicateForm.confirmName.errors.length, 0)
})
it('should have label and placeholder on firstName field', () => {
assert.equal(userForm.firstName.placeholder, firstNamePlaceholder)
assert.equal(userForm.firstName.label, firstNameLabel)
})
})
type SomeDateFormInputs = {
min: IFormField<Date>
max: IFormField<Date>
}
class SomeDateForm extends Form<SomeDateFormInputs> {
static min = new FormField({ validators: [new MinDateValidator({ min: new Date() })] })
static max = new FormField({ value: null })
static dynamicFormValidators = {
max: [new DynamicMinDateValidator({ matcher: 'min', isRequired: false })],
}
}
type TSomeDateForm = SomeDateForm & SomeDateFormInputs
describe('# Form Validators', () => {
const userForm = new UserForm({ firstName: 'par' }) as TUserForm
let today = new Date()
let tomorrow = new Date()
tomorrow.setDate(today.getDate() + 1)
let yesterday = new Date()
yesterday.setDate(today.getDate() - 1)
const someDateFormInputs = new SomeDateForm({ min: today }) as TSomeDateForm
it('should set the field to invalid as it is below the min length ', () => {
assert.equal(userForm.firstName.isValid, false)
})
it('should set field to invalid as it is required', () => {
assert.equal(userForm.password.isValid, false)
})
it('should set field to invalid as date < minimum', () => {
userForm.dob.value = new Date('10/15/2022')
assert.equal(userForm.dob.isValid, false)
})
it('should set field to invalid as date > minimum', () => {
userForm.dob.value = new Date('10/25/2028')
assert.equal(userForm.dob.isValid, false)
})
it('should set the field to invalid as it is not a valid email', () => {
userForm.email.value = 'pb1636'
assert.equal(userForm.email.isValid, false)
})
it('should set the field to valid as it is above the min length ', () => {
userForm.firstName.value = 'Paribaker'
assert.equal(userForm.firstName.isValid, true)
})
it('should set field to valid as it is required', () => {
userForm.password.value = 'testing213'
assert.equal(userForm.password.isValid, true)
})
it('should set field to valid as date < minimum', () => {
userForm.dob.value = new Date('10/25/2022')
assert.equal(userForm.dob.isValid, true)
})
it('should set field to valid as date > minimum', () => {
userForm.dob.value = new Date('10/18/2025')
assert.equal(userForm.dob.isValid, true)
})
it('should set the field to valid as it a valid email', () => {
userForm.email.value = 'testing123@yahoo.com'
assert.equal(userForm.email.isValid, true)
})
it('should set the min date to valid since it is greater than today and max is not required', () => {
someDateFormInputs.min.value = today
assert.equal(someDateFormInputs.min.isValid, true)
assert.equal(someDateFormInputs.max.isValid, true)
})
it('should first validate the max date is higher than the min date', () => {
someDateFormInputs.max.value = tomorrow
console.log(someDateFormInputs.value)
assert.equal(someDateFormInputs.max.isValid, true)
})
it('should first validate the max date is lower than the min date', () => {
someDateFormInputs.max.value = yesterday
console.log(someDateFormInputs.value)
assert.equal(someDateFormInputs.max.isValid, false)
})
it('should not validate if it is not required and there is no value', () => {
assert.equal(userForm.websiteUrl.isValid, true)
})
})
describe('# Form entry & validation', () => {
const defaultAddress = 'test street'
const patchAddress = 'my street'
class UserAddressForm2 extends UserAddressForm {
static street = new FormField({ validators: [], value: defaultAddress })
}
it('should indicate the form is valid after entry', () => {
const userForm = new UserForm() as TUserForm
userForm.firstName.value = 'pariszcxczxczx'
userForm.email.value = 'pasdaad@gmail.com'
userForm.dob.value = new Date('10/26/2025')
userForm.password.value = '123456'
userForm.confirmPassword.value = '123456'
const userAddressForm = userForm.address.groups[0] as TUserAddressForm
userAddressForm.city.value = 'test1'
userAddressForm.street.value = 'yes1'
assert.equal(userForm.isValid, true)
})
let userAddressForm = new UserAddressForm() as TUserAddressForm
it('should declare the form invalid initially', () => {
assert.equal(userAddressForm.isValid, false)
})
it('should declare the form valid after entry', () => {
userAddressForm = new UserAddressForm({
street: 'decatur',
city: 'Santa Clara',
}) as TUserAddressForm
assert.equal(userAddressForm.street.isValid, true)
assert.equal(userAddressForm.city.isValid, true)
assert.equal(userAddressForm.isValid, true)
})
it('should prefill city and street with default values', () => {
const userAddressForm2 = new UserAddressForm2() as TUserAddressForm
assert.equal(userAddressForm2.street.value, defaultAddress)
assert.equal(
JSON.stringify(userAddressForm2.value),
JSON.stringify({ street: defaultAddress, city: '', apartmentNumber: null }),
)
})
it('should prefill values of form using patch value and override field default', () => {
const userAddressForm2 = new UserAddressForm2() as TUserAddressForm
userAddressForm2.street.value = patchAddress
assert.equal(userAddressForm2.street.value, patchAddress)
assert.equal(userAddressForm2.value.toString(), { street: patchAddress, city: '' }.toString())
})
it('should prefill values of form using ph value and override field default with formarrays', () => {
let value = {
firstName: 'lorem',
email: 'test@formstofill.com',
password: 'testing123',
confirmPassword: 'testing123',
dob: new Date('12/20/2022'),
websiteUrl: '',
address: [
{ street: 'testing', city: 'testing', apartmentNumber: null },
{ street: 'testing1', city: 'testing1', apartmentNumber: null },
],
}
let userForm1 = new UserForm({ ...value }) as TUserForm
assert.equal(JSON.stringify(userForm1.value), JSON.stringify(value))
})
it('should mark the field as invalid if the matching field is not the same', () => {
const values = {
usersName: 'pari',
confirmName: 'baker',
}
let newForm = new CrossFieldForm(values) as TCrossFieldForm
newForm.value.confirmName
assert.equal(newForm.confirmName.isValid, false)
newForm.validate()
assert.equal(newForm.confirmName.errors.length, 1)
newForm.confirmName.value = 'pari'
assert.equal(newForm.confirmName.isValid, true)
})
it('should mark the field as invalid first then valid, because TrueFalseValidator failed', () => {
type TUserFormTrue = IUserForm & { trueVal: IFormField<boolean> } & UserForm
class UserFormTrue extends UserForm {
static trueVal = new FormField({
value: true,
validators: [new TrueFalseValidator({ truthy: false })],
})
}
let userForm1 = new UserFormTrue() as TUserFormTrue
assert.equal(userForm1.trueVal.isValid, false)
userForm1.trueVal.value = false
assert.equal(userForm1.trueVal.isValid, true)
})
it('should mark the field as valid and ignore the validator', () => {
type TUserFormTrue = IUserForm & { trueVal: IFormField<boolean> } & UserForm
class UserFormTrue extends UserForm {
static trueVal = new FormField({
value: true,
validators: [new TrueFalseValidator({ isRequired: false })],
})
}
let userForm1 = new UserFormTrue() as TUserFormTrue
assert.equal(userForm1.trueVal.isValid, true)
})
it('should allow setting a field value to null', () => {
const nullField = new FormField({ value: null })
assert.equal(nullField.value, null)
})
})
describe('# Form array functions', () => {
it('should add another formgroup to the formarray', () => {
let userAddressForm = new UserForm() as TUserForm
userAddressForm.address.add(new UserAddressForm())
assert.equal(userAddressForm.address.groups.length, 2)
})
it('should remove a formgroup from the formarray', () => {
let userAddressForm = new UserForm() as TUserForm
userAddressForm.address.remove(0)
assert.equal(userAddressForm.address.groups.length, 0)
})
})
// Although TS is compile time and this actually would just probably be a no-op we can get the TS errors here something messed up the types
describe('# TS tests', () => {
it('Checks name types of fields() and field()', () => {
const motivationName = 'motivationName'
const ageName = 'ageName'
const myFields = {
motivation: new FormField({
name: motivationName,
value: '',
}),
age: new FormField({
name: ageName,
}),
}
class MyFormImpl extends Form<typeof myFields> {}
const stuff = new MyFormImpl()
const result = stuff.fields.map((myField) => {
type shouldMatchName = Expect<
Equals<typeof myField['name'], typeof motivationName | typeof ageName>
>
return myField
})
type shouldHaveProperName = Expect<
Equals<typeof stuff['field']['motivation']['name'], typeof motivationName>
>
})
})
describe('# ExtractFormFields', () => {
class MyForm extends Form<ExtractFormFields<typeof MyForm>> {
static name = new FormField()
static age = new FormField<number>()
}
type TMyForm = MyForm & ExtractFormFields<typeof MyForm>
it('should extract the form fields', () => {
let myForm = new MyForm() as TMyForm
assert.equal(myForm.name instanceof FormField, true)
assert.equal(myForm.age instanceof FormField, true)
})
it('should have age as FormField<number>', () => {
let myForm = new MyForm() as TMyForm
type AgeFieldType = typeof myForm.age
type ExpectedType = FormField<number>
type isCorrectType = [Expect<Equals<AgeFieldType, ExpectedType>>]
})
})
})