React component for configurable forms. Optionally connect to the backend to fetch external config and submit form data to.
- 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
:
// 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)}
/>;
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]
)}
/>;
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}
|
Provide options for any <Form>
component in children.
Required to use API hooks.
Same as for <Formula>
, except:
- Without
config
,id
,dataId
-
children: ReactElement
: App, wrapped forms
config
, id
, dataId
and children
from <Formula>
See src/api.js for all API hooks.
import { useForms } from '@visma/formula';
function ListForms() {
const forms = useForms({ status: 'published', visibility: 'public' });
// ...
}
import { useForm } from '@visma/formula';
function FormTitle({ id }) {
const form = useForm(id);
return <h1>{form.title}</h1>;
}
import { Formula, useMutations } from '@visma/formula';
// ...
const { submit } = useMutations();
<Formula
onSubmit={async (...args) => {
try {
return await submit(...args);
} catch (error) {
logger(error);
throw error;
}
}}
// ...
/>;
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 🏆',
});
}
});
}
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>
);
}
}
});
}
-
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}
...
/>
...