Skip to content

Commit

Permalink
feat: implement CvForm, CvFormGroup and CvFormItem (#1452)
Browse files Browse the repository at this point in the history
* feat(cv-form): add CvForm component

* feat(cv-form-group): add CvFormGroup component

* feat(cv-form-item): add CvFormItem component

* feat: add bottom margin to bx--form-item elements

Repeat the styling used on v2 for form item element class to give
them some spacing at the stories

* docs: add stories for form, form-group and form-item components
  • Loading branch information
felipebritor authored May 16, 2023
1 parent 8faa627 commit d93b825
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .storybook/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ $carbon--theme: $carbon--theme--white;
padding: $layout-04;
background-color: $ui-background;
color: $text-01;

// set space between form items
.bx--form-item {
margin-bottom: 2rem;
}
}

/**
Expand Down
193 changes: 193 additions & 0 deletions src/components/CvForm/CvForm.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { action } from '@storybook/addon-actions';
import { sbCompPrefix } from '../../global/storybook-utils';
import CvButton from '../CvButton';
import CvTextInput from '../CvTextInput';
import CvTextArea from '../CvTextArea';
import { CvForm, CvFormGroup, CvFormItem } from '.';

<Meta title={`${sbCompPrefix}/CvForm`} component={CvForm} />

export const Template = args => ({
components: {
CvForm,
CvFormGroup,
CvFormItem,
CvButton,
CvTextInput,
CvTextArea,
},
setup: () => {
const doSubmit = action('cv-form -submit event');
return {
args: { ...args, template: undefined },
onSubmit(ev) {
console.dir([].slice.call(ev.target, [0, ev.target.length]));
doSubmit(ev);
},
}
},
template: args.template,
});

export const defaultTemplate = `
<cv-form @submit.prevent="onSubmit">
<cv-text-input
label="Text input label"
placeholder="Optional placeholder text"
helperText="Optional helper text here; if message is more than one line text should wrap (~100 character count maximum)"
/>
<cv-text-area
label="Text area label"
placeholder="Optional placeholder text"
helperText="Optional helper text here; if message is more than one line text should wrap (~100 character count maximum)"
/>
<cv-button>Submit</cv-button>
</cv-form>
`;

export const formGroupTemplate = `
<cv-form-group v-bind="args">
<template #label>
FormGroup label-legend
</template>
<template #content>
<cv-text-input label="First name" />
<cv-text-input label="Last name" />
</template>
</cv-form-group>
`;

export const formItemTemplate = `
<cv-form-item>
<label for="text-input-3" class="bx--label">Text Input label</label>
<input id="text-input-3" type="text" class="bx--text-input" placeholder="Optional placeholder text" />
</cv-form-item>
`;

# CvForm

These components are purely wrapper elements for use in creating forms.

## Usage CvForm

CvForm has no properties and a single default slot

<Canvas>
<Story
name="Default"
parameters={{
controls: {
exclude: ['template']
},
docs: { source: { code: defaultTemplate } },
}}
args={{
template: defaultTemplate,
}}
argTypes={{
default: {
control: 'none',
table: {
type: { summary: 'text | html | Component' },
category: 'slots',
}
},
}}
>
{Template.bind({})}
</Story>
</Canvas>


## CvFormGroup

Used inside a form to group components such as checkboxes and radio buttons.

<Canvas>
<Story
name="FormGroup-Default"
parameters={{
controls: {
exclude: ['template', 'default']
},
docs: { source: { code: formGroupTemplate.replace('v-bind="args"', '') } },
}}
args={{
template: formGroupTemplate,
}}
argTypes={{
content: {
control: 'none',
table: {
type: { summary: 'text | html | Component' },
category: 'slots',
},
},
label: {
control: 'none',
table: {
type: { summary: 'text | html | Component' },
category: 'slots',
},
description: 'Legend element content',
},
invalid: {
type: 'boolean',
table: {
type: { summary: 'boolean' },
category: 'props',
},
description: 'Specify whether the `FormGroup` is invalid',
},
message: {
type: 'string',
table: {
type: { summary: 'string' },
category: 'props',
},
description: 'Specify a message to be placed at the end of the `FormGroup`',
},
noMargin: {
type: 'boolean',
table: {
type: { summary: 'boolean' },
category: 'props',
},
description: 'Remove default margins set on `FormGroup`',
},
}}
>
{Template.bind({})}
</Story>
</Canvas>

## CvFormItem

Used inside a form to provide positional styling.

<Canvas>
<Story
name="FormItem-Default"
parameters={{
controls: {
exclude: ['template']
},
docs: { source: { code: formItemTemplate } },
}}
args={{
template: formItemTemplate,
}}
argTypes={{
default: {
control: 'none',
table: {
type: { summary: 'text | html | Component' },
category: 'slots',
}
},
}}
>
{Template.bind({})}
</Story>
</Canvas>
9 changes: 9 additions & 0 deletions src/components/CvForm/CvForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<form :class="`cv-form ${carbonPrefix}--form`">
<slot></slot>
</form>
</template>

<script setup>
import { carbonPrefix } from '../../global/settings';
</script>
27 changes: 27 additions & 0 deletions src/components/CvForm/CvFormGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<fieldset
:class="[
`cv-form-group ${carbonPrefix}--fieldset`,
{ [`${carbonPrefix}--fieldset--no-margin`]: noMargin },
]"
:data-invalid="invalid"
>
<legend :class="`${carbonPrefix}--label`">
<slot name="label"></slot>
</legend>
<slot name="content"></slot>
<div v-if="message" :class="`${carbonPrefix}--form__requirements`">
{{ message }}
</div>
</fieldset>
</template>

<script setup>
import { carbonPrefix } from '../../global/settings';
const props = defineProps({
invalid: Boolean,
message: String,
noMargin: Boolean,
});
</script>
9 changes: 9 additions & 0 deletions src/components/CvForm/CvFormItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div :class="`cv-form-item ${carbonPrefix}--form-item`">
<slot></slot>
</div>
</template>

<script setup>
import { carbonPrefix } from '../../global/settings';
</script>
32 changes: 32 additions & 0 deletions src/components/CvForm/__tests__/CvForm.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { render } from '@testing-library/vue';
import { CvForm } from '..';

describe('CvForm', () => {
it('defines a form as the root element', () => {
const { container } = render(CvForm);

const wrapper = container.firstElementChild;
expect(wrapper.tagName).toBe('FORM');
});

it('slots content inside the form', () => {
const dummyElement = '<button>OK</button>';
const { container } = render(CvForm, {
slots: { default: dummyElement },
});

const form = container.firstElementChild;
const button = form.querySelector('button');
expect(button).not.toBeNull();
});

it('sets attributes at the form element', () => {
const dummyId = 'dummy-id';
const { getByTestId } = render(CvForm, {
attrs: { 'data-testid': dummyId },
});

const form = getByTestId('dummy-id');
expect(form).not.toBeNull();
});
});
61 changes: 61 additions & 0 deletions src/components/CvForm/__tests__/CvFormGroup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render } from '@testing-library/vue';
import { CvFormGroup } from '..';

describe('CvFormGroup', () => {
it('defines a fieldset as the root element', () => {
const { container } = render(CvFormGroup);

const wrapper = container.firstElementChild;
expect(wrapper.tagName).toBe('FIELDSET');
});

it('sets a legend element with a slot space', () => {
const dummyLabel = 'dummy label';
const { getByText } = render(CvFormGroup, {
slots: { label: dummyLabel },
});

const legend = getByText(dummyLabel);
expect(legend).not.toBeNull();
expect(legend.tagName).toBe('LEGEND');
});

it('slots content inside the fieldset', () => {
const dummyElement = '<input type="text" />';
const { container } = render(CvFormGroup, {
slots: { content: dummyElement },
});

const fieldset = container.firstElementChild;
const input = fieldset.querySelector('input');
expect(input).not.toBeNull();
});

it('displays a message if one is passed', () => {
const dummyMessage = 'some dummy message';
const { getByText } = render(CvFormGroup, {
props: { message: dummyMessage },
});

const element = getByText(dummyMessage);
expect(element).not.toBeNull();
});

it('styles fieldset with no margin class when noMargin is passed', () => {
const { container } = render(CvFormGroup, {
props: { noMargin: true },
});

const wrapper = container.firstElementChild;
expect(wrapper.classList.contains('bx--fieldset--no-margin')).toBeTruthy();
});

it('defines "data-invalid" attribute when invalid is passed', () => {
const { container } = render(CvFormGroup, {
props: { invalid: true },
});

const wrapper = container.firstElementChild;
expect(wrapper.getAttribute('data-invalid')).toBe('true');
});
});
34 changes: 34 additions & 0 deletions src/components/CvForm/__tests__/CvFormItem.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render } from '@testing-library/vue';
import { CvFormItem } from '..';

describe('CvForm', () => {
it('defines a div with corresponding form item classes as the root element', () => {
const { container } = render(CvFormItem);

const wrapper = container.firstElementChild;
expect(wrapper.tagName).toBe('DIV');
expect(wrapper.classList.contains('cv-form-item')).toBeTruthy();
expect(wrapper.classList.contains('bx--form-item')).toBeTruthy();
});

it('slots content inside the form item wrapper', () => {
const dummyElement = '<input type="text" />';
const { container } = render(CvFormItem, {
slots: { default: dummyElement },
});

const form = container.firstElementChild;
const input = form.querySelector('input');
expect(input).not.toBeNull();
});

it('sets attributes at the form item wrapper element', () => {
const dummyId = 'dummy-id';
const { getByTestId } = render(CvFormItem, {
attrs: { 'data-testid': dummyId },
});

const element = getByTestId('dummy-id');
expect(element).not.toBeNull();
});
});
5 changes: 5 additions & 0 deletions src/components/CvForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import CvForm from './CvForm';
import CvFormGroup from './CvFormGroup';
import CvFormItem from './CvFormItem';

export { CvForm, CvFormGroup, CvFormItem };

0 comments on commit d93b825

Please sign in to comment.