saagie-ui
Version:
Saagie UI from Saagie Design System
367 lines (331 loc) • 9.28 kB
JavaScript
import React, {
useEffect, Fragment, useState, useRef,
} from 'react';
import Formsy from 'formsy-react';
import PropTypes from 'prop-types';
import Sticky from 'react-sticky-el';
import { Page } from '../../core/layout/page/Page';
import { PageContent } from '../../core/layout/page/PageContent';
import { PageFooter } from '../../core/layout/page/PageFooter';
import { Icon } from '../../core/atoms/icon/Icon';
import { PageLoader } from '../../core/layout/page/PageLoader';
import { FormMultiStepsHeader } from './FormMultiStepsHeader';
import { FormMultiStepsFooter } from './FormMultiStepsFooter';
import useLayoutFocusMode from '../../core/helpers/useLayoutFocusMode';
const propTypes = {
/**
* Badge content for the header component
*/
badge: PropTypes.node,
/**
* Children must be <FormMultiStepsItem> components
*/
children: PropTypes.node,
/**
* Icon name for the header component
*/
icon: PropTypes.string,
isContentLoading: PropTypes.bool,
/**
* Toggle icon color for the header component
*/
isIconColored: PropTypes.bool,
isFormLoading: PropTypes.bool,
/**
* Add to pass from Tabs view to Wizard view
*/
isWizard: PropTypes.bool,
/**
* Display message in the footer when isFormLoading is true
*/
loadingMessage: PropTypes.node,
onCancel: PropTypes.func,
/**
* onChange(values, { invalidateField })
*/
onChange: PropTypes.func,
/**
* onContinue(values, { invalidateField })
*/
onContinue: PropTypes.func,
/**
* onSubmit(values, { invalidateField })
*/
onSubmit: PropTypes.func,
submitLabel: PropTypes.node,
/**
* Title content for the header component
*/
title: PropTypes.node,
};
const defaultProps = {
badge: null,
children: '',
icon: null,
isContentLoading: false,
isIconColored: false,
isFormLoading: false,
isWizard: false,
loadingMessage: '',
onCancel: () => window.history.back(),
onChange: () => {},
onContinue: () => {},
onSubmit: () => {},
submitLabel: 'Save',
title: '',
};
export const FormMultiSteps = ({
badge,
children,
icon,
isContentLoading,
isIconColored,
isFormLoading,
isWizard,
loadingMessage,
onCancel,
onChange,
onContinue,
onSubmit,
submitLabel,
title,
}) => {
useLayoutFocusMode();
const [currentPage, setCurrentPage] = useState(0);
const [lastAvailablePage, setLastAvailablePage] = useState(0);
const steps = (React.Children.toArray(children)).reduce((acc, cur) => {
if (cur.type !== Fragment) {
return [...acc, cur];
}
// If child is a Fragment, spread its children
return [...acc, ...(React.Children.toArray(cur.props.children))];
}, []);
const isLastStep = currentPage === (steps.length - 1);
const stepsArrayRef = useRef(steps.map(() => React.createRef()));
const [stepsValidations, setStepsValidations] = useState(steps.map(() => true));
const stepsValidationsRef = useRef();
stepsValidationsRef.current = stepsValidations;
const getFormValues = () => stepsArrayRef.current.reduce((obj, ref) => ({
...obj,
...(
ref.current
? ref.current.getModel()
: {}
),
}), {});
// Enable first validation of forms
useEffect(() => {
setTimeout(() => {
stepsArrayRef.current.forEach((ref) => {
if (ref && ref.current) {
ref.current.validateForm();
}
});
});
}, [stepsArrayRef.current]);
const scrollToTopOfPage = () => {
const $scrollElement = document.querySelector('.sui-l-app-layout__page-scroll');
if (!$scrollElement) {
return;
}
$scrollElement.scrollTop = 0;
};
const goto = (page) => {
if (isFormLoading) {
return;
}
if (page < 0) {
return;
}
if (page >= steps.length) {
submitStep(); // eslint-disable-line no-use-before-define
return;
}
scrollToTopOfPage();
setCurrentPage(page);
setLastAvailablePage(page > lastAvailablePage ? page : lastAvailablePage);
};
const invalidateStep = (page) => {
setStepsValidations(stepsValidationsRef.current.map(
(isStepValid, stepPage) => (stepPage === page ? false : isStepValid)
));
};
const validateStep = (page) => {
setStepsValidations(stepsValidationsRef.current.map(
(isStepValid, stepPage) => (stepPage === page ? true : isStepValid)
));
};
const invalidateField = (fieldName, message) => {
stepsArrayRef.current.forEach((ref, page) => {
if (!ref.current.getModel()[fieldName]) {
return;
}
// Delay field validation for changeValue event
setTimeout(() => {
invalidateStep(page);
ref.current.updateInputsWithError({
[fieldName]: message,
});
});
});
};
const onFormChange = (stepValues) => {
onChange(
// Form values
{
...getFormValues(),
...stepValues,
},
// Form actions
{
invalidateField,
},
);
};
const submitForm = () => {
if (isFormLoading) {
return;
}
// Submit all steps Formsy forms
stepsArrayRef.current.forEach((ref) => {
if (ref.current) {
ref.current.submit();
}
});
// If one step is not valid
if (stepsValidationsRef.current.some((value) => value === false)) {
return;
}
onSubmit(
getFormValues(),
// Form actions
{
invalidateField,
},
);
};
const submitStep = () => {
if (isFormLoading) {
return;
}
// Submit current step Formsy form
if (stepsArrayRef.current[currentPage].current) {
stepsArrayRef.current[currentPage].current.submit();
}
// Not working :(
if (!stepsValidationsRef.current[currentPage] && currentPage >= lastAvailablePage) {
return;
}
if (!isLastStep) {
onContinue(
getFormValues(),
// Form actions
{
invalidateField,
},
);
goto(currentPage + 1);
return;
}
submitForm();
};
return (
<Page
size="md"
>
<PageContent>
{(title || isWizard) && (
<FormMultiStepsHeader
icon={icon}
isIconColored={isIconColored}
title={title}
badge={badge}
>
{isWizard && (
<button
type="button"
className="sui-a-button as--min-width-lg"
onClick={onCancel}
>
Cancel
</button>
)}
</FormMultiStepsHeader>
)}
{ !isWizard && steps && steps.length > 1 && (
<Sticky
className="sui-h-mb-lg"
scrollElement=".sui-l-app-layout__page-scroll"
style={{
background: 'white',
zIndex: 2,
}}
>
<div className="sui-m-tabs as--lg as--fill as--auto@md">
<div className="sui-m-tabs__wrapper">
{steps.map((Step, stepPage) => (
<button
key={stepPage} // eslint-disable-line react/no-array-index-key
type="button"
onClick={() => { goto(stepPage); }}
className={`sui-m-tabs__item ${currentPage === stepPage ? 'as--active' : ''}`}
>
{Step.props.name || '-'}
{!stepsValidationsRef.current[stepPage] && (
<small className="sui-h-text-danger sui-h-ml-sm">
<Icon name="fa-warning" />
</small>
)}
</button>
))}
</div>
</div>
</Sticky>
)}
{
isContentLoading
? (
<PageLoader isLoading />
)
: steps.map((Step, stepPage) => (
<Formsy
key={stepPage} // eslint-disable-line react/no-array-index-key
style={{
display: stepPage !== currentPage ? 'none' : false,
pointerEvents: isFormLoading ? 'none' : false,
}}
ref={stepsArrayRef.current[stepPage]}
onChange={onFormChange}
onValid={() => validateStep(stepPage)}
onInvalid={() => invalidateStep(stepPage)}
>
{isWizard && Step.props && !!Step.props.name && (
<h3>
{Step.props.name || '-'}
</h3>
)}
{Step}
</Formsy>
))
}
</PageContent>
<PageFooter>
<FormMultiStepsFooter
isWizard={isWizard}
isFormLoading={isFormLoading}
loadingMessage={loadingMessage}
submitLabel={submitLabel}
stepsValidations={stepsValidationsRef.current}
page={currentPage}
lastAvailablePage={lastAvailablePage}
setPage={goto}
submitStep={submitStep}
submitForm={submitForm}
onCancel={onCancel}
/>
</PageFooter>
</Page>
);
};
FormMultiSteps.propTypes = propTypes;
FormMultiSteps.defaultProps = defaultProps;