@visma/formula 🏎
React component for configurable forms. Optionally connect to the backend to fetch external config and submit form data to.
Requirements
- 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
- Add Vite / Webpack alias for
@emotion/core
:
export default defineConfig({
resolve: {
alias: {
'@emotion/core': '@emotion/react',
},
},
});
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();
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>
);
}
children.props.children.reverse();
children.props.children
.find((element) => element && original(element)?.type === DialogActions)
?.props.children.reverse();
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];
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
-
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();
-
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}
...
/>
...