UNPKG

@shopgate/engage

Version:
65 lines • 15.7 kB
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;