@visma/formula

0.4.261 • Public • Published

@visma/formula 🏎

React component for configurable forms. Optionally connect to the backend to fetch external config and submit form data to.

Requirements

  1. Material UI v4, MUI v5 and react-intl are required. Install and set up if necessary:
npm i @visma/formula @emotion/styled @emotion/react @mui/x-date-pickers @mui/base @mui/material @material-ui/core @material-ui/styles @material-ui/icons @material-ui/lab react-intl --legacy-peer-deps
  1. Add Vite / Webpack alias for @emotion/core:
// vite.config.js
//...
export default defineConfig({
   resolve: {
     alias: {
       '@emotion/core': '@emotion/react',
     },
   },
});

// webpack.config.js
module.exports = {
  //...
   resolve: {
     alias: {
       '@emotion/core': '@emotion/react',
     },
   },
};

## Examples

### Login form

```js
import Formula from '@visma/formula';

<Formula
  config={{
    title: 'Log In',
    elements: [
      {
        key: 'email',
        type: 'email',
        name: 'Email Address',
        required: true,
      },
      {
        key: 'password',
        type: 'password',
        name: 'Password',
        required: true,
      },
    ],
  }}
  onSubmit={({ values }) => console.log(values)}
/>;

Use external config, prefill some fields

import Formula from '@visma/formula';

<Formula
  axios={(axios) => {
    axios.defaults.baseURL = 'https://example.com/formula/api';
    axios.defaults.headers.common.Authorization = 'Bearer <token>';
  }}
  id="1"
  // Assuming form has at least a formGroup with key `customer`, containing
  // fields with keys `firstName` & `lastName`.
  formData={useMemo(
    () => ({
      customer: {
        firstName: user.firstName,
        lastName: user.lastName,
      },
    }),
    [user]
  )}
/>;

Components

<Formula>

Props

One of config, id or dataId is required. Rest are optional.

Name Type Description
config Form Form config
formData any Optional, prefilled form data. Ensure the reference does not change undesirably, e.g. using useMemo.
id string External form config id
dataId string Resume editing
onPreSubmit `async (args: Args, event: SubmitEvent) => void \ boolean \
onSubmit ({ values }) => void Override default submit handler
onPostSubmit (dataId, { values }) => void Get dataId of submitted form data
confirm `boolean \ { title: ReactElement, description: ReactElement }`
axios axios => void Get access to API client's axios instance e.g. to set defaults
dateFnsLocale Locale from date-fns Examples:
import useDateFnsLocale from '@visma/react-app-locale-utils/lib/useDateFnsLocale.js';
import { fi } from 'date-fns/locale';
children ReactElement Override default submit button. Set <></> (empty React Frament) to render nothing.
review boolean Show review after the form has been submitted. Default: true
forceReview boolean Show review directly. Default: false
reviewProps { actions: ReactNode, showSuccessText: boolean, highlightSuccessText: boolean, hideNotAnswered: boolean } actions: Additional action buttons
showSuccessText: show success text and summary in review, default true
highlightSuccessText: make summary more noticeable, default false
hideNotAnswered: hide not answered fields in review, user can see the full form by unchecking a checkbox, default false
fillProps { actions: ReactNode, disableSteps: boolean, disableResetFormdata: boolean, disableElementButtons: boolean, showScores: boolean, disablePrint: boolean } actions: Additional action buttons
disableSteps: disables steps when filling the form, default false
disableResetFormdata: disable resetting formData to initial formData in disabled fields, default false
disableElementButtons: disable all button elements, default false
showScores: show scores if required metadata is available, default false
disablePrint: disable print button in ConfirmDialog, default false
confirmComponent, previewField, reviewField component Customize
customMessages { submit: string, reviewSubmitConfirmation: string, confirmDialogTitle: string, confirmDialogConsent: string, confirmDialogPreview: string, confirmDialogSendButton: string, confirmDialogCancelButton: string, error: string } Overrides default texts in submit button, confirmation dialog, confirm message and error.
buttonActions object Functions for button elements, {functionKey: (buttonActionProps) => boolean}

<FormulaProvider>

Provide options for any <Form> component in children.

Required to use API hooks.

Props

Same as for <Formula>, except:

  • Without config, id, dataId
  • children: ReactElement: App, wrapped forms

<Form>

Props

config, id, dataId and children from <Formula>

Hooks

See src/api.js for all API hooks.

List forms

import { useForms } from '@visma/formula';

function ListForms() {
  const forms = useForms({ status: 'published', visibility: 'public' });
  // ...
}

Form config details

import { useForm } from '@visma/formula';

function FormTitle({ id }) {
  const form = useForm(id);

  return <h1>{form.title}</h1>;
}

Intercept built-in submit function

import { Formula, useMutations } from '@visma/formula';
// ...
const { submit } = useMutations();

<Formula
  onSubmit={async (...args) => {
    try {
      return await submit(...args);
    } catch (error) {
      logger(error);
      throw error;
    }
  }}
  // ...
