Commit 8956ad76 authored by Peter Hegman's avatar Peter Hegman Committed by Paul Slaughter

Add `parseRailsFormFields` helper

Useful when integrating Vue into a Rails form

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55702
parent c3228bff
import Vue from 'vue';
import createFlash from '~/flash';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue';
const getInputAttrs = (el) => {
const input = el.querySelector('input');
return {
id: input.id,
name: input.name,
value: input.value,
placeholder: input.placeholder,
};
};
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
......@@ -22,7 +12,7 @@ export const initExpiresAtField = () => {
return null;
}
const inputAttrs = getInputAttrs(el);
const { expiresAt: inputAttrs } = parseRailsFormFields(el);
return new Vue({
el,
......@@ -43,7 +33,7 @@ export const initProjectsField = () => {
return null;
}
const inputAttrs = getInputAttrs(el);
const { projects: inputAttrs } = parseRailsFormFields(el);
if (window.gon.features.personalAccessTokensScopedToProjects) {
return new Promise((resolve) => {
......
import { convertToCamelCase } from '~/lib/utils/text_utility';
export const serializeFormEntries = (entries) =>
entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {});
......@@ -51,3 +53,95 @@ export const serializeFormObject = (form) =>
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 } = input;
const attributes = {
name,
id,
value,
...(placeholder && { placeholder }),
};
// 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,
};
}, {});
};
......@@ -23,7 +23,7 @@
= render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
.js-access-tokens-expires-at
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off'
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
.form-group
= f.label :scopes, _('Scopes'), class: 'label-bold'
......@@ -31,7 +31,7 @@
- if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects
%input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' }
%input{ type: 'hidden', name: 'personal_access_token[projects]', id: 'personal_access_token_projects', data: { js_name: 'projects' } }
.gl-mt-3
= f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' }
......@@ -25,18 +25,22 @@ describe('access tokens', () => {
});
describe.each`
initFunction | mountSelector | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => {
initFunction | mountSelector | fieldName | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => {
describe('when mount element exists', () => {
const nameAttribute = `access_tokens[${fieldName}]`;
const idAttribute = `access_tokens_${fieldName}`;
beforeEach(() => {
const mountEl = document.createElement('div');
mountEl.classList.add(mountSelector);
const input = document.createElement('input');
input.setAttribute('name', 'foo-bar');
input.setAttribute('id', 'foo-bar');
input.setAttribute('name', nameAttribute);
input.setAttribute('data-js-name', fieldName);
input.setAttribute('id', idAttribute);
input.setAttribute('placeholder', 'Foo bar');
input.setAttribute('value', '1,2');
......@@ -57,8 +61,8 @@ describe('access tokens', () => {
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
name: 'foo-bar',
id: 'foo-bar',
name: nameAttribute,
id: idAttribute,
value: '1,2',
placeholder: 'Foo bar',
});
......
import { serializeForm, serializeFormObject, isEmptyValue } from '~/lib/utils/forms';
import {
serializeForm,
serializeFormObject,
isEmptyValue,
parseRailsFormFields,
} from '~/lib/utils/forms';
describe('lib/utils/forms', () => {
const createDummyForm = (inputs) => {
......@@ -135,4 +140,160 @@ describe('lib/utils/forms', () => {
});
});
});
describe('parseRailsFormFields', () => {
let mountEl;
beforeEach(() => {
mountEl = document.createElement('div');
mountEl.classList.add('js-foo-bar');
});
afterEach(() => {
mountEl = null;
});
it('parses fields generated by Rails and returns object with HTML attributes', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
<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="contact_info_phone">
<input type="hidden" placeholder="Job title" value="" name="user[job_title]" id="user_job_title" data-js-name="jobTitle">
<textarea name="user[bio]" id="user_bio" data-js-name="bio">Foo bar</textarea>
<select name="user[timezone]" id="user_timezone" data-js-name="timezone">
<option value="utc+12">[UTC - 12] International Date Line West</option>
<option value="utc+11" selected>[UTC - 11] American Samoa</option>
</select>
<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">
<input type="radio" name="user[access_level]" value="regular" id="user_access_level_regular" data-js-name="accessLevel">
<input type="radio" name="user[access_level]" value="admin" id="user_access_level_admin" checked data-js-name="access_level">
<input name="user[private_profile]" type="hidden" value="0">
<input type="radio" name="user[private_profile]" id="user_private_profile" value="1" checked data-js-name="privateProfile">
<input name="user[email_notifications]" type="hidden" value="0">
<input type="radio" name="user[email_notifications]" id="user_email_notifications" value="1" data-js-name="emailNotifications">
`;
expect(parseRailsFormFields(mountEl)).toEqual({
name: {
name: 'user[name]',
id: 'user_name',
value: 'Administrator',
placeholder: 'Name',
},
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',
},
jobTitle: {
name: 'user[job_title]',
id: 'user_job_title',
value: '',
placeholder: 'Job title',
},
bio: {
name: 'user[bio]',
id: 'user_bio',
value: 'Foo bar',
},
timezone: {
name: 'user[timezone]',
id: 'user_timezone',
value: 'utc+11',
},
interests: [
{
name: 'user[interests][]',
id: 'user_interests_vue',
value: 'Vue',
checked: true,
},
{
name: 'user[interests][]',
id: 'user_interests_graphql',
value: 'GraphQL',
checked: false,
},
],
accessLevel: [
{
name: 'user[access_level]',
id: 'user_access_level_regular',
value: 'regular',
checked: false,
},
{
name: 'user[access_level]',
id: 'user_access_level_admin',
value: 'admin',
checked: true,
},
],
privateProfile: [
{
name: 'user[private_profile]',
id: 'user_private_profile',
value: '1',
checked: true,
},
],
emailNotifications: [
{
name: 'user[email_notifications]',
id: 'user_email_notifications',
value: '1',
checked: false,
},
],
});
});
it('returns an empty object if there are no inputs', () => {
expect(parseRailsFormFields(mountEl)).toEqual({});
});
it('returns an empty object if inputs do not have `name` attributes', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" id="user_name">
<input type="text" placeholder="Email" value="foo@bar.com" id="user_contact_info_email">
<input type="text" placeholder="Phone" value="(123) 456-7890" id="user_contact_info_phone">
`;
expect(parseRailsFormFields(mountEl)).toEqual({});
});
it('does not include field if `data-js-name` attribute is missing', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
<input type="text" placeholder="Email" value="foo@bar.com" name="user[email]" id="email">
`;
expect(parseRailsFormFields(mountEl)).toEqual({
name: {
name: 'user[name]',
id: 'user_name',
value: 'Administrator',
placeholder: 'Name',
},
});
});
it('throws error if `mountEl` argument is not passed', () => {
expect(() => parseRailsFormFields()).toThrow(new TypeError('`mountEl` argument is required'));
});
it('throws error if `mountEl` argument is `null`', () => {
expect(() => parseRailsFormFields(null)).toThrow(
new TypeError('`mountEl` argument is required'),
);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment