What does "bypassing the SchemaForm" mean?

The schema-based forms library in vets-websiterenders a component called SchemaForm. This component uses react-jsonschema-formto render form fields using the schemaand uiSchemafor a page in the formConfig.

Bypassing the SchemaFormcomponent means you can render a form page using plain React components. No schema. No uiSchema.

You may bypass SchemaFormfor one page at a time. This means you can use schemaand uiSchemawhen it's helpful, and ignore it when it isn't.

How to bypass SchemaForm

In your form's formConfigfor a given page, you may supply CustomPageand CustomPageReviewproperties, which contain React components.

Example

// form/config.js
import CustomPage from './CustomPage'; // React component
import CustomPageReview from './CustomPageReview'; // React component

export const formConfig = {
  // Some config here...
  chapters: {
    chapterOneName: {
      // Chapter config here...
      pages: {
        mySchemalessPage: {
          path: 'my-schemaless-page',
          title: 'Bypassing the SchemaForm',
          CustomPage,
          CustomPageReview,
          schema: { // This does still need to be here or it'll throw an error
            type: 'object',
            properties: {}, // The properties can be empty
          },
          uiSchema: {},
        }
      }
    }
  }
}
JS

There are a few important things going on here:

  1. We pass CustomPageto the page's config.

  2. We pass CustomPageReviewto the page's config.

  3. We still supply a schemaand uiSchema, but leave uiSchema (essentially) empty; if we leave them out entirely, the library will throw an error.

Defining Schema for a CustomPage

In this example we’re defining a schema to be used in a CustomPage form. Because there is no CustomPageReview, the schema for the page must be defined. This gets passed through to the props.data object in the CustomPage component.

...
import MyCustomPage from '../components/MyCustomPage';

const formConfig = {
      ...
      pages: {
        bypassSchemaPage1: {
          path: 'bypass-schema-page-1',
          title: 'Add Emergency Contact Information',
          CustomPage: MyCustomPage,
          uiSchema: {}, // UI schema is completely ignored
          schema: {
            type: 'object',
            properties: {
              phoneNumber: {
                type: 'string',
              },
            },
          },
        },
      },
      ...
}
export default formConfig;
JS

CustomPage props

  • name: String

    • The name of the page in the formConfig.

    • In the example above, it's bypassSchemaPage1.

  • title: String

    • The title of the page in the formConfig.

    • In the example above, it's Add Emergency Contact Information.

  • data: Object

    • The entire form data.

    • Important: It contains the form data for the entire form, not just for the page.

  • pagePerItemIndex: Number | undefined

    • If showPagePerItemis true for the page, this contains the array index.

  • onReviewPage: Boolean

    • True if the page is being rendered in edit mode on the review page.

    • This is useful for rendering either the form navigation buttons or the update button on the review page.

    • In the Common tasks section below, onReviewPage is used to display custom information for both CustomPage and ReviewPage that share the same view logic:
      onReviewPage ? updatePage() : goToPath(returnPath);

  • trackingPrefix: String

    • The tracking prefix specified in for the form in formConfig.

  • uploadFile: Function

    • The function to call to upload a file.

    • Not supplied when CustomPageis rendered on the review page.

  • goBack: () => void

    • CustomPage only.

    • The function to call to move back a page in the form.

    • Not supplied when CustomPageis rendered on the review page.

    • In the Common tasks section below, goForward is supplied to the FormNavButtons to render the “Go Back” button.

  • goForward: () => void

    • CustomPage only.

    • The function to call to move forward a page in the form.

    • Caveat: Rather than assigning this to a button's onClickevent handler, make that button a submit button and use <form onSubmit={goForward}>...</form>so the user only navigates to the next page if there are no validation errors.

  • goToPath: Function

    • CustomPage only.

    • Function to call if you need to go to a specific path within the form.

    • When calling this function with a valid active path, it will return you to that page. For example, goToPath('/second-page').

    • An invalid path will send you back one page, so be careful.

  • updatePage: () => void

    • Edit mode on on review page (CustomPage) only.

    • Supplied only when rendering CustomPageon the review page.

    • The function to call when the user is finished editing the data.

    • In the Common tasks section below, updatePage is used to update the review page:
      onReviewPage ? updatePage() : ...

  • editPage: () => void

    • CustomPageReview only.

    • The function to call to initiate editing the data on the review page.

The Review Page

There are three options for showing your page content on the Review Page:

  1. Use the CustomPageReview.

  2. Automatically render the page content using schemaand uiSchema. Important: You may still use CustomPagewith this option; see the Using scheme and uiSchema section below.

  3. Hide the page entirely.

