chore(learn): Merge learn in to the client app
This commit is contained in:
18
client/src/components/formHelpers/BlockSaveButton.js
Normal file
18
client/src/components/formHelpers/BlockSaveButton.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
|
||||
function BlockSaveButton(props) {
|
||||
return (
|
||||
<Button block={true} bsStyle='primary' {...props} type='submit'>
|
||||
{props.children || 'Save'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
BlockSaveButton.displayName = 'BlockSaveButton';
|
||||
BlockSaveButton.propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
|
||||
export default BlockSaveButton;
|
30
client/src/components/formHelpers/BlockSaveButton.test.js
Normal file
30
client/src/components/formHelpers/BlockSaveButton.test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* global expect */
|
||||
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
import BlockSaveButton from './BlockSaveButton';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
test('<BlockSaveButton /> snapshot', () => {
|
||||
const component = renderer.create(<BlockSaveButton />);
|
||||
let tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Button text should default to "Save"', () => {
|
||||
const enzymeWrapper = Enzyme.render(<BlockSaveButton />);
|
||||
|
||||
expect(enzymeWrapper.text()).toBe('Save');
|
||||
});
|
||||
|
||||
test('Button text should match "children"', () => {
|
||||
const enzymeWrapper = Enzyme.render(
|
||||
<BlockSaveButton>My Text Here</BlockSaveButton>
|
||||
);
|
||||
|
||||
expect(enzymeWrapper.text()).toBe('My Text Here');
|
||||
});
|
19
client/src/components/formHelpers/BlockSaveWrapper.js
Normal file
19
client/src/components/formHelpers/BlockSaveWrapper.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
const style = {
|
||||
padding: '0 15px'
|
||||
};
|
||||
|
||||
function BlockSaveWrapper({ children }) {
|
||||
return <div style={style}>{children}</div>;
|
||||
}
|
||||
|
||||
BlockSaveWrapper.displayName = 'BlockSaveWrapper';
|
||||
BlockSaveWrapper.propTypes = propTypes;
|
||||
|
||||
export default BlockSaveWrapper;
|
16
client/src/components/formHelpers/BlockSaveWrapper.test.js
Normal file
16
client/src/components/formHelpers/BlockSaveWrapper.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* global expect */
|
||||
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
import BlockSaveWrapper from './BlockSaveWrapper';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
test('<BlockSaveWrapper /> snapshot', () => {
|
||||
const component = renderer.create(<BlockSaveWrapper />);
|
||||
let tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
84
client/src/components/formHelpers/Form.js
Normal file
84
client/src/components/formHelpers/Form.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { reduxForm } from 'redux-form';
|
||||
|
||||
import { FormFields, BlockSaveButton, BlockSaveWrapper } from './';
|
||||
|
||||
const propTypes = {
|
||||
buttonText: PropTypes.string,
|
||||
enableSubmit: PropTypes.bool,
|
||||
errors: PropTypes.object,
|
||||
fields: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
})
|
||||
),
|
||||
formFields: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
hideButton: PropTypes.bool,
|
||||
id: PropTypes.string.isRequired,
|
||||
initialValues: PropTypes.object,
|
||||
options: PropTypes.shape({
|
||||
ignored: PropTypes.arrayOf(PropTypes.string),
|
||||
required: PropTypes.arrayOf(PropTypes.string),
|
||||
types: PropTypes.objectOf(PropTypes.string)
|
||||
}),
|
||||
submit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export function DynamicForm({
|
||||
// redux-form
|
||||
errors,
|
||||
fields,
|
||||
handleSubmit,
|
||||
fields: { _meta: { allPristine } },
|
||||
|
||||
// HOC
|
||||
buttonText,
|
||||
enableSubmit,
|
||||
hideButton,
|
||||
id,
|
||||
options,
|
||||
submit
|
||||
}) {
|
||||
return (
|
||||
<form
|
||||
id={`dynamic-${id}`}
|
||||
onSubmit={handleSubmit(submit)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<FormFields errors={errors} fields={fields} options={options} />
|
||||
<BlockSaveWrapper>
|
||||
{hideButton ? null : (
|
||||
<BlockSaveButton
|
||||
disabled={
|
||||
(allPristine && !enableSubmit) ||
|
||||
!!Object.keys(errors).filter(key => errors[key]).length
|
||||
}
|
||||
>
|
||||
{buttonText ? buttonText : null}
|
||||
</BlockSaveButton>
|
||||
)}
|
||||
</BlockSaveWrapper>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
DynamicForm.displayName = 'DynamicForm';
|
||||
DynamicForm.propTypes = propTypes;
|
||||
|
||||
const DynamicFormWithRedux = reduxForm()(DynamicForm);
|
||||
|
||||
export default function Form(props) {
|
||||
return (
|
||||
<DynamicFormWithRedux
|
||||
{...props}
|
||||
fields={props.formFields}
|
||||
form={props.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Form.propTypes = propTypes;
|
43
client/src/components/formHelpers/Form.test.js
Normal file
43
client/src/components/formHelpers/Form.test.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/* global expect */
|
||||
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
import { DynamicForm } from './Form';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
const defaultTestProps = {
|
||||
errors: {},
|
||||
fields: {
|
||||
_meta: {
|
||||
allPristine: true,
|
||||
name: 'name',
|
||||
onChange: () => {},
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
handleSubmit: () => {},
|
||||
|
||||
buttonText: 'Submit',
|
||||
enableSubmit: true,
|
||||
formFields: ['name', 'website'],
|
||||
hideButton: false,
|
||||
id: 'my-test-form',
|
||||
options: {
|
||||
types: {
|
||||
name: 'text',
|
||||
website: 'url'
|
||||
},
|
||||
required: ['website']
|
||||
},
|
||||
submit: () => {}
|
||||
};
|
||||
|
||||
test('<DynamicForm /> snapshot', () => {
|
||||
const component = renderer.create(<DynamicForm {...defaultTestProps} />);
|
||||
let tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
86
client/src/components/formHelpers/FormFields.js
Normal file
86
client/src/components/formHelpers/FormFields.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { kebabCase, startCase } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Col,
|
||||
ControlLabel,
|
||||
FormControl,
|
||||
HelpBlock
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
|
||||
import './form-fields.css';
|
||||
|
||||
const propTypes = {
|
||||
errors: PropTypes.objectOf(PropTypes.string),
|
||||
fields: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
options: PropTypes.shape({
|
||||
errors: PropTypes.objectOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(null)])
|
||||
),
|
||||
ignored: PropTypes.arrayOf(PropTypes.string),
|
||||
placeholder: PropTypes.bool,
|
||||
required: PropTypes.arrayOf(PropTypes.string),
|
||||
types: PropTypes.objectOf(PropTypes.string)
|
||||
})
|
||||
};
|
||||
|
||||
function FormFields(props) {
|
||||
const { errors = {}, fields, options = {} } = props;
|
||||
const {
|
||||
ignored = [],
|
||||
placeholder = true,
|
||||
required = [],
|
||||
types = {}
|
||||
} = options;
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(fields)
|
||||
.filter(field => !ignored.includes(field))
|
||||
.map(key => fields[key])
|
||||
.map(({ name, onChange, value, pristine }) => {
|
||||
const key = kebabCase(name);
|
||||
const type = name in types ? types[name] : 'text';
|
||||
return (
|
||||
<div className='inline-form-field' key={key}>
|
||||
<Col sm={3} xs={12}>
|
||||
{type === 'hidden' ? null : (
|
||||
<ControlLabel htmlFor={key}>{startCase(name)}</ControlLabel>
|
||||
)}
|
||||
</Col>
|
||||
<Col sm={9} xs={12}>
|
||||
<FormControl
|
||||
bsSize='lg'
|
||||
componentClass={type === 'textarea' ? type : 'input'}
|
||||
id={key}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder ? name : ''}
|
||||
required={required.includes(name)}
|
||||
rows={4}
|
||||
type={type}
|
||||
value={value}
|
||||
/>
|
||||
{name in errors && !pristine ? (
|
||||
<HelpBlock>
|
||||
<Alert bsStyle='danger'>{errors[name]}</Alert>
|
||||
</HelpBlock>
|
||||
) : null}
|
||||
</Col>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormFields.displayName = 'FormFields';
|
||||
FormFields.propTypes = propTypes;
|
||||
|
||||
export default FormFields;
|
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BlockSaveButton /> snapshot 1`] = `
|
||||
<button
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
`;
|
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BlockSaveWrapper /> snapshot 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": "0 15px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
@@ -0,0 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DynamicForm /> snapshot 1`] = `
|
||||
<form
|
||||
id="dynamic-my-test-form"
|
||||
style={
|
||||
Object {
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="inline-form-field"
|
||||
>
|
||||
<div
|
||||
className="col-sm-3 col-xs-12"
|
||||
>
|
||||
<label
|
||||
className="control-label"
|
||||
htmlFor="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="col-sm-9 col-xs-12"
|
||||
>
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
id="name"
|
||||
name="name"
|
||||
onChange={[Function]}
|
||||
placeholder="name"
|
||||
required={false}
|
||||
rows={4}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": "0 15px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
6
client/src/components/formHelpers/form-fields.css
Normal file
6
client/src/components/formHelpers/form-fields.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.inline-form-field {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
73
client/src/components/formHelpers/index.js
Normal file
73
client/src/components/formHelpers/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import normalizeUrl from 'normalize-url';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
export { default as BlockSaveButton } from './BlockSaveButton.js';
|
||||
export { default as BlockSaveWrapper } from './BlockSaveWrapper.js';
|
||||
export { default as Form } from './Form.js';
|
||||
export { default as FormFields } from './FormFields.js';
|
||||
|
||||
const normalizeOptions = {
|
||||
stripWWW: false
|
||||
};
|
||||
|
||||
// callIfDefined(fn: (Any) => Any) => (value: Any) => Any
|
||||
export function callIfDefined(fn) {
|
||||
return value => (value ? fn(value) : value);
|
||||
}
|
||||
|
||||
// formatUrl(url: String) => String
|
||||
export function formatUrl(url) {
|
||||
if (typeof url === 'string' && url.length > 4 && url.indexOf('.') !== -1) {
|
||||
// prevent trailing / from being stripped during typing
|
||||
let lastChar = '';
|
||||
if (url.substring(url.length - 1) === '/') {
|
||||
lastChar = '/';
|
||||
}
|
||||
// prevent normalize-url from stripping last dot during typing
|
||||
if (url.substring(url.length - 1) === '.') {
|
||||
lastChar = '.';
|
||||
}
|
||||
return normalizeUrl(url, normalizeOptions) + lastChar;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function isValidURL(data) {
|
||||
/* eslint-disable camelcase */
|
||||
return isURL(data, { require_protocol: true });
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
||||
export function makeOptional(validator) {
|
||||
return val => (val ? validator(val) : true);
|
||||
}
|
||||
|
||||
export function makeRequired(validator) {
|
||||
return val => (val ? validator(val) : false);
|
||||
}
|
||||
|
||||
export function createFormValidator(fieldValidators) {
|
||||
const fieldKeys = Object.keys(fieldValidators);
|
||||
return values =>
|
||||
fieldKeys
|
||||
.map(field => {
|
||||
if (fieldValidators[field](values[field])) {
|
||||
return null;
|
||||
}
|
||||
return { [field]: !fieldValidators[field](values[field]) };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reduce((errors, error) => ({ ...errors, ...error }), {});
|
||||
}
|
||||
|
||||
export function getValidationState(field) {
|
||||
if (field.pristine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/https?:\/\/glitch\.com\/edit\/#!\/.*/g.test(field.value)) {
|
||||
return 'glitch-warning';
|
||||
}
|
||||
|
||||
return field.error ? 'error' : 'success';
|
||||
}
|
Reference in New Issue
Block a user