/>;

Customize

Confirm dialog (confirmComponent)

Example:

import {
  DialogActions,
  DialogContent,
  DialogContentText,
} from '@material-ui/core';
import produce, { original } from 'immer';
import { FormattedMessage, useIntl } from 'react-intl';

export function CustomConfirm({ config, formData, children }) {
  const intl = useIntl();

  // children, the original dialog, is readonly – use produce from immer to make deep immutable changes.
  return produce(children, (children) => {
    const dialogContentElement = children.props.children.find(
      (element) => element && original(element)?.type === DialogContent
    );

    if (config.meta?.showScoreOnPreview && dialogContentElement) {
      dialogContentElement.props.children.splice(
        2,
        0,
        <DialogContentText>
          <FormattedMessage
            defaultMessage="Vastauksesi antavat sinulle {score} pistettä."
            values={{
              score: Math.ceil(Math.random() * config.meta.maxScore),
            }}
          />
        </DialogContentText>
      );
    }

    // Reverse dialog children order 🤪
    children.props.children.reverse();

    // Reverse dialog action button order
    children.props.children
      .find((element) => element && original(element)?.type === DialogActions)
      ?.props.children.reverse();

    // If set, override consent message
    const consentElement = dialogContentElement?.props.children.find(
      (element) => element?.key === 'consent'
    );
    if (consentElement) {
      consentElement.props.label = intl.formatMessage({
        defaultMessage:
          'Kyllä, haluan lähettää tiedot ja osallistua palkinnon arvontaan 🏆',
      });
    }
  });
}

Preview (previewField) & Review Field (reviewField)

Example:

import produce from 'immer';
import { sortBy } from 'lodash';

export function CustomPreviewField({ formData, uiSchema, children }) {
  const dataElement = children[1];

  // children, the original field, is readonly – use produce from immer to make deep immutable changes.
  return produce(children, (children) => {
    if (uiSchema['ui:options'].element.meta.showScoreOnPreview) {
      const highlight = sortBy(
        uiSchema['ui:options'].element.meta.highlightColors,
        ['scoreGreaterThan']
      )
        .reverse()
        .find(({ scoreGreaterThan }) => scoreGreaterThan < formData);

      if (highlight) {
        children[1] = (
          <div style={{ display: 'flex' }}>
            <div style={{ flex: '1 1' }}>{dataElement}</div>
            <div
              style={{
                height: '1.2rem',
                width: '1.2rem',
                color: highlight.color,
              }}
            >
              <svg
                xmlns="http://www.w3.org/2000/svg"
                className="h-6 w-6"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
                strokeWidth={2}
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                />
              </svg>
            </div>
          </div>
        );
      }
    }
  });
}

Load library dynamically

  1. Call init from @visma/formula/lib/dll before using the API:

    import { init } from '@visma/formula/lib/dll';
    import App from 'components/App';
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    async function main() {
      await init('https://example.com/formula');
    
      ReactDOM.render(
        <React.StrictMode>
          <App />
        </React.StrictMode>,
        document.getElementById('root')
      );
    }
    
    main();
  2. Import the API from @visma/formula/lib/dll. Note that all components and hooks are available only using the default export:

    import DLL from '@visma/formula/lib/dll';
    
    <DLL.Formula
      axios={(axios) => {
        axios.defaults.baseURL = 'https://example.com/formula/api';
        axios.defaults.headers.common.Authorization = 'Bearer <token>';
      }}
      id="1"
    />;

EXAMPLES
App wrapped with FormulaProvider
 async function main() {
  await init('https://example.com/formula');


  ReactDOM.render(
    <DLL.FormulaProvider
    axios={(axios) => {
      axios.defaults.baseURL = 'https://example.com/formula';
    }}
  >
    <App />
  </DLL.FormulaProvider>
    , document.getElementById('root'));
}
main();



After wrapping App with the Provider, Formula component can be used anywhere inside the App

FormulaComponent

   import {IntlProvider} from "react-intl";
   import DLL from "@visma/formula/lib/dll";
   import React from "react";
   
   const FormulaComponent = (props) => {
     return (
       <IntlProvider locale={'fi-FI'}>
         <DLL.Form
           id={props?.formId}
           dataId={props?.formResponseId}
           credentials={props?.credentials}
           ...
         >
         </DLL.Form>
       </IntlProvider>
     );
   }
   
   export default FormulaComponent;

Using FormulaComponent inside App
   ...
   <FormulaComponent
     formId={formId}
     formResponseId={formResponseId}
     credentials={formulaToken}
     ...
   />
   ...

Dependents (1)

Package Sidebar

Install

npm i @visma/formula

Weekly Downloads

220

Version

0.4.261

License

ISC

Unpacked Size

5.71 MB

Total Files

463

Last publish

Collaborators

  • visma_bot
  • arnosaine
  • juusoko