--- tags: v3, documentation, formbuilder --- # FormBuilder Docs (WIP) | Most of this content is from Jord's Form Builder walkthrough on 8/19/22 where we went through creating a custom field and form - Utilies the *Lego Pattern* to create composable components and form system - `legoTypes.ts` -- Form Builder turns form configuration objects into form UI - Keeps forms consistent and composable into larger form structures over time - Form Builder can be *combined* with `txBuilder` but you can also use `txBuilder` on its own without the form -- transactions can be attached independently to a button outside of a form `legoTypes.ts` - Imperative view of the Form Builder and what you're expected to have on each entity - Contains the types needed for each lego component. For example, the form lego type: ```ts= export type FormLegoBase<Lookup extends LookupType = LookupType> = { id: string; title: string; subtitle?: string; description?: string; tx?: TXLego; fields: FieldLegoBase<Lookup>[]; requiredFields?: Record<string, boolean>; log?: boolean; devtool?: boolean; submitButtonText?: string; }; ``` - This lego type tells us that each form must have an `id`, `title`, and `fields` as well as several optional properties. - The `log` and `devtool` properties can be included to access debugging utilities. - Setting `log={true}` logs out the form lego's values to the console - Setting `devtool={true}` leverages the React Hook Form devtools that can be opened in the browser while working with forms ### FormBuilder Life Cycles - FormBuilder *is* a context in the sense that it's convenient to be a context and form methods can be passed to the fields, but it can be used *per page* - There is additional config needed to support the use of custom fields - `FormBuilder.tsx` contains the lifecycle methods - Form Builder manages several life cycles that make working with forms (especially with the asynchronous nature of web3): - Manages every step from submitting the proposal, requesting the signature, receiving the tx hash, error/success states (including poll errors and successes) - Manages this across the entire form lifecycle so that the form knows what to render (and which errors to return) at each point in the form lifecycle - These error and success states can be accessed and displayed in modals and toasts (or any other UI) - Leaning on these lifecycle methods allows us to not rely on updating the *entire app state* on state changes such as approving a token - Can put a callback function in the lifecycle method - FormBuilder Lifecycle Methods Example: - Token that needs user approval entered in to the *Token Address* field for tributing to a DAO - User will see that the token data is loading, and then be prompted to approve the token - Once approved, user will be notified that the approval state has changed to a success and can continue with the tribute transaction ### Form Building - Example of building a real form for *Issuing DAO Shares and Loot* - Call on the Moloch transaction - Display the form with appropriate fields on a page - Form Builder and the Legos handle the form scaffolding and can support a full transaction or can do an asynchronous call - Renders UI for forms that are built with the legos in the form config - Config can be a`forms.ts` file within the app package you're building, such as within the `core-app` package at `core-app/src/legos/form.ts` - Helps with validation -- will add *each form within the app* to this config - Can validate the form and fields and find issues *within this config* instead of needing to wait until the UI is wired up - If the form object passes validation in the config then you'll have confidence it'll render ### Form Builder Custom Fields - Form Builder supports the use of custom fields, but you'll need to include them in the form's `config.tsx` ```tsx // app-name/src/legos/config.tsx import { CoreFieldLookup } from '@daohaus/haus-form-builder'; import { TributeInput } from '../components/customFields/tributeInput'; import { FieldLegoBase, FormLegoBase } from '@daohaus/common-utilities'; export const CustomFields = { ...CoreFieldLookup, tributeInput: TributeInput, }; export type CustomFieldLego = FieldLegoBase<typeof CustomFields>; export type CustomFormLego = FormLegoBase<typeof CustomFields>; ``` - In this example, the `TributeInput` is a custom field being utilized in the Tribute Form and so it needs to be added to the form config via the `CustomFields` object - Be sure to spread the `...CoreFieldLookup` into this object so that you'll retain access to the standard fields, and add your custom input with the name you'll use as the id (such as `tributeInput`) for the `TributeInput` custom component - Including the types enables validation for the custom field `tributeInput` when including it in the form's config object (in `form.ts`, for example) - The `FORM` object in the form config is looking to these types so by adding the typing here, you can then have type validation when constructing the form's config - This validation informs if the necessary fields are being passed in - The `FormBuilderFactory` component does a lookup for components in either the custom fields or the core fields if no custom fields have been added ### Interplay with Transaction Builder - Looking at `Dao.tsx` page in the `core-app` as an example of how you can pass necessary state in the `TxBuilder` such as `chainId` and `provider` - If state is needed, can include in the `TxBuilder` component that wraps the page layout - Includes a generic `appState` prop where you can pass in other state that may be needed - Can have a transaction go into `appState` and grab a value - `chainId`, `daoId`, and `safeId` are namespaced and can be accessed via their own props - `appState` can be *any object* you'd like, so this could come from any state store - Typically the `<TxBuilder/>` is **one layer lower** than the other app contexts so that you're able to pull from the other contexts when using `TxBuilder` and dump whatever is needed - Can drop the `<FormBuilder/>` component into a page that is wrapped with `TxBuilder` but `TxBuilder` can also be used on its own - Once the form config is scaffolded, the form will then show in the UI with the defined fields and corresponding validation and components ### Building the Form Config - The form UI is built imperatively from the form's config - First step is to give the form a name and add it onto the existing `FORM` object in the app's `form.ts` config file - You'll want to be sure to add this to the form config, not the types - If we're building a form with the purpose of *issuing DAO shares and loot* we can name the form something such as `ISSUE`: ```ts= // app-name/src/legos/form.ts export const FORM: Record<string, CustomFormLego> = { ISSUE: { id: 'issue', title: 'Issue DAO Tokens', subtitle: 'Token Proposal', description: 'Request membership or increased stake in the DAO. Any tribute must be available.', requiredFields: { title: true, description: true }, tx: TX.POST_SIGNAL, fields: [FIELD.TITLE, FIELD.DESCRIPTION, FIELD.LINK], }, }; ``` - `id` adds a reference to the form - `title`, `subtitle`, and `description` will render in the UI and can reference an existing design - `fields` is an array of the fields that the form uses -- let's take a look at `fields.ts`: `fields.ts` - `/legos/fields.ts` - Contains objects for each form field such as `TITLE` and `DESCRIPTION` - Add in metadata such as `id`, `type`, `label`, and `placeholder` -- these are then composed together to render `label` and `placeholder` for example ```ts // app-name/src/legos/fields.ts import { CustomFieldLego } from './config'; export const FIELD: Record<string, CustomFieldLego> = { TITLE: { id: 'title', type: 'input', label: 'Title', placeholder: 'Enter title', }, DESCRIPTION: { id: 'description', type: 'textarea', label: 'Description', placeholder: 'Enter description', }, LINK: { id: 'link', type: 'input', label: 'Link', placeholder: 'http://', expectType: 'url', }, TRIBUTE: { id: 'tribute', type: 'tributeInput', label: 'Tribute', selectId: 'TRIBUTE_SELECT', }, }; ``` - This saves time by composing together the commonly used form items such as `label` and `placeholder` - `type` is the component that the field uses -- the `TITLE` uses the `input` component so the type is set as `input` - Back to the form config -- adding our fields will render the ones that we need: - `fields: [FIELD.TITLE, FIELD.DESCRIPTION, FIELD.LINK],` - This form renders the title, description, and link components - When ready to render the form, you can pass the form's name into the `<FormBuilder />` component, such as: ```tsx // YourForm.tsx import { FormBuilder } from '@daohaus/haus-form-builder'; import { CustomFields } from '../legos/config'; import { FORM } from '../legos/form'; export function FormTest() { return <FormBuilder form={FORM.ISSUE} customFields={CustomFields} />; } ``` - Be sure to pass in the name that you gave your form in the form config (`form.ts`) - You'll then see the form's title, subtitle (if added), description (if added), and fields in the UI - `expectType`: can pass this in to tap into validation objects (including RHF's as well as custom ones included in the `validation.ts` file in our `common-utilities` package) - Example: the `LINK` field uses the `url` validation by passing in `expectType: url` - For debugging, you'll can include the `log` and `devtool` form config properties which will log out the form values and open React Hook Form's DevTools in the browser - `log`: if `true`, logs out the current field's values to the console - `devtool`: if `true`, allows access to the [React Hook Form DevTools](https://react-hook-form.com/dev-tools) to see a more detailed and robust view into the form for debugging ### Building a Custom Field - Start by building the React component for the custom field - Example of building a `toWeiInput` (may be added to our base components eventually) - When adding your custom component to the form config you'll have autocomplete/IntelliSense on anything that's a prop for that component - Due to the composability of Form Builder, when building a custom field, there will be a lot of *wrapping* components such as `<WrappedInput/>` #### Steps 1) Create the component as you would any other React component, such as `toWeiInput.tsx` 2) Decide which inputs you need (and the corresponding `Wrapped` versions): ```tsx= // toWeiInput.tsx import { Buildable, Field, WrappedInput } from '@daohaus/ui; import React from 'react'; export const ToWeiInput = (props: Buildable<Field>) => { return ( <WrappedInput {...props} /> ) } ``` - Any field passed into Form Builder can use the `Buildable` type but can also use React's `ComponentProps` type for things out of the ordinary - In this example, `WrappedInput` uses `Buildable<Field>` as the type so we want to be sure to replicate it in our component that wraps it - Once this is set, can go through a process of narrowing down by destructuring the props: ```tsx= // toWeiInput.tsx import { toBaseUnits, ValidateField } from '@daohaus/common-utilities'; import { Buildable, Field, WrappedInput } from '@daohaus/ui; import React from 'react'; import { RegisterOptions } from 'react-hook-form'; export const ToWeiInput = (props: Buildable<Field>) => { const newRules: RegisterOptions = { ...props.rules, setValueAs: (val) => toBaseUnits(val), validate: (val) => ValidateField.number(val), } return ( <WrappedInput {...props} rules={newRules} /> ) } ``` - `rules` leverages RHF's `register` options and includes things like validations for each field in the form - Recommended to use the `requiredFields` property in the form config (on the specific form entity) instead of setting required at the component level since this individual field may not be required in every instance - Similarly, adding a specific validation *to the custom field itself* would make it so *every* instance of the field would need to use the same validation - Sometimes this is intentional and needed, but other times it may make more sense to add to the field lego instead. Think about the implications of each approach and how the field will be used - Since we know that the `toWeiInput` custom field will *always* require a number, it makes sense to set the validation at the custom field level - When extending (or narrowing) the props (such as creating new rules) you'll want to be sure to *spread the existing rules* onto your new object so that you're not overwriting everything 3) Open the config file (`legos/config.tsx`) and add your custom field to the `CustomFields` object: ```tsx= // `legos/config.tsx` import { ToWeiInput } from 'your-component-path'; export const CustomFields = { ...CoreFieldLookup, tributeInput: TributeInput, toWeiInput: ToWeiInput, }; ``` - Once the custom field has been added to the form's config, you'll be able to access it in `fields.ts` as a field's`type` 4) Add your new custom field to the `fields.ts` configuration: ``` // ...your fields TO_WEI: { id: 'shouldOverwrite', type: 'toWei', label: 'Should Overwrite', }, ``` - As soon as you enter the `type` for your custom field you'll have autocompleted *based on the props of your custom field* - Much of the other fields can be considered boilerplate at this stage - `id` and `label` should be overwritten in the specific form to avoid having the same `id` and `label` for *every instance* of the custom field - You're able to add any prop that is available on the `Buildable` type 5) Add your custom field to your form entity in `form.ts`: ```ts= // app-name/src/legos/form.ts export const FORM: Record<string, CustomFormLego> = { ISSUE: { id: 'issue', title: 'Issue DAO Tokens', subtitle: 'Token Proposal', description: 'Request membership or increased stake in the DAO. Any tribute must be available.', requiredFields: { title: true, description: true }, tx: TX.POST_SIGNAL, fields: [FIELD.TITLE, FIELD.DESCRIPTION, FIELD.LINK, {...FIELD_TO_WEI, label: 'Shares Requested', id: 'sharesRequested'}, {...FIELD_TO_WEI, label: 'Loot Requested', id: 'lootRequested'}, ], }, }; ``` - The `id` will be what is queried for in the form, so you'll want to include unique names for each instance of the custom field if you're using more than one - In the example, the `FIELD_TO_WEI` is used two times: once for shares and once for loot. Each instance has it's own `label` and `id` so that they'll be uniquely accessed on the form as: - `.formValues.title`, `.formValues.description`, `.formValues.link`, `.formValues.sharesRequested`, `.formValues.lootRequested` ## Integrating Transaction Builder - FormBuilder and Transaction Builder can be used separately but they'll also often be paired such as in the example we've been working through where we're creating a form for issuing tokens and shares - This is a DAO transaction using the current DAO's contract we'll create a contract lego since we'll want to reuse this in the future ### Using the Contract Lego - Contracts in `core-app/src/legos/contracts.ts` make use of the *Contract Lego* imported from `@daohaus/common-utilities` - Contract Legos are added as object entities in this file similarly to how the form entities are added in the Form Builder config - Each lego has a unique name (such as `POSTER`) and the following properties: - `type` - `local` or `static` - `local` looks through an ABI store that can be passed into the transaction builders - `static` has the ABI pinned onto it - `contractName` - Main purpose is for debugging - `abi` - Contract's ABI - `targetAddress` - Address for the transaction -- this can come from the `<TxBuilder />` context that wraps the page/view being built out - Let's look at creating the `CURRENT_DAO` Contract Lego as an example since it leverages the `<TxBuilder/>` context: ```ts //contracts.ts import { LOCAL_ABI } from '@daohaus/abi-utilities'; import { ContractLego } from '@daohaus/common-utilities'; export const CONTRACT: Record<string, ContractLego> = { CURRENT_DAO: { type: 'static', contractName: 'Current DAO (Baal)', abi: LOCAL_ABI_BAAL, targetAddress: '.daoId', }, }; ``` - `<TxBuilder/>` has props that help with passing state - `appState` prop which can be a catch all, but there are certain props that are "first class citizens" with named properties, such as `daoId` that is used in the above Contract Lego example via `.daoId` ### Building a Transaction **NOTE**: Include docs on how to do a regular contract call instead of the multicall outlined below - Once the contract lego is created and in the `contracts.ts` file (such as our `CURRENT_DAO` contract lego we added in the previous step) we'll want to bring it into the actual transaction builder config - We recommend also keeping this config in your app package's legos folder, such as `app-name/src/legos/tx.ts` - We'll build out the transaction entity similar to how we've built the other pieces -- by adding a new entity onto the `TX` object. - This particular example leverages the `buildMultiCallTX` function since the action we're taking is submitting a proposal. **Using Multicall ** ```ts= //app-name/src/legos/tx.ts import { buildMultiCallTX } from '@daohaus/tx-builder-feature'; import { CONTRACT } from './contracts'; export const TX = { ISSUE: buildMultiCallTX() actions: [ { contract: CONTRACT.CURRENT_DAO, method: 'mintShares', args: [ '.formValues.recepient', '.formValues.sharesRequested' ]}, { contract: CONTRACT.CURRENT_DAO, method: 'mintLoot', args: [ '.formValues.recepient', '.formValues.lootRequested' ]}, ] }; ``` - Note: any value coming from Form Builder is accessed by `.formValues.fieldId` -- such as `formValues.sharesRequested` - `buildMultiCallTX` targets the multicall and does the necessary encoding - Needs `id` and `actions` - `actions` are the actions aggregated by the call -- these are encoded separately and then passed as a single hex string to the Baal contract - ## Questions for Devs Building Forms These answers can help inform our guides and potentially recipes/spells for forms and transactions. We can source responses to try to create a map for devs creating forms and transactions. - How do you know which transactions to use when starting a form? - Did you start with the form or with the transaction, or did you do both at the same time going back and forth? - Who is the audience: - Who is going to be building a form? - DAOhaus devs? Boost devs? - This can help us inform the overview/sales pitch for using Form Builder and txBuilder -