Skip to content

Commit

Permalink
feat: create field component
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcamargo committed Sep 26, 2020
1 parent f28608e commit 8db47d3
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 0 deletions.
40 changes: 40 additions & 0 deletions src/base/services/field/field.js
@@ -0,0 +1,40 @@
import propBasedCssClassService from '@base/services/prop-based-css-class/prop-based-css-class';

const _public = {};

_public.buildCssClasses = ({ required, blocked, element } = {}) => {
const baseCssClass = getBaseCssClass();
const cssClasses = [baseCssClass, buildRequiredCssClass(required, element, baseCssClass)];
propBasedCssClassService.handleBooleanProp(
{ blocked },
isValidBooleanProp,
cssClasses,
baseCssClass
);
return cssClasses.join(' ').replace(/\s+/g, ' ').trim();
};

function getBaseCssClass(){
return 't-field';
}

function isValidBooleanProp(propName){
return ['blocked'].includes(propName);
}

function buildRequiredCssClass(required, element, baseCssClass){
return shouldAppendRequiredCssClass(required, element) ? `${baseCssClass}-required` : '';
}

function shouldAppendRequiredCssClass(required, element){
if(required)
return true;
if(required === undefined)
return containsRequiredFormControl(element);
}

function containsRequiredFormControl(element){
return element && !!element.querySelector('[required]');
}

export default _public;
30 changes: 30 additions & 0 deletions src/base/services/field/field.test.js
@@ -0,0 +1,30 @@
import fieldService from './field';

describe('Field Service', () => {
it('should build css classes', () => {
expect(fieldService.buildCssClasses()).toEqual('t-field');
});

it('should append required modifier css class if field contains a required form control', () => {
const fieldEl = document.createElement('div');
const inputEl = document.createElement('input');
inputEl.setAttribute('required','');
fieldEl.appendChild(inputEl);
expect(fieldService.buildCssClasses({ element: fieldEl }).includes('t-field-required')).toEqual(true);
});

it('should optionally append required modifier css class programmatically', () => {
const cssClasses = fieldService.buildCssClasses({ required: true });
expect(cssClasses).toEqual('t-field t-field-required');
});

it('should optionally not append required modifier css class programmatically', () => {
const cssClasses = fieldService.buildCssClasses({ required: false });
expect(cssClasses).toEqual('t-field');
});

it('should append blocked modifier css class if it has been given as true', () => {
const cssClasses = fieldService.buildCssClasses({ blocked: true });
expect(cssClasses).toEqual('t-field t-field-blocked');
});
});
1 change: 1 addition & 0 deletions src/base/styles/_index.js
Expand Up @@ -3,6 +3,7 @@ import './banner.styl';
import './button.styl';
import './col.styl';
import './container.styl';
import './field.styl';
import './form-control.styl';
import './loader.styl';
import './row.styl';
20 changes: 20 additions & 0 deletions src/base/styles/field.styl
@@ -0,0 +1,20 @@
.t-field
display inline-block
&.t-field-blocked
display block
&.t-field-required
.t-field-label
&:after
content '*'
position absolute
top 0
right -8px
color var(--t-color-red)

.t-field-label
position relative
color var(--t-color-grey-dark)
font-size var(--t-font-size-xxs)
font-weight bold
& + .t-field-content
margin-top 5px
111 changes: 111 additions & 0 deletions src/react/components/field/field.doc.js
@@ -0,0 +1,111 @@
module.exports = {
name: 'Field',
description: 'Container for form controls like input, select or textarea.',
properties: [
{
name: 'label',
type: 'String',
values: 'Any',
required: true
},
{
name: 'required',
type: 'Boolean',
values: 'true, false'
},
{
name: 'blocked',
type: 'Boolean',
values: 'true, false'
}
],
examples: [
{
title: 'Default Field',
controller: function(){
const { Field, Input } = taslonicReact;

return function(){
return (
<Field label="Name">
<Input />
</Field>
);
}
}
},
{
title: 'Required Field',
description: 'If you pass a required form control to the field, it will automatically show an asterisk mark.',
controller: function(){
const { Field, Input } = taslonicReact;

return function(){
return (
<Field label="Name">
<Input required />
</Field>
);
}
}
},
{
title: 'Dynamic Required Field',
description: 'You can optionally control the asterisk mark visibility regardless of the form control passed to the field.',
controller: function(){
const { useState } = React;
const { Field, Input, Row, Col, Button } = taslonicReact;

return function(){
const [required, setRequired] = useState(true);

return (
<>
<Row>
<Col>
<Field label="Name" required={required}>
<Input required={required} />
</Field>
</Col>
</Row>
<Row>
<Col>
<Button onClick={() => setRequired(!required)}>
Toggle Required
</Button>
</Col>
</Row>
</>
);
}
}
},
{
title: 'Blocked Field',
description: 'Blocked fields behave like a block.',
controller: function(){
const { useState } = React;
const { Field, Input, Row, Col } = taslonicReact;

return function(){
const [required, setRequired] = useState(true);

return (
<Row>
<Col md="6">
<Field label="First Name" blocked>
<Input blocked />
</Field>
</Col>
<Col md="6">
<Field label="Last Name" blocked>
<Input blocked />
</Field>
</Col>
</Row>
);
}
}
}
]
};
26 changes: 26 additions & 0 deletions src/react/components/field/field.js
@@ -0,0 +1,26 @@
import React, { useState, useEffect, useRef } from 'react';
import fieldService from '@base/services/field/field';

