Initial commit

This commit is contained in:
Tanner Linsley
2016-10-20 15:08:27 -06:00
parent 0fb6f182af
commit 76542c448e
8 changed files with 917 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+162
View File
@@ -1,2 +1,164 @@
# react-table
A fast, lightweight, opinionated table and datagrid built on React
[![Chart.js on Slack](https://img.shields.io/badge/slack-Chart.js-blue.svg)](https://react-table-slack.herokuapp.com/)
## Features
- Lightweight at 3kb (and just 2kb more for styles)
- No composition needed
- Uses customizable JSX and callbacks for everything
- Client-side pagination and sorting
- Server-side support
- Minimal Design & easy to theme
Why you may **not** want to use `react-table`:
- No support for infinite scrolling. In our experience, infinite scrolling complicates data grids
## Installation
```bash
$ npm install react-table
```
## Quick Usage
```javascript
import ReactTable from 'react-table'
const data = [{
name: 'Tanner Linsley',
age: 26,
friend: {
name: 'Jason Maurer',
age: 23,
}
},{
...
}]
const columns = [{
header: 'Name',
accessor: 'name',
}, {
header: 'Age',
accessor: 'age'
}, {
header: 'Friend Name',
accessor: d => d.friend.name
}, {
header: 'Friend Age',
accessor: 'friend.age'
}]
<ReactTable
data=[{...}]
columns={[]}
/>
```
## Client-side Data
To use client-side data, simply pass the `data` prop an array. Client-side filtering and pagination is built in, and your table will update gracefully if you change any props.
## Server-side Data
If you want to handle pagination, and sorting on the server, `react-table` makes it easy on you. Instead of passing the `data` prop an array, you provide a function instead.
This function will be called on mount, pagination events, and sorting events. It also provides you all of the parameters to help you query and format your data.
```javascript
<ReactTable
data={(params, callback) => {
// params will give you all the info you need to query and sort your data
params == {
page: 0, // The page index the user is requesting
pageSize: 20, // The current pageSize
pages: -1, // The amount of existing pages (-1 means there is no page data yet)
sorting: [ // An array of column sort models (yes, you can multi-sort!)
{
id: 'columnID', // The columnID (usually the accessor string, but can be overridden for server-side or required if the column accessor is a function)
ascending: true or false
}
]
}
// Query your data however you'd like, then structure your response like so:
const result = {
rows: [...], // Your data for the current page/sorting model
pages: 10 // optionally provide how many pages exist (this is only needed if you choose to display page numbers, and only the first time you make the call or if the page count changes)
}
// You can return a promise that resolve the result
return Axios.post('/myDataEnpoint', params) // resolves to `result`
// or use the manual callback whenever you please
setTimeout(() => {
callback(result)
}, 5000)
// That's it!
}}
/>
```
## Multi-Sort
When clicking on a column header, hold shift to multi-sort! You can toggle `aascending` `descending` and `none` for multi-sort columns. Clicking on a header without holding shift will clear the multi-sort and replace it with the single sort of that column. It's quite handy!
## Default Props
```javascript
{
className: '-striped -highlight',
pageSize: 20,
minRows: 0,
data: [],
previousComponent: <button {...props} className='-btn'>{props.children}</button>,
nextComponent: <button {...props} className='-btn'>{props.children}</button>,
previousText: 'Previous',
nextText: 'Next',
loadingComponent: <span>Loading...</span>,
column: { // default properties for every column's model
sortable: true,
show: true
}
}
```
You can easily override the core defaults like so:
```javascript
import { ReactTableDefaults } from 'react-table'
Object.assign(ReactTableDefaults, {
pageSize: 10,
minRows: 3,
// etc...
})
```
Or just define them on the component
```javascript
<ReactTable
pageSize={10}
minRows={3}
// etc...
})
```
## Column Props
```javascript
[{
// Required
header: 'Header Name' or JSX eg. ({data, column}) => <div>Header Name</div>,
accessor: 'propertyName' or Accessor eg. (row) => row.propertyName,
// Optional
id: 'myProperty', // A unique ID is needed if the accessor is not a string or if you would like to override the column name used in server-side calls
render: JSX eg. ({row, value, index}) => <span>{value}</span>, // Provide a JSX element or stateless function to render whatever you want as the column's cell with access to the entire row
sortable: true,
sort: 'asc' or 'desc',
show: true,
width: Number, // Locks the column width to this amount
minWidth: Number // Allows the column to flex above this minimum amount
}]
```
+56
View File
@@ -0,0 +1,56 @@
{
"name": "react-table",
"version": "0.0.1",
"description": "A fast, lightweight, opinionated table and datagrid built on React",
"license": "MIT",
"homepage": "https://github.com/tannerlinsley/react-table#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/tannerlinsley/react-table.git"
},
"keywords": [
"react",
"table",
"datagrid"
],
"main": "src/index.js",
"scripts": {
"build:js": "rm -rf react-table.js && browserify src/index.js --external react -s react-table -t babelify -tg uglifyify -o react-table.js",
"build:css": "rm -rf react-table.css && stylus src/index.styl -o react-table.css --use ./node_modules/nib/lib/nib.js --compress",
"watch": "onchange 'src/**' -i -- npm-run-all build:*",
"prepublish": "npm-run-all build:*"
},
"dependencies": {
"classnames": "^2.2.5",
"lodash": "^4.16.4"
},
"devDependencies": {
"babel-cli": "6.14.0",
"babel-eslint": "6.1.2",
"babel-preset-es2015": "6.14.0",
"babel-preset-react": "6.11.1",
"babel-preset-stage-2": "6.13.0",
"babelify": "^7.3.0",
"browserify": "13.1.0",
"onchange": "^3.0.2",
"react": "15.3.1",
"rollupify": "^0.3.4",
"standard": "8.0.0",
"stylus": "^0.54.5",
"uglifyify": "3.0.3"
},
"standard": {
"parser": "babel-eslint",
"ignore": [
"lib",
"react-table.js"
]
},
"babel": {
"presets": [
"es2015",
"stage-2",
"react"
]
}
}
+1
View File
File diff suppressed because one or more lines are too long
+10
View File
File diff suppressed because one or more lines are too long
+411
View File
@@ -0,0 +1,411 @@
import React from 'react'
import classnames from 'classnames'
//
import _ from './utils'
const defaultButton = (props) => (
<button {...props} className='-btn'>{props.children}</button>
)
export const ReactTableDefaults = {
className: '-striped -highlight',
pageSize: 20,
minRows: 0,
data: [],
previousComponent: defaultButton,
nextComponent: defaultButton,
previousText: 'Previous',
nextText: 'Next',
loadingComponent: <span>Loading...</span>,
column: {
sortable: true,
show: true
}
}
export default React.component({
getDefaultProps () {
return ReactTableDefaults
},
getInitialState () {
return {
sorting: false
}
},
componentWillMount () {
this.update(this.props)
},
componentWillReceiveProps (nextProps) {
this.update(nextProps)
},
update (props) {
const resetState = {
loading: false,
page: 0,
pages: -1
// columns: {} for column hiding in the future
}
this.setState(resetState)
const newState = Object.assign({}, this.state, resetState)
this.isAsync = typeof props.data === 'function'
this.buildColumns(props, newState)
this.buildData(props, newState)
},
buildColumns (props) {
this.hasHeaderGroups = false
props.columns.forEach(column => {
if (column.columns) {
this.hasHeaderGroups = true
}
})
this.headerGroups = []
this.decoratedColumns = []
let currentSpan = []
const addHeader = (columns, column = {}) => {
this.headerGroups.push(Object.assign({}, column, {
columns: columns
}))
currentSpan = []
}
const makeDecoratedColumn = (column) => {
const dcol = Object.assign({}, this.props.column, column)
if (typeof dcol.accessor === 'string') {
dcol.id = dcol.id || dcol.accessor
const accessorString = dcol.accessor
dcol.accessor = row => _.get(row, accessorString)
}
if (!dcol.id) {
console.warn('No column ID found for column: ', dcol)
}
if (!dcol.accessor) {
console.warn('No column accessor found for column: ', dcol)
}
return dcol
}
props.columns.forEach((column, i) => {
if (column.columns) {
column.columns.forEach(nestedColumn => {
this.decoratedColumns.push(makeDecoratedColumn(nestedColumn))
})
if (this.hasHeaderGroups) {
if (currentSpan.length > 0) {
addHeader(currentSpan)
}
addHeader(_.takeRight(this.decoratedColumns, column.columns.length), column)
}
} else {
this.decoratedColumns.push(makeDecoratedColumn(column))
currentSpan.push(_.last(this.decoratedColumns))
}
})
if (this.hasHeaderGroups && currentSpan.length > 0) {
addHeader(currentSpan)
}
},
getInitSorting () {
const initSorting = this.decoratedColumns.filter(d => {
return typeof d.sort !== 'undefined'
}).map(d => {
return {
id: d.id,
asc: d.sort === 'asc'
}
})
return initSorting.length ? initSorting : [{
id: this.decoratedColumns[0].id,
asc: true
}]
},
buildData (props, state) {
const sorting = state.sorting === false ? this.getInitSorting() : state.sorting
const setData = (data) => {
this.setState({
sorting,
data,
page: state.page,
loading: false
})
}
if (this.isAsync) {
this.setState({
loading: true
})
const cb = (res) => {
if (!res) {
return Promise.reject('Uh Oh! Nothing was returned in ReactTable\'s data callback!')
}
if (res.pages) {
this.setState({
pages: res.pages
})
}
// Only access the data. Sorting is done server side.
const accessedData = this.accessData(res.rows)
setData(accessedData)
}
// Fetch data with current state
const dataRes = props.data({
sorting,
page: state.page || 0,
pageSize: props.pageSize,
pages: state.pages
}, cb)
if (dataRes && dataRes.then) {
dataRes.then(cb)
}
} else {
// Return locally accessed, sorted data
const accessedData = this.accessData(props.data)
const sortedData = this.sortData(accessedData, sorting)
setData(sortedData)
}
},
accessData (data) {
return data.map((d) => {
const row = {
__original: d
}
this.decoratedColumns.forEach(column => {
row[column.id] = column.accessor(d)
})
return row
})
},
sortData (data, sorting) {
const resolvedSorting = sorting.length ? sorting : this.getInitSorting()
return _.orderBy(data, resolvedSorting.map(sort => {
return row => {
if (row[sort.id] === null || row[sort.id] === undefined) {
return -Infinity
}
return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id]
}
}), resolvedSorting.map(d => d.asc ? 'asc' : 'desc'))
},
setPage (page) {
if (this.isAsync) {
return this.buildData(this.props, Object.assign({}, this.state, {page}))
}
this.setState({
page
})
},
render () {
const data = this.state.data ? this.state.data : []
const pagesLength = this.isAsync ? this.state.pages : Math.ceil(data.length / this.props.pageSize)
const startRow = this.props.pageSize * this.state.page
const endRow = startRow + this.props.pageSize
const pageRows = this.isAsync ? data.slice(0, this.props.pageSize) : data.slice(startRow, endRow)
const padRows = pagesLength > 1 ? _.range(this.props.pageSize - pageRows.length)
: this.props.minRows ? _.range(Math.max(this.props.minRows - pageRows.length, 0))
: []
const canPrevious = this.state.page > 0
const canNext = this.state.page + 1 < pagesLength
const PreviousComponent = this.props.previousComponent
const NextComponent = this.props.previousComponent
return (
<div className={classnames(this.props.className, 'ReactTable')}>
<table>
{this.hasHeaderGroups && (
<thead className='-headerGroups'>
<tr>
{this.headerGroups.map((column, i) => {
return (
<th
key={i}
colSpan={column.columns.length}>
<div
className='-th-inner'>
{typeof column.header === 'function' ? (
<column.header
data={this.props.data}
column={column}
/>
) : column.header}
</div>
</th>
)
})}
</tr>
</thead>
)}
<thead>
<tr>
{this.decoratedColumns.map((column, i) => {
const sort = this.state.sorting.find(d => d.id === column.id)
const show = typeof column.show === 'function' ? column.show() : column.show
return (
<th
key={i}
className={classnames(
sort ? (sort.asc ? 'sort-asc' : 'sort-desc') : '',
{
'cursor-pointer': column.sortable,
'hidden': !show
}
)}
onClick={(e) => {
column.sortable && this.sortColumn(column, e.shiftKey)
}}>
<div
className='-th-inner'
style={{
width: column.width + 'px',
minWidth: column.minWidth + 'px'
}}>
{typeof column.header === 'function' ? (
<column.header
data={this.props.data}
column={column}
/>
) : column.header}
</div>
</th>
)
})}
</tr>
</thead>
<tbody>
{pageRows.map((row, i) => {
return (
<tr key={i}>
{this.decoratedColumns.map((column, i2) => {
const Cell = column.render
const show = typeof column.show === 'function' ? column.show() : column.show
return (
<td
className={classnames({hidden: !show})}
key={i2}>
<div
className='-td-inner'
style={{
width: column.width + 'px',
minWidth: column.minWidth + 'px'
}}>
{typeof Cell === 'function' ? (
<Cell
value={row[column.id]}
row={row.__original}
index={i}
/>
) : typeof Cell !== 'undefined' ? Cell
: row[column.id]}
</div>
</td>
)
})}
</tr>
)
})}
{padRows.map((row, i) => {
return (
<tr key={i}>
{this.decoratedColumns.map((column, i2) => {
const show = typeof column.show === 'function' ? column.show() : column.show
return (
<td
className={classnames({hidden: !show})}
key={i2}>
<div
className='-td-inner'
style={{
width: column.width + 'px',
minWidth: column.minWidth + 'px'
}}>&nbsp;</div>
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
{pagesLength > 1 && (
<div className='-pagination'>
<div className='-left'>
<PreviousComponent
onClick={canPrevious && ((e) => this.previousPage(e))}
disabled={!canPrevious}>
{this.props.previousText}
</PreviousComponent>
</div>
<div className='-center'>
Page {this.state.page + 1} of {pagesLength}
</div>
<div className='-right'>
<NextComponent
onClick={canNext && ((e) => this.nextPage(e))}
disabled={!canNext}>
{this.props.nextText}
</NextComponent>
</div>
</div>
)}
<div className={classnames('-loading', {'-active': this.state.loading})}>
<div className='-loading-inner'>
{this.props.loadingComponent}
</div>
</div>
</div>
)
},
sortColumn (column, additive) {
const existingSorting = this.state.sorting || []
const sorting = _.clone(this.state.sorting || [])
const existingIndex = sorting.findIndex(d => d.id === column.id)
if (existingIndex > -1) {
const existing = sorting[existingIndex]
if (existing.asc) {
existing.asc = false
if (!additive) {
_.remove(sorting, d => d)
sorting.push(existing)
}
} else {
if (additive) {
sorting.splice(existingIndex, 1)
} else {
existing.asc = true
}
}
} else {
if (additive) {
sorting.push({
id: column.id,
asc: true
})
} else {
_.remove(sorting, d => d)
sorting.push({
id: column.id,
asc: true
})
}
}
const page = (existingIndex === 0 || (!existingSorting.length && sorting.length)) ? 0 : this.state.page
this.buildData(this.props, Object.assign({}, this.state, {page, sorting}))
},
nextPage (e) {
e.preventDefault()
this.setPage(this.state.page + 1)
},
previousPage (e) {
e.preventDefault()
this.setPage(this.state.page - 1)
}
})
+191
View File
@@ -0,0 +1,191 @@
@import 'nib'
$easeOutQuad = cubic-bezier(0.250, 0.460, 0.450, 0.940)
.ReactTable
position:relative
table
width: 100%
border-collapse: collapse
border: 1px solid alpha(black, .1)
display: block
overflow-x: auto
.-td
.-th
&-inner
white-space: nowrap
text-overflow: ellipsis
padding: 7px 5px
overflow: hidden
z-index:0
transition: .3s ease
transition-property: width, min-width, padding, opacity
th
td
&:hover
.-td
.-th
&-inner
position:relative
overflow:visible
z-index:1
text-shadow:
0 0 1px white,
0 0 2px white,
1px 0 3px white,
1px 0 4px white,
2px 0 5px white,
2px 0 6px white,
3px 0 7px white,
3px 0 8px white,
4px 0 9px white,
4px 0 10px white,
5px 0 11px white,
5px 0 12px white,
6px 0 13px white,
6px 0 14px white,
7px 0 15px white,
7px 0 16px white,
8px 0 17px white,
8px 0 18px white,
9px 0 19px white,
9px 0 20px white
> *
text-shadow: none
&.hidden
border:0 !important
opacity: 0 !important
.-th-inner
.-td-inner
width: 0 !important
min-width: 0 !important
padding: 0 !important
thead
box-shadow: 0px 5px 20px 0 alpha(black, .1)
z-index: 0
&.-headerGroups
border-bottom: 1px solid alpha(black, .05)
th
td
background: alpha(black, .03)
tr
text-align:center
th
td
width: 1%
background: white
font-weight:500
color: $darkColor
border-right: 1px solid alpha(black, .05)
transition box-shadow .3s easeOutBack
box-shadow:inset 0 0 0 0 transparent
&.sort-asc
box-shadow:inset 0 3px 0 0 alpha(black, .6)
&.sort-desc
box-shadow:inset 0 -3px 0 0 alpha(black, .6)
tbody
z-index:0
tr
border-bottom: solid 1px alpha(black, .05)
&:last-child
border-bottom: 0
&:first-child td
box-shadow: inset 0 20px 20px -20px alpha(black, .2)
td
border-right:1px solid alpha(black, .02)
&:last-child
border-right:0
&.striped
tbody tr:nth-child(even)
background: alpha(black, .03)
&.highlight
tbody tr:hover
background: alpha(black, .05)
.-pagination
width:100%
display:flex
margin-top:5px
justify-content: space-between
align-items: center
flex-wrap: wrap
.-btn
appearance:none
display:block
width:100%
border: 0
border-radius: 3px
padding: 6px
font-size: 1em
color: alpha(black, .6)
background: alpha(black, .1)
transition: all .1s ease
&[disabled]
opacity: .5
cursor: default
&:not([disabled]):hover
background: alpha(black, .3)
color: white
.-left
.-center
.-right
flex: 1
// min-width:150px
margin-bottom:5px
.-left
.-right
text-align: center
.-center
text-align:center
padding: 0 5px
.-loading
display:block
position:absolute
left:0
right:0
top:0
bottom:0
background: alpha(white, .8)
transition: all .3s ease
z-index: 2
opacity: 0
pointer-events: none
> div
position:absolute
display: block
text-align:center
width:100%
top:50%
left: 0
font-size: 15px
color: alpha(black, .6)
transform: translateY(-52%)
transition: .3s $easeOutQuad
&.-active
opacity: 1
pointer-events: all
> div
transform: translateY(50%)
+85
View File
@@ -0,0 +1,85 @@
export default {
get,
takeRight,
last,
orderBy,
range,
clone,
remove
}
function remove (a, b) {
return a.filter(function (o, i) {
var r = b(o)
if (r) {
a.splice(i, 1)
return true
}
return false
})
}
function get (a, b) {
if (isArray(b)) {
b = b.join('.')
}
return b
.replace('[', '.').replace(']', '')
.split('.')
.reduce(
function (obj, property) {
return obj[property]
}, a
)
}
function takeRight (arr, n) {
const start = n > arr.length ? 0 : arr.length - n
return arr.slice(start)
}
function last (arr) {
return arr[arr.length - 1]
}
function range (n) {
const arr = []
for (let i = 0; i < n; i++) {
arr.push(n)
}
return arr
}
function orderBy (arr, funcs) {
return arr.sort((a, b) => {
for (let i = 0; i < funcs.length; i++) {
const comp = funcs[i]
const ca = comp(a)
const cb = comp(b)
if (ca > cb) {
return 1
}
if (ca < cb) {
return -1
}
}
return 0
})
}
function clone (a) {
return JSON.parse(JSON.stringify(a, function (key, value) {
if (typeof value === 'function') {
return value.toString()
}
return value
}))
}
// ########################################################################
// Helpers
// ########################################################################
function isArray (a) {
return Array.isArray(a)
}