@shopgate/engage
Version:
Shopgate's ENGAGE library.
65 lines • 15.7 kB
JavaScript
import _camelCase from"lodash/camelCase";var _excluded=["validationErrors"];var _Builder,_defineProperty2;function _typeof(obj){if(typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"){_typeof=function _typeof(obj){return typeof obj;};}else{_typeof=function _typeof(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj;};}return _typeof(obj);}function _slicedToArray(arr,i){return _arrayWithHoles(arr)||_iterableToArrayLimit(arr,i)||_nonIterableRest();}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance");}function _iterableToArrayLimit(arr,i){var _arr=[];var _n=true;var _d=false;var _e=undefined;try{for(var _i=arr[Symbol.iterator](),_s;!(_n=(_s=_i.next()).done);_n=true){_arr.push(_s.value);if(i&&_arr.length===i)break;}}catch(err){_d=true;_e=err;}finally{try{if(!_n&&_i["return"]!=null)_i["return"]();}finally{if(_d)throw _e;}}return _arr;}function _arrayWithHoles(arr){if(Array.isArray(arr))return arr;}function _objectWithoutProperties(source,excluded){if(source==null)return{};var target=_objectWithoutPropertiesLoose(source,excluded);var key,i;if(Object.getOwnPropertySymbols){var sourceSymbolKeys=Object.getOwnPropertySymbols(source);for(i=0;i<sourceSymbolKeys.length;i++){key=sourceSymbolKeys[i];if(excluded.indexOf(key)>=0)continue;if(!Object.prototype.propertyIsEnumerable.call(source,key))continue;target[key]=source[key];}}return target;}function _objectWithoutPropertiesLoose(source,excluded){if(source==null)return{};var target={};var sourceKeys=Object.keys(source);var key,i;for(i=0;i<sourceKeys.length;i++){key=sourceKeys[i];if(excluded.indexOf(key)>=0)continue;target[key]=source[key];}return target;}function _extends(){_extends=Object.assign||function(target){for(var i=1;i<arguments.length;i++){var source=arguments[i];for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){target[key]=source[key];}}}return target;};return _extends.apply(this,arguments);}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function");}}function _defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor);}}function _createClass(Constructor,protoProps,staticProps){if(protoProps)_defineProperties(Constructor.prototype,protoProps);if(staticProps)_defineProperties(Constructor,staticProps);return Constructor;}function _callSuper(_this,derived,args){function isNativeReflectConstruct(){if(typeof Reflect==="undefined"||!Reflect.construct)return false;if(Reflect.construct.sham)return false;if(typeof Proxy==="function")return true;try{return!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}));}catch(e){return false;}}derived=_getPrototypeOf(derived);return _possibleConstructorReturn(_this,isNativeReflectConstruct()?Reflect.construct(derived,args||[],_getPrototypeOf(_this).constructor):derived.apply(_this,args));}function _possibleConstructorReturn(self,call){if(call&&(_typeof(call)==="object"||typeof call==="function")){return call;}return _assertThisInitialized(self);}function _assertThisInitialized(self){if(self===void 0){throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return self;}function _getPrototypeOf(o){_getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function _getPrototypeOf(o){return o.__proto__||Object.getPrototypeOf(o);};return _getPrototypeOf(o);}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function");}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,writable:true,configurable:true}});if(superClass)_setPrototypeOf(subClass,superClass);}function _setPrototypeOf(o,p){_setPrototypeOf=Object.setPrototypeOf||function _setPrototypeOf(o,p){o.__proto__=p;return o;};return _setPrototypeOf(o,p);}function _defineProperty(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true});}else{obj[key]=value;}return obj;}import React,{Component,Fragment}from'react';import PropTypes from'prop-types';import{logger}from'@shopgate/pwa-core/helpers';import Portal from'@shopgate/pwa-common/components/Portal';import{BEFORE,AFTER}from'@shopgate/pwa-common/constants/Portals';import{I18n}from'@shopgate/engage/components';import{Form}from'..';import ActionListener from"./classes/ActionListener";import{ELEMENT_TYPE_EMAIL,ELEMENT_TYPE_PASSWORD,ELEMENT_TYPE_TEXT,ELEMENT_TYPE_NUMBER,ELEMENT_TYPE_SELECT,ELEMENT_TYPE_COUNTRY,ELEMENT_TYPE_PROVINCE,ELEMENT_TYPE_CHECKBOX,ELEMENT_TYPE_RADIO,ELEMENT_TYPE_DATE,ELEMENT_TYPE_PHONE,ELEMENT_TYPE_PHONE_PICKER,ELEMENT_TYPE_MULTISELECT}from"./Builder.constants";import ElementText from"./ElementText";import ElementSelect from"./ElementSelect";import ElementMultiSelect from"./ElementMultiSelect";import ElementRadio from"./ElementRadio";import ElementCheckbox from"./ElementCheckbox";import ElementPhoneNumber from"./ElementPhoneNumber";import buildFormElements from"./helpers/buildFormElements";import buildFormDefaults from"./helpers/buildFormDefaults";import buildCountryList from"./helpers/buildCountryList";import buildProvinceList from"./helpers/buildProvinceList";import buildValidationErrorList from"./helpers/buildValidationErrorList";import{sanitizePortalName}from"./helpers/common";/**
* Optional select element
* @type {Object}
*/var emptySelectOption={'':''};/**
* Takes a form configuration and handles rendering and updates of the form fields.
* Note: Only one country and one province element is supported per FormBuilder instance.
*/var Builder=/*#__PURE__*/function(_Component){function Builder(props){var _this2;_classCallCheck(this,Builder);_this2=_callSuper(this,Builder,[props]);// Prepare internal state
/**
* Retrieves a form element REACT component by the given type or null if the type is unknown.
* @param {string} type The type value of the element to return.
* @returns {*|ElementText|ElementSelect|ElementCheckbox|ElementRadio|null}
*/_defineProperty(_this2,"getFormElementComponent",function(type){return _this2.props.elements[type]||Builder.defaultElements[type]||null;});/**
* Sorts the elements by "sortOrder" property
*
* @typedef {Object} FormElement
* @property {number} sortOrder
*
* @param {FormElement} element1 First element
* @param {FormElement} element2 Second element
* @returns {number}
*/_defineProperty(_this2,"elementSortFunc",function(element1,element2){// Keep current sort order when no specific sort order was set for both
if(element1.sortOrder===undefined||element2.sortOrder===undefined){return 0;}// Sort in ascending order of sortOrder otherwise
return element1.sortOrder-element2.sortOrder;});/**
* Element change handler based on it's type. It takes a state change and performs form actions on
* in to allow customization. The final result is then written to the component state.
* @param {string} elementId Element to create the handler for
* @param {string} value Element value
*/_defineProperty(_this2,"elementChangeHandler",function(elementId,value){// "newState" is the state changes before any form actions have been applied
var newState=_extends({},_this2.state,{formData:_extends({},_this2.state.formData,_defineProperty({},elementId,value))});// Handle context sensitive functionality by via "action" listener and use the "new" state
var updatedNewState=_this2.actionListener.notify(elementId,_this2.state,newState);// Form actions can append validation errors by adding that field to the new state
// Split out validation errors from final state and
var _updatedNewState$vali=updatedNewState.validationErrors,validationErrors=_updatedNewState$vali===void 0?{}:_updatedNewState$vali,finalState=_objectWithoutProperties(updatedNewState,_excluded);// "hasErrors" is true, when a visible + required field is empty or validation errors appeared!
var hasErrors=Object.keys(validationErrors).length>0;// Check "required" fields for all visible elements and enable rendering on changes
_this2.formElements.forEach(function(formElement){if(!finalState.elementVisibility[formElement.id]||!formElement.required){return;}var tmpVal=finalState.formData[formElement.id];var tmpResult=tmpVal===null||tmpVal===undefined||tmpVal===''||tmpVal===false;hasErrors=hasErrors||tmpResult;});// Handle state internally and send an "onChange" event to parent if this finished
_this2.setState(finalState);// Transform to external structure (unavailable ones will be set undefined)
var updateData={};_this2.formElements.forEach(function(el){if(el.custom){if(updateData.customAttributes===undefined){updateData.customAttributes={};}updateData.customAttributes[el.id]=finalState.formData[el.id];}else{updateData[el.id]=finalState.formData[el.id];}});// Trigger the given update action of the parent and provide all new validation errors to it
_this2.props.handleUpdate(updateData,hasErrors,// Output validation errors in the same structure (array) as the component takes them
hasErrors?Object.keys(validationErrors).map(function(k){return{path:k,message:validationErrors[k]};}):[]);});/**
* Takes an element of any type and renders it depending on type.
* Also puts portals around the element.
* @param {string} formName Name of the form
* @param {Object} element The data of the element to be rendered
* @param {string} elementErrorText The error text to be shown for this specific element
* @returns {JSX.Element}
*/_defineProperty(_this2,"renderElement",function(formName,element,elementErrorText){var formData=_this2.state.formData;var elementName="".concat(_this2.props.name,"_").concat(element.id);var elementValue=formData[element.id];var elementVisible=_this2.state.elementVisibility[element.id]||false;// Take a dynamic REACT element based on its type
var Element=_this2.getFormElementComponent(element.type);if(!Element){logger.error("Unknown form element type: ".concat(element.type));return null;}// Country and province elements have their data injected, if not already present
var elementData=element;switch(element.type){case ELEMENT_TYPE_COUNTRY:{elementData.options=element.options||_this2.countryList;break;}case ELEMENT_TYPE_PROVINCE:{// Province selection only makes sense with a country being selected, or from custom options
var countryElement=_this2.formElements.find(function(el){return el.type===ELEMENT_TYPE_COUNTRY;});elementData.options=countryElement&&formData[countryElement.id]?buildProvinceList(formData[countryElement.id],// Auto-select with "empty" when not required
element.required?null:emptySelectOption):{};break;}default:break;}return React.createElement(Element,{name:elementName,element:elementData,errorText:elementErrorText,value:elementValue,visible:elementVisible,formName:formName});});_this2.state={elementVisibility:{},formData:{}};// Reorganize form elements into a structure that can be easily rendered
var formElements=buildFormElements(props.config,_this2.elementChangeHandler);// Compute defaults
var formDefaults=buildFormDefaults(formElements,props.defaults);// Assign defaults to state
_this2.state.formData=formDefaults;// Handle fixed visibilities
formElements.forEach(function(element){// Assume as visible except it's explicitly set to "false"
_this2.state.elementVisibility[element.id]=element.visible!==false;});_this2.actionListener=new ActionListener(buildProvinceList,formDefaults);_this2.actionListener.attachAll(formElements);// Sort the elements after attaching action listeners to keep action hierarchy same as creation
_this2.formElements=formElements.sort(_this2.elementSortFunc);// Assemble combined country/province list based on the config element
var _countryElement=_this2.formElements.find(function(el){return el.type===ELEMENT_TYPE_COUNTRY;});if(_countryElement){_this2.countryList=buildCountryList(_countryElement,emptySelectOption);var provinceElement=_this2.formElements.find(function(el){return el.type===ELEMENT_TYPE_PROVINCE;});if(provinceElement&&provinceElement.required&&!!formDefaults[_countryElement.id]&&!formDefaults[provinceElement.id]){// Set default for province field for given country
var _Object$keys3=Object.keys(buildProvinceList(formDefaults[_countryElement.id])),_Object$keys4=_slicedToArray(_Object$keys3,1),first=_Object$keys4[0];if(first){_this2.state.formData[provinceElement.id]=first;}}}// Final form initialization, by triggering actionListeners and enable rendering for elements
var _newState=_this2.state;_this2.formElements.forEach(function(element){_newState=_this2.actionListener.notify(element.id,_this2.state,_newState);});_this2.state=_newState;return _this2;}_inherits(Builder,_Component);return _createClass(Builder,[{key:"render",value:/**
* Renders the component based on the given config
* @return {JSX.Element}
*/function render(){var _this3=this;var _this$props=this.props,name=_this$props.name,className=_this$props.className,onSubmit=_this$props.onSubmit;// Convert validation errors for easier handling
var validationErrors=buildValidationErrorList(this.props.validationErrors);var validationErrorsAmount=Object.entries(validationErrors).length;return React.createElement(Form,{className:_camelCase(name),onSubmit:onSubmit},validationErrorsAmount>0&&React.createElement("div",{className:"sr-only"},React.createElement(I18n.Text,{string:"login.errorAmount",params:{amount:validationErrorsAmount}})),React.createElement("div",{className:className},this.formElements.map(function(element){return React.createElement(Fragment,{key:"".concat(name,"_").concat(element.id)},React.createElement(Portal,{name:"".concat(sanitizePortalName(name),".").concat(sanitizePortalName(element.id),".").concat(BEFORE),props:{formName:name,errorText:validationErrors[element.id]||'',element:element}}),React.createElement(Portal,{name:"".concat(sanitizePortalName(name),".").concat(sanitizePortalName(element.id)),props:{formName:name,errorText:validationErrors[element.id]||'',element:element}},_this3.renderElement(name,element,validationErrors[element.id]||'')),React.createElement(Portal,{name:"".concat(sanitizePortalName(name),".").concat(sanitizePortalName(element.id),".").concat(AFTER),props:{formName:name,errorText:validationErrors[element.id]||'',element:element}}));})));}}]);}(Component);_Builder=Builder;_defineProperty(Builder,"defaultElements",(_defineProperty2={},_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty2,ELEMENT_TYPE_EMAIL,ElementText),ELEMENT_TYPE_PASSWORD,ElementText),ELEMENT_TYPE_TEXT,ElementText),ELEMENT_TYPE_NUMBER,ElementText),ELEMENT_TYPE_SELECT,ElementSelect),ELEMENT_TYPE_MULTISELECT,ElementMultiSelect),ELEMENT_TYPE_COUNTRY,ElementSelect),ELEMENT_TYPE_PROVINCE,ElementSelect),ELEMENT_TYPE_CHECKBOX,ElementCheckbox),ELEMENT_TYPE_RADIO,ElementRadio),_defineProperty(_defineProperty(_defineProperty(_defineProperty2,ELEMENT_TYPE_DATE,ElementText),ELEMENT_TYPE_PHONE,ElementText),ELEMENT_TYPE_PHONE_PICKER,ElementPhoneNumber)));_defineProperty(Builder,"defaultProps",{className:null,defaults:{},elements:_Builder.defaultElements,onSubmit:function onSubmit(){},validationErrors:[]/**
* Initializes the component.
* @param {Object} props The components props.
*/});export default Builder;