An internal form library designed to integrate into our hubspot React Extension UI projects. This package simplifies nested form creation by using a config-driven approach, making it easy to manage and validate forms by scope.
To Install the package, run:
npm install @hubspot-form
Understanding Form-config concepts
// formState is a dictionary
const formState = {
// user_info is a scope on formState dictionary
user_info: {
fields: {
first_name: createField({
name: first_name,
type: text
}),
// array field
addresses: [
{
fields: {
type: createField({
name: type,
type: select,
options: [
{ label: "Permanent", value: "Permanent" },
{ label: "Mailing", value: "Mailing" }
]
}),
street: createField({
name: street,
type: text,
}),
unit_n: createField({
name: unit_n,
type: text,
required: false,
})
},
// nested scope validation
scope_validation: {
isValid: false,
}
}
],
},
// user_info scope validation
scope_validation: {
isValid: false,
}
}
}
// UI usage
<Scope name="user_info">
<CustomInput name="first_name">
{formState.user_info.fields.addresses.map((address, index) => (
<>
<Scope name="fields.addresses" index={index}>
<CustomInput name="type">
<CustomInput name="street">
<CustomInput name="unit_n">
</Scope>
<Button onClick={() => {
// remove address item
handleDeleteItem(
{
scope: "user_info.fields.",
name: "addresses",
index
}
)
}}>
Remove Address
</Button>
</>
))}
<Button onClick={() => {
handleNewItem(
{
scope: "user_info.fields",
name: "addresses",
data: {}
}
)
}}>
Add new Address
</Button>
</Scope>
<Button
isDisabled={!isValidScope(formState, "user_info")}
onClick={() => {
const { data } = handleSubmit()
// { first_name: "John Doe", addresses: [] }
}}>
Add new Address
</Button>
// feature functions:
import {
createField,
handleNewItem,
handleUpdateField,
handleResetScope,
handleDeleteItem,
handleSubmit,
isScopeValid,
} from '@owm-hubspot-form';
// create fields fn is necessary to create a new
// this function will infer FormField type.
first_name: createField({
name: "first_name",
type: "text",
label: "First Name",
defaultValue: contact?.first_name,
validate: (value) => {},
visibility: (state, scope, parent) => {},
rules: {
minDigits: 0,
maxDigits: 0,
isEmai: true
},
}),
// handleNewItem fn will add a item in one array field:
// on this function we are adding a new task on scope "board",
// field > in-progress which is an array of tasks
handleNewItem({
scope: "board",
name: "in-progress",
data: {
task: "new task",
priority: "2",
id: new Date().toString()
}
})
// handleUpdateField is used to update field properties dynamically
// in this example we are updating last_name on scope user_info
// to be dynamically required.
handleUpdateField({
scope: "user_info",
name: "last_name",
data: {
...user_info.fields.middle_name,
required: true,
}
})
// Fn is required to reset the fields values on that particular scope
handleResetScope({
scope: "user_info",
});
// handleSubmit fn will return all your scope values with:
// data: {properties: { first_name: "value" }}
const response = handleSubmit()
// handleInputChange fn will return all your scope values with:
// data: {properties: { first_name: "value" }}
handleInputChange({
scope:, // string
name, // string
value, // fieldValues
});
import React, { useEffect, useState } from "react";
import {
FormProvider,
createField,
type FormState,
handleSubmit
} from '@owm-hubspot-form';
const MOCKED_USER_DATA = {
first_name: "John"
middle_name: null,
last_name: "Doe",
banks: [
{
id: new Date(),
institution: "",
account: "",
transit_n: ""
agenc_n: "",
}
]:
}
// Create your first scope config "user_info" to support your mocked data
const getUserInfoFields = (contact: typeof MOCKED_USER_DATA) => {
return {
fields: {
// normal field
first_name: createField({
name: "first_name",
type: "text",
defaultValue: contact?.first_name
}),
// not required field
middle_name: createField({
name: "middle_name",
type: "text",
required: false,
defaultValue: contact?.middle_name
}),
// visibility field by "scope"
last_name: createField({
name: "last_name",
type: "text",
required: false,
defaultValue: contact?.last_name
visibility: (state, scope) => {
// state is your whole form config obj
// scope is your current scope fields "user_info"
return scope.fields.first_name.value
}
}),
// array field on scope
// here you could loop your banks data and returns the array of items
banks: [
{
fields: {
institution: createField({
name: "institution",
type: "text",
}),
account: createField({
name: "account",
type: "text",
})
transit_n: createField({
name: "transit_n",
type: "text",
// example of validation
validate: (value => {
if(value && value?.length >= 6 ) {
return "Value should have max: 6 digits"
}
})
})
agency_n: createField({
name: "agency_n",
type: "text",
validate: (value => {
if(value && value?.length >= 4) {
return "Value should have max: 4 digits"
}
}),
// rules feature is still in dev mode, for now it does not validate the field, you should use validate fn above
rules: {
minDigits: 1,
maxDigits: 5,
isEmail: true,
}
})
},
// Scope validation means that:
// If all Field has value, is visible or it's not required
// we validate the scope.
scope_validation: {
isValid: false,
}
}
],
// This scope relies on "banks" to be validated as banks is part of the scope
scope_validation: {
isValid: false,
}
}
}
}
// configure your scopes types like this:
type FormConfig= {
user_info = ReturnType<typeof getUserInfoFields>;
// you can have more scope as needed
}
// Now, let's display make use of the form on the page.
const MyPanel = () => {
const [formState, setFormState] = useState<FormState<FormConfig>>();
const initFormState = async () => {
// await contact fetching
const response = MOCKED_USER_DATA
// set USER_INFO scope to the state, with pre-filled fields based on response
setFormState({
user_info: getUserInfoFields(response)
});
};
useEffect(() => {
initFormState()
}, [])
return(
<FormProvider<FormConfig> initialConfig={formState}>
<FormContext.Consumer>
{(formContext: IFormContextProps<FormConfig>) => {
const { formState } = formContext;
return (
<>
<Scope name="user_info" index={0}>
<CustomInput name="first_name" />
<CustomInput name="middle_name" />
<CustomInput name="last_name" />
// here you can loop and pass the index on scope
<Scope name="fields.banks" index={0}>
<CustomInput name="institution" />
<CustomInput name="account" />
<CustomInput name="transit_n" />
<CustomInput name="agency_n" />
</Scope>
</Scope>
<Button type="button" isDisabled={!isFormScopeValid(formState,['user_info'])} onClick={async () => {
// handleSubmit will return:
// data: { properties: [field]: value }
const formData = handleSubmit();
await submit(formData)
}}
>
Submit
</Button>
</>
)}}
</FormContext.Consumer>
</FormProvider>
);
};
export default MyPanel;