export const Field = ({ label, required, blocked, children }) => {
const [cssClasses, setCssClasses] = useState('');
const fieldElement = useRef();

useEffect(() => {
setCssClasses(buildCssClasses(required, blocked, fieldElement.current));
}, [required, blocked, fieldElement, setCssClasses]);

return (
<span ref={fieldElement} className={cssClasses}>
<label className="t-field-label">
{ label }
</label>
<div className="t-field-content">
{children}
</div>
</span>
);
};

function buildCssClasses(required, blocked, element){
return fieldService.buildCssClasses({ required, blocked, element });
}
35 changes: 35 additions & 0 deletions src/react/components/field/field.test.js
@@ -0,0 +1,35 @@
import React from 'react';
import { mount } from 'enzyme';
import testingService from '@react/services/testing/testing';
import { Field } from './field';

describe('Field', () => {
function mountComponent({ label, required, blocked } = {}, content = <input />){
return mount(
<Field label={ label } required={ required } blocked={ blocked }>
{ content }
</Field>
);
}

it('should have base css class', () => {
const wrapper = mountComponent();
expect(testingService.getRootElProp(wrapper, 'className')).toEqual('t-field');
});

it('should render a label', () => {
const label = 'Email';
const wrapper = mountComponent({ label });
expect(wrapper.find('label').text()).toEqual(label);
});

it('should contain required css class if required prop has been given as true', () => {
const wrapper = mountComponent({ required: true });
expect(testingService.getRootElProp(wrapper, 'className').includes('t-field-required')).toEqual(true);
});

it('should contain required css class if no required prop has been passed but content is required', () => {
const wrapper = mountComponent({}, <input type="text" required />);
expect(testingService.getRootElProp(wrapper, 'className').includes('t-field-required')).toEqual(true);
});
});
1 change: 1 addition & 0 deletions src/react/components/index.js
Expand Up @@ -2,6 +2,7 @@ export { Banner } from '@react/components/banner/banner';
export { Button } from '@react/components/button/button';
export { Col } from '@react/components/col/col';
export { Container } from '@react/components/container/container';
export { Field } from '@react/components/field/field';
export { Input } from '@react/components/input/input';
export { Loader } from '@react/components/loader/loader';
export { Row } from '@react/components/row/row';
93 changes: 93 additions & 0 deletions src/vue/components/field/field.doc.js
@@ -0,0 +1,93 @@
module.exports = {
name: 'Field',
description: 'Container for form controls like input, select or textarea.',
properties: [
{
name: 'label',
type: 'String',
values: 'Any',
required: true
},
{
name: 'required',
type: 'Boolean',
values: 'true, false'
},
{
name: 'blocked',
type: 'Boolean',
values: 'true, false'
}
],
examples: [
{
title: 'Default Field',
template: `
<t-field label="Name">
<t-input />
</t-field>
`
},
{
title: 'Required Field',
description: 'If you pass a required form control to the field, it will automatically show an asterisk mark.',
template: `
<t-field label="Name">
<t-input required />
</t-field>
`
},
{
title: 'Dynamic Required Field',
description: 'You can optionally control the asterisk mark visibility regardless of the form control passed to the field.',
controller: {
data(){
return {
required: true
};
},
methods: {
toggleRequired(){
this.required = !this.required;
}
}
},
template: `
<div>
<t-row>
<t-col>
<t-field label="Name" :required="required">
<t-input :required="required" />
</t-field>
</t-col>
</t-row>
<t-row>
<t-col>
<t-button @click="toggleRequired">
Toggle Required
</t-button>
</t-col>
</t-row>
</div>
`
},
{
title: 'Blocked Field',
description: 'Blocked fields behave like a block.',
template: `
<t-row>
<t-col md="6">
<t-field label="First Name" blocked>
<t-input blocked />
</t-field>
</t-col>
<t-col md="6">
<t-field label="Last Name" blocked>
<t-input blocked />
</t-field>
</t-col>
</t-row>
`
}
]
};
8 changes: 8 additions & 0 deletions src/vue/components/field/field.html
@@ -0,0 +1,8 @@
<span :class="classes">
<label class="t-field-label">
{{ label }}
</label>
<div class="t-field-content">
<slot></slot>
</div>
</span>

0 comments on commit 8db47d3

Please sign in to comment.