npm i -S @thinknimble/tn-forms
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'
/**
* Step #1: Build the type annotation for the form inputs - this will be used to infer the value of the form
* Important Note: Since the interface of the form fields is IFormField and value is not required any type will be automatically cast to <T>|Undefined
*
*/
export type LoginFormInputs = {
email: IFormField<string>
password: IFormField<string>
}
/**
* Step #2: Build the form, this should extend the BaseForm class and be given a the input types,
* the input types declared in step one will be used to infer the form value's type
*
*
*/
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' })],
})
}
/**
*
* Step #3: Create a union type this is an optional step that enables dot method notation on your form to access fields directly
* e.g const loginForm = new LoginForm() as TLoginForm
* loginForm.email
*
*/
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',
})
// if and id or name are not provided one will be generated automatically
// All fields are optional
// get the value
email.value
// check if the field is valid (calls the validate method silently)
email.isValid
// validate the field this will trigger the error handler and add errors to the field
email.validate()
//get the errors (must call the validate method first)
email.errors
Sample User form with Cross Field Validation and Form Arrays
// Build the interface to retrieve th fields in dot notation
// optionally provide a type for the value of each field IFormField<type> any is used as a default
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') }),
],
})
// collecttion of sub forms inside the main form -- see next example
static address = new FormArray<IUserAddressForm>({
name: 'address',
groups: [new UserAddressForm()],
})
// add cross field validators to the dynamicFormValidators object
// This implementation is a bit more complex for ReactJS and ReactNative (see the accompanying tn-forms-react module)
static dynamicFormValidators = {
confirmPassword: [new MustMatchValidator({ matcher: 'password' })],
}
}
//initialize the form
const userForm = new UserForm()
// validate the form
userForm.validate()
// check if the form is valid
userForm.isValid
// get the value as an object
userForm.value
//get an individial value
userForm.value.firstName
//add a formfield to the input
<!-- Vue.js -->
<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
<!-- Vue.js -->
<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 })] })
}
<!-- Vue.js -->
<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
// This method is useful for adding dynamic validators on the fly in response to other fields
// Note for react this method is preferred to confrom to react's deep object mutability
type TUserForm = UserForm & UserFormInputs
const userForm = new UserForm() as TUserForm
userForm.addFormLevelValidator('firstName', new MinLengthValidator())
Javascript
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',
})
// if and id or name are not provided one will be generated automatically
// All fields are optional
// get the value
email.value
// check if the field is valid (calls the validate method silently)
email.isValid
// validate the field this will trigger the error handler and add errors to the field
email.validate()
//get the errors (must call the validate method first)
email.errors
// Build the interface to retrieve th fields in dot notation
// optionally provide a type for the value of each field IFormField<type> any is used as a default
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') }),
],
})
// collecttion of sub forms inside the main form -- see next example
static address = new FormArray({
name: 'address',
groups: [new UserAddressForm()],
})
// add cross field validators to the dynamicFormValidators object
static dynamicFormValidators = {
confirmPassword: [new MustMatchValidator({ matcher: 'password' })],
}
}
//initialize the form
const userForm = new UserForm()
// validate the form
userForm.validate()
// check if the form is valid
userForm.isValid
// get the value as an object
userForm.value
//get an individial value
userForm.value.firstName
//add a formfield to the input
<!-- SEE NOTE FOR SHORHAND METHOD RECOMMENDED-->
<!-- Vue.js -->
<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()
<!-- Vue.js -->
<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"
/>
class UserAddressForm extends Form {
static street = new FormField({ validators: [], value: 'this', label: 'Street' })
static city = new FormField({ validators: [new MinLengthValidator({ minLength: 5 })] })
}
<!-- Vue.js -->
<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"
/>
// This method is useful for adding dynamic validators on the fly in response to other fields
// Note for react this method is preferred to confrom to react's deep object mutability
const userForm = new UserForm()
userForm.addFormLevelValidator('firstName', new MinLengthValidator())
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})
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}`,
}),
)
}
}
}
- 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)
- Fix missing export issue
- Always try to convert to real date
- Fix exports bug
- Dynamic Minimum Date Validator was added.
- Export FormLevelValidator class.
- Bump package versions to address security issues in dependencies.
- 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
- Removed es7 private variables to accomodate Vue3 Proxies
- TN-Forms built with typescript
- Removed adding dynamic validators to formarrays
- Dynamic Validators can now be added to the form with the static variable dynamicFormValidators
- dynamicFormValidators is a reserved keyword for dynamic form level validators
- Update to class copy method for bugfix array values in memory
- Bugfix for v1.0.8
- Issue with building new code
- Moved tn-validators to this package
- Re-organized files
- Added Url and Pattern validators
- fields as direct properties
- bug in max/min value validator fixed
- 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)
- bug error for value fixed
- bug error for value fixed
- Moved tn-validators to this package
- Changed from momentjs to luxon
- Updated babel and webpack to resolve chokidar security vuln.