Stateless table component

old
Javi Velasco 2015-11-14 10:26:50 +01:00
parent 832e01fe27
commit 272a37fd4c
10 changed files with 255 additions and 303 deletions

View File

@ -1,50 +0,0 @@
import React from 'react';
import Checkbox from '../../checkbox';
import style from './style';
class Head extends React.Component {
static propTypes = {
className: React.PropTypes.string,
model: React.PropTypes.object,
onSelect: React.PropTypes.func,
selected: React.PropTypes.bool
};
static defaultProps = {
className: '',
model: {},
selected: false
};
handleSelectChange = (event) => {
this.props.onSelect(event);
};
renderCellSelectable () {
if (this.props.onSelect) {
return (
<th className={style.selectable}>
<Checkbox onChange={this.handleSelectChange} checked={this.props.selected}/>
</th>
);
}
}
render () {
return (
<thead data-react-toolbox-table='head' className={this.props.className}>
<tr>
{ this.renderCellSelectable() }
{
Object.keys(this.props.model).map((key) => {
return (<th key={key}>{key}</th>);
})
}
</tr>
</thead>
);
}
}
export default Head;

View File

@ -1,97 +0,0 @@
import React from 'react';
import Checkbox from '../../checkbox';
import style from './style';
// Private
const _castType = (type) => {
let input_type = 'text';
if (type === Date) {
input_type = 'date';
} else if (type === Number) {
input_type = 'number';
} else if (type === Boolean) {
input_type = 'checkbox';
}
return input_type;
};
const _castValue = (value, type) => {
let cast = value;
if (value && type === Date) {
cast = new Date(value).toISOString().slice(0, 10);
}
return cast;
};
class Row extends React.Component {
static propTypes = {
changed: React.PropTypes.bool,
className: React.PropTypes.string,
data: React.PropTypes.object,
index: React.PropTypes.number,
onChange: React.PropTypes.func,
onSelect: React.PropTypes.func,
selected: React.PropTypes.bool
};
static defaultProps = {
className: ''
};
handleInputChange = (key, event) => {
this.props.onChange(event, this, key, event.target.value);
};
handleSelectChange = (event) => {
this.props.onSelect(event, this);
};
renderCell (key) {
let value = this.props.data[key];
if (this.props.onChange) {
const attr = this.props.model[key];
value = _castValue(value, attr.type);
return (
<input
type={_castType(attr.type)}
value={value}
onChange={this.handleInputChange.bind(null, key)}
/>
);
} else {
return value;
}
}
renderCellSelectable () {
if (this.props.onSelect) {
return (
<td className={style.selectable}>
<Checkbox onChange={this.handleSelectChange} checked={this.props.selected}/>
</td>
);
}
}
render () {
let className = `${this.props.className} ${style.row}`;
if (this.props.changed) className += ` ${style.changed}`;
if (this.props.onChange) className += ` ${style.editable}`;
if (this.props.selected) className += ` ${style.selected}`;
return (
<tr data-react-toolbox-table='row' className={className}>
{ this.renderCellSelectable() }
{
Object.keys(this.props.model).map((key) => {
return (<td key={key}>{this.renderCell(key)}</td>);
})
}
</tr>
);
}
}
export default Row;

View File

@ -1,37 +0,0 @@
@import "../../base";
@import "../config";
.row {
transition: background-color $animation-duration $animation-curve-default;
&:last-child {
border-color: transparent;
}
> td {
> input {
display: block;
width: 100%;
background-color: transparent;
border: none;
}
}
}
.selected, .row:hover {
background-color: $table-row-highlight;
}
.editable > * {
cursor: pointer;
}
.changed {
// @TODO: We've to create a style for changed rows.
}
.selectable {
padding-right: 0 !important;
width: 1.8 * $unit;
> * {
margin: 0;
}
}

39
components/table/head.jsx Normal file
View File

