UNPKG

react-minimalistic-use-form

Version:

Minimalistic react hook for handling forms without much pain.

569 lines 31 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { useRef, useEffect, useCallback, useMemo, useReducer, } from 'react'; import { getDefaultDateValue } from '../utils/getDefaultDateValue'; import { getInitialState, reducer } from './state'; import { eventTypes, htmlAttributes, htmlInputTypes, STATE_ACTIONS, } from './enums'; import { validatePlugins } from '../utils/validatePlugins'; import { useIsUpdated } from '../hooks/is-updated'; var TOUCHED_CLASS_NAME = 'is-touched'; var ERROR_CLASS_NAME = 'has-error'; var ELEMENT_TAG_NAME_SELECT = 'SELECT'; var CHECKBOX_DEFAULT_VALUE = 'on'; var supportedFormElements = [ 'text', 'email', 'password', 'checkbox', 'radio', 'number', 'textarea', 'date', 'tel', 'search', 'url', 'color', ]; var validityDefaultErrorMessages = { badInput: function () { return 'Invalid input'; }, patternMismatch: function (_a) { var pattern = _a.pattern; return "Please match the format requested : \"".concat(pattern, "\""); }, rangeOverflow: function (_a) { var value = _a.value; return "Value must be less than or equal to ".concat(value, "."); }, rangeUnderflow: function (_a) { var value = _a.value; return "Value must be greater than or equal to ".concat(value, "."); }, stepMismatch: function (_a) { var step = _a.step; return "Please enter a valid value. Number must have step of ".concat(step); }, tooLong: function (_a) { var maxLength = _a.maxLength, value = _a.value; return "Please lengthen this text to ".concat(maxLength, " characters or less (you are currently using ").concat(value, " character)."); }, tooShort: function (_a) { var minLength = _a.minLength, value = _a.value; return "Please lengthen this text to ".concat(minLength, " characters or more (you are currently using ").concat(value, " character)."); }, typeMismatch: function (_a) { var type = _a.type; return "Type mismatch. Must be type of \"".concat(type, "\"."); }, valueMissing: function () { return 'Please fill in this field.'; }, }; /** * * This function is used on <resetForm>. The function updates the field value natively with initialValue * provided either via initialValues or custom one in case of custom controlled input. * Since Events (change | click) must be fired manually in order to call event handlers * of parent component that controls the input we have to update value via setter function * which will be picked up by input event handler attached as element attribute callback * * Why? Because React tracks when you set the value property on an input to keep track of the node's value. * When you dispatch a change event, it checks it's last value against the current value * (https://github.com/facebook/react/blob/dd5fad29616f706f484938663e93aaadd2a5e594/packages/react-dom/src/client/inputValueTracking.js#L129) * and if they're the same it does not call any event handlers (as no change has taken place as far as react is concerned). * So we have to set the value in a way that React's value setter function * (https://github.com/facebook/react/blob/dd5fad29616f706f484938663e93aaadd2a5e594/packages/react-dom/src/client/inputValueTracking.js#L78-L81) * will not be called, which is where the setNativeValue comes into play. * This function was a team effort: https://github.com/facebook/react/issues/10135#issuecomment-401496776 * @param {Node} element * @param {String} attributeToUpdate * @param {String | Boolean} value */ export var setNativeValue = function (_a) { var element = _a.element, _b = _a.attributeToUpdate, attributeToUpdate = _b === void 0 ? htmlAttributes.value : _b, _c = _a.value, value = _c === void 0 ? '' : _c; var valueSetter = (Object.getOwnPropertyDescriptor(element, attributeToUpdate) || {}).set; var prototype = Object.getPrototypeOf(element); var prototypeValueSetter = (Object.getOwnPropertyDescriptor(prototype, attributeToUpdate) || {}).set; if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { prototypeValueSetter.call(element, value); } else if (valueSetter) { valueSetter.call(element, value); } else { throw new Error('The given element does not have a value setter'); } }; export var useForm = function (_a) { var _b = _a === void 0 ? {} : _a, _c = _b.initialValues, initialValues = _c === void 0 ? {} : _c, _d = _b.errorClassName, errorClassName = _d === void 0 ? ERROR_CLASS_NAME : _d, _e = _b.touchedClassName, touchedClassName = _e === void 0 ? TOUCHED_CLASS_NAME : _e, _f = _b.scrollToError, scrollToError = _f === void 0 ? false : _f, scrollToErrorOptions = _b.scrollToErrorOptions, _g = _b.validateOnInput, validateOnInput = _g === void 0 ? true : _g, _h = _b.validateOnSubmit, validateOnSubmit = _h === void 0 ? false : _h, _j = _b.validateOnMount, validateOnMount = _j === void 0 ? false : _j, _k = _b.debounceValidation, debounceValidation = _k === void 0 ? false : _k, _l = _b.debounceTime, debounceTime = _l === void 0 ? 300 : _l, _m = _b.plugins, plugins = _m === void 0 ? {} : _m; var _o = __read(useReducer(reducer, getInitialState(initialValues)), 2), state = _o[0], dispatch = _o[1]; var formRef = useRef(null); var touchedRef = useRef({}); var validatorDebounceTimeout = useRef(undefined); var throwFormRefError = function () { throw new Error('formRef is empty! useForm "formRef" needs to be attached to form element'); }; var getFormElements = useCallback(function (form) { if (form === null || form === undefined) { return throwFormRefError(); } var formElements = form.elements; return __spreadArray([], __read(formElements), false).filter(function (element) { return supportedFormElements.includes(element.type) || element.tagName === ELEMENT_TAG_NAME_SELECT; }); }, []); /** * Method runs on each change of state.errors to properly update "isFormValid". */ var updateIsFormValid = useCallback(function () { var formElements = getFormElements(formRef.current); var allValidityValid = formElements.every(function (element) { return element.validity.valid; }); var noErrors = Object.values(state.errors).every(function (fieldErrors) { return Object.keys(fieldErrors).length === 0; }); var isFormValid = allValidityValid && noErrors; dispatch({ type: STATE_ACTIONS.SET_IS_FORM_VALID, payload: { isFormValid: isFormValid } }); return isFormValid; }, [getFormElements, state.errors]); var _scrollToError = useCallback(function (element) { return __awaiter(void 0, void 0, void 0, function () { var inputLabel, elementToScrollInto; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: inputLabel = (_a = element === null || element === void 0 ? void 0 : element.closest('label')) !== null && _a !== void 0 ? _a : document.querySelector("label[for=\"".concat(element.name, "\"")); elementToScrollInto = inputLabel !== null && inputLabel !== void 0 ? inputLabel : element; if (!((plugins === null || plugins === void 0 ? void 0 : plugins.scrollToError) !== undefined)) return [3 /*break*/, 2]; return [4 /*yield*/, plugins.scrollToError(elementToScrollInto)]; case 1: _b.sent(); return [2 /*return*/]; case 2: elementToScrollInto.scrollIntoView(scrollToErrorOptions); return [2 /*return*/]; } }); }); }, [plugins, scrollToErrorOptions]); var setFieldErrorClassName = useCallback(function (_a) { var classList = _a.classList; classList.add(errorClassName); }, [errorClassName]); var unsetFieldErrorClassName = useCallback(function (_a) { var classList = _a.classList; classList.remove(errorClassName); }, [errorClassName]); /** * Set error class name and update the state */ var setFieldErrors = useCallback(function (_a) { var element = _a.element, errors = _a.errors; var name = element.name; dispatch({ type: STATE_ACTIONS.SET_FIELD_ERRORS, payload: { name: name, errors: errors } }); }, []); /** * Unset error classname and update the state */ var unsetFieldErrors = useCallback(function (element) { var name = element.name; dispatch({ type: STATE_ACTIONS.SET_FIELD_ERRORS, payload: { name: name, errors: {} } }); }, []); /** * Check element html validity and return an object with errors */ var getFieldValidityErrors = useCallback(function (element) { var _a; var elementErrors = {}; if (!((_a = element.validity) === null || _a === void 0 ? void 0 : _a.valid)) { for (var validityName in element.validity) { // @ts-ignore if (validityDefaultErrorMessages.hasOwnProperty(validityName) === true && element.validity[validityName] === true) { // eslint-disable-line elementErrors[validityName] = validityDefaultErrorMessages[validityName](element); } } } return elementErrors; }, []); var updateError = useCallback(function (_a) { var element = _a.element, shouldScrollToError = _a.shouldScrollToError, _b = _a.shouldSetErrorClassName, shouldSetErrorClassName = _b === void 0 ? true : _b, _c = _a.shouldSetFieldError, shouldSetFieldError = _c === void 0 ? true : _c; return __awaiter(void 0, void 0, void 0, function () { var name, value, validator, elementErrors, _d, _e, elementValidatorErrors, otherFieldsErrors; var _f, _g; return __generator(this, function (_h) { switch (_h.label) { case 0: name = element.name, value = element.value; validator = plugins.validator; elementErrors = getFieldValidityErrors(element); if (!(validator !== undefined)) return [3 /*break*/, 2]; return [4 /*yield*/, validator({ name: name, value: value, values: state.values, target: element, })]; case 1: _d = _h.sent(), _e = name, elementValidatorErrors = _d[_e], otherFieldsErrors = __rest(_d, [typeof _e === "symbol" ? _e : _e + ""]); elementErrors = __assign(__assign({}, elementValidatorErrors), elementErrors); Object.entries(otherFieldsErrors).forEach(function (_a) { var _b = __read(_a, 2), fieldName = _b[0], fieldErrors = _b[1]; var formElement = getFormElements(formRef.current).find(function (_element) { return _element.name === fieldName; }); if (!formElement) return; var fieldValidityErrors = getFieldValidityErrors(formElement); var fieldCombinedErrors = __assign(__assign({}, fieldErrors), fieldValidityErrors); if (shouldSetErrorClassName) { setFieldErrorClassName(formElement); } if (shouldSetFieldError) { setFieldErrors({ element: formElement, errors: fieldCombinedErrors }); } }); _h.label = 2; case 2: if (Object.keys(elementErrors).length === 0 && shouldSetFieldError) { unsetFieldErrorClassName(element); unsetFieldErrors(element); return [2 /*return*/, (_f = {}, _f[name] = {}, _f)]; } if (shouldScrollToError) { _scrollToError(element); } if (shouldSetErrorClassName) { setFieldErrorClassName(element); } if (shouldSetFieldError) { setFieldErrors({ element: element, errors: elementErrors }); } return [2 /*return*/, (_g = {}, _g[name] = elementErrors, _g)]; } }); }); }, [_scrollToError, state.values, plugins, getFieldValidityErrors, setFieldErrors, unsetFieldErrors, getFormElements, setFieldErrorClassName, unsetFieldErrorClassName]); var resetForm = function () { var form = formRef.current; var overriddenInitialValues = state.overriddenInitialValues; getFormElements(form) .forEach(function (element) { var classList = element.classList, type = element.type, value = element.value, name = element.name; classList.remove(errorClassName, touchedClassName); setNativeValue({ element: element, value: overriddenInitialValues[name] }); if (type === htmlInputTypes.checkbox || type === htmlInputTypes.radio) { var checked = type === htmlInputTypes.radio ? value === overriddenInitialValues[name] : overriddenInitialValues[name] === true; setNativeValue({ element: element, value: checked, attributeToUpdate: htmlAttributes.checked }); element.dispatchEvent(new window.InputEvent(eventTypes.click, { bubbles: true })); } element.dispatchEvent(new window.InputEvent(eventTypes.input, { bubbles: true })); element.dispatchEvent(new window.InputEvent(eventTypes.change, { bubbles: true })); }); touchedRef.current = {}; dispatch({ type: STATE_ACTIONS.RESET_FORM }); }; var isTouched = useCallback(function (name) { return touchedRef.current[name] === true; }, []); var validateInputOnChange = useCallback(function (event) { return __awaiter(void 0, void 0, void 0, function () { var shouldValidate; return __generator(this, function (_a) { switch (_a.label) { case 0: shouldValidate = validateOnInput === true && isTouched(event.target.name); if (!shouldValidate) return [2 /*return*/]; if (debounceValidation) { // @ts-ignore clearTimeout(validatorDebounceTimeout.current); event.persist(); validatorDebounceTimeout.current = setTimeout(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, updateError({ element: event.target, shouldScrollToError: scrollToError })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }, debounceTime); return [2 /*return*/]; } return [4 /*yield*/, updateError({ element: event.target, shouldScrollToError: scrollToError })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }, [debounceValidation, debounceTime, scrollToError, updateError, validateOnInput, isTouched]); var onChange = useCallback(function (event) { return __awaiter(void 0, void 0, void 0, function () { var _a, name, value, type, checked; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = event.target, name = _a.name, value = _a.value, type = _a.type, checked = _a.checked; dispatch({ type: STATE_ACTIONS.SET_FIELD_VALUE, payload: { name: name, type: type, checked: checked, value: value, }, }); return [4 /*yield*/, validateInputOnChange(event)]; case 1: _b.sent(); return [2 /*return*/]; } }); }); }, [validateInputOnChange]); var setFieldTouched = useCallback(function (element) { element.classList.add(touchedClassName); touchedRef.current[element.name] = true; }, [touchedClassName]); var onBlur = useCallback(function (_a) { var target = _a.target; return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_b) { switch (_b.label) { case 0: /* Once blur is triggered, input is set to touched which _flags_ onChange handler to do live validation as the user types */ setFieldTouched(target); if (!(validateOnInput === true)) return [3 /*break*/, 2]; return [4 /*yield*/, updateError({ element: target, shouldScrollToError: scrollToError })]; case 1: _b.sent(); _b.label = 2; case 2: return [2 /*return*/]; } }); }); }, [updateError, validateOnInput, scrollToError, setFieldTouched]); var setIsSubmitting = useCallback(function (isSubmitting) { dispatch({ type: STATE_ACTIONS.SET_IS_SUBMITTING, payload: { isSubmitting: isSubmitting } }); }, []); var validateForm = useCallback(function (_a) { var _b = _a === void 0 ? {} : _a, _c = _b.shouldTouchField, shouldTouchField = _c === void 0 ? true : _c, _d = _b.shouldSetErrorClassName, shouldSetErrorClassName = _d === void 0 ? true : _d, _e = _b.shouldScrollToError, shouldScrollToError = _e === void 0 ? scrollToError : _e; return __awaiter(void 0, void 0, void 0, function () { var _formElements, errorsArray, errors; return __generator(this, function (_f) { switch (_f.label) { case 0: _formElements = getFormElements(formRef.current); if (shouldTouchField === true) { _formElements.forEach(function (element) { return setFieldTouched(element); }); } return [4 /*yield*/, Promise.all(_formElements.map(function (element) { return updateError({ element: element, shouldScrollToError: shouldScrollToError, shouldSetErrorClassName: shouldSetErrorClassName, shouldSetFieldError: false, }); }))]; case 1: errorsArray = _f.sent(); errors = errorsArray.reduce(function (acc, fieldErrors) { return (__assign(__assign({}, acc), fieldErrors)); }, {}); dispatch({ type: STATE_ACTIONS.SET_ERRORS, payload: { errors: errors } }); return [2 /*return*/, errors]; } }); }); }, [getFormElements, setFieldTouched, updateError, scrollToError]); var onSubmit = function (callbackFn) { return function (event) { return __awaiter(void 0, void 0, void 0, function () { var _isFormValid, _errors, _formElements, elementToScrollInto; return __generator(this, function (_a) { switch (_a.label) { case 0: event.persist(); setIsSubmitting(true); _isFormValid = state.isFormValid; _errors = state.errors; if (!(validateOnSubmit === true)) return [3 /*break*/, 2]; _formElements = getFormElements(formRef.current); return [4 /*yield*/, validateForm({ shouldScrollToError: scrollToError })]; case 1: _errors = _a.sent(); _isFormValid = Object.values(_errors).every(function (fieldErrors) { return Object.keys(fieldErrors).length === 0; }); if (!_isFormValid && scrollToError === true) { elementToScrollInto = _formElements.find(function (element) { return !element.validity.valid || Object.keys(_errors[element.name]).length > 0; }); if (elementToScrollInto !== undefined) { _scrollToError(elementToScrollInto); } } _a.label = 2; case 2: return [4 /*yield*/, callbackFn({ event: event, isFormValid: _isFormValid, errors: _errors, values: state.values, })]; case 3: _a.sent(); return [2 /*return*/]; } }); }); }; }; /** * getElementInitialValue gets elements "right" initialValue where multiple cases are covered: * 1) Form field doesn't have explicitly set attribute <value> and it's not set in useForm via initialValues prop * 2) Form field doesn't have explicitly set attribute <value> but it's set in useForm via initialValues prop * 3) Form field has explicitly set attribute <value> (controlled input) and it's not set in useForm via initialValues prop * 4) Form field has explicitly set attribute <value> (controlled input) and has set value via initialValues prop - in this * case controlled input has precedence and the value from initialValues is overridden */ var getElementInitialValue = useCallback(function (_a) { var name = _a.name, type = _a.type, value = _a.value; var elementInitialValue = value; var hasInitialValue = name in state.initialValues; var isCheckbox = type === htmlInputTypes.checkbox; var isDefaultNativeHtmlCheckboxValue = value === CHECKBOX_DEFAULT_VALUE; if (isCheckbox && hasInitialValue === true) { elementInitialValue = isDefaultNativeHtmlCheckboxValue ? state.initialValues[name] : elementInitialValue === 'true'; } else if (isCheckbox && hasInitialValue === false) { elementInitialValue = isDefaultNativeHtmlCheckboxValue ? false : elementInitialValue === 'true'; } else if (!isCheckbox && hasInitialValue === true) { elementInitialValue = Boolean(value) === false ? state.initialValues[name] : elementInitialValue; } else if (!isCheckbox && hasInitialValue === false) { elementInitialValue = type === htmlInputTypes.date ? getDefaultDateValue() : elementInitialValue; } return elementInitialValue; }, [state.initialValues]); /** * This function ensures that consumer can have custom controlled form fields. In that case * initial value on the form will be overridden by consumer's custom controlled value * to persist valid form values in case form gets submitted * without changing the value in custom controlled input fields */ var bindInitialValues = useCallback(function (form) { var initialValuesToOverride = {}; getFormElements(form) .forEach(function (element) { var elementInitialValue = getElementInitialValue(element); var shouldOverrideInitialValue = element.type !== htmlInputTypes.radio || (element.type === htmlInputTypes.radio && element.checked === true); if (shouldOverrideInitialValue) { initialValuesToOverride[element.name] = elementInitialValue; } // eslint-disable-next-line no-param-reassign element.value = elementInitialValue; if (element.type === htmlInputTypes.checkbox) { // eslint-disable-next-line no-param-reassign element.checked = elementInitialValue === true; } }); var updatedInitialValues = __assign(__assign({}, state.initialValues), initialValuesToOverride); // Set proper initial <checked> attribute for radio buttons __spreadArray([], __read(form.elements), false).filter(function (element) { return element.type === htmlInputTypes.radio; }) .forEach(function (element) { // eslint-disable-next-line no-param-reassign element.checked = element.value === updatedInitialValues[element.name]; }); dispatch({ type: STATE_ACTIONS.SET_OVERRIDDEN_INITIAL_VALUES, payload: { overriddenInitialValues: updatedInitialValues } }); }, [getElementInitialValue, state.initialValues, getFormElements]); var _validatePlugins = useCallback(function () { return validatePlugins(plugins); }, [plugins]); useEffect(function () { _validatePlugins(); if (validateOnMount) { validateForm({ shouldSetErrorClassName: false, shouldTouchField: false }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [validateOnMount]); useIsUpdated(function () { updateIsFormValid(); }, [state.errors]); useEffect(function () { var form = formRef.current; bindInitialValues(form); }, [bindInitialValues]); var bindUseForm = useMemo(function () { return ({ formRef: formRef, onBlur: onBlur, onChange: onChange, values: state.values, }); }, [formRef, onChange, onBlur, state.values]); return { resetForm: resetForm, onChange: onChange, onBlur: onBlur, onSubmit: onSubmit, validateForm: validateForm, isFormValid: state.isFormValid, isSubmitting: state.isSubmitting, formRef: formRef, values: state.values, errors: state.errors, bindUseForm: bindUseForm, setIsSubmitting: setIsSubmitting, touched: touchedRef.current, isTouched: isTouched, }; }; //# sourceMappingURL=useForm.js.map