UNPKG

riec

Version:

Modern React component for inline edit of text/select values, written in Typescript

499 lines (452 loc) 13.6 kB
import React__default, { useRef, useCallback, useEffect, createElement, Fragment } from 'react'; import { useMachine } from '@xstate/react'; import { Machine, assign } from 'xstate'; var getInlineEditMachine = function getInlineEditMachine(_ref) { var value = _ref.value, isDisabled = _ref.isDisabled, allowEditWhileLoading = _ref.allowEditWhileLoading, optimisticUpdate = _ref.optimisticUpdate, validate = _ref.validate, onChange = _ref.onChange, saveTimeout = _ref.saveTimeout, savedDuration = _ref.savedDuration, errorDuration = _ref.errorDuration; return Machine({ id: 'inlineEdit', initial: 'view', context: { value: value, newValue: '', oldValue: '', isValid: validate && typeof validate === 'function' ? validate(value) : true }, states: { view: { entry: 'reset', on: { CLICK: { target: 'edit', cond: 'isEnabled' }, FOCUS: { target: 'edit', cond: 'isEnabled' }, SAVED: { target: 'saved', actions: 'commitChange' } } }, edit: { entry: 'validate', on: { CHANGE: { target: 'edit', actions: 'change' }, ESC: 'view', ENTER: [{ target: 'loading', cond: 'shouldSend' }, { target: 'view' }], BLUR: [{ target: 'loading', cond: 'shouldSend' }, { target: 'view' }] } }, loading: { entry: [optimisticUpdate ? 'optimisticUpdate' : 'noAction', 'sendChange'], on: { CLICK: { target: 'edit', cond: 'canEditWhileLoading' }, FOCUS: { target: 'edit', cond: 'canEditWhileLoading' }, SAVED: { target: 'saved', actions: 'commitChange' } }, after: { SAVE_TIMEOUT: { target: 'error', actions: optimisticUpdate ? 'cancelChange' : 'noAction' } } }, saved: { on: { CLICK: { target: 'edit', cond: 'isEnabled' }, FOCUS: { target: 'edit', cond: 'isEnabled' }, SAVED: { target: 'saved', actions: 'commitChange' } }, after: { SAVED_DURATION: { target: 'view' } } }, error: { on: { CLICK: { target: 'edit', cond: 'isEnabled' }, FOCUS: { target: 'edit', cond: 'isEnabled' }, SAVED: { target: 'saved', actions: 'commitChange' } }, after: { ERROR_DURATION: { target: 'view' } } } } }, { actions: { change: assign({ newValue: function newValue(_, event) { return event.value; } }), reset: assign({ newValue: function newValue(context) { return context.value; } }), optimisticUpdate: assign({ oldValue: function oldValue(context) { return context.value; }, value: function value(context) { return context.newValue; } }), noAction: function noAction() {}, sendChange: function sendChange(context) { onChange(context.newValue); }, commitChange: assign({ value: function value(_, event) { return event.value; } }), cancelChange: assign({ value: function value(context) { return context.oldValue; } }), validate: validate && typeof validate === 'function' ? assign({ isValid: function isValid(context) { return validate(context.newValue); } }) : function () {} }, guards: { shouldSend: function shouldSend(context) { return context.isValid && context.newValue !== context.value; }, isEnabled: function isEnabled() { return !isDisabled; }, canEditWhileLoading: function canEditWhileLoading() { return !isDisabled && allowEditWhileLoading; } }, delays: { SAVE_TIMEOUT: saveTimeout, SAVED_DURATION: savedDuration, ERROR_DURATION: errorDuration } }); }; var InputType; (function (InputType) { InputType["Text"] = "text"; InputType["Number"] = "number"; InputType["Email"] = "email"; InputType["Password"] = "password"; InputType["Date"] = "date"; InputType["Range"] = "range"; InputType["TextArea"] = "textarea"; InputType["Select"] = "select"; })(InputType || (InputType = {})); var InputType$1 = InputType; var Input = function Input(_ref) { var value = _ref.value, type = _ref.type, editProps = _ref.editProps, editClassProp = _ref.editClassProp, options = _ref.options, valueKey = _ref.valueKey, labelKey = _ref.labelKey, handleChange = _ref.handleChange, handleKeyDown = _ref.handleKeyDown, handleBlur = _ref.handleBlur; //========================== // Focus input as it mounts // ========================= var inputRef = useRef(null); var textareaRef = useRef(null); var selectRef = useRef(null); var getRef = useCallback(function () { if (type === InputType$1.Select) { return selectRef; } if (type === InputType$1.TextArea) { return textareaRef; } return inputRef; }, [type]); useEffect(function () { var controlRef = getRef(); if (controlRef.current) { setTimeout(function () { if (controlRef.current) { // Focus input controlRef.current.focus(); if (controlRef === inputRef || controlRef === textareaRef) { // If it is not a Select => select input content controlRef.current.select(); } } }, 10); } }, [getRef]); //========================== // Select // ========================= if (type === InputType$1.Select) { return React__default.createElement("select", Object.assign({}, editProps, editClassProp, { ref: selectRef, value: value, onChange: function onChange(event) { return handleChange(event.target.value); }, onKeyDown: handleKeyDown, onBlur: handleBlur }), options.map(function (option) { return React__default.createElement("option", { key: option[valueKey], value: option[valueKey] }, option[labelKey]); })); } //========================== // TextArea // ========================= if (type === InputType$1.TextArea) { return React__default.createElement("textarea", Object.assign({}, editProps, editClassProp, { ref: textareaRef, value: value, onChange: function onChange(event) { return handleChange(event.target.value); }, onKeyDown: handleKeyDown, onBlur: handleBlur })); } //========================== // All Others // ========================= return React__default.createElement("input", Object.assign({}, editProps, editClassProp, { ref: inputRef, type: type, value: value, onChange: function onChange(event) { return handleChange(event.target.value); }, onKeyDown: handleKeyDown, onBlur: handleBlur })); }; var InlineEdit = function InlineEdit(_ref) { var value = _ref.value, onChange = _ref.onChange, _ref$type = _ref.type, type = _ref$type === void 0 ? InputType$1.Text : _ref$type, format = _ref.format, render = _ref.render, validate = _ref.validate, _ref$isDisabled = _ref.isDisabled, isDisabled = _ref$isDisabled === void 0 ? false : _ref$isDisabled, _ref$allowEditWhileLo = _ref.allowEditWhileLoading, allowEditWhileLoading = _ref$allowEditWhileLo === void 0 ? false : _ref$allowEditWhileLo, _ref$optimisticUpdate = _ref.optimisticUpdate, optimisticUpdate = _ref$optimisticUpdate === void 0 ? true : _ref$optimisticUpdate, _ref$saveTimeout = _ref.saveTimeout, saveTimeout = _ref$saveTimeout === void 0 ? 2000 : _ref$saveTimeout, _ref$savedDuration = _ref.savedDuration, savedDuration = _ref$savedDuration === void 0 ? 700 : _ref$savedDuration, _ref$errorDuration = _ref.errorDuration, errorDuration = _ref$errorDuration === void 0 ? 1000 : _ref$errorDuration, editProps = _ref.editProps, viewClass = _ref.viewClass, editClass = _ref.editClass, disabledClass = _ref.disabledClass, loadingClass = _ref.loadingClass, invalidClass = _ref.invalidClass, savedClass = _ref.savedClass, errorClass = _ref.errorClass, _ref$showNewLines = _ref.showNewLines, showNewLines = _ref$showNewLines === void 0 ? true : _ref$showNewLines, _ref$options = _ref.options, options = _ref$options === void 0 ? [] : _ref$options, _ref$valueKey = _ref.valueKey, valueKey = _ref$valueKey === void 0 ? 'value' : _ref$valueKey, _ref$labelKey = _ref.labelKey, labelKey = _ref$labelKey === void 0 ? 'label' : _ref$labelKey; //========================== // XState Machine // ========================= var _useMachine = useMachine(getInlineEditMachine({ value: value, isDisabled: isDisabled, allowEditWhileLoading: allowEditWhileLoading, optimisticUpdate: optimisticUpdate, validate: validate, onChange: onChange, saveTimeout: saveTimeout, savedDuration: savedDuration, errorDuration: errorDuration })), current = _useMachine[0], send = _useMachine[1]; //========================== // Send SAVED event when a // new value is received // ========================= var isFirstRun = useRef(true); useEffect(function () { // Prevent triggering SAVED // on first render if (isFirstRun.current) { isFirstRun.current = false; return; } // Trigger it on value changes send({ type: 'SAVED', value: value }); }, [value]); //========================== // Event Handlers // ========================= var handleChange = function handleChange(value) { send({ type: 'CHANGE', value: value }); if (type === InputType$1.Select) { send('ENTER'); } }; var handleBlur = function handleBlur() { send('BLUR'); }; var handleKeyDown = function handleKeyDown(event) { if (event.keyCode === 13 && type !== InputType$1.TextArea) { send('ENTER'); } else if (event.keyCode === 27) { send('ESC'); } }; //========================== // CSS Classes View // ========================= var viewClassNames = []; if (viewClass) { viewClassNames.push(viewClass); } if (loadingClass && current.value === 'loading') { viewClassNames.push(loadingClass); } if (savedClass && current.value === 'saved') { viewClassNames.push(savedClass); } if (errorClass && current.value === 'error') { viewClassNames.push(errorClass); } if (disabledClass && isDisabled) { viewClassNames.push(disabledClass); } var viewClassProp = viewClassNames.length > 0 ? { className: viewClassNames.join(' ') } : {}; //========================== // CSS Classes Edit // ========================= var editClassNames = []; if (editClass) { editClassNames.push(editClass); } if (invalidClass && !current.context.isValid) { editClassNames.push(invalidClass); } var editClassProp = editClassNames.length > 0 ? { className: editClassNames.join(' ') } : {}; //========================== // Format View Value // ========================= var viewValue = current.context.value; // If Select => get label if (type === InputType$1.Select) { var valueOption = options.find(function (option) { return option[valueKey] + '' === current.context.value; }); if (valueOption) { viewValue = valueOption[labelKey]; } } // If format function, apply if (format) { viewValue = format(viewValue); } // If TextArea and showNewLine, do it if (type === InputType$1.TextArea && showNewLines) { viewValue = viewValue.split('\n').map(function (item, key) { return createElement("span", { key: key }, item, createElement("br", null)); }); } //========================== // Render // ========================= return createElement(Fragment, null, (current.value === 'view' || current.value === 'loading' || current.value === 'saved' || current.value === 'error') && createElement("span", Object.assign({}, viewClassProp, { onClick: function onClick() { return send('CLICK'); }, onFocus: function onFocus() { return send('FOCUS'); }, tabIndex: 0 }), render ? render(viewValue) : viewValue), current.value === 'edit' && createElement(Input, { type: type, value: current.context.newValue, editProps: editProps, editClassProp: editClassProp, options: options, valueKey: valueKey, labelKey: labelKey, handleChange: handleChange, handleKeyDown: handleKeyDown, handleBlur: handleBlur })); }; export default InlineEdit; export { InputType$1 as InputType }; //# sourceMappingURL=riec.esm.js.map