From ff547d81b0d65410afa3b5c7908e76a94627606d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20Przoda=C5=82a?= Date: Mon, 13 Nov 2017 22:16:02 +0100 Subject: [PATCH 1/3] WIP eslint config change --- .eslintrc.json | 47 ++----- package.json | 7 +- src/components/AutocompleteField.js | 8 +- src/components/CheckboxField.jsx | 3 +- src/components/ErrorField.jsx | 23 +++- src/components/ErrorsContainer.jsx | 2 +- src/components/FieldConnect.jsx | 190 +++++++++++++++------------- src/components/Form.jsx | 77 ++++++----- src/components/ListField.jsx | 184 +++++++++++++++------------ src/components/NumberField.jsx | 33 ++--- src/components/ObjectField.jsx | 46 ++++--- src/components/SelectField.jsx | 35 ++--- src/components/SubmitField.jsx | 7 + src/components/TextField.jsx | 58 +++++---- src/components/TextareaField.jsx | 39 +++--- src/constants/defaultProps.js | 3 + src/constants/propTypes.js | 29 +++++ 17 files changed, 420 insertions(+), 371 deletions(-) create mode 100644 src/constants/defaultProps.js create mode 100644 src/constants/propTypes.js diff --git a/.eslintrc.json b/.eslintrc.json index fdf1993..ce75581 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,41 +1,20 @@ { "env": { - "browser": true, - "es6": true, "jest": true }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "parser": "babel-eslint", - "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - }, - "sourceType": "module" - }, - "plugins": [ - "react" - ], + "extends": ["airbnb"], "rules": { - "indent": [ - "error", - 4 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "react/display-name": [1] + "indent": [2, 4], + "complexity": [2, { "max": 6 }], + "react/jsx-indent": [2, 4], + "react/jsx-indent-props": [2, 4], + "no-use-before-define": [2, { + "functions": false, + "classes": false, + "variables": false + }] + }, + "globals": { + "document": true } } \ No newline at end of file diff --git a/package.json b/package.json index 2fa502b..ade49bb 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "author": "Mariusz PrzodaƂa", "license": "MIT", "dependencies": { + "classnames": "^2.2.5", + "lodash": "^4.17.4", + "prop-types": "^15.6.0", "react": "^16.0.0", "react-dom": "^16.0.0" }, @@ -49,7 +52,6 @@ "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.22.0", "babel-traverse": "^6.26.0", - "classnames": "^2.2.5", "coveralls": "^2.13.1", "css-loader": "^0.26.1", "enzyme": "^3.1.0", @@ -59,12 +61,11 @@ "eslint-plugin-babel": "^4.1.2", "eslint-plugin-import": "^2.7.0", "eslint-plugin-jsx-a11y": "^5.1.1", - "eslint-plugin-react": "^7.2.0", + "eslint-plugin-react": "^7.4.0", "extract-text-webpack-plugin": "^1.0.1", "form-schema-validation": "^1.11.1", "jest": "^21.2.1", "jsdom": "^9.11.0", - "lodash": "^4.17.4", "pre-commit": "^1.2.2", "raf": "^3.4.0", "react-addons-test-utils": "^15.6.2", diff --git a/src/components/AutocompleteField.js b/src/components/AutocompleteField.js index 324e1f2..d3436f7 100644 --- a/src/components/AutocompleteField.js +++ b/src/components/AutocompleteField.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import escapeRegExp from 'lodash/escapeRegExp'; import FieldConnect from './FieldConnect'; import ErrorField from './ErrorField'; import { get, cloneArray } from '../helpers'; @@ -49,9 +50,10 @@ export class AutocompleteField extends Component { return true; } - suggestionsFilter(escapedValue, searchKey) { - if (searchKey) { - return option => !!get(option, searchKey ,'').match(new RegExp(escapedValue, "i")) + suggestionsFilter(value, searchKey) { + const escapedValue = escapeRegExp(value); + if(searchKey) { + return option => !!get(option, searchKey, '').match(new RegExp(escapedValue, 'i')); } return option => option.match(escapedValue); diff --git a/src/components/CheckboxField.jsx b/src/components/CheckboxField.jsx index 28d1802..20afce2 100644 --- a/src/components/CheckboxField.jsx +++ b/src/components/CheckboxField.jsx @@ -11,6 +11,7 @@ export class CheckboxField extends React.Component { checked: props.value || false, value: props.checkboxValue || true }; + this.toggleValue = this.toggleValue.bind(this); } componentWillReceiveProps({ value, checkboxValue }) { @@ -52,7 +53,7 @@ export class CheckboxField extends React.Component { type="checkbox" checked={this.state.checked} name={name} - onChange={::this.toggleValue} + onChange={this.toggleValue} placeholder={placeholder} className={className} {...fieldAttributes} diff --git a/src/components/ErrorField.jsx b/src/components/ErrorField.jsx index 50b56fe..f8d1996 100644 --- a/src/components/ErrorField.jsx +++ b/src/components/ErrorField.jsx @@ -8,14 +8,16 @@ export const ErrorField = ({ ErrorComponent, }) => { const errorsList = Array.isArray(errors) ? errors : [errors]; + if (ErrorComponent) { + return ( + + ); + } return ( - ErrorComponent && - - ||
{errorsList.map(error => (
@@ -37,4 +39,11 @@ ErrorField.propTypes = { ErrorComponent: PropTypes.func, }; +ErrorField.defaultProps = { + errors: [], + className: '', + itemClassName: '', + ErrorComponent: undefined, +}; + export default ErrorField; diff --git a/src/components/ErrorsContainer.jsx b/src/components/ErrorsContainer.jsx index 1bd9041..1295232 100644 --- a/src/components/ErrorsContainer.jsx +++ b/src/components/ErrorsContainer.jsx @@ -53,4 +53,4 @@ ErrorsContainer.contextTypes = { getAllErrors: PropTypes.func, }; -export default ErrorsContainer; \ No newline at end of file +export default ErrorsContainer; diff --git a/src/components/FieldConnect.jsx b/src/components/FieldConnect.jsx index 5957fba..1541bec 100644 --- a/src/components/FieldConnect.jsx +++ b/src/components/FieldConnect.jsx @@ -21,6 +21,18 @@ export const FieldConnect = (Component) => { this.getFieldAttributes = this.getFieldAttributes.bind(this); } + getChildContext() { + return { + getSchema: this.context.getSchema, + getPath: this.getPath, + }; + } + + componentWillMount() { + this.updateModelWithValueOrOptions(); + this.registerListeners(); + } + shouldComponentUpdate(nextProps) { const newProps = Object.assign({}, nextProps); const oldProps = Object.assign({}, this.props); @@ -33,88 +45,10 @@ export const FieldConnect = (Component) => { ); } - updateModelWithValueOrOptions() { - const { name, value, options, defaultOption } = this.props; - const { setModel } = this.context; - - if (!name || typeof setModel !== 'function') { - return; - } - - if (value) { - setModel(name, value); - } else if (Array.isArray(options) && options.length && defaultOption !== undefined) { - setModel( - name, - options[defaultOption].label ? - options[defaultOption].value : - options[defaultOption] - ); - } - } - - registerListeners() { - const { onModelChange, onEmitEvents } = this.props; - const { eventsEmitter } = this.context; - if (eventsEmitter) { - if (typeof onModelChange === 'function') { - this.onModelChangeMethod = data => onModelChange(data, this); - eventsEmitter.listen('modelChange', this.onModelChangeMethod); - } - if (onEmitEvents) { - if (Array.isArray(onEmitEvents)) { - onEmitEvents.forEach(({ name, method }) => { - const listener = { - name, - method: data => method(data, this), - }; - this.listeners.push(listener); - eventsEmitter.listen(name, listener.method); - }); - return; - } - const { name, method } = onEmitEvents; - const listener = { - name, - method: data => method(data, this), - }; - this.listeners.push(listener); - eventsEmitter.listen(name, listener.method); - } - } - } - - unregisterListeners() { - const { onEmitEvents } = this.props; - const { eventsEmitter } = this.context; - if (eventsEmitter) { - if (typeof this.onModelChangeMethod === 'function') { - eventsEmitter.unlisten('modelChange', this.onModelChangeMethod); - } - if (onEmitEvents && this.listeners.length > 0) { - this.listeners.forEach(({ name, method }) => { - eventsEmitter.unlisten(name, method); - }); - } - } - } - - componentWillMount() { - this.updateModelWithValueOrOptions(); - this.registerListeners(); - } - componentWillUnmount() { this.unregisterListeners(); } - getChildContext() { - return { - getSchema: this.context.getSchema, - getPath: this.getPath, - }; - } - onChangeData(value) { const { name, callbacks: { onChange } } = this.props; const { setModel, eventsEmitter, getPath } = this.context; @@ -160,7 +94,7 @@ export const FieldConnect = (Component) => { getPropsFromSchema() { const { name } = this.props; const { getSchema } = this.context; - if (typeof getSchema !== 'function') return; + if (typeof getSchema !== 'function') return false; return getSchema(name); } @@ -168,12 +102,6 @@ export const FieldConnect = (Component) => { return this.context.eventsEmitter; } - submit(event) { - const { submitForm } = this.context; - if (typeof submitForm !== 'function') return; - submitForm(event); - } - getValidationErrors() { const { name, callbacks: { onError } } = this.props; const { getValidationErrors } = this.context; @@ -196,6 +124,76 @@ export const FieldConnect = (Component) => { return Object.assign({}, { onFocus, onBlur }, fieldAttributes); } + submit(event) { + const { submitForm } = this.context; + if (typeof submitForm !== 'function') return; + submitForm(event); + } + + updateModelWithValueOrOptions() { + const { name, value, options, defaultOption } = this.props; + const { setModel } = this.context; + + if (!name || typeof setModel !== 'function') { + return; + } + + if (value) { + setModel(name, value); + } else if (Array.isArray(options) && options.length && defaultOption !== undefined) { + setModel(name, + options[defaultOption].label ? + options[defaultOption].value : + options[defaultOption]); + } + } + + registerListeners() { + const { onModelChange, onEmitEvents } = this.props; + const { eventsEmitter } = this.context; + if (eventsEmitter) { + if (typeof onModelChange === 'function') { + this.onModelChangeMethod = data => onModelChange(data, this); + eventsEmitter.listen('modelChange', this.onModelChangeMethod); + } + if (onEmitEvents) { + if (Array.isArray(onEmitEvents)) { + onEmitEvents.forEach(({ name, method }) => { + const listener = { + name, + method: data => method(data, this), + }; + this.listeners.push(listener); + eventsEmitter.listen(name, listener.method); + }); + return; + } + const { name, method } = onEmitEvents; + const listener = { + name, + method: data => method(data, this), + }; + this.listeners.push(listener); + eventsEmitter.listen(name, listener.method); + } + } + } + + unregisterListeners() { + const { onEmitEvents } = this.props; + const { eventsEmitter } = this.context; + if (eventsEmitter) { + if (typeof this.onModelChangeMethod === 'function') { + eventsEmitter.unlisten('modelChange', this.onModelChangeMethod); + } + if (onEmitEvents && this.listeners.length > 0) { + this.listeners.forEach(({ name, method }) => { + eventsEmitter.unlisten(name, method); + }); + } + } + } + hasValidationError() { return this.getValidationErrors().length > 0; } @@ -237,13 +235,14 @@ export const FieldConnect = (Component) => { getPath: PropTypes.func, }; - FieldConnector.defaultProps = { - callbacks: {}, - }; - FieldConnector.propTypes = { name: PropTypes.string, - value: PropTypes.any, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.array, + PropTypes.object, + ]), fieldAttributes: PropTypes.shape({}), options: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.string, @@ -270,6 +269,17 @@ export const FieldConnect = (Component) => { }), }; + FieldConnector.defaultProps = { + name: '', + value: undefined, + fieldAttributes: {}, + options: [], + defaultOption: 0, + onModelChange: undefined, + onEmitEvents: undefined, + callbacks: {}, + }; + return FieldConnector; }; diff --git a/src/components/Form.jsx b/src/components/Form.jsx index 524479a..b3aefa0 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -4,11 +4,16 @@ import Storage from './Storage'; import { cloneObject } from '../helpers'; class Form extends React.Component { + static getDefaultModelValue(schema) { + if (schema && typeof schema.getDefaultValues === 'function') return schema.getDefaultValues(); + return {}; + } + constructor(props) { super(props); this.state = { - schema: props.schema || {}, - model: props.model || this.getDefaultModelValue(props.schema), + schema: props.schema, + model: props.model || Form.getDefaultModelValue(props.schema), validationErrors: {}, validateOnChange: props.validateOnChange, }; @@ -33,17 +38,17 @@ class Form extends React.Component { this.handlePromiseValidation = this.handlePromiseValidation.bind(this); } - submitListener() { - return this.submitForm(); - } - - validateListener(schema) { - return this.validateModel(this.storage.getModel(), schema || this.state.schema); - } - - resetListener(model) { - const newModel = model || this.getDefaultModelValue(this.state.schema); - this.storage.setModel(newModel); + getChildContext() { + return { + setModel: this.setModel, + getModel: this.getModel, + getSchema: this.getSchema, + submitForm: this.submitForm, + getValidationErrors: this.getValidationErrors, + getAllValidationErrors: this.getAllValidationErrors, + getPath: this.getPath, + eventsEmitter: this.eventsEmitter, + }; } componentWillMount() { @@ -65,11 +70,6 @@ class Form extends React.Component { } } - getDefaultModelValue(schema) { - if (schema && typeof schema.getDefaultValues === 'function') return schema.getDefaultValues(); - return {}; - } - setStateModel(model, callback) { this.setState({ model }, callback); } @@ -102,6 +102,19 @@ class Form extends React.Component { return this.props.id; } + submitListener() { + return this.submitForm(); + } + + validateListener(schema) { + return this.validateModel(this.storage.getModel(), schema || this.state.schema); + } + + resetListener(model) { + const newModel = model || Form.getDefaultModelValue(this.state.schema); + this.storage.setModel(newModel); + } + handleSchemaValidation(schema, model) { const validationResults = schema.validate(model); if (validationResults instanceof Promise) { @@ -157,26 +170,13 @@ class Form extends React.Component { const model = cloneObject(modelData); if (Object.keys(validationErrors).length > 0) { if (this.props.onError) this.props.onError(validationErrors, model); - return; + return false; } this.setState({ validateOnChange: false }); this.props.onSubmit(model); return model; } - getChildContext() { - return { - setModel: this.setModel, - getModel: this.getModel, - getSchema: this.getSchema, - submitForm: this.submitForm, - getValidationErrors: this.getValidationErrors, - getAllValidationErrors: this.getAllValidationErrors, - getPath: this.getPath, - eventsEmitter: this.eventsEmitter, - }; - } - render() { const { children, className, subform, id } = this.props; @@ -228,12 +228,21 @@ Form.propTypes = { registerEvent: PropTypes.func, listen: PropTypes.func, unregisterEvent: PropTypes.func, - unlisten: PropTypes.func - }) + unlisten: PropTypes.func, + }), }; Form.defaultProps = { id: 'form', + className: '', + model: undefined, + schema: {}, + onError: undefined, + validateOnChange: false, + customValidation: undefined, + subform: false, + children: '', + eventsEmitter: undefined, }; export default Form; diff --git a/src/components/ListField.jsx b/src/components/ListField.jsx index 4ed3fc4..f2fc92f 100644 --- a/src/components/ListField.jsx +++ b/src/components/ListField.jsx @@ -4,19 +4,24 @@ import Storage from './Storage'; import FieldConnect from './FieldConnect'; export class ListField extends React.Component { - static defaultProps = { - value: [], - }; - static generateItemId() { return Math.random().toString(36).substring(7); } + static getModelFromProps(props) { + if (props.value.length > 0) { + return props.value.map(item => ({ + id: ListField.generateItemId(), + value: item, + })); + } + return []; + } constructor(props, { getSchema }) { super(props); this.state = { schema: getSchema(props.name), - model: this.getModelFromProps(props), - validationErrors: {} + model: ListField.getModelFromProps(props), + validationErrors: {}, }; this.storage = new Storage(this.state.model); @@ -30,16 +35,25 @@ export class ListField extends React.Component { this.setStateModel = this.setStateModel.bind(this); } + getChildContext() { + return { + setModel: this.setModel, + getModel: this.getModel, + getSchema: this.getSchema, + getValidationErrors: this.getValidationErrors, + }; + } + + componentWillMount() { + this.storage.listen(this.setStateModel); + } + componentWillReceiveProps({ value }) { let shouldSetState = false; value.forEach((item, key) => { if (item !== this.state.model[key].value) shouldSetState = true; }); - if (shouldSetState) this.storage.setModel(this.getModelFromProps({ value })); - } - - componentWillMount() { - this.storage.listen(this.setStateModel); + if (shouldSetState) this.storage.setModel(ListField.getModelFromProps({ value })); } componentWillUnmount() { @@ -51,20 +65,10 @@ export class ListField extends React.Component { this.props.onChange(model.map(item => item.value)); } - getModelFromProps(props) { - if (props.value.length > 0) { - return props.value.map(item => ({ - id: ListField.generateItemId(), - value: item - })); - } - return []; - } - setModel(name, value, callback) { const key = name.split('-')[1]; const model = Array.from(this.state.model); - model[parseInt(key)].value = value; + model[parseInt(key, 10)].value = value; this.storage.setModel(model, callback); } @@ -81,7 +85,7 @@ export class ListField extends React.Component { const [fieldName, key] = name.split('-'); const { getValidationErrors } = this.context; const validationErrors = getValidationErrors(fieldName); - return validationErrors[parseInt(key)] || []; + return validationErrors[parseInt(key, 10)] || []; } getDefaultValueForListItem() { @@ -97,53 +101,14 @@ export class ListField extends React.Component { return undefined; } - addListElement() { - const model = Array.from(this.state.model); - model.push({ - id: ListField.generateItemId(), - value: this.getDefaultValueForListItem() - }); - this.setState({ model }); - } - - removeListElement(key) { - const model = Array.from(this.state.model); - model.splice(key, 1); - this.setState({ model }); - this.props.onChange(model.map(item => item.value)); - } - - isAddAllowed() { - const { maxLength } = this.props; - const { model } = this.state; - if (typeof maxLength === 'number') return model.length < maxLength; - return true; - } - - isRemoveAllowed() { - const { minLength } = this.props; - const { model } = this.state; - if (typeof minLength === 'number') return model.length > minLength; - return true; - } - - getChildContext() { - return { - setModel: this.setModel, - getModel: this.getModel, - getSchema: this.getSchema, - getValidationErrors: this.getValidationErrors - }; - } - getList(children) { const { name, removeButton: { wrapperClassName, className, - value - } = {}, + value, + }, hideRemoveButton, itemWrapperClassName, } = this.props; @@ -153,49 +118,81 @@ export class ListField extends React.Component { return this.state.model.map((item, key) => { const child = React.cloneElement(children, { name: `${name}-${key}`, + index: key, value: item.value, - key: item.id + key: item.id, }); return (
{child} {!hideRemoveButton && isRemoveAllowed &&
- this.removeListElement(key)} className={className} > {value || 'Remove'} - +
}
); }); } + isAddAllowed() { + const { maxLength } = this.props; + const { model } = this.state; + if (typeof maxLength === 'number') return model.length < maxLength; + return true; + } + + isRemoveAllowed() { + const { minLength } = this.props; + const { model } = this.state; + if (typeof minLength === 'number') return model.length > minLength; + return true; + } + + addListElement() { + const model = Array.from(this.state.model); + model.push({ + id: ListField.generateItemId(), + value: this.getDefaultValueForListItem(), + }); + this.setState({ model }); + } + + removeListElement(key) { + const model = Array.from(this.state.model); + model.splice(key, 1); + this.setState({ model }); + this.props.onChange(model.map(item => item.value)); + } + render() { const { children, className, wrapperClassName, label, - addButton = {}, + addButton, hideAddButton, - fieldAttributes + fieldAttributes, + name, } = this.props; const isAddAllowed = this.isAddAllowed(); return (
- {label && } + {label && }
{this.getList(children)}
- {!hideAddButton && isAddAllowed && {addButton.value || 'Add'} - } + }
); } @@ -203,14 +200,14 @@ export class ListField extends React.Component { ListField.contextTypes = { getSchema: PropTypes.func, - getValidationErrors: PropTypes.func + getValidationErrors: PropTypes.func, }; ListField.childContextTypes = { setModel: PropTypes.func, getModel: PropTypes.func, getSchema: PropTypes.func, - getValidationErrors: PropTypes.func + getValidationErrors: PropTypes.func, }; ListField.propTypes = { @@ -221,21 +218,50 @@ ListField.propTypes = { label: PropTypes.string, addButton: PropTypes.shape({ className: PropTypes.string, - value: PropTypes.node + value: PropTypes.node, }), removeButton: PropTypes.shape({ wrapperClassName: PropTypes.string, className: PropTypes.string, - value: PropTypes.node + value: PropTypes.node, }), hideAddButton: PropTypes.bool, hideRemoveButton: PropTypes.bool, onChange: PropTypes.func.isRequired, name: PropTypes.string, - value: PropTypes.any, + value: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.shape({}), + PropTypes.string, + PropTypes.number, + ]), + ), fieldAttributes: PropTypes.shape({}), - minLength: PropTypes.number, - maxLength: PropTypes.number, + minLength: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.bool, + ]), + maxLength: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.bool, + ]), +}; + +ListField.defaultProps = { + children: '', + className: '', + wrapperClassName: '', + itemWrapperClassName: '', + label: '', + addButton: {}, + removeButton: {}, + hideAddButton: false, + hideRemoveButton: false, + name: '', + value: [], + fieldAttributes: {}, + minLength: false, + maxLength: false, }; export default FieldConnect(ListField); diff --git a/src/components/NumberField.jsx b/src/components/NumberField.jsx index 4aedd6a..fdcfce5 100644 --- a/src/components/NumberField.jsx +++ b/src/components/NumberField.jsx @@ -1,12 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; import FieldConnect from './FieldConnect'; import ErrorField from './ErrorField'; -import classnames from 'classnames'; +import { fieldDefaultPropTypes } from '../constants/propTypes'; +import { fieldDefaultProps } from '../constants/defaultProps'; const parseByType = (value, type) => { if (type === 'float') return parseFloat(value); - return parseInt(value); + return parseInt(value, 10); }; export const NumberField = ({ @@ -24,7 +26,7 @@ export const NumberField = ({ fieldAttributes = {}, }) => (
- {label && } + {label && } (
- {label && } + {label && } (
- {label && } + {label && }