Created
July 13, 2015 12:31
-
-
Save bennadel/de29e3ab36c54add21b7 to your computer and use it in GitHub Desktop.
Rendering Large Datasets With AngularJS And ReactJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html ng-app="Demo"> | |
<head> | |
<meta charset="utf-8" /> | |
<title> | |
Rendering Large Datasets With AngularJS | |
</title> | |
<link rel="stylesheet" type="text/css" href="./demo.css"></link> | |
</head> | |
<body ng-controller="AppController as vm"> | |
<h1> | |
Rendering Large Datasets With AngularJS | |
</h1> | |
<form> | |
<strong>Filter Data</strong>: | |
<input type="text" ng-model="vm.form.filter" /> | |
<!-- | |
If the user is filtering the data, we want to offer some insight into | |
the breadth of the filtering. | |
--> | |
<span ng-if="vm.form.filter"> | |
— | |
Filtering <strong>{{ vm.form.filter }}</strong> | |
over {{ vm.dataPoints }} data points, | |
{{ vm.visibleCount }} found. | |
</span> | |
<!-- Provide tooling to unmount and remount the grid. --> | |
<a ng-if="vm.grid.length" ng-click="vm.unmountGrid()">Unmount Grid</a> | |
<a ng-if="! vm.grid.length" ng-click="vm.remountGrid()">Remount Grid</a> | |
</form> | |
<table width="100%" cellspacing="2" ng-class="{ filtered: vm.form.filter }"> | |
<tr ng-repeat="row in vm.grid track by row.id"> | |
<td> | |
{{ row.id }} | |
</td> | |
<td | |
ng-repeat="item in row.items track by item.id" | |
class="item" | |
ng-class="{ hidden: item.isHiddenByFilter }"> | |
{{ item.value }} | |
</td> | |
</tr> | |
</table> | |
<!-- Load scripts. --> | |
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.min.js"></script> | |
<script type="text/javascript"> | |
// Create an application module for our demo. | |
angular.module( "Demo", [] ); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// I control the root of the application. | |
angular.module( "Demo" ).controller( | |
"AppController", | |
function provideAppController( $scope ) { | |
var vm = this; | |
// We'll start out with a grid with 10,000 items. | |
vm.grid = generateGrid( 1000, 10 ); | |
// Calculate the number of data-points that may have filtering. | |
vm.dataPoints = ( vm.grid.length * vm.grid[ 0 ].items.length ); | |
// I hold the form data for use with ngModel. | |
vm.form = { | |
filter: "" | |
}; | |
// I hold the number of items that are visible based on filtering. | |
vm.visibleCount = 0; | |
// As the user interacts with filter, we need to update the view-model | |
// to reflect the matching items. | |
$scope.$watch( "vm.form.filter", handleFilterChange ); | |
// Expose the public API. | |
vm.remountGrid = remountGrid; | |
vm.unmountGrid = unmountGrid; | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I update the visibility of the items when the filter is updated. | |
function handleFilterChange( newValue, oldValue ) { | |
if ( newValue === oldValue ) { | |
return; | |
} | |
// Reset the visible count. As we iterate of the items checking | |
// for visibility, we can increment this count as necessary. | |
vm.visibleCount = 0; | |
for ( var r = 0, rowCount = vm.grid.length ; r < rowCount ; r++ ) { | |
var row = vm.grid[ r ]; | |
for ( var c = 0, columnCount = row.items.length ; c < columnCount ; c++ ) { | |
var item = row.items[ c ]; | |
// The item is hidden if the given filter text cannot be | |
// found in the value of the item. | |
item.isHiddenByFilter = ( newValue && ( item.value.indexOf( newValue ) === -1 ) ); | |
// If the item isn't hidden, track it as part of the visible | |
// set of data. | |
if ( ! item.isHiddenByFilter ) { | |
vm.visibleCount++; | |
} | |
} | |
} | |
} | |
// I repopulate the grid with data. This will help separate processing | |
// performance characteristics from page-load processing. | |
function remountGrid() { | |
vm.grid = generateGrid( 1000, 10 ); | |
vm.dataPoints = ( vm.grid.length * vm.grid[ 0 ].items.length ); | |
vm.visibleCount = 0; | |
vm.form.filter = ""; | |
} | |
// I clear the grid of data. This will help separate processing | |
// performance characteristics from page-load processing. | |
function unmountGrid() { | |
vm.grid = []; | |
vm.dataPoints = 0; | |
vm.visibleCount = 0; | |
vm.form.filter = ""; | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I generate a grid of items with the given dimensions. The grid is | |
// represented as a two dimensional grid, of sorts. Each row has an | |
// object that has an items collection. | |
function generateGrid( rowCount, columnCount ) { | |
var valuePoints = [ | |
"Daenerys", "Jon", "Sansa", "Arya", "Stannis", "Gregor", "Tyrion", | |
"Theon", "Joffrey", "Ramsay", "Cersei", "Bran", "Margaery", | |
"Melisandre", "Daario", "Jamie", "Eddard", "Myrcella", "Robb", | |
"Jorah", "Petyr", "Tommen", "Sandor", "Oberyn", "Drogo", "Ygritte" | |
]; | |
var valueIndex = 0; | |
var grid = []; | |
for ( var r = 0 ; r < rowCount ; r++ ) { | |
var row = { | |
id: r, | |
items: [] | |
}; | |
for ( var c = 0 ; c < columnCount ; c++ ) { | |
row.items.push({ | |
id: ( r + "-" + c ), | |
value: valuePoints[ valueIndex ], | |
isHiddenByFilter: false | |
}); | |
if ( ++valueIndex >= valuePoints.length ) { | |
valueIndex = 0; | |
} | |
} | |
grid.push( row ); | |
} | |
return( grid ); | |
} | |
} | |
); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title> | |
Rendering Large Datasets With ReactJS | |
</title> | |
<link rel="stylesheet" type="text/css" href="./demo.css"></link> | |
</head> | |
<body> | |
<h1> | |
Rendering Large Datasets With ReactJS | |
</h1> | |
<div id="content"> | |
<!-- This content will be replaced with the React rendering. --> | |
</div> | |
<!-- Load scripts. --> | |
<script src="../../vendor/reactjs/react-0.13.3.min.js"></script> | |
<script src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script> | |
<script type="text/jsx"> | |
// I manage the Demo widget. | |
var Demo = React.createClass({ | |
// I provide the initial view-model, before the component is mounted. | |
getInitialState: function() { | |
// We'll start out with a grid with 10,000 items. | |
return({ | |
grid: this.generateGrid( 1000, 10 ), | |
form: { | |
filter: "" | |
} | |
}); | |
}, | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I render the view using the current state and properties collections. | |
render: function() { | |
if ( this.state.grid.length ) { | |
var dataPoints = ( this.state.grid.length * this.state.grid[ 0 ].items.length ); | |
} else { | |
var dataPoints = 0; | |
} | |
var visibleCount = this.getVisibleCount(); | |
return( | |
<div> | |
<DemoForm | |
dataPoints={ dataPoints } | |
visibleCount={ visibleCount } | |
filter={ this.state.form.filter } | |
onFilterChange={ this.setFilter } | |
isMounted={ !! this.state.grid.length } | |
onUnmount={ this.unmountGrid } | |
onRemount={ this.remountGrid } | |
/> | |
<DemoTable | |
grid={ this.state.grid } | |
filter={ this.state.form.filter } | |
/> | |
</div> | |
); | |
}, | |
// I repopulate the grid with data. This will help separate processing | |
// performance characteristics from page-load processing. | |
remountGrid: function() { | |
this.setState({ | |
grid: this.generateGrid( 1000, 10 ), | |
form: { | |
filter: "" | |
} | |
}); | |
}, | |
// I update the state for filtering. | |
setFilter: function( newFilter ) { | |
// When we update the filter, we don't have to mutate any other state | |
// since the filtering is actually applied in the render() methods. | |
this.setState({ | |
form: { | |
filter: newFilter | |
} | |
}); | |
}, | |
// I clear the grid of data. This will help separate processing performance | |
// characteristics from page-load processing. | |
unmountGrid: function() { | |
this.setState({ | |
grid: [], | |
form: { | |
filter: "" | |
} | |
}); | |
}, | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I calculate and return the visible count of items based on the current | |
// state of the filtering. | |
getVisibleCount: function() { | |
var count = 0; | |
for ( var r = 0, rowCount = this.state.grid.length ; r < rowCount ; r++ ) { | |
var row = this.state.grid[ r ]; | |
for ( var c = 0, columnCount = row.items.length ; c < columnCount ; c++ ) { | |
var item = row.items[ c ]; | |
var isHidden = ( this.state.form.filter && ( item.value.indexOf( this.state.form.filter ) === -1 ) ); | |
if ( ! isHidden ) { | |
count++; | |
} | |
} | |
} | |
return( count ); | |
}, | |
// I generate a grid of items with the given dimensions. The grid is | |
// represented as a two dimensional grid, of sorts. Each row has an object | |
// that has an items collection. | |
generateGrid: function( rowCount, columnCount ) { | |
var valuePoints = [ | |
"Daenerys", "Jon", "Sansa", "Arya", "Stannis", "Gregor", "Tyrion", | |
"Theon", "Joffrey", "Ramsay", "Cersei", "Bran", "Margaery", | |
"Melisandre", "Daario", "Jamie", "Eddard", "Myrcella", "Robb", | |
"Jorah", "Petyr", "Tommen", "Sandor", "Oberyn", "Drogo", "Ygritte" | |
]; | |
var valueIndex = 0; | |
var grid = []; | |
for ( var r = 0 ; r < rowCount ; r++ ) { | |
var row = { | |
id: r, | |
items: [] | |
}; | |
for ( var c = 0 ; c < columnCount ; c++ ) { | |
row.items.push({ | |
id: ( r + "-" + c ), | |
value: valuePoints[ valueIndex ], | |
isHiddenByFilter: false | |
}); | |
if ( ++valueIndex >= valuePoints.length ) { | |
valueIndex = 0; | |
} | |
} | |
grid.push( row ); | |
} | |
return( grid ); | |
} | |
}); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// I manage the Form widget. | |
var DemoForm = React.createClass({ | |
// I handle user-based changes on the input form. When the user updates the | |
// filtering, we need to let the calling context know about it. | |
handleFilterChange: function( event ) { | |
this.props.onFilterChange( this.refs.filter.getDOMNode().value ); | |
}, | |
// I handle the user's desire to remount the data. | |
handleRemount: function( event ) { | |
this.props.onRemount(); | |
}, | |
// I handle the user's desire to unmount the data. | |
handleUnmount: function( event ) { | |
this.props.onUnmount(); | |
}, | |
// I render the view using the current state and properties collections. | |
render: function() { | |
var fitlerInsight = null; | |
// If the user has entered filter text, we want to show some insight into | |
// the breadth of the filtering. | |
// -- | |
// CAUTION: We have to have these awkward and explicit spaces { " " } | |
// because the JSX strips out certain pieces of whitespace, leaving | |
// the input butted-up against the label. | |
if ( this.props.filter ) { | |
fitlerInsight = ( | |
<span> | |
— | |
Filtering <strong>{ this.props.filter }</strong> | |
{ " " } over { this.props.dataPoints } data points, | |
{ " " } { this.props.visibleCount } found. | |
</span> | |
); | |
} | |
// Provide some tooling to unmount and remount the data. | |
if ( this.props.isMounted ) { | |
var mountAction = <a onClick={ this.handleUnmount }>Unmount Grid</a>; | |
} else { | |
var mountAction = <a onClick={ this.handleRemount }>Remount Grid</a>; | |
} | |
// CAUTION: We have to have these awkward and explicit spaces { " " } | |
// because the JSX strips out certain pieces of whitespace, leaving | |
// the input butted-up against the label. | |
return( | |
<form> | |
<strong>Filter Data</strong>: | |
{ " " } | |
<input | |
type="text" | |
ref="filter" | |
value={ this.props.filter } | |
onChange={ this.handleFilterChange } | |
/> | |
{ " " } | |
{ fitlerInsight } | |
{ " " } | |
{ mountAction } | |
</form> | |
); | |
} | |
}); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// I manage the Table widget. | |
var DemoTable = React.createClass({ | |
// I render the view using the current state and properties collections. | |
render: function() { | |
// If the table is being filtered, we want to add a class to the table to | |
// set a default style for all the non-hidden elements. | |
var tableClasses = this.props.filter | |
? "filtered" | |
: null | |
; | |
// Creating a local reference so we don't have to .bind() the iterator. | |
var filter = this.props.filter; | |
// Translate the grid into a collection of rows. | |
var rows = this.props.grid.map( | |
function transformRow( row ) { | |
return( | |
<DemoTableRow | |
key={ row.id } | |
row={ row } | |
filter={ filter } | |
/> | |
); | |
} | |
); | |
return( | |
<table width="100%" cellSpacing="2" className={ tableClasses }> | |
<tbody> | |
{ rows } | |
</tbody> | |
</table> | |
); | |
} | |
}); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// I manage the Table rows. | |
var DemoTableRow = React.createClass({ | |
// I render the view using the current state and properties collections. | |
render: function() { | |
var columns = [ | |
<td> | |
{ this.props.row.id } | |
</td> | |
]; | |
// Creating a local reference so we don't have to .bind() the iterator. | |
var filter = this.props.filter; | |
// Translate each item into a TD element. If there is filtering being | |
// applied, some of the TD elements will have the "hidden" class. | |
this.props.row.items.forEach( | |
function transformItem( item ) { | |
var classes = "item"; | |
if ( filter && ( item.value.indexOf( filter ) === -1 ) ) { | |
classes += " hidden"; | |
} | |
columns.push( | |
<td key={ item.id } className={ classes }> | |
{ item.value } | |
</td> | |
); | |
} | |
); | |
return( | |
<tr> | |
{ columns } | |
</tr> | |
); | |
} | |
}); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// Render the root Demo and mount it inside the given element. | |
React.render( <Demo />, document.getElementById( "content" ) ); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment