前面基本了解了组件的基本用法,在本节会实现一个更高级的例子。另外需要注意
本节代码是采用V15版本的createClass()、React.DOM和JSX实现的
,有时间的同学可以改成类实现的方式。
html的世界中最复杂的UI控制就是表格了,原因是table它依赖本地环境来创建,不同的系统不同的浏览器对table的实现并不一样,也导致有些功能换了个浏览器不就好用了。
完成后的需要如下:
基本实现
表头tr实现
表格的结构是 table–> thead --> tr --> td 共4级。
var Excel = React.createClass({
// displayName: 'Excel',
render: function() {
return (
React.DOM.table(null,
React.DOM.thead(null,
React.DOM.tr(null,
this.props.headers.map(function(title, idx) { //map是个工具方法,用于list数据的循环
return React.DOM.th({key: idx}, title);
})
)
)
)
);
}
});
组件调用,下面的data
数据暂时未用。
var headers = [
"Book", "Author", "Language", "Published", "Sales"
];
var data = [
["The Lord of the Rings", "J. R. R. Tolkien", "English", "1954–1955", "150 million"],
["Le Petit Prince (The Little Prince)", "Antoine de Saint-Exupéry", "French", "1943", "140 million"],
["Harry Potter and the Philosopher's Stone", "J. K. Rowling", "English", "1997", "107 million"],
["And Then There Were None", "Agatha Christie", "English", "1939", "100 million"],
["Dream of the Red Chamber", "Cao Xueqin", "Chinese", "1754–1791", "100 million"],
["The Hobbit", "J. R. R. Tolkien", "English", "1937", "100 million"],
["She: A History of Adventure", "H. Rider Haggard", "English", "1887", "100 million"],
];
ReactDOM.render(
React.createElement(Excel, {
headers: headers,
initialData: data,
}),
document.getElementById("app")
);
美化表格,添加CSS样式
这个和普通的html写法一样,因为最后所有的元素全会渲染成html代码,比如定义如下样式,然后通过
<link rel="stylesheet" type="text/css" href="table.css">
引入即可。
td {
border-top: 1px solid black;
cursor: cell;
padding: 5px;
}
th {
cursor: pointer;
padding: 5px;
}
table {
border: 1px solid black;
margin: 20px;
}
表格td实现
这里实现也比较简单,还是用map循环
<script>
var Excel = React.createClass({
displayName: 'Excel',
//数据验证
propTypes: {
headers: React.PropTypes.arrayOf(
React.PropTypes.string
),
initialData: React.PropTypes.arrayOf(
React.PropTypes.arrayOf(
React.PropTypes.any
)
),
},
//绑定state数据,注意表格头数据是死的,所以不添加到state中
getInitialState() {
return {data: this.props.initialData};
},
render: function() {
return (
//沉浸表格头
React.DOM.table(null,
React.DOM.thead(null,
React.DOM.tr(null,
this.props.headers.map(function(title, idx) {
return React.DOM.th({key: idx}, title);
})
)
),
//渲染表格内容,根据数据结构循环即可
React.DOM.tbody(null,
this.state.data.map(function(row, idx) {
return (
React.DOM.tr({key: idx},
row.map(function(cell, idx) {
return React.DOM.td({key: idx}, cell);
})
)
);
})
)
)
);
}
});
var headers = [
"Book", "Author", "Language", "Published", "Sales"
];
var data = [
["The Lord of the Rings", "J. R. R. Tolkien", "English", "1954-1955", "150 million"],
["Le Petit Prince (The Little Prince)", "Antoine de Saint-Exupéry", "French", "1943", "140 million"],
["Harry Potter and the Philosopher's Stone", "J. K. Rowling", "English", "1997", "107 million"],
["And Then There Were None", "Agatha Christie", "English", "1939", "100 million"],
["Dream of the Red Chamber", "Cao Xueqin", "Chinese", "1754-1791", "100 million"],
["The Hobbit", "J. R. R. Tolkien", "English", "1937", "100 million"],
["She: A History of Adventure", "H. Rider Haggard", "English", "1887", "100 million"],
];
ReactDOM.render(
React.createElement(Excel, {
headers: headers,
initialData: data,
}),
document.getElementById("app")
);
</script>
表格排序
实现的效果是点击表格头,就会按列内容进行排序。核心代码如下:
修改表格头,添加排序事件
React.DOM.table(null,
React.DOM.thead({onClick: this._sort}, //添加这行代码
React.DOM.tr(null,
this.props.headers.map(function(title, idx) {
return React.DOM.th({key: idx}, title);
})
)
),
实现排序事件
_sort: function(e) {
var column = e.target.cellIndex; //得到当前列
var data = this.state.data.slice(); //得到表格数据副本
data.sort(function(a, b) {
return a[column] > b[column] ? 1 : -1; //实现数据排序算法
});
this.setState({ //保存数据,重新渲染
data: data,
});
},
添加排序视觉响应
其就是给点击的列添加一个图标啥的,比如通过state来拓展:
//添加全局状态
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
};
},
//重设排序规则
_sort: function(e) {
var column = e.target.cellIndex;
var data = this.state.data.slice();
var descending = this.state.sortby === column && !this.state.descending;
data.sort(function(a, b) {
return descending
? (a[column] < b[column] ? 1 : -1)
: (a[column] > b[column] ? 1 : -1);
});
this.setState({
data: data,
sortby: column,
descending: descending,
});
},
//沉浸时添加图标
render: function() {
return (
React.DOM.table(null,
React.DOM.thead({onClick: this._sort},
React.DOM.tr(null,
this.props.headers.map(function(title, idx) {
if (this.state.sortby === idx) {
title += this.state.descending ? ' \u2191' : ' \u2193'
}
return React.DOM.th({key: idx}, title);
}, this)
)
),
)
);
}
编辑表格
这个功能实现起来也非常简单,总的来说是就是:1、给单元格加点击事件,确定当前单元格;2、在当前位置添加text控件;3、回传新数据给state;4、删除text控件;
添加点击事件
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
edit: null, // [row index, cell index]
};
},
React.DOM.tbody({onDoubleClick: this._showEditor}, //点击事件
this.state.data.map(function(row, rowidx) {
return (
React.DOM.tr({key: rowidx},
row.map(function(cell, idx) {
var content = cell;
var edit = this.state.edit;
//因为是先设计了state,所以会自动重新render,这里会把当前位置添加一个input控件,用form来包裹,借助回车触发保存事件
if (edit && edit.row === rowidx && edit.cell === idx) {
//添加保存事件
content = React.DOM.form({onSubmit: this._save},
React.DOM.input({
type: 'text',
defaultValue: cell,
})
);
}
return React.DOM.td({
key: idx,
'data-row': rowidx,
}, content);
}, this)
)
);
}, this)
)
实现点击事件
_showEditor: function(e) {
this.setState({edit: { //edit为一个自定义属性,需要在 getInitialState()方法中先注册
row: parseInt(e.target.dataset.row, 10), //行索引坐标
cell: e.target.cellIndex, //列索引坐标
}});
},
添加保存事件
_save: function(e) {
e.preventDefault();
var input = e.target.firstChild; //取得输入框引用
var data = this.state.data.slice();
data[this.state.edit.row][this.state.edit.cell] = input.value; //改值
this.setState({ //重设数据,再render()
edit: null,
data: data,
});
},
表格搜索
添加搜索开关
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
edit: null, // [row index, cell index],
search: false, //搜索开关
};
},
因功能代码比较多,所以把原render方法,简单重构一下
render: function() {
return (
React.DOM.div(null,
this._renderToolbar(), //搜索工具栏
this._renderTable() //原表格实现,搜索框可放在这里实现
)
);
}
//声明搜索开关事件
_renderToolbar: function() {
return React.DOM.button(
{
onClick: this._toggleSearch,
className: 'toolbar',
},
'search'
);
},
//实现开关事件,因为事先把search设置成了一个state数据,所以此值的改变会自动调用render()方法
_preSearchData: null,
_toggleSearch: function() {
if (this.state.search) {
this.setState({
data: this._preSearchData, //在搜索前先复制一份之前的数据防止数据丢失
search: false,
});
this._preSearchData = null;
} else {
this._preSearchData = this.state.data;
this.setState({
search: true,
});
}
},
添加搜索输入框
_renderSearch: function() {
if (!this.state.search) {
return null;
}
return (
React.DOM.tr({onChange: this._search}, //搜索事件声明
this.props.headers.map(function(_ignore, idx) {
return React.DOM.td({key: idx},
React.DOM.input({
type: 'text',
'data-idx': idx,
})
);
})
)
);
},
然后在组件render()方法中添加,即多添加一个返回元素,即在this._renderTable()中添加以下代码。
this._renderSearch(),
实现搜索功能
注意,上述的搜索功能是放在了tr上面来实现的,这样的好处是方便表格定位
_search: function(e) {
var needle = e.target.value.toLowerCase();
if (!needle) { //当搜索字符删除时,渲染前一份数据
this.setState({data: this._preSearchData});
return;
}
//否则过滤当前数据
var idx = e.target.dataset.idx;
var searchdata = this._preSearchData.filter(function(row) {
return row[idx].toString().toLowerCase().indexOf(needle) > -1;
});
this.setState({data: searchdata});
},
数据下载
可下载为json或csv数据,统一做到工具栏中,这里用a
标签的原因是html5的新功能,如果链接为一文件则下载文件。
_renderToolbar: function() {
return React.DOM.div({className: 'toolbar'},
React.DOM.button({ //搜索按钮
onClick: this._toggleSearch,
}, 'Search'),
React.DOM.a({ //下载按钮
onClick: this._download.bind(this, 'json'),
href: 'data.json',
}, 'Export JSON'),
React.DOM.a({ //下载按钮
onClick: this._download.bind(this, 'csv'),
href: 'data.csv',
}, 'Export CSV')
);
}
功能实现
_download: function(format, ev) {
//组织数据
var contents = format === 'json'
? JSON.stringify(this.state.data)
: this.state.data.reduce(function(result, row) {
return result
+ row.reduce(function(rowresult, cell, idx) {
return rowresult
+ '"'
+ cell.replace(/"/g, '""')
+ '"'
+ (idx < row.length - 1 ? ',' : '');
}, '')
+ "\n";
}, '');
//数据封装
var blob = new Blob([contents], {type: 'text/' + format});
//重设<a>标签的属性实现下载
var URL = window.URL || window.webkitURL;
ev.target.href = URL.createObjectURL(blob);
ev.target.download = 'data.' + format;
},
下载完整代码
Table.js
<!DOCTYPE html>
<html>
<head>
<title>Table</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="03.00.table.css">
</head>
<body>
<div id="app">
<!-- my app renders here -->
</div>
<script src="react/build/react.js"></script>
<script src="react/build/react-dom.js"></script>
<script src="babel/browser.js"></script>
<script type="text/babel">
var Excel = React.createClass({
displayName: 'Excel',
propTypes: {
headers: React.PropTypes.arrayOf(
React.PropTypes.string
),
initialData: React.PropTypes.arrayOf(
React.PropTypes.arrayOf(
React.PropTypes.string
)
),
},
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
edit: null, // [row index, cell index],
search: false,
};
},
_sort: function(e) {
var column = e.target.cellIndex;
var data = this.state.data.slice();
var descending = this.state.sortby === column && !this.state.descending;
data.sort(function(a, b) {
return descending
? (a[column] < b[column] ? 1 : -1)
: (a[column] > b[column] ? 1 : -1);
});
this.setState({
data: data,
sortby: column,
descending: descending,
});
},
_showEditor: function(e) {
this.setState({edit: {
row: parseInt(e.target.dataset.row, 10),
cell: e.target.cellIndex,
}});
},
_save: function(e) {
e.preventDefault();
var input = e.target.firstChild;
var data = this.state.data.slice();
data[this.state.edit.row][this.state.edit.cell] = input.value;
this.setState({
edit: null,
data: data,
});
},
_preSearchData: null,
_toggleSearch: function() {
if (this.state.search) {
this.setState({
data: this._preSearchData,
search: false,
});
this._preSearchData = null;
} else {
this._preSearchData = this.state.data;
this.setState({
search: true,
});
}
},
_search: function(e) {
var needle = e.target.value.toLowerCase();
if (!needle) {
this.setState({data: this._preSearchData});
return;
}
var idx = e.target.dataset.idx;
var searchdata = this._preSearchData.filter(function(row) {
return row[idx].toString().toLowerCase().indexOf(needle) > -1;
});
this.setState({data: searchdata});
},
_download: function(format, ev) {
var contents = format === 'json'
? JSON.stringify(this.state.data)
: this.state.data.reduce(function(result, row) {
return result
+ row.reduce(function(rowresult, cell, idx) {
return rowresult
+ '"'
+ cell.replace(/"/g, '""')
+ '"'
+ (idx < row.length - 1 ? ',' : '');
}, '')
+ "\n";
}, '');
var URL = window.URL || window.webkitURL;
var blob = new Blob([contents], {type: 'text/' + format});
ev.target.href = URL.createObjectURL(blob);
ev.target.download = 'data.' + format;
},
render: function() {
return (
<div>
{this._renderToolbar()}
{this._renderTable()}
</div>
);
},
_renderToolbar: function() {
return (
<div className="toolbar">
<button onClick={this._toggleSearch}>Search</button>
<a onClick={this._download.bind(this, 'json')}
href="data.json">Export JSON</a>
<a onClick={this._download.bind(this, 'csv')}
href="data.csv">Export CSV</a>
</div>
);
},
_renderSearch: function() {
if (!this.state.search) {
return null;
}
return (
<tr onChange={this._search}>
{this.props.headers.map(function(_ignore, idx) {
return <td key={idx}><input type="text" data-idx={idx}/></td>;
})}
</tr>
);
},
_renderTable: function() {
return (
<table>
<thead onClick={this._sort}>
<tr>{
this.props.headers.map(function(title, idx) {
if (this.state.sortby === idx) {
title += this.state.descending ? ' \u2191' : ' \u2193';
}
return <th key={idx}>{title}</th>;
}, this)
}</tr>
</thead>
<tbody onDoubleClick={this._showEditor}>
{this._renderSearch()}
{this.state.data.map(function(row, rowidx) {
return (
<tr key={rowidx}>{
row.map(function(cell, idx) {
var content = cell;
var edit = this.state.edit;
if (edit && edit.row === rowidx && edit.cell === idx) {
content = (
<form onSubmit={this._save}>
<input type="text" defaultValue={cell} />
</form>
);
}
return <td key={idx} data-row={rowidx}>{content}</td>;
}, this)}
</tr>
);
}, this)}
</tbody>
</table>
);
}
});
var headers = [
"Book", "Author", "Language", "Published", "Sales"
];
var data = [
["The Lord of the Rings", "J. R. R. Tolkien", "English", "1954-1955", "150 million"],
["Le Petit Prince (The Little Prince)", "Antoine de Saint-Exupéry", "French", "1943", "140 million"],
["Harry Potter and the Philosopher's Stone", "J. K. Rowling", "English", "1997", "107 million"],
["And Then There Were None", "Agatha Christie", "English", "1939", "100 million"],
["Dream of the Red Chamber", "Cao Xueqin", "Chinese", "1754-1791", "100 million"],
["The Hobbit", "J. R. R. Tolkien", "English", "1937", "100 million"],
["She: A History of Adventure", "H. Rider Haggard", "English", "1887", "100 million"],
];
var Ex = ReactDOM.render(
React.createElement(Excel, {
headers: headers,
initialData: data
}),
document.getElementById("app")
);
</script>
</body>
</html>
table.css
html {
background: white;
font: 16px Arial;
}
input {
font: 16px Arial;
}
td {
border-top: 1px solid black;
cursor: cell;
padding: 5px;
}
th {
cursor: pointer;
padding: 5px;
}
table {
border: 1px solid black;
margin: 20px;
}
.toolbar {
margin-left: 20px;
}
.toolbar a, .toolbar button { /* thanks css3buttongenerator.com! */
background: #3498db;
background-image: linear-gradient(to bottom, #3498db, #2980b9);
border-radius: 28px;
box-shadow: 0px 1px 3px #666666;
color: #ffffff;
font-size: 14px;
padding: 10px 20px 10px 20px;
text-decoration: none;
border: 0;
margin-right: 5px;
}
.toolbar a:hover, .toolbar button:hover {
background: #3cb0fd;
background-image: linear-gradient(to bottom, #3cb0fd, #3498db);
text-decoration: none;
}