Using CustomPageReview

To bypass the automatic data rendering, you may supply a React component to CustomPageReview in your page's config. See CustomPage propsfor the props that will be passed to this component.

Note: You may use CustomPageReviewwith or without CustomPage. If your page has a CustomPagecomponent and CustomPageReview, the schemaand uiSchemawill be ignored altogether, so you don't need to spend time writing them.

Using schema

You may bypass the SchemaFormon the form page but still retain the automatic review page data rendering by doing the following in your page's config:

  1. Add the schemaas normal.

  2. Set CustomPageReviewto null.

An example of this type of form config can be found in the above section Custom Page Schema and the below example outlining Common tasks.

Note: If there is a CustomPage, but CustomPageReviewis undefinedand schema.propertiesis empty, the Forms Library will throw an error. This is to make sure you're deliberately choosing to supply a custom page using automatic data rendering or hiding the page.

Hiding the page

If you want the page to appear in the normal form flow but not on the review page, do the following in your page's config:

  1. Supply a CustomPage.

  2. Set CustomPageReviewto null.

  3. Set schema.propertiesto {}.

Common tasks

There are a few common tasks you'll need to perform when bypassing the SchemaForm.

Adding values to the data object

When using a CustomPage, you’ll need to call setData to update form data, along with adding any validation that’s required to reach the next page.

Example

The following example is a simplified version of this CustomForm.

// CustomPage.jsx
import React from 'react';
import { setData } from 'platform/forms-system/src/js/actions';
import { TextField } from '@department-of-veterans-affairs/formulate';
import FormNavButtons from '~/platform/forms-system/src/js/components/FormNavButtons';

const CustomPage = ({
  title,
  data,
  onReviewPage,
  goBack,
  goToPath,
  updatePage,
  setFormData, // setData
}) => {
  // Set state for this form
  const [phoneObj, setphoneObj] = useState({
      value: data.phoneNumber || '',
      dirty: false,
    }
  );

  ... 
  const updateFormData = event => {
    ... // Do some kind of validation before setting form data
    
    // use setData to set form schema data
    setFormData({ ...data,  phoneNumber: phoneObj.value});

    // Redirect to page using goToPath, or if on review page, 
    // return to review mode instead of edit mode.
    onReviewPage ? updatePage() : goToPath(returnPath);

  }
  const onPhoneChange = updatedPhoneValue => {
    setPhoneObject(updatedPhoneValue);
  }
  ...
  
  const navButtons = <FormNavButtons goBack={goBack} submitToContinue/>;
  const updateButton = <button type="submit">Review update button</button>;
  return (
    <form onSubmit={updateFormData}>
      <fieldset>
        <legend
          id="emergency-contact-date-description"
          className="vads-u-font-family--serif"
          name="addIssue"
        >
          { title }
        </legend>
        <TextInput
            id="add-ec"
            name="add-ec"
            type="text"
            label={myCustomEmergencyContactPhoneLabel}
            required
            field={phoneObj}
            onValueChange={onPhoneChange}
            errorMessage={ 
              (submitted || phoneObj.dirty) // do some kind of validation here
            }
          />
          {onReviewPage ? updateButton : navButtons}
      </fieldset>
    </form>
  )
};

// map setData to component props
const mapDispatchToProps = {
  setFormData: setData,
};

export default connect(
  null, 
  mapDispatchToProps,
)(CustomPage);
JS

There are a few important parts to note here:

  • We call a custom submit function which calls the forms-system setData which sets the data object for this part of the form schema.

  • The navigation buttons are imported from here.

  • goForwardis supplied to the onSubmitevent handler prop and submitToContinueis passed to FormNavButtons.

    • This is so the Continuebutton is type="submit"and navigation only occurs if there are no validation errors.

    • If const navButtons = <FormNavButtons goBack={goBack} goForward={goForward} />, then the user would continue to the next page even if there were validation errors.

  • updateButtonis type="submit"for the same reasons outlined above.

  • {onReviewPage ? updateButton : navButtons }renders the context-appropriate buttons depending on where the CustomPageis rendered.

Render the edit button on CustomPageReview

const AddEmergencyContactReview = ({ data, editPage }) => (
  <>
    <h1>
      Hello, {data.phoneNumber} is your emergency contact info phone number.
    </h1>
    <button onClick={editPage}>
      Edit
    </button>
  </>
);

export default AddEmergencyContactReview;
JS

There's less going on here. Just make sure your CustomPageReviewhas an edit button that calls editPageif the user needs to be able to edit the data on that page.