├── .gitignore
├── README.md
├── index.html
├── package.json
├── src
├── intent.js
├── intents
│ ├── column-sort.js
│ ├── container-resize.js
│ ├── filter-rows.js
│ ├── table-data.js
│ └── user-scroll.js
├── main.js
├── model.js
├── models
│ ├── make-column-widths.js
│ ├── make-filtered-rows.js
│ ├── make-sorted-rows.js
│ └── make-visible-indices.js
├── view.js
└── views
│ ├── filter-controls.js
│ ├── root.js
│ ├── row.js
│ ├── tbody.js
│ └── thead.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | target
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dynamic-width-filtering-sorting-scroll-table-with-data-loading
2 |
3 | This branch is in turn based off of https://github.com/justinwoo/react-rxjs-flow/tree/dynamic-width-scroll-table, and has more featuers and still ends up combining the multiple features together by combining relevant streams.
4 |
5 | ## Before
6 |
7 | ```js
8 | let columns$ = Rx.Observable.just(['ID (fixed width)', 'ID * 10', 'Random Number']);
9 | let defaultColumnWidths$ = Rx.Observable.just([300, null, null]);
10 |
11 | let tableHeight$ = Rx.Observable.just(500);
12 | let rowHeight$ = Rx.Observable.just(30);
13 | let rowCount$ = Rx.Observable.just(10000);
14 | let scrollTop$ = actions.scrollTop$.startWith(0);
15 |
16 | let visibleIndices$ = makeVisibleIndices$(
17 | tableHeight$, rowHeight$, rowCount$, scrollTop$
18 | );
19 |
20 | let containerWidth$ = actions.containerWidth$.startWith(window.innerWidth);
21 | let columnWidths$ = makeColumnWidths$(
22 | defaultColumnWidths$,
23 | containerWidth$
24 | );
25 | ```
26 |
27 | ## After
28 | ```js
29 | let columnSort$ = actions.columnSort$.startWith(defaultColumnSort);
30 | let filterEvenRows$ = actions.filterEvenRows$.startWith(false);
31 |
32 | // Actually load data in
33 | let tableData$ = actions.tableData$.startWith(defaultTableData);
34 | let columns$ = tableData$.map(data => data.columns);
35 | let defaultColumnWidths$ = tableData$.map(data => data.defaultColumnWidths);
36 |
37 | // Row data is mapped from the table data stream
38 | let rawRows$ = tableData$.map(data => data.rows);
39 |
40 | // Sort rows with information about how columns are sorted
41 | let sortedRows$ = makeSortedRows$(rawRows$, columnSort$);
42 |
43 | // Then we take that to filter the rows by criteria
44 | // In this case whether filtering by even-numbered ids or not
45 | let rows$ = makeFilteredRows$(sortedRows$, filterEvenRows$);
46 | let rowCount$ = rows$.map(rows => rows.length);
47 |
48 | let tableHeight$ = Rx.Observable.just(500);
49 | let rowHeight$ = Rx.Observable.just(30);
50 | let scrollTop$ = actions.scrollTop$.startWith(0);
51 |
52 | let visibleIndices$ = makeVisibleIndices$(
53 | tableHeight$, rowHeight$, rowCount$, scrollTop$
54 | );
55 |
56 | let containerWidth$ = actions.containerWidth$.startWith(window.innerWidth);
57 | let columnWidths$ = makeColumnWidths$(
58 | defaultColumnWidths$,
59 | containerWidth$
60 | );
61 | ```
62 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dynamic-width-scroll-table",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack -wdc --progress",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "private": true,
11 | "author": "Justin Woo",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "babel-core": "^5.6.15",
15 | "babel-loader": "^5.3.1",
16 | "node-libs-browser": "^0.5.2",
17 | "react": "^0.13.3",
18 | "rx": "^2.5.3",
19 | "webpack": "^1.10.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/intent.js:
--------------------------------------------------------------------------------
1 | import {scrollTop$} from './intents/user-scroll';
2 | import {containerWidth$} from './intents/container-resize';
3 | import {tableData$} from './intents/table-data';
4 | import {columnSort$} from './intents/column-sort';
5 | import {filterEvenRows$} from './intents/filter-rows';
6 |
7 | function intent() {
8 | let actions = {
9 | scrollTop$,
10 | containerWidth$,
11 | tableData$,
12 | columnSort$,
13 | filterEvenRows$
14 | };
15 | return actions;
16 | }
17 |
18 | export default intent;
19 |
--------------------------------------------------------------------------------
/src/intents/column-sort.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | let columnSortAction = new Rx.Subject();
4 |
5 | export let getHandleColumnSort = (currentColumn, sortDirection, newColumn) => {
6 | let newSortDirection;
7 | if (newColumn === currentColumn) {
8 | newSortDirection = sortDirection === 1 ? -1 : 1;
9 | } else {
10 | newSortDirection = 1;
11 | }
12 | return () => {
13 | columnSortAction.onNext({
14 | column: newColumn,
15 | sortDirection: newSortDirection
16 | });
17 | }
18 | };
19 |
20 | export let columnSort$ = columnSortAction;
21 |
--------------------------------------------------------------------------------
/src/intents/container-resize.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | let containerResizeAction = new Rx.Subject();
4 |
5 | export let setContainerWidth = value => containerResizeAction.onNext(value);
6 |
7 | export let containerWidth$ = containerResizeAction;
8 |
--------------------------------------------------------------------------------
/src/intents/filter-rows.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | let filterEvenRowsAction = new Rx.Subject();
4 |
5 | export let getHandleFilterEvens = (currentFilterEvens) => {
6 | return () => filterEvenRowsAction.onNext(currentFilterEvens ? false : true);
7 | }
8 |
9 | export let filterEvenRows$ = filterEvenRowsAction;
10 |
--------------------------------------------------------------------------------
/src/intents/table-data.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | let tableDataAction = new Rx.Subject();
4 |
5 | export let setTableData = value => tableDataAction.onNext(value);
6 |
7 | export let tableData$ = tableDataAction;
8 |
--------------------------------------------------------------------------------
/src/intents/user-scroll.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | let scrollAction = new Rx.Subject();
4 |
5 | export let handleOnScroll = e => scrollAction.onNext(e);
6 |
7 | export let scrollTop$ = scrollAction.map(
8 | e => e.target.scrollTop
9 | );
10 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import intent from './intent';
4 | import model from './model';
5 | import view from './view';
6 |
7 | function main() {
8 | let actions = intent();
9 | let state$ = model(actions);
10 | let output$ = view(state$);
11 |
12 | return output$;
13 | }
14 |
15 | let target = document.getElementById('app');
16 |
17 | main().subscribe(Output => React.render(Output, target));
18 |
19 | // load up fixture data
20 | let fixtureData = {
21 | columns: ['ID (fixed width)', 'ID * 10', 'Random Number'],
22 | defaultColumnWidths: [300, null, null],
23 | rows: []
24 | };
25 |
26 | for (let i = 0; i < 10000; i++) {
27 | fixtureData.rows.push([
28 | i,
29 | i * 10,
30 | Math.floor(Math.random() * 10000)
31 | ]);
32 | }
33 |
34 | import {tableData$} from './intents/table-data';
35 |
36 | tableData$.onNext(fixtureData);
37 |
--------------------------------------------------------------------------------
/src/model.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | import makeVisibleIndices$ from './models/make-visible-indices';
4 | import makeColumnWidths$ from './models/make-column-widths';
5 | import makeSortedRows$ from './models/make-sorted-rows';
6 | import makeFilteredRows$ from './models/make-filtered-rows';
7 |
8 | let defaultTableData = {
9 | columns: [],
10 | defaultColumnWidths: [],
11 | rows: []
12 | };
13 |
14 | let defaultColumnSort = {
15 | column: null,
16 | sortDirection: null
17 | };
18 |
19 | function model(actions) {
20 | let columnSort$ = actions.columnSort$.startWith(defaultColumnSort);
21 | let filterEvenRows$ = actions.filterEvenRows$.startWith(false);
22 |
23 | let tableData$ = actions.tableData$.startWith(defaultTableData);
24 | let columns$ = tableData$.map(data => data.columns);
25 | let defaultColumnWidths$ = tableData$.map(data => data.defaultColumnWidths);
26 | let rawRows$ = tableData$.map(data => data.rows);
27 |
28 | let sortedRows$ = makeSortedRows$(rawRows$, columnSort$);
29 |
30 | let rows$ = makeFilteredRows$(sortedRows$, filterEvenRows$);
31 | let rowCount$ = rows$.map(rows => rows.length);
32 |
33 | let tableHeight$ = Rx.Observable.just(500);
34 | let rowHeight$ = Rx.Observable.just(30);
35 | let scrollTop$ = actions.scrollTop$.startWith(0);
36 |
37 | let visibleIndices$ = makeVisibleIndices$(
38 | tableHeight$, rowHeight$, rowCount$, scrollTop$
39 | );
40 |
41 | let containerWidth$ = actions.containerWidth$.startWith(window.innerWidth);
42 | let columnWidths$ = makeColumnWidths$(
43 | defaultColumnWidths$,
44 | containerWidth$
45 | );
46 |
47 | return Rx.Observable.combineLatest(
48 | tableHeight$,
49 | rowHeight$,
50 | columns$,
51 | rowCount$,
52 | visibleIndices$,
53 | columnWidths$,
54 | rows$,
55 | columnSort$,
56 | filterEvenRows$,
57 | (
58 | tableHeight,
59 | rowHeight,
60 | columns,
61 | rowCount,
62 | visibleIndices,
63 | columnWidths,
64 | rows,
65 | columnSort,
66 | filterEvenRows
67 | ) => ({
68 | tableHeight,
69 | rowHeight,
70 | columns,
71 | rowCount,
72 | visibleIndices,
73 | columnWidths,
74 | rows,
75 | columnSort,
76 | filterEvenRows
77 | })
78 | );
79 | }
80 |
81 | export default model;
82 |
--------------------------------------------------------------------------------
/src/models/make-column-widths.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | function makeColumnWidths$(defaultColumnWidths$, containerWidth$) {
4 | return Rx.Observable.combineLatest(
5 | defaultColumnWidths$, containerWidth$,
6 | (defaultColumnWidths, containerWidth) => {
7 | let computation = defaultColumnWidths.reduce(function (agg, width) {
8 | if (typeof width === 'number') {
9 | agg.remainingWidth -= width;
10 | agg.autoSizeColumns -= 1;
11 | }
12 | return agg;
13 | }, {
14 | autoSizeColumns: defaultColumnWidths.length,
15 | remainingWidth: containerWidth
16 | });
17 |
18 | let standardWidth = computation.remainingWidth / computation.autoSizeColumns;
19 |
20 | return defaultColumnWidths.map(function (width) {
21 | if (width) {
22 | return width;
23 | } else {
24 | return standardWidth;
25 | }
26 | });
27 | }
28 | );
29 | }
30 |
31 | export default makeColumnWidths$;
32 |
--------------------------------------------------------------------------------
/src/models/make-filtered-rows.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | function makeFilteredRows$(rows$, filterEvenRows$) {
4 | return Rx.Observable.combineLatest(
5 | rows$,
6 | filterEvenRows$,
7 | (rows, filterEvenRows) => {
8 | if (filterEvenRows) {
9 | return rows.reduce((agg, row) => {
10 | if (row[0] % 2 !== 0) {
11 | agg.push(row);
12 | }
13 | return agg;
14 | }, []);
15 | } else {
16 | return rows;
17 | }
18 | }
19 | );
20 | }
21 |
22 | export default makeFilteredRows$;
23 |
--------------------------------------------------------------------------------
/src/models/make-sorted-rows.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | function makeSortedRows(rawRows$, columnSort$) {
4 | return Rx.Observable.combineLatest(
5 | rawRows$,
6 | columnSort$,
7 | (rawRows, columnSort) => {
8 | if (columnSort.column === null) {
9 | return rawRows;
10 | } else {
11 | let {sortDirection, column} = columnSort;
12 | let newRows = rawRows.map(x => x);
13 | newRows.sort((left, right) => {
14 | let a = left[column];
15 | let b = right[column];
16 | let sortResult;
17 | if (a < b) {
18 | sortResult = -1;
19 | } else if (a > b) {
20 | sortResult = 1;
21 | } else {
22 | sortResult = 0;
23 | }
24 | return sortDirection === 1 ? sortResult : -1 * sortResult;
25 | });
26 |
27 | return newRows;
28 | }
29 | }
30 | );
31 | }
32 |
33 | export default makeSortedRows;
34 |
--------------------------------------------------------------------------------
/src/models/make-visible-indices.js:
--------------------------------------------------------------------------------
1 | import Rx from 'rx';
2 |
3 | function makeVisibleIndices$(tableHeight$, rowHeight$, rowCount$, scrollTop$) {
4 | let firstVisibleRow$ = Rx.Observable.combineLatest(scrollTop$, rowHeight$,
5 | (scrollTop, rowHeight) => Math.floor(scrollTop / rowHeight)
6 | ).distinctUntilChanged();
7 |
8 | let visibleRows$ = Rx.Observable.combineLatest(tableHeight$, rowHeight$,
9 | (tableHeight, rowHeight) => Math.ceil(tableHeight / rowHeight)
10 | );
11 |
12 | let visibleIndices$ = Rx.Observable.combineLatest(
13 | rowCount$, visibleRows$, firstVisibleRow$,
14 | (rowCount, visibleRows, firstVisibleRow) => {
15 | let visibleIndices = [];
16 | let lastRow = firstVisibleRow + visibleRows + 1;
17 |
18 | if (lastRow > rowCount) {
19 | firstVisibleRow -= lastRow - rowCount;
20 | }
21 |
22 | for (let i = 0; i <= visibleRows; i++) {
23 | visibleIndices.push(i + firstVisibleRow);
24 | }
25 | return visibleIndices;
26 | }
27 | );
28 |
29 | return visibleIndices$;
30 | }
31 |
32 | export default makeVisibleIndices$;
33 |
--------------------------------------------------------------------------------
/src/view.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Root from './views/root';
4 |
5 | function view(state$) {
6 | return state$.map(state => );
7 | }
8 |
9 | export default view;
10 |
--------------------------------------------------------------------------------
/src/views/filter-controls.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PureRenderMixin from 'react/lib/ReactComponentWithPureRenderMixin';
3 |
4 | import {
5 | getHandleFilterEvens
6 | } from '../intents/filter-rows';
7 |
8 | let FilterControls = React.createClass({
9 | mixins: [PureRenderMixin],
10 |
11 | render: function () {
12 | let {
13 | filterEvenRows
14 | } = this.props;
15 |
16 | return (
17 |
18 |
24 |
25 | );
26 | }
27 | });
28 |
29 | export default FilterControls;
30 |
--------------------------------------------------------------------------------
/src/views/root.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {handleOnScroll} from '../intents/user-scroll';
4 | import {setContainerWidth} from '../intents/container-resize';
5 |
6 | import THead from './thead';
7 | import TBody from './tbody';
8 | import FilterControls from './filter-controls';
9 |
10 | let Root = React.createClass({
11 | render: function () {
12 | let {
13 | tableHeight,
14 | rowHeight,
15 | columns,
16 | rowCount,
17 | visibleIndices,
18 | columnWidths,
19 | rows,
20 | columnSort,
21 | filterEvenRows
22 | } = this.props;
23 |
24 | let staticHeaderTableStyle = {
25 | overflowX: 'hidden',
26 | borderBottom: '1px solid black',
27 | width: '100%'
28 | };
29 |
30 | let scrollTableContainerStyle = {
31 | position: 'relative',
32 | overflowX: 'hidden',
33 | borderBottom: '1px solid black',
34 | height: tableHeight + 'px',
35 | };
36 |
37 | return (
38 |
53 | );
54 | },
55 |
56 | componentDidMount: function () {
57 | let scrollTableContainerNode = React.findDOMNode(this.refs.ScrollTableContainer);
58 | let getScrollTableContainerNodeWidth = () => scrollTableContainerNode.offsetWidth;
59 |
60 | Rx.Observable.fromEvent(window, 'resize')
61 | .map(getScrollTableContainerNodeWidth)
62 | .debounce(50)
63 | .distinctUntilChanged()
64 | .startWith(getScrollTableContainerNodeWidth())
65 | .subscribe(value => setContainerWidth(value));
66 | }
67 | });
68 |
69 | export default Root;
70 |
--------------------------------------------------------------------------------
/src/views/row.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PureRenderMixin from 'react/lib/ReactComponentWithPureRenderMixin';
3 |
4 | let Row = React.createClass({
5 | mixins: [PureRenderMixin],
6 |
7 | render: function () {
8 | let {
9 | index,
10 | rowHeight,
11 | columnWidths,
12 | row
13 | } = this.props;
14 |
15 | let top = index * rowHeight;
16 | let trStyle = {
17 | position: 'absolute',
18 | top: top + 'px',
19 | width: '100%',
20 | borderBottom: '1px solid grey'
21 | };
22 |
23 | let nodes;
24 | if (row === undefined) {
25 | nodes = null;
26 | } else {
27 | nodes = columnWidths.map((width, i) => {
28 | return (
29 |
30 | {String(row[i])}
31 | |
32 | );
33 | });
34 | }
35 |
36 | return (
37 |
38 | {nodes}
39 |
40 | );
41 | }
42 | });
43 |
44 | export default Row;
45 |
--------------------------------------------------------------------------------
/src/views/tbody.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PureRenderMixin from 'react/lib/ReactComponentWithPureRenderMixin';
3 |
4 | import Row from './row';
5 |
6 | let TBody = React.createClass({
7 | mixins: [PureRenderMixin],
8 |
9 | render: function () {
10 | let {
11 | rowHeight,
12 | visibleIndices,
13 | columnWidths,
14 | rows
15 | } = this.props;
16 |
17 | let nodes = visibleIndices.map((index, i) => {
18 | let rowProps = {
19 | index,
20 | rowHeight,
21 | columnWidths,
22 | row: rows[index]
23 | };
24 | let renderKey = index % visibleIndices.length;
25 | return (
26 |
30 | );
31 | });
32 |
33 | return (
34 |
35 | {nodes}
36 |
37 | );
38 | }
39 | });
40 |
41 | export default TBody;
42 |
--------------------------------------------------------------------------------
/src/views/thead.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PureRenderMixin from 'react/lib/ReactComponentWithPureRenderMixin';
3 |
4 | import {getHandleColumnSort} from '../intents/column-sort';
5 |
6 | let THead = React.createClass({
7 | mixins: [PureRenderMixin],
8 |
9 | render: function () {
10 | let {
11 | columns,
12 | columnWidths,
13 | columnSort
14 | } = this.props;
15 |
16 | let {
17 | column: currentColumn,
18 | sortDirection
19 | } = columnSort;
20 |
21 | let nodes = columns.map((value, i) => {
22 | let thStyle = {width: columnWidths[i] + 'px', cursor: 'pointer'};
23 | let sortArrow = '';
24 | if (currentColumn === i) {
25 | if (sortDirection === 1) {
26 | sortArrow = '↑';
27 | } else {
28 | sortArrow = '↓';
29 | }
30 | }
31 | return (
32 |
34 | {value + ' ' + sortArrow}
35 | |
36 | );
37 | });
38 | return (
39 |
40 |
41 | {nodes}
42 |
43 |
44 | );
45 | }
46 | });
47 |
48 | export default THead;
49 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 |
5 | entry: [
6 | './src/main'
7 | ],
8 |
9 | output: {
10 | path: path.join(__dirname, 'target'),
11 | filename: 'main.js'
12 | },
13 |
14 | module: {
15 | loaders: [{
16 | test: function (filename) {
17 | if (filename.indexOf('node_modules') !== -1) {
18 | return false;
19 | } else {
20 | return /\.js$/.test(filename) !== -1;
21 | }
22 | },
23 | loaders: ['babel-loader']
24 | }]
25 | },
26 |
27 | resolve: {
28 | modulesDirectories: [path.join(__dirname, 'src'), 'node_modules']
29 | }
30 |
31 | };
32 |
--------------------------------------------------------------------------------