@ -0,0 +1,39 @@
import React from 'react';
import Checkbox from '../checkbox';
import style from './style';
const TableHead = ({model, onSelect, selected}) => {
let selectCell;
const contentCells = Object.keys(model).map((key) => {
return <th key={key}>{key}</th>;
});
if (onSelect) {
selectCell = (
<th key='select' className={style.selectable}>
<Checkbox onChange={onSelect} checked={selected} />
</th>
);
}
return (
<thead>
<tr>{[selectCell, ...contentCells]}</tr>
</thead>
);
};
TableHead.propTypes = {
className: React.PropTypes.string,
model: React.PropTypes.object,
onSelect: React.PropTypes.func,
selected: React.PropTypes.bool
};
TableHead.defaultProps = {
className: '',
model: {},
selected: false
};
export default TableHead;

View File

@ -1,121 +1,78 @@
import React from 'react';
import Head from './components/head';
import Row from './components/row';
import TableHead from './head';
import TableRow from './row';
import style from './style';
import utils from '../utils';
class Table extends React.Component {
static propTypes = {
className: React.PropTypes.string,
dataSource: React.PropTypes.array,
model: React.PropTypes.object,
heading: React.PropTypes.bool,
model: React.PropTypes.object,
onChange: React.PropTypes.func,
onSelect: React.PropTypes.func
onSelect: React.PropTypes.func,
selected: React.PropTypes.array,
source: React.PropTypes.array
};
static defaultProps = {
className: '',
dataSource: [],
heading: true
heading: true,
selected: [],
source: []
};
state = {
all: false,
dataSource: utils.cloneObject(this.props.dataSource),
selected_index: []
};
componentWillReceiveProps = (next_props) => {
if (next_props.dataSource) {
this.setState({dataSource: utils.cloneObject(next_props.datasSource)});
handleFullSelect = () => {
if (this.props.onSelect) {
const {source, selected} = this.props;
const newSelected = source.length === selected.length ? [] : source.map((i, idx) => idx);
this.props.onSelect(newSelected);
}
};
handleRowChange = (event, row, key, value) => {
const dataSource = this.state.dataSource;
dataSource[row.props.index][key] = value;
this.setState({ dataSource });
handleRowSelect = (index) => {
if (this.props.onSelect) {
const position = this.props.selected.indexOf(index);
const newSelected = [...this.props.selected];
if (position !== -1) newSelected.splice(position, 1); else newSelected.push(index);
this.props.onSelect(newSelected);
}
};
handleRowChange = (index, key, value) => {
if (this.props.onChange) {
this.props.onChange(event, dataSource[row.props.index], dataSource);
this.props.onChange(index, key, value);
}
};
handleRowSelect = (event, instance) => {
const index = instance.props.index;
const selected_index = this.state.selected_index;
const selected = selected_index.indexOf(index) === -1;
if (selected) {
selected_index.push(index);
} else {
delete selected_index[selected_index.indexOf(index)];
}
this.setState({ selected_index: selected_index });
this.props.onSelect(event, this.getSelected());
};
handleRowsSelect = () => {
const all = !this.state.all;
this.setState({ all });
this.props.onSelect(event, this.getSelected(all));
};
isChanged = (data, base) => {
let changed = false;
Object.keys(data).map((key) => {
if (data[key] !== base[key]) {
changed = true;
}
});
return changed;
};
getSelected = (all = false) => {
const rows = [];
this.state.dataSource.map((row, index) => {
if (all || this.state.selected_index.indexOf(index) !== -1) rows.push(row);
});
return rows;
}
renderHead () {
if (this.props.heading) {
return (
<Head
model={this.props.model}
onSelect={this.props.onSelect ? this.handleRowsSelect : null}
selected={this.state.all}
/>
);
const {model, selected, source} = this.props;
const isSelected = selected.length === source.length;
return <TableHead model={model} onSelect={this.handleFullSelect} selected={isSelected} />;
}
}
renderBody () {
return (
<tbody>
{
this.state.dataSource.map((data, index) => {
return (
<Row
changed={this.isChanged(data, this.props.dataSource[index])}
data={data}
index={index}
key={index}
model={this.props.model}
onChange={this.props.onChange ? this.handleRowChange : null}
onSelect={this.props.onSelect ? this.handleRowSelect : null}
selected={this.state.all || this.state.selected_index.indexOf(index) !== -1}
/>
);
})
}
</tbody>
);
const rows = this.props.source.map((data, idx) => {
return (
<TableRow
data={data}
index={idx}
key={idx}
model={this.props.model}
onChange={this.props.onChange ? this.handleRowChange.bind(this, idx) : null}
onSelect={this.handleRowSelect.bind(this, idx)}
selected={this.props.selected.indexOf(idx) !== -1}
/>
);
});
return <tbody>{rows}</tbody>;
}
render () {
const className = `${this.props.className} ${style.root}`;
let className = style.root;
if (this.props.className) className += ` ${this.props.className}`;
return (
<table data-react-toolbox='table' className={className}>
{ this.renderHead() }

View File

@ -1,43 +1,60 @@
# Table
The Table component is an enhanced version of the standard HTML `<table>`. A data-table consists of rows and columns of well-formatted data, presented with appropriate user interaction capabilities. This component uses a solid typed model, helping you to create formatted formated cells. These cells can be editable if you subscribe to `onChange` method who dispatch then new dataSource with each change.
The Table component is an enhanced version of the standard HTML `<table>`. A data-table consists of rows and columns of well-formatted data, presented with appropriate user interaction capabilities. This component uses a solid typed model, helping you to create formatted formated cells. These cells can be editable if you subscribe to `onChange` method who dispatch then new source with each change.
<!-- example -->
```jsx
import Table from 'react-toolbox/lib/table';
const UserModel = {
name: {type: String}
,
twitter: {type: String}
,
birthdate: {type: Date}
,
cats: {type: Number}
,
dogs: {type: Number}
,
name: {type: String},
twitter: {type: String},
birthdate: {type: Date},
cats: {type: Number},
dogs: {type: Number},
active: {type: Boolean}
};
const users = [
{name: 'Javi Jimenez', twitter: '@soyjavi', birthdate: new Date(1980, 3, 11), cats: 1}
,
{name: 'Javi Jimenez', twitter: '@soyjavi', birthdate: new Date(1980, 3, 11), cats: 1},
{name: 'Javi Velasco', twitter: '@javivelasco', birthdate: new Date(1987, 1, 1), dogs: 1, active: true}
];
const TableTest = () => (
<Table model={UserModel} dataSource={users} />
)
class TableTest extends React.Component {
state = { selected: [], source: users };
handleChange = (row, key, value) => {
const source = this.state.source;
source[row][key] = value;
this.setState({source});
};
handleSelect = (selected) => {
this.setState({selected});
};
render () {
return (
<Table
model={UserModel}
onChange={this.handleChange}
onSelect={this.handleSelect}
selected={this.state.selected}
source={this.state.source}
/>
);
}
}
```
## Properties
| Name | Type | Default | Description|
|:-----|:-----|:-----|:-----|
| `className` | `String` | `''` | Sets a class to style of the Component.|
| `dataSource` | `Array` | | array representing all items for show.|
| `model` | `Object` | | If true, component will be disabled.|
| `className` | `String` | `''` | Sets a custom class to style the Component.|
| `heading` | `Bool` | `true` | If true, component will show a heading using model field names.|
| `onChange` | `Function` | | Callback function that is fired when the components's value changes.|
| `onSelect` | `Function` | | Callback function when selects a determinate row.|
| `model` | `Object` | | Object describing the data model that represents each object in the `source`.|
| `onChange` | `Function` | | Callback function that is fired when an item in a row changes. If set, rows are editable. |
| `onSelect` | `Function` | | Callback function invoked when the row selection changes.|
| `selected` | `Array` | | Array of indexes of the items in the source that should appear as selected.|
| `source` | `Array` | | Array of objects representing each item to show.|

72
components/table/row.jsx Normal file
View File

@ -0,0 +1,72 @@
import React from 'react';
import Checkbox from '../checkbox';
import utils from '../utils';
import style from './style';
class TableRow extends React.Component {
static propTypes = {
data: React.PropTypes.object,
onChange: React.PropTypes.func,
onSelect: React.PropTypes.func,
selected: React.PropTypes.bool
};
handleInputChange = (key, type, event) => {
const value = type === 'checkbox' ? event.target.checked : event.target.value;
this.props.onChange(key, value);
};
renderSelectCell () {
if (this.props.onSelect) {
return (
<td className={style.selectable}>
<Checkbox checked={this.props.selected} onChange={this.props.onSelect} />
</td>
);
}
}
renderCells () {
return Object.keys(this.props.model).map((key) => {
return <td key={key}>{this.renderCell(key)}</td>;
});
}
renderCell (key) {
const value = this.props.data[key];
if (this.props.onChange) {
return this.renderInput(key, value);
} else if (value) {
return value.toString();
}
}
renderInput (key, value) {
const inputType = utils.inputTypeForPrototype(this.props.model[key].type);
const inputValue = utils.prepareValueForInput(value, inputType);
const checked = inputType === 'checkbox' && value ? true : null;
return (
<input
checked={checked}
onChange={this.handleInputChange.bind(null, key, inputType)}
type={inputType}
value={inputValue}
/>
);
}
render () {
let className = style.row;
if (this.props.onChange) className += ` ${style.editable}`;
if (this.props.selected) className += ` ${style.selected}`;
return (
<tr data-react-toolbox-table='row' className={className}>
{ this.renderSelectCell() }
{ this.renderCells() }
</tr>
);
}
}
export default TableRow;

View File

@ -10,7 +10,6 @@
height: $table-row-height;
line-height: $table-row-height;
border-bottom: $table-row-divider;
}
th {
font-weight: $font-weight-bold;
@ -21,5 +20,35 @@
th, td {
position: relative;
padding: 0 $table-row-offset;
&.selectable {
width: 1.8 * $unit;
padding-right: 0;
> * {
margin: 0;
}
}
}
}
.row {
transition: background-color $animation-duration $animation-curve-default;
&:last-child {
border-color: transparent;
}
> td {
> input {
display: block;
width: 100%;
background-color: transparent;
border: 0;
}
}
}
.selected, .row:hover {
background-color: $table-row-highlight;
}
.editable > * {
cursor: pointer;
}

View File

@ -44,6 +44,21 @@ module.exports = {
return JSON.parse(JSON.stringify(object));
},
inputTypeForPrototype (prototype) {
if (prototype === Date) return 'date';
if (prototype === Number) return 'number';
if (prototype === Boolean) return 'checkbox';
return 'text';
},
prepareValueForInput (value, type) {
if (type === 'date') return new Date(value).toISOString().slice(0, 10);
if (type === 'checkbox') {
return value ? 'on' : null;
}
return value;
},
events: require('./events'),
prefixer: require('./prefixer'),
time: require('./time'),

View File

@ -12,17 +12,23 @@ const UserModel = {
const users = [
{name: 'Javi Jimenez', twitter: '@soyjavi', birthdate: new Date(1980, 3, 11), cats: 1},
{name: 'Javi Velasco', twitter: '@javivelasco', birthdate: new Date(1987, 1, 1), dogs: 1, active: true}
{name: 'Javi Velasco', twitter: '@javivelasco', birthdate: new Date(1987, 1, 1), dogs: 1, owner: true}
];
class TableTest extends React.Component {
handleTableChange = (event, row, dataSource) => {
console.log('handleTableChange', row, dataSource);
state = {
selected: [],
source: users
};
handleTableRowSelect = (event, selected) => {
console.log('handleTableRowSelect', selected);
handleChange = (row, key, value) => {
const source = this.state.source;
source[row][key] = value;
this.setState({source});
};
handleSelect = (selected) => {
this.setState({selected});
};
render () {
@ -32,9 +38,10 @@ class TableTest extends React.Component {
<p style={{marginBottom: '10px'}}>Organized data.</p>
<Table
model={UserModel}
dataSource={users}
onChange={this.handleTableChange}
onSelect={this.handleTableRowSelect}
onChange={this.handleChange}
onSelect={this.handleSelect}
selected={this.state.selected}
source={this.state.source}
/>
</section>
);