diff --git a/README.md b/README.md index fb40f86..bbff32f 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,15 @@ Property | Type | Description :-----------------------|:------------------------|:---------------------------------- ariaLabelledby |string |`aria-labelledby` attribute classNames |Object.<string> |CSS class names +defaultValue |number |Default value +defaultValues |Object |Default values maxValue |number |Maximum value it can accept minValue |number |Minimum value it can accept name |string |Name of `form` input onChange |Function |`onChange` callback step |number |Increment/decrement value -value |number |Default value -values |Array.<number> |Default range of values +value |number |Current value +values |Object |Current range of values ## Development diff --git a/example/js/App.js b/example/js/App.js index a91a95d..415a568 100644 --- a/example/js/App.js +++ b/example/js/App.js @@ -8,25 +8,35 @@ class App extends React.Component { this.state = { value: 5, values: { - min: 2, + min: 5, max: 10, }, }; } handleValuesChange(component, values) { + console.log(values); + this.setState({ values: values, }); } handleValueChange(component, value) { + console.log(value); + this.setState({ value: value, }); } render() { + const defaultValue = 2; + const defaultValues = { + min: 2, + max: 8, + }; + return (
+ + + + + ); } diff --git a/karma.conf.js b/karma.conf.js index 68ae1a6..94efbdc 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,6 +6,7 @@ module.exports = function(config) { files: [ 'node_modules/react/dist/react.js', + 'node_modules/lodash/index.js', 'node_modules/babelify/polyfill.js', 'src/**/*.js', 'test/**/*.js' diff --git a/src/InputRange/InputRange.js b/src/InputRange/InputRange.js index a69c75f..d876cac 100644 --- a/src/InputRange/InputRange.js +++ b/src/InputRange/InputRange.js @@ -2,7 +2,7 @@ import React from 'react'; import Slider from './Slider'; import Track from './Track'; import ValueTransformer from './ValueTransformer'; -import { autobind, captialize, clamp, distanceTo, extend } from './util'; +import { autobind, captialize, clamp, distanceTo, extend, isEmpty, isNumber, omit } from './util'; import { maxMinValuePropType } from './propTypes'; import defaultClassNames from './defaultClassNames'; @@ -53,6 +53,7 @@ class InputRange extends React.Component { // Initial state const state = { + didChange: false, percentages: { min: 0, max: 0, @@ -69,7 +70,8 @@ class InputRange extends React.Component { this.state = state; this.valueTransformer = new ValueTransformer(this); - this.isMultiValue = this.props.hasOwnProperty('values'); + this.isMultiValue = this.props.hasOwnProperty('defaultValues') || + this.props.hasOwnProperty('values'); // Auto-bind autobind([ @@ -85,24 +87,27 @@ class InputRange extends React.Component { } componentWillReceiveProps(nextProps) { - this.setPositionsByProps(nextProps); + const props = omit(nextProps, ['defaultValue', 'defaultValues']); + + this.setPositionsByProps(props); } shouldComponentUpdate(nextProps, nextState) { const currentProps = this.props; const currentState = this.state; - - return ( + const shouldUpdate = ( currentState.values.min !== nextState.values.min || currentState.values.max !== nextState.values.max || currentState.value !== nextState.value || currentProps.minValue !== nextProps.minValue || currentProps.maxValue !== nextProps.maxValue ); + + return shouldUpdate; } componentDidUpdate() { - if (this.props.onChange) { + if (this.props.onChange && this.state.didChange) { let results = this.state.values.max; if (this.isMultiValue) { @@ -111,6 +116,10 @@ class InputRange extends React.Component { this.props.onChange(this, results); } + + this.setState({ + didChange: true, + }); } // Getters / Setters @@ -168,6 +177,10 @@ class InputRange extends React.Component { } setPositionByValue(slider, value) { + if (!isNumber(value)) { + return; + } + const validValue = clamp(value, this.props.minValue, this.props.maxValue); const position = this.valueTransformer.positionFromValue(validValue); @@ -175,6 +188,10 @@ class InputRange extends React.Component { } setPositionsByValues(values) { + if (!values || !isNumber(values.min) || !isNumber(values.max)) { + return; + } + const validValues = { min: clamp(values.min, this.props.minValue, this.props.maxValue), max: clamp(values.max, this.props.minValue, this.props.maxValue), @@ -190,9 +207,13 @@ class InputRange extends React.Component { setPositionsByProps(props) { if (this.isMultiValue) { - this.setPositionsByValues(props.values); + const values = !isEmpty(props.values) ? props.values : props.defaultValues; + + this.setPositionsByValues(values); } else { - this.setPositionByValue(this.refs.sliderMax, props.value); + const value = isNumber(props.value) ? props.value : props.defaultValue; + + this.setPositionByValue(this.refs.sliderMax, value); } } @@ -325,6 +346,8 @@ class InputRange extends React.Component { InputRange.propTypes = { ariaLabelledby: React.PropTypes.string, classNames: React.PropTypes.objectOf(React.PropTypes.string), + defaultValue: maxMinValuePropType, + defaultValues: maxMinValuePropType, maxValue: maxMinValuePropType, minValue: maxMinValuePropType, name: React.PropTypes.string, @@ -338,7 +361,6 @@ InputRange.defaultProps = { classNames: defaultClassNames, minValue: 0, maxValue: 10, - value: 0, step: 1, }; diff --git a/src/InputRange/propTypes.js b/src/InputRange/propTypes.js index 6fd2b84..1915273 100644 --- a/src/InputRange/propTypes.js +++ b/src/InputRange/propTypes.js @@ -9,13 +9,21 @@ export function maxMinValuePropType(props) { const minValue = props.minValue; const value = props.value; const values = props.values; + const defaultValue = props.defaultValue; + const defaultValues = props.defaultValues; - if (!numberPredicate(value)) { - return new Error('`value` must be a number'); + if (!values && + !defaultValues && + !numberPredicate(value) && + !numberPredicate(defaultValue)) { + return new Error('`value` or `defaultValue` must be a number'); } - if (!value && !objectOf(values, numberPredicate)) { - return new Error('`values` must be an object of numbers'); + if (!value && + !defaultValue && + !objectOf(values, numberPredicate) && + !objectOf(defaultValues, numberPredicate)) { + return new Error('`values` or `defaultValues` must be an object of numbers'); } if (minValue >= maxValue) { diff --git a/src/InputRange/util.js b/src/InputRange/util.js index 92c72fd..a355739 100644 --- a/src/InputRange/util.js +++ b/src/InputRange/util.js @@ -6,6 +6,23 @@ function extend() { return Object.assign.apply(Object, arguments); } +function includes(array, value) { + return array.indexOf(value) > -1; +} + +function omit(obj, omitKeys) { + const keys = Object.keys(obj); + const outputObj = {}; + + keys.forEach((key) => { + if (!includes(omitKeys, key)) { + outputObj[key] = obj[key]; + } + }); + + return outputObj; +} + function captialize(string) { return string.charAt(0).toUpperCase() + string.slice(1); } @@ -18,6 +35,18 @@ function isNumber(number) { return typeof number === 'number'; } +function isEmpty(obj) { + if (!obj) { + return true; + } + + if (Array.isArray(obj)) { + return obj.length === 0; + } + + return Object.keys(obj).length === 0; +} + function arrayOf(array, predicate) { if (!Array.isArray(array)) { return false; @@ -63,8 +92,10 @@ const util = { clamp, distanceTo, extend, + isEmpty, isNumber, objectOf, + omit, }; export default util; diff --git a/test/InputRange.spec.js b/test/InputRange.spec.js index 4c70ec5..35d3848 100644 --- a/test/InputRange.spec.js +++ b/test/InputRange.spec.js @@ -1,5 +1,6 @@ import React from 'react'; import InputRange from 'InputRange'; +import _ from 'lodash'; import { removeComponent, renderComponent } from './TestUtil'; let inputRange; @@ -50,7 +51,6 @@ describe('InputRange', () => { min: 2, max: 10, }, - value: 0, step: 1, }; @@ -65,7 +65,7 @@ describe('InputRange', () => { spyOn(inputRange, 'setPositionsByProps'); }); - it('should set the initial position for slider', () => { + it('should set the current position for slider', () => { const newProps = { maxValue: 20, minValue: 0, @@ -73,13 +73,17 @@ describe('InputRange', () => { min: 5, max: 8, }, + defaultValues: { + min: 1, + max: 10, + }, value: 0, step: 1, }; inputRange.componentWillReceiveProps(newProps); - expect(inputRange.setPositionsByProps).toHaveBeenCalledWith(newProps); + expect(inputRange.setPositionsByProps).toHaveBeenCalledWith(_.omit(newProps, 'defaultValues')); }); }); @@ -140,40 +144,58 @@ describe('InputRange', () => { let onChange; beforeEach(() => { - nextState = Object.assign({}, inputRange.state); onChange = jasmine.createSpy('onChange'); }); describe('if `onChange` callback is provided', () => { - describe('if multiple values is provided', () => { + describe('if it is an initial change', () => { beforeEach(() => { removeComponent(inputRange); inputRange = renderComponent(); + nextState = Object.assign({}, inputRange.state, { didChange: false }); }); - it('should execute `onChange` callback with the changed values', () => { + it('should not execute `onChange` callback', () => { inputRange.state = nextState; inputRange.componentDidUpdate(); - expect(onChange).toHaveBeenCalledWith(inputRange, values); + expect(onChange).not.toHaveBeenCalledWith(inputRange, values); }); }); - describe('if multiple values is provided', () => { - let value; + describe('if it is not an initial change', () => { + describe('if multiple values is provided', () => { + beforeEach(() => { + removeComponent(inputRange); + inputRange = renderComponent(); + nextState = Object.assign({}, inputRange.state, { didChange: true }); + }); - beforeEach(() => { - value = 1; + it('should execute `onChange` callback with the changed values', () => { + inputRange.state = nextState; + inputRange.componentDidUpdate(); - removeComponent(inputRange); - inputRange = renderComponent(); + expect(onChange).toHaveBeenCalledWith(inputRange, values); + }); }); - it('should execute `onChange` callback with the changed value', () => { - inputRange.state = nextState; - inputRange.componentDidUpdate(); + describe('if single value is provided', () => { + let value; + + beforeEach(() => { + value = 1; - expect(onChange).toHaveBeenCalledWith(inputRange, value); + removeComponent(inputRange); + inputRange = renderComponent(); + nextState = Object.assign({}, inputRange.state, { didChange: true }); + }); + + it('should execute `onChange` callback with the changed value', () => { + inputRange.state = nextState; + inputRange.componentDidUpdate(); + + expect(onChange).toHaveBeenCalledWith(inputRange, value); + }); }); }); }); @@ -441,29 +463,62 @@ describe('InputRange', () => { spyOn(inputRange, 'setPositionByValue'); }); - it('should set the position of max slider if it only accepts single value', () => { - const props = { - value: 1, - }; + describe('if it only accepts single value', () => { + beforeEach(() => { + inputRange.isMultiValue = false; + }); + + it('should set the position of max slider', () => { + const props = { + value: 1, + }; + + inputRange.setPositionsByProps(props); - inputRange.isMultiValue = false; - inputRange.setPositionsByProps(props); + expect(inputRange.setPositionByValue).toHaveBeenCalledWith(inputRange.refs.sliderMax, props.value); + }); + + it('should set the position of max slider using default value if current value is undefined', () => { + const props = { + defaultValue: 6, + }; - expect(inputRange.setPositionByValue).toHaveBeenCalledWith(inputRange.refs.sliderMax, props.value); + inputRange.setPositionsByProps(props); + + expect(inputRange.setPositionByValue).toHaveBeenCalledWith(inputRange.refs.sliderMax, props.defaultValue); + }); }); - it('should set the position of all sliders if it accepts multiple values', () => { - const props = { - values: { - min: 2, - max: 12, - }, - }; + describe('if it only accepts multiple values', () => { + beforeEach(() => { + inputRange.isMultiValue = true; + }); + + it('should set the position of all sliders', () => { + const props = { + values: { + min: 2, + max: 12, + }, + }; + + inputRange.setPositionsByProps(props); - inputRange.isMultiValue = true; - inputRange.setPositionsByProps(props); + expect(inputRange.setPositionsByValues).toHaveBeenCalledWith(props.values); + }); - expect(inputRange.setPositionsByValues).toHaveBeenCalledWith(props.values); + it('should set the position of all sliders using default values if current values are undefined', () => { + const props = { + defaultValues: { + min: 6, + max: 10, + }, + }; + + inputRange.setPositionsByProps(props); + + expect(inputRange.setPositionsByValues).toHaveBeenCalledWith(props.defaultValues); + }); }); });