UNPKG

saagie-ui

Version:

Saagie UI from Saagie Design System

367 lines (331 loc) 9.28 kB
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;