Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created July 13, 2015 12:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/de29e3ab36c54add21b7 to your computer and use it in GitHub Desktop.
Save bennadel/de29e3ab36c54add21b7 to your computer and use it in GitHub Desktop.
Rendering Large Datasets With AngularJS And ReactJS
<!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">
&mdash;
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>
<!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>
&mdash;
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