gitlab-ce/app/assets/javascripts/lib/utils/forms.js

215 lines
6.2 KiB
JavaScript

import { convertToCamelCase } from '~/lib/utils/text_utility';
export const serializeFormEntries = (entries) =>
entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {});
export const serializeForm = (form) => {
const fdata = new FormData(form);
const entries = Array.from(fdata.keys()).map((key) => {
let val = fdata.getAll(key);
// Microsoft Edge has a bug in FormData.getAll() that returns an undefined
// value for each form element that does not match the given key:
// https://github.com/jimmywarting/FormData/issues/80
val = val.filter((n) => n);
return { name: key, value: val.length === 1 ? val[0] : val };
});
return serializeFormEntries(entries);
};
/**
* Like trim but without the error for non-string values.
*
* @param {String, Number, Array} - value
* @returns {String, Number, Array} - the trimmed string or the value if it isn't a string
*/
export const safeTrim = (value) => (typeof value === 'string' ? value.trim() : value);
/**
* Check if the value provided is empty or not
*
* It is being used to check if a form input
* value has been set or not.
*
* @param {String, Number, Array} - Any form value
* @returns {Boolean} - returns false if a value is set
*
* @example
* returns true for '', ' ', [], null, undefined
*/
export const isEmptyValue = (value) => value == null || safeTrim(value).length === 0;
/**
* Check if the value has a minimum string length
*
* @param {String, Number, Array} - Any form value
* @param {Number} - minLength
* @returns {Boolean}
*/
export const hasMinimumLength = (value, minLength) =>
!isEmptyValue(value) && value.length >= minLength;
/**
* Checks if the given value can be parsed as an integer as it is (without cutting off decimals etc.)
*
* @param {String, Number, Array} - Any form value
* @returns {Boolean}
*/
export const isParseableAsInteger = (value) =>
!isEmptyValue(value) && Number.isInteger(Number(safeTrim(value)));
/**
* Checks if the parsed integer value from the given input is greater than a certain number
*
* @param {String, Number, Array} - Any form value
* @param {Number} - greaterThan
* @returns {Boolean}
*/
export const isIntegerGreaterThan = (value, greaterThan) =>
isParseableAsInteger(value) && parseInt(value, 10) > greaterThan;
/**
* Regexp that matches service desk setting email structure.
* Taken from app/models/service_desk_setting.rb custom_email
*/
const SERVICE_DESK_SETTING_EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
/**
* Checks if the input is a valid service desk setting email address
*
* @param {String} - value
* @returns {Boolean}
*/
export const isServiceDeskSettingEmail = (value) => SERVICE_DESK_SETTING_EMAIL_REGEXP.test(value);
/**
* Regexp that matches user email structure.
* Taken from DeviseEmailValidator
*/
const USER_EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/;
/**
* Checks if the input is a valid user email address
*
* @param {String} - value
* @returns {Boolean}
*/
export const isUserEmail = (value) => USER_EMAIL_REGEXP.test(value);
/**
* A form object serializer
*
* @param {Object} - Form Object
* @returns {Object} - Serialized Form Object
*
* @example
* Input
* {"project": {"value": "hello", "state": false}, "username": {"value": "john"}}
*
* Returns
* {"project": "hello", "username": "john"}
*/
export const serializeFormObject = (form) =>
Object.fromEntries(
Object.entries(form).reduce((acc, [name, { value }]) => {
if (!isEmptyValue(value)) {
acc.push([name, value]);
}
return acc;
}, []),
);
/**
* Parse inputs of HTML forms generated by Rails.
*
* This can be helpful when mounting Vue components within Rails forms.
*
* If called with an HTML element like:
*
* ```html
* <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail">
* <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contactInfoPhone">
* <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests">
* <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests">
* ```
*
* It will return an object like:
*
* ```javascript
* {
* contactInfoEmail: {
* name: 'user[contact_info][email]',
* id: 'user_contact_info_email',
* value: 'foo@bar.com',
* placeholder: 'Email',
* },
* contactInfoPhone: {
* name: 'user[contact_info][phone]',
* id: 'user_contact_info_phone',
* value: '(123) 456-7890',
* placeholder: 'Phone',
* },
* interests: [
* {
* name: 'user[interests][]',
* id: 'user_interests_vue',
* value: 'Vue',
* checked: true,
* },
* {
* name: 'user[interests][]',
* id: 'user_interests_graphql',
* value: 'GraphQL',
* checked: false,
* },
* ],
* }
* ```
*
* @param {HTMLInputElement} mountEl
* @returns {Object} object with form fields data.
*/
export const parseRailsFormFields = (mountEl) => {
if (!mountEl) {
throw new TypeError('`mountEl` argument is required');
}
const inputs = mountEl.querySelectorAll('[name]');
return [...inputs].reduce((accumulator, input) => {
const fieldName = input.dataset.jsName;
if (!fieldName) {
return accumulator;
}
const fieldNameCamelCase = convertToCamelCase(fieldName);
const { id, placeholder, name, value, type, checked, maxLength, pattern } = input;
const attributes = {
name,
id,
value,
...(placeholder && { placeholder }),
...(input.hasAttribute('maxlength') && { maxLength }),
...(pattern && { pattern }),
};
// Store radio buttons and checkboxes as an array so they can be
// looped through and rendered in Vue
if (['radio', 'checkbox'].includes(type)) {
return {
...accumulator,
[fieldNameCamelCase]: [
...(accumulator[fieldNameCamelCase] || []),
{ ...attributes, checked },
],
};
}
return {
...accumulator,
[fieldNameCamelCase]: attributes,
};
}, {});
};