Skip to content

Commit

Permalink
Merge pull request #369 from react-native-training/rating_component
Browse files Browse the repository at this point in the history
[NEW FEATURE] Ratings Component
  • Loading branch information
Monte Thakkar committed May 14, 2017
2 parents 6d42287 + b9ba8cb commit 1364ffa
Show file tree
Hide file tree
Showing 14 changed files with 612 additions and 148 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
*.log
site
coverage
jsconfig.json
57 changes: 57 additions & 0 deletions docs/API/rating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
An extendable Ratings components for React Native with gestures and an intuitive API

> This component was inspired from [react-native-ratings](https://github.com/Monte9/react-native-ratings) by [Monte Thakkar](https://github.com/Monte9).
### Demo

![Demo gif](http://i.imgur.com/hpo67Dq.gifv)

```js
import { Rating } from 'react-native-elements';

ratingCompleted(rating) {
console.log("Rating is: " + rating)
}

<Rating
showRating
onFinishRating={this.ratingCompleted}
style={{ paddingVertical: 10 }}
/>

<Rating
type='heart'
ratingCount={3}
imageSize={60}
showRating
onFinishRating={this.ratingCompleted}
/>


const WATER_IMAGE = require('./water.png')

<Rating
type='custom'
ratingImage={WATER_IMAGE}
ratingColor='#3498db'
ratingBackgroundColor='#c8c7c8'
ratingCount={10}
imageSize={30}
onFinishRating={this.ratingCompleted}
style={{ paddingVertical: 10 }}
/>
```

#### Rating Props

| prop | default | type | description |
| ---- | ---- | ----| ---- |
| **onFinishRating** | none | function | Callback method when the user finishes rating. Gives you the final rating value as a whole number **(required)** |
| type | star | string | Choose one of the built-in types: `star`, `rocket`, `bell`, `heart` or use type `custom` to render a custom image (optional) |
| ratingImage | star | string | Pass in a custom image source; use this along with `type='custom'` prop above (optional) |
| ratingColor | #f1c40f | string (color) | Pass in a custom fill-color for the rating icon; use this along with `type='custom'` prop above (optional) |
| ratingBackgroundColor | white | string (color) | Pass in a custom background-fill-color for the rating icon; use this along with `type='custom'` prop above (optional) |
| ratingCount | 5 | number | The number of rating images to display (optional) |
| imageSize | 50 | number | The size of each rating image (optional) |
| showRating | none | boolean | Displays the Built-in Rating UI to show the rating value in real-time (optional) |
| style | none | function | Exposes style prop to add additonal styling to the container view (optional) |
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<a href="https://www.npmjs.com/package/react-native-elements"><img src="https://img.shields.io/npm/v/react-native-elements.svg?style=flat-square"></a>
<a href="https://www.npmjs.com/package/react-native-elements"><img src="https://img.shields.io/npm/dm/react-native-elements.svg?style=flat-square"></a>
<a href="https://travis-ci.org/react-native-training/react-native-elements"><img src="https://img.shields.io/travis/react-native-training/react-native-elements/master.svg?style=flat-square"></a>
<a href="https://reactnativetraining.herokuapp.com/"><img src="https://reactnativetraining.herokuapp.com/badge.svg"></a>
<a href="https://reactnativetraining.herokuapp.com/"><img src="https://reactnativetraining.herokuapp.com/badge.svg"></a>
</p>

<br />
Expand Down Expand Up @@ -79,6 +79,7 @@ import { Button } from 'react-native-elements';
- [x] [Slider Component](https://react-native-training.github.io/react-native-elements/API/slider/)
- [x] [Tile Component](https://react-native-training.github.io/react-native-elements/API/tile/)
- [x] [Avatar Component](https://react-native-training.github.io/react-native-elements/API/avatar/)
- [x] [Rating Component](https://react-native-training.github.io/react-native-elements/API/rating/)

## Documentation

Expand Down
8 changes: 8 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"husky": "^0.13.3",
"jest": "^19.0.2",
"lint-staged": "^3.4.0",
"lodash": "^4.17.4",
"prettier": "^1.1.0",
"react": "^15.4.2",
"react-addons-test-utils": "^15.4.2",
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Col from './grid/Col';
import Tile from './tile/Tile';
import Slider from './slider/Slider';
import Avatar from './avatar/Avatar';
import Rating from './rating/Rating';

const Elements = {
Badge,
Expand Down Expand Up @@ -56,6 +57,7 @@ const Elements = {
Tile,
Slider,
Avatar,
Rating,
};

module.exports = Elements; // eslint-disable-line no-undef
262 changes: 262 additions & 0 deletions src/rating/Rating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
var _ = require('lodash');

import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
View,
Animated,
PanResponder,
Dimensions,
Image,
StyleSheet,
} from 'react-native';

import Text from '../text/Text';

const SCREEN_WIDTH = Dimensions.get('window').width;
const SCREEN_HEIGHT = Dimensions.get('window').height;

const STAR_IMAGE = require('./images/star.png');
const HEART_IMAGE = require('./images/heart.png');
const ROCKET_IMAGE = require('./images/rocket.png');
const BELL_IMAGE = require('./images/bell.png');

const STAR_WIDTH = 60;

const TYPES = {
star: {
source: STAR_IMAGE,
color: '#f1c40f',
backgroundColor: 'white',
},
heart: {
source: HEART_IMAGE,
color: '#e74c3c',
backgroundColor: 'white',
},
rocket: {
source: ROCKET_IMAGE,
color: '#2ecc71',
backgroundColor: 'white',
},
bell: {
source: BELL_IMAGE,
color: '#f39c12',
backgroundColor: 'white',
},
};

export default class Rating extends Component {
static defaultProps = {
type: 'star',
ratingImage: require('./images/star.png'),
ratingColor: '#f1c40f',
ratingBackgroundColor: 'white',
ratingCount: 5,
imageSize: STAR_WIDTH,
onFinishRating: () => console.log('Attach a function here.'),
};

constructor(props) {
super(props);

const { onFinishRating } = this.props;

const position = new Animated.ValueXY();
const newValue = new Animated.ValueXY();
newValue.setValue({ x: 0, y: 500 });

const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (event, gesture) => {
position.setValue({ x: gesture.dx, y: gesture.dy });
this.setState({ value: gesture.dx });
},
onPanResponderRelease: (event, gesture) => {
onFinishRating(this.getCurrentRating());
},
});

this.state = { panResponder, position };
}

getPrimaryViewStyle() {
const { position } = this.state;
const { imageSize, ratingCount, type } = this.props;

const color = TYPES[type].color;

const width = position.x.interpolate({
inputRange: [
-ratingCount * (imageSize / 2),
0,
ratingCount * (imageSize / 2),
],
outputRange: [0, ratingCount * imageSize / 2, ratingCount * imageSize],
extrapolate: 'clamp',
});

return {
backgroundColor: color,
width,
height: width ? imageSize : 0,
};
}

getSecondaryViewStyle() {
const { position } = this.state;
const { imageSize, ratingCount, type } = this.props;

const backgroundColor = TYPES[type].backgroundColor;

const width = position.x.interpolate({
inputRange: [
-ratingCount * (imageSize / 2),
0,
ratingCount * (imageSize / 2),
],
outputRange: [ratingCount * imageSize, ratingCount * imageSize / 2, 0],
extrapolate: 'clamp',
});

return {
backgroundColor,
width,
height: width ? imageSize : 0,
};
}

renderRatings() {
const { imageSize, ratingCount, type } = this.props;
const source = TYPES[type].source;

return _(ratingCount).times(index => (
<View key={index} style={styles.starContainer}>
<Image
source={source}
style={{ width: imageSize, height: imageSize }}
/>
</View>
));
}

getCurrentRating() {
const { value } = this.state;
const { imageSize, ratingCount } = this.props;
const startingValue = ratingCount / 2;
let currentRating = 0;

if (value > ratingCount * imageSize / 2) {
currentRating = ratingCount;
} else if (value > imageSize) {
currentRating = Math.ceil(startingValue + value / imageSize);
} else if (value < -ratingCount * imageSize / 2) {
currentRating = 0;
} else if (value < imageSize) {
currentRating = Math.ceil(startingValue + value / imageSize);
} else {
currentRating = Math.ceil(startingValue);
}

return currentRating;
}

displayCurrentRating() {
const { ratingCount, type } = this.props;

const color = TYPES[type].color;

return (
<View style={styles.ratingView}>
<Text style={styles.ratingText}>Rating: </Text>
<Text style={[styles.currentRatingText, { color }]}>
{this.getCurrentRating()}
</Text>
<Text style={styles.maxRatingText}>/{ratingCount}</Text>
</View>
);
}

render() {
const {
type,
ratingImage,
ratingColor,
ratingBackgroundColor,
style,
showRating,
} = this.props;

if (type === 'custom') {
custom = {
source: ratingImage,
color: ratingColor,
backgroundColor: ratingBackgroundColor,
};
TYPES.custom = custom;
}

return (
<View style={style}>
{showRating && this.displayCurrentRating()}
<View
style={styles.starsWrapper}
{...this.state.panResponder.panHandlers}
>
<View style={styles.starsInsideWrapper}>
<Animated.View style={this.getPrimaryViewStyle()} />
<Animated.View style={this.getSecondaryViewStyle()} />
</View>
{this.renderRatings()}
</View>
</View>
);
}
}

const styles = StyleSheet.create({
starsWrapper: {
flexDirection: 'row',
},
starsInsideWrapper: {
position: 'absolute',
top: 0,
left: 0,
flexDirection: 'row',
},
ratingView: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 5,
},
ratingText: {
fontSize: 15,
textAlign: 'center',
fontFamily: 'Trebuchet MS',
color: '#34495e',
},
currentRatingText: {
fontSize: 30,
textAlign: 'center',
fontFamily: 'Trebuchet MS',
},
maxRatingText: {
fontSize: 18,
textAlign: 'center',
fontFamily: 'Trebuchet MS',
color: '#34495e',
},
});

Rating.propTypes = {
type: PropTypes.string,
ratingImage: Image.propTypes.source,
ratingColor: PropTypes.string,
ratingBackgroundColor: PropTypes.string,
ratingCount: PropTypes.number,
imageSize: PropTypes.number,
onFinishRating: PropTypes.func,
showRating: PropTypes.bool,
style: PropTypes.any,
};
13 changes: 13 additions & 0 deletions src/rating/__tests__/Rating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Rating from '../Rating';

describe('Rating Component', () => {
it('should render without issues', () => {
const component = shallow(<Rating />);

expect(component.length).toBe(1);
expect(toJson(component)).toMatchSnapshot();
});
});

0 comments on commit 1364ffa

Please sign in to comment.