TN Forms
Installation
npm i -S @thinknimble/tn-forms
Intro
Tn Forms was created to provide a consistent mechanism for creating forms in webapps within teams. By streamlining the form creation process the aim is to improve code cohesion and interoprability within a team. Anyone picking up the code base should be able to easily create and update forms with little thought beyond the how-to of these forms.
The library supports creating simple forms, dynamic forms (using form arrays) or using standalone fields. It also includes a rich set of validators that can be easily extended to create new validators on the fly.
The library is written in Typescript and exposes all of the types used to build the forms and can be used with pure JS
Sample Forms
import Form, { FormField, IFormField, RequiredValidator } from '@thinknimble/tn-forms'
export type LoginFormInputs = {
email: IFormField<string>
password: IFormField<string>
}
export class LoginForm extends Form<LoginFormInputs>{
static email = FormField.create({validators:[new EmailValidator({message:"Please enter a valid email address to login"})]})
static password = FormField.create({validators:[new RequiredValidator({message: "Please enter a password to login"})]})
}
export type TLoginForm = LoginFormInputs & LoginForm
Standalone Fields
import Form, {
FormField,
MinLengthValidator,
RequiredValidator,
EmailValidator,
} from '@thinknimble/tn-forms'
let email:IFormField = new FormField({value:"Init Value", validators:[new RequiredValidator()], name:'email',id:"my-field",label:"email label"})
email.value
email.isValid
email.validate()
email.errors
Sample User form with Cross Field Validation and Form Arrays
export type UserFormInputs = {
firstName: IFormField<string>
password: IFormField<string>
confirmPassword: IFormField<string>
dob: IFormField<string>
email: IFormField<string>
address: IFormArray<IUserAddressForm>
}
class UserForm extends Form<UserFormInputs> {
static firstName = new FormField({ validators: [new MinLengthValidator({ minLength: 5 })] })
static email = new FormField({ validators: [new EmailValidator()], label:"Email" })
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 dynamicFormValidators = {
confirmPassword: [new MustMatchValidator({ matcher: 'password' })],
}
}
const userForm = new UserForm()
userForm.validate()
userForm.isValid
userForm.value
userForm.value.firstName
<label :for="userForm.firstName.label">{{userForm.firstName.label}}</label>
<input type="text" :name="userForm.firstName.name" :placeholder="userForm.firstName.name" v-model="userForm.firstName.value" />
To use shorthand field method (access a field with dot notation) you need to decalre a union type of the form and its interface
type TUserForm = UserForm & UserFormInputs
const userForm = new UserForm() as TUserForm
this will give you direct access to the fields as properties of the class
<label :for="userForm.firstName.label">{{userForm.firstName.label}}</label>
<input type="text" :name="userForm.firstName.name" :placeholder="userForm.firstName.name" v-model="userForm.firstName.value" />
Dynamic Form with form arrays
export type UserAddressFormInputs = {
street: IFormField
city: IFormField
}
class UserAddressForm extends Form<UserAddressFormInputs> {
static street = new FormField({ validators: [], value: 'this', label:"Street" })
static city = new FormField({ validators: [new MinLengthValidator({ minLength: 5 })] })
}
<label :for="userForm.firstName.label">{{userForm.firstName.label}}</label>
<input type="text" :name="userForm.firstName.name" :placeholder="userForm.firstName.name" v-model="userForm.firstName.value" />
<label :for="userForm.adress.groups[0].street.label">{{userForm.adress.groups[0].street.label}}</label>
<input type="text" :name="userForm.adress.groups[0].street.name" :placeholder="userForm.adress.groups[0].street.name" v-model="userForm.adress.groups[0].street.value" />
Add Dynamic validators on the fly
type TUserForm = UserForm & UserFormInputs
const userForm = new UserForm() as TUserForm
userForm.addFormLevelValidator("firstName",new MinLengthValidator())
Javascript
Stanalone Fields
import { FormField, IFormField, RequiredValidator,FormArray, IFormArray,IFormField } from '@thinknimble/tn-forms'
let email = new FormField({value:"Init Value", validators:[new RequiredValidator()], name:'email',id:"my-field",label:"email label"})
email.value
email.isValid
email.validate()
email.errors
Basic Form
class UserForm extends Form {
static firstName = new FormField({ validators: [new MinLengthValidator({ minLength: 5 })] })
static email = new FormField({ validators: [new EmailValidator()], label:"Email" })
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({
name: 'address',
groups: [new UserAddressForm()],
})
static dynamicFormValidators = {
confirmPassword: [new MustMatchValidator({ matcher: 'password' })],
}
}
const userForm = new UserForm()
userForm.validate()
userForm.isValid
userForm.value
userForm.value.firstName
<label :for="userForm.firstName.label">{{userForm.firstName.label}}</label>
<input type="text" :name="userForm.firstName.name" :placeholder="userForm.firstName.name" v-model="userForm.firstName.value" />
const userForm = new UserForm()
<label :for="userForm.firstName.label">{{userForm.firstName.label}}</label>
<input type="text" :name="userForm.firstName.name" :placeholder="userForm.firstName.name" v-model="userForm.firstName.value" />
Dynamic Form with form arrays
class UserAddressForm extends Form {
static street = new FormField({ validators: [], value: 'this', label:"Street" })
static city = new FormField({ validators: [new MinLengthValidator({ minLength: 5 })] })
}
<label :for="userForm.firstName.label">{{userForm.firstName.label}}</label>
<input type="text" :name="userForm.firstName.name" :placeholder="userForm.firstName.name" v-model="userForm.firstName.value" />
<label :for="userForm.adress.groups[0].street.label">{{userForm.adress.groups[0].street.label}}</label>
<input type="text" :name="userForm.adress.groups[0].street.name" :placeholder="userForm.adress.groups[0].street.name" v-model="userForm.adress.groups[0].street.value" />
Add Dynamic validators on the fly
const userForm = new UserForm()
userForm.addFormLevelValidator("firstName",new MinLengthValidator())
Validators
validators can be added to forms they all extend the base Validator class each validator can have its own additional variables plus 3 common ones.
code
a unique code for the validator
message
a unique message for the validator
isRequired
isRequired will only validate a field if there is a value
RequiredValidator
Validates a field is not null, undefiend or empty
new RequiredValidator()
MinLengthValidator
Validates a field has a certain minimum length (if the value of the field is an array it will check arary length)
new MinLengthValidator({minLength: int})
MinDateValidator/MaxDateValidator
Validates a field has a certain minimum/maxium date (this is a static validator)
new MinDateValidator/MaxDateValidator({min/max: str|date})
MinimumValueValidator/MaximumValidator
Validates a field has a certain minimum/maxium value (this is a static validator)
new MinValueValidator/MaxValueValidator({min/max: str|int})
PatternValidator
Validates a field matches a pattern
new PatternValidator({pattern:str/<Regex>})
UrlValidator
Validates a field has a link pattern (ftp/http/https)
new UrlValidator()
MustMatchValidator
Validates a field matches another field
new MustMatchValidator({matcher:<string-field-name>})
TrueFalseValidator
Validates a field is true or false depending on true false value
new MustMatchValidator({truthy:boolean})
Custom Validators
The validators class is easily extendable and allows you to create your own validators on the fly
Simple Validator
import {Validator, notNullOrUndefined} from '@thinknimble/tn-forms'
export class MyValidator extends Validator {
// if you intend to override the default variables message & code define a constructor with a call to super
// you can pass additional variables as well
valueToEquals = null
constructor({ message = 'This is a required field', code = 'required', isRequired=true, valueToEqual=null } = {}) {
super({ message, code, isRequired })
this.valueToEquals = valueToEqual
}
// override if needed
get enableValidate() {
return this.isRequired
}
// caller method that gets executed by the validate method
call(value: any) {
if (!this.enableValidate && !notNullOrUndefined(value)) {
return
}
// you can use any of the provided utility functions
if (!notNullOrUndefined(value)) {
throw new Error(JSON.stringify({ code: this.code, message: this.message }))
} else if (value !== valueToEqual) {
throw new Error(JSON.stringify({ code: this.code, message: this.message }))
}
}
}
Dynamic Validator
export class MustMatchValidator extends Validator {
matcher: string | null
#matchingField: any
constructor({ message = 'Value must match', code = 'mustMatch', isRequired=true, matcher = '' } = {}) {
super({ message, code, isRequired })
this.matcher = matcher
}
// override if needed
// set matching field is required to set dynamically follow the matching field's value
setMatchingField(form: IForm<any>) {
if (this.matcher && form.field[this.matcher]) {
this.#matchingField = form.field[this.matcher]
return
}
throw new Error('Matching Field does not exist on form')
}
// override if needed
get matchingVal() {
return this.#matchingField ? this.#matchingField.value : null
}
// override if needed
get enableValidate() {
return this.isRequired
}
call(value: any) {
if (!this.enableValidate && !notNullOrUndefined(value)) {
return
}
if (this.matchingVal !== value) {
throw new Error(
JSON.stringify({
code: this.code,
message: `${this.message}`,
}),
)
}
}
}
Future Enhancments
- Add Dynamic validator types to the form class to handle on its own
- Make dynamic versions of min/max value/date validators
- Add async validators
- Add field accessor (to reduce verbosity) formInstance.formField should act as formInstance.field.formField
- Add additional options for form fields (placeholder, id, type, etc) to let users loop over formInstance.fields accessor
- add reset form function which re-applies initial value from form class to instance
- (optional) Add vue and react directives (framework)
- (optional) Add vue and react components (framework)
Change Log
v3.1.1 release date 01/04/2023
- Prevent value to be null rather allow it to be undefined, so that we don't force users to coalesce their values when assigning to inputs
v3.1.0 release date 01/04/2023
- Removed es7 private variables to accomodate Vue3 Proxies
v3.0.0 release date 01/04/2023
- TN-Forms built with typescript
- Removed adding dynamic validators to formarrays
v1.0.7 release date 06/13/2021
- Dynamic Validators can now be added to the form with the static variable dynamicFormValidators
- dynamicFormValidators is a reserved keyword for dynamic form level validators
v1.0.8 release date 07/13/2021
- Update to class copy method for bugfix array values in memory
v1.0.9 release date 07/13/2021
v1.0.10 release date 07/13/2021
- Issue with building new code
v2.0 release date 07/13/2021
- Moved tn-validators to this package
v2.0.1 & v2.0.2 & v2.0.3 release date 11/11/2021
v2.0.4 release date 11/11/2021
- Added Url and Pattern validators
v2.0.5 release date 11/11/2021
- fields as direct properties
v2.0.6 release date 11/11/2021
- bug in max/min value validator fixed
v2.0.7 release date 11/15/2021
- bug in field value was incorrectly being set for FormArrays. Error was triggered because kwargs will now contain direct assignment of field (this error was pre-existing but did not trigger)
v2.0.8 release date 11/30/2021
- bug error for value fixed
v2.0.9 release date 11/30/2021
- bug error for value fixed
v2.0.0 release date 07/13/2021
- Moved tn-validators to this package
v2.1.0 release date 06/14/2022
- Changed from momentjs to luxon
- Updated babel and webpack to resolve chokidar security vuln.