informed
Version:
A lightweight framework and utility for building powerful forms in React applications
375 lines (346 loc) • 12 kB
JavaScript
import { slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.js';
import React, { useContext, useRef, useState, useMemo, useEffect } from 'react';
import { ScopeContext, MultistepApiContext, MultistepStateContext } from '../Context.js';
import { useFormApi } from './useFormApi.js';
import { useFormController } from './useFormController.js';
var useMultistep = function useMultistep(_ref) {
var initialStep = _ref.initialStep,
multistepApiRef = _ref.multistepApiRef;
// Get the formApi
var _useFormController = useFormController(),
validate = _useFormController.validate,
asyncValidate = _useFormController.asyncValidate,
getFormState = _useFormController.getFormState,
getFieldState = _useFormController.getFieldState,
emitter = _useFormController.emitter;
var formApi = useFormApi();
// Get scope for relevance
var scope = useContext(ScopeContext);
// Track number of steps
var nSteps = useRef(0);
// Track current step
var currentStep = useRef();
// Track array of steps
var _useState = useState(function () {
return [];
}),
_useState2 = _slicedToArray(_useState, 1),
steps = _useState2[0];
// Track our steps by name
var _useState3 = useState(function () {
return new Map();
}),
_useState4 = _slicedToArray(_useState3, 1),
stepsMap = _useState4[0];
// Form state will be used to trigger rerenders
var _useState5 = useState({
steps: [],
goal: null
}),
_useState6 = _slicedToArray(_useState5, 2),
multistepState = _useState6[0],
setState = _useState6[1];
// YES! this is important! Otherwise it would get a new api object every render
/// That would cause unessissarry re-renders! so do not remove useMemeo!
var multistepApi = useMemo(function () {
// ---------- Define the api functions ----------
var register = function register(name, step) {
// Create step meta
var stepMeta = _objectSpread2(_objectSpread2({}, step), {}, {
index: nSteps.current
});
// Add step to ordered array
steps.push(stepMeta);
// Add step to named map
stepsMap.set(name, stepMeta);
// Inc number of steps
nSteps.current = nSteps.current + 1;
// Determine if we have initial goal and it just registered
var initialGoal = null;
var startingStep = null;
// There is no initial step so we start at first one
if (!initialStep) {
startingStep = steps[0].name;
}
// Otherwise we wait until our initial step has registered and then set our goal!
else if (initialStep && name === initialStep) {
initialGoal = initialStep;
startingStep = steps[0].name;
}
// console.log('WTF', name, initialGoal, startingStep);
// Update the state
setState(function (prev) {
if (!prev.current && startingStep) {
// Update the current step
currentStep.current = startingStep;
}
return _objectSpread2(_objectSpread2({}, prev), {}, {
steps: steps,
goal: prev.goal || initialGoal,
initialGoal: prev.goal || initialGoal,
current: prev.current || startingStep
});
});
};
var deregister = function deregister(step) {
var stepMeta = stepsMap.get(step);
// Remove step at index
steps.splice(stepMeta.index, 1);
// Update indexes
steps.forEach(function (s, i) {
return s.index = i;
});
// Remove step to named map
stepsMap["delete"](step);
// Dec number of steps
nSteps.current = steps.length;
// console.log('WTF', name);
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
steps: steps
});
});
};
var getNextStep = function getNextStep() {
// Get current step meta
var stepMeta = stepsMap.get(currentStep.current);
// Start searching from current step for a relevant next step
var nextStep;
for (var i = stepMeta.index + 1; i < steps.length; i++) {
// Potential next step
nextStep = steps[i];
// Check relevance
var formState = getFormState();
if (nextStep.relevant ? nextStep.relevant({
formState: formState,
formApi: formApi,
scope: scope,
relevanceDeps: nextStep.relDepsRef.current
}) : true) {
return nextStep.name;
}
}
// IF we get here there are not next steps so we return nothing
return undefined;
};
var getPreviousStep = function getPreviousStep() {
// Get current step meta
var stepMeta = stepsMap.get(currentStep.current);
// Start searching from current step for a relevant next step
var previousStep;
for (var i = stepMeta.index - 1; i >= 0; i--) {
// Potential previous step
previousStep = steps[i];
// Check relevance
var formState = getFormState();
if (previousStep.relevant ? previousStep.relevant({
formState: formState,
formApi: formApi,
scope: scope,
relevanceDeps: previousStep.relDepsRef.current
}) : true) {
return previousStep.name;
}
}
// IF we get here there are no previous steps so we return nothing
return undefined;
};
// Helper function for next
var proceed = function proceed(nextStep, cb) {
// Get the multistep state values
if (cb && typeof cb === 'function') {
var fieldState = getFieldState(currentStep.current);
// Simply making value --> values because it makes more sense in this context
var subState = _objectSpread2(_objectSpread2({}, fieldState), {}, {
values: fieldState.value,
errors: fieldState.error
});
cb(subState).then(function () {
// Update the current step
currentStep.current = nextStep;
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
current: nextStep
});
});
})["catch"](function () {
// TODO mayyybe do something here ??
});
} else {
// Update the current step
currentStep.current = nextStep;
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
current: nextStep
});
});
}
};
var next = function next(cb) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
skip = _ref2.skip;
// Get the next step
var nextStep = getNextStep();
if (nextStep) {
// Touch all the fields
if (!skip) formApi.touchAllFields();
// Validate the form
if (!skip) validate();
// Async validate the form
// We pass in a callback to proceed if we succeed async validation!
if (!skip) asyncValidate(function () {
return proceed(nextStep, cb);
});
// Only proceed if we are valid and we are NOT currently async validating
if (skip || getFormState().valid && getFormState().validating === 0) {
proceed(nextStep, cb);
} else if (!getFormState().valid) {
// If we are not valid then clear out the goal
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
goal: null
});
});
}
}
};
var previous = function previous() {
// Get the next step
var previousStep = getPreviousStep();
// Clean up all multistep errors
steps.forEach(function (step) {
formApi.clearError(step.name);
});
// Update the current step
if (previousStep) {
// Update the current step
currentStep.current = previousStep;
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
current: previousStep
});
});
}
};
var setCurrent = function setCurrent(step) {
// Get current step meta
var goalIndex = stepsMap.get(step).index;
var currIndex = stepsMap.get(currentStep.current).index;
// Clean up all multistep errors
steps.forEach(function (step) {
formApi.clearError(step.name);
});
// console.log('GOAL', goalIndex, 'CURRENT', currIndex);
// If the goal is behind then just go straight there
if (goalIndex < currIndex) {
// Update the current step
currentStep.current = step;
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
current: step
});
});
}
// If the goal is ahead then start walking! ;)
else {
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
goal: step
});
});
}
};
var metGoal = function metGoal() {
// Update the state
setState(function (prev) {
// We clear out initialGoal also so a reset does not make us try to get there again
return _objectSpread2(_objectSpread2({}, prev), {}, {
goal: null,
initialGoal: null
});
});
};
var getCurrentStep = function getCurrentStep() {
return currentStep.current;
};
// ---------- Define the api ----------
var api = {
register: register,
deregister: deregister,
next: next,
previous: previous,
getNextStep: getNextStep,
getPreviousStep: getPreviousStep,
setCurrent: setCurrent,
metGoal: metGoal,
getCurrentStep: getCurrentStep
};
// Set the ref
if (multistepApiRef) {
multistepApiRef.current = api;
}
// return the api
return api;
}, []);
// Register for events when multistep relevance changes
useEffect(function () {
var listener = function listener() {
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
nextStep: multistepApi.getNextStep(),
previousStep: multistepApi.getPreviousStep()
});
});
};
emitter.on('multistep-relevance', listener);
return function () {
emitter.removeListener('multistep-relevance', listener);
};
}, []);
// Also re evaluate when current changes
useEffect(function () {
if (multistepState.current) {
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
nextStep: multistepApi.getNextStep(),
previousStep: multistepApi.getPreviousStep()
});
});
}
}, [multistepState.current]);
// Register for events when reset is called
useEffect(function () {
var listener = function listener() {
// Update the state
setState(function (prev) {
return _objectSpread2(_objectSpread2({}, prev), {}, {
goal: prev.goal || prev.initialGoal
});
});
};
emitter.on('reset', listener);
return function () {
emitter.removeListener('reset', listener);
};
}, []);
// Render funtion that will provide state and api
var render = function render(children) {
return /*#__PURE__*/React.createElement(MultistepApiContext.Provider, {
value: multistepApi
}, /*#__PURE__*/React.createElement(MultistepStateContext.Provider, {
value: multistepState
}, children));
};
return _objectSpread2(_objectSpread2(_objectSpread2({}, multistepApi), multistepState), {}, {
render: render
});
};
export { useMultistep };