<!DOCTYPE html>
<html>
<head>
<script data-require="react@*" data-semver="0.12.2" src="http://fb.me/JSXTransformer-0.12.2.js"></script>
<script data-require="react@*" data-semver="0.12.2" src="http://fb.me/react-0.12.2.js"></script>
<script data-require="react@*" data-semver="0.12.2" src="http://fb.me/react-with-addons-0.12.2.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="main"></div>
<script src="Dispatcher.js"></script>
<script src="filterSetRadio.js" type="text/jsx"></script>
<script src="filterSetCheckbox.js" type="text/jsx"></script>
<script src="tab.js" type="text/jsx"></script>
<script src="tabSet.js" type="text/jsx"></script>
<script src="resultList.js" type="text/jsx"></script>
<script src="app.js" type="text/jsx"></script>
</body>
</html>
/* Styles go here */
body {
font-family: sans-serif;
font-size: 12px;
}
.filters {
float: left;
padding: 10px;
box-sizing: border-box;
width: 30%;
}
.results {
float: right;
padding: 10px;
box-sizing: border-box;
width: 65%;
}
.select-all span.button {
display: inline-block;
padding: 0 5px;
border: 1px solid #000;
border-radius: 5px;
}
.select-all span.button.active {
background: green;
}
.select-all span.state {
padding: 0 5px;
border: 0;
}
.tabset nav {
display: block;
}
.tabset nav ul,
ul.filterset {
list-style: none;
margin: 0;
padding: 0;
}
.tabset nav li {
float: left;
display: inline-block;
padding: 10px;
cursor: pointer;
}
.tabset nav li {
background: #ddd;
margin-left: 1px;
}
.tabset nav li:first-child {
margin-left: 0;
}
.tabset nav li.active {
background: #eee;
}
.tab {
display: none;
clear: both;
padding: 10px;
background: #efefef;
}
.tab.active {
display:block;
}
'use strict';
var Store = {};
/**
* Utility responsible for triggering the special/custom events from within the components.
* With this utility you are able to register, unregister and trigger custom event handlers.
*
* @class Dispatcher
* @public
* @version 1.0
* @author Ricardo Machado <rmachado@travix.com>
* @example
* var fnHandler = function( data, store ){
* console.log('Storing my data in the store');
* store.myData = data;
* };
* var myObject = {
* foo: bar
* };
*
* Dispatcher.register('myCustomEvent', fnHandler);
* Dispatcher.unregister('myCustomEvent', fnHandler);
* Dispatcher.trigger('myCustomEvent', myObject);
*/
var Dispatcher = {
/**
* @property {Object} _eventHandlers Private property to track the event handlers registered
* @private
*/
_eventHandlers: {},
/**
* Registers a handler for a given event name
*
* @method register
* @param {String} eventName Name of the event that will trigger the function
* @param {Function} handlerFn Function to execute when triggering the event
* @return {void}
* @public
*/
register: function( eventName, handlerFn ){
if( (typeof(eventName) === 'string') && (typeof(handlerFn) === 'function') && ('call' in handlerFn) ){
if( !(eventName in this._eventHandlers) ) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push( handlerFn );
}
},
/**
* Un-registers a handler for a given event name
*
* @method unregister
* @param {String} eventName Name of the event used to register the function
* @param {Function} handlerFn Same function used on the register
* @return {void}
* @public
*/
unregister: function( eventName, handlerFn ){
if( (typeof(eventName) === 'string') && (typeof(handlerFn) === 'function') && ('call' in handlerFn) ){
if( (eventName in this._eventHandlers) ) {
var index;
while( (index = this._eventHandlers[eventName].indexOf(handlerFn)) !== -1 ){
this._eventHandlers[eventName].splice(index,1);
}
if( !this._eventHandlers[eventName].length ){
delete this._eventHandlers[eventName];
}
}
}
},
/**
* Triggers the functions registered for a specific event.
*
* @method dispatch
* @param {String} eventName Name of the event that you want to run the functions from.
* @param {Object|undefined} [err] Optional. In case there's an error, this parameter contains the error object.
* @param {Object|undefined} [data] Optional. Data related to the event.
* @return {void}
* @public
*/
dispatch: function( eventName, err, data ){
if( (typeof(eventName) === 'string') && (eventName in this._eventHandlers) ){
this._eventHandlers[eventName].forEach( function(fn){
fn( err, data, Store );
});
}
}
};
/** @jsx React.createElement */
var App = React.createClass({
getDefaultProps: function() {
},
filters: [[{
label: 'blue',
criteria: {
value: 'blue'
}
}, {
label: 'Red',
criteria: {
value: 'red'
}
}, {
label: 'Green',
criteria: {
value: 'green'
}
}, {
label: 'Purple',
criteria: {
value: 'purple'
}
}],
[{
label: 'S',
criteria: {
value: 's'
}
}, {
label: 'M',
criteria: {
value: 'm'
}
}, {
label: 'L',
criteria: {
value: 'l'
}
}, {
label: 'XL',
criteria: {
value: 'xl'
}
}],
[{
label: 'All',
criteria: {
value: '-1',
comparator: 'greaterThan'
}
}, {
label: '>10',
criteria: {
value: '10',
comparator: 'greaterThan'
}
}, {
label: '>50',
criteria: {
value: '50',
comparator: 'greaterThan'
}
}, {
label: '<50',
criteria: {
value: '50',
comparator: 'lessThan'
}
}, {
label: '<25',
criteria: {
value: '25',
comparator: 'lessThan'
}
}]],
results: [{
color: 'red',
size: 'm',
age: 10
}, {
color: 'blue',
size: 's',
age: 18
}, {
color: 'green',
size: 'l',
age: 40
}, {
color: 'yellow',
size: 'xl',
age: 23
}, {
color: 'purple',
size: 's',
age: 64
}],
getInitialState: function() {
return {
filterCriteria: {},
results: this.results
};
},
componentDidMount: function () {
Dispatcher.register('filter1:change', this.handleFilterChange);
Dispatcher.register('filter2:change', this.handleFilterChange);
Dispatcher.register('filter3:change', this.handleFilterChange);
},
componentWillUnmount: function() {
Dispatcher.unregister('filter1:change', this.handleFilterChange);
Dispatcher.unregister('filter2:change', this.handleFilterChange);
Dispatcher.unregister('filter3:change', this.handleFilterChange);
},
handleFilterChange: function(filterKey, filters) {
var filterCriteria = this.state.filterCriteria;
filterCriteria[filterKey] = filters;
// Filter results, AND comparator per filter set, OR comparator for filter item
var filteredResults = this.results.filter(function(item, index) {
var drop = true;
for (var key in item) {
if (filterCriteria.hasOwnProperty(key) && filterCriteria[key].length > 0) {
var keep = false;
for (var i in filterCriteria[key]) {
switch (filterCriteria[key][i].criteria.comparator) {
case 'greaterThan':
keep = keep || item[key] > parseInt(filterCriteria[key][i].criteria.value);
break;
case 'lessThan':
keep = keep || item[key] < parseInt(filterCriteria[key][i].criteria.value);
break;
default:
keep = keep || item[key] === filterCriteria[key][i].criteria.value;
break;
}
}
drop = drop && keep;
}
}
return drop;
});
this.setState({
filterCriteria: filterCriteria,
results: filteredResults
});
},
handleReset: function() {
Dispatcher.dispatch('filter1:reset');
Dispatcher.dispatch('filter2:reset');
Dispatcher.dispatch('filter3:reset');
this.setState({
filterCriteria: [],
results: this.results
});
},
render: function() {
return (
<div>
<div className="filters">
<TabSet>
<Tab title="Tab 1">
<h2>Tab 1</h2>
<h3>Filter by color</h3>
<FilterSetCheckbox title="Filter set 1"
name="color"
eventName="filter1"
filters={this.filters[0]} />
<h3>Filter by size</h3>
<FilterSetCheckbox title="Filter set 2"
name="size"
eventName="filter2"
filters={this.filters[1]} />
</Tab>
<Tab title="Tab 2">
<h2>Tab 2</h2>
<h3>Filter by age</h3>
<FilterSetRadio title="Filter set 3"
eventName="filter3"
name="age"
filters={this.filters[2]}
defaultChecked="All" />
</Tab>
</TabSet>
</div>
<div className="results">
<button type="button" onClick={this.handleReset}>Reset all filter</button>
<ResultList list={this.state.results} />
</div>
</div>
);
}
});
React.render(<App />, document.getElementById('main'));
/** @jsx React.createElement */
var TabSet = React.createClass({
getDefaultProps: function() {
return {};
},
getInitialState: function() {
return {
selectedTab: 0
};
},
handleTabClick: function(index) {
this.setState({
selectedTab: index
});
},
render: function() {
var items = [];
if(this.props.children && ('length' in this.props.children)) {
items = this.props.children.filter(function(item) {
return item.type.displayName;
});
} else if (this.props.children && (this.props.children.type.displayName === 'Tab')) {
items = [ this.props.children ];
}
return (
<div className="tabset">
<nav>
<ul>
{items.map(function(item, index) {
var className = (this.state.selectedTab === index) ? 'active' : '';
return <li key={'tabLink_' + index}
className={className}
onClick={this.handleTabClick.bind(this, index)}>
{item.props.title}
</li>;
}.bind(this))}
</ul>
</nav>
{items.map(function(item, index) {
item.props.selected = (this.state.selectedTab === index);
item.props.key='tab_' + index;
return React.createElement(Tab, item.props);
}.bind(this))}
</div>
);
}
});
/** @jsx React.createElement */
var Tab = React.createClass({
getDefaultProps: function() {
return {
selected: true
};
},
componentWillReceiveProps: function(newProps) {
this.setState({
selected: newProps.selected
});
},
getInitialState: function() {
return {
selected: this.props.selected
};
},
render: function() {
var className= this.state.selected ? 'active' : '';
return (
<div className={'tab ' + className}>
{this.props.children}
</div>
);
}
});
/** @jsx React.createElement */
var ResultList = React.createClass({
getDefaultProps: function() {
return {
list: []
};
},
render: function() {
return (
<ul>
{this.props.list.map(function(item, index) {
return (
<li key={'list_item_' + index}>
Color: {item.color}<br />
Size: {item.size}<br />
Age: {item.age}
</li>
);
})}
</ul>
);
}
});
/** @jsx React.createElement */
var FilterSetCheckbox = React.createClass({
propTypes: {
name: React.PropTypes.string,
filters: React.PropTypes.array.isRequired,
defaultAllSelected: React.PropTypes.bool,
eventName: React.PropTypes.string,
label1: React.PropTypes.string,
label2: React.PropTypes.string
},
getDefaultProps: function() {
return {
name: 'filterSet',
eventName: 'filterSet',
defaultAllSelected: false,
label1: 'Select All',
label2: 'Unselect All'
};
},
getInitialState: function() {
return {
filters: this.initFilters(this.props.defaultAllSelected)
};
},
componentDidMount: function() {
Dispatcher.register(this.props.eventName + ':reset', this.handleReset);
},
componentWillUnmount: function() {
Dispatcher.unregister(this.props.eventName + ':reset', this.handleReset);
},
handleReset: function() {
this.setState({
filters: this.initFilters(false),
});
},
initFilters: function(value) {
return this.props.filters.map(function(item) {
item.isChecked = value;
return item;
});
},
handleChange: function(index) {
var filters = this.state.filters;
filters[index].isChecked = !filters[index].isChecked;
this.setState({
filters: filters
});
this.dispatchChange();
},
handleSelectAll: function(value) {
var filters = this.initFilters(value);
this.setState({
filters: filters
});
this.dispatchChange();
},
dispatchChange: function() {
var filters = this.state.filters.filter(function(item) {
return item.isChecked;
});
Dispatcher.dispatch(this.props.eventName + ':change', this.props.name, filters);
},
render: function() {
var items = [];
var selectedItems = 0;
this.state.filters.map(function(item, index) {
if (item.isChecked) {
selectedItems++;
}
var key = this.props.name + '-' + index;
items.push(
<li key={key}>
<input type="checkbox"
id={key}
checked={item.isChecked}
onChange={this.handleChange.bind(this, index)} />
<label htmlFor={key}>{item.label}</label>
</li>
);
}.bind(this));
var currentState = 'some';
if (selectedItems <= 0) {
currentState = 'none';
} else if (selectedItems >= this.props.filters.length) {
currentState = 'all';
}
var label1ClassName = (currentState === 'all') ? 'active' : '';
var label1 = <span className={'button ' + label1ClassName} onClick={this.handleSelectAll.bind(this, true)}>{this.props.label1}</span>;
var label2ClassName = (currentState === 'none') ? 'active' : '';
var label2 = <span className={'button ' + label2ClassName} onClick={this.handleSelectAll.bind(this, false)}>{this.props.label2}</span>;
return (
<ul className="filterset">
<li>
<label className="select-all">
<span className="state">{currentState}</span>
{label1} {label2}
</label>
</li>
{items}
</ul>
);
}
});
/** @jsx React.createElement */
var FilterSetRadio = React.createClass({
propTypes: {
name: React.PropTypes.string,
filters: React.PropTypes.array.isRequired,
defaultChecked: React.PropTypes.string,
eventName: React.PropTypes.string
},
getDefaultProps: function() {
return {
name: 'filterSet',
eventName: 'filterSet',
defaultChecked: ''
};
},
getInitialState: function() {
return {
checked: this.props.defaultChecked
};
},
componentDidMount: function() {
Dispatcher.register(this.props.eventName + ':reset', this.handleReset);
},
componentWillUnmount: function() {
Dispatcher.unregister(this.props.eventName + ':reset', this.handleReset);
},
handleReset: function() {
this.setState({
checked: this.props.defaultChecked
});
},
handleChange: function(index) {
this.setState({
checked: this.props.filters[index].label
});
Dispatcher.dispatch(this.props.eventName + ':change', this.props.name, [this.props.filters[index]]);
},
render: function() {
return (
<ul className="filterset">
{this.props.filters.map(function(item, index) {
var key = this.props.name + '-' + index;
return (
<li key={key}>
<input type="radio"
id={key}
name={this.props.name}
value={item.label}
checked={item.label === this.state.checked}
onChange={this.handleChange.bind(this, index)} />
<label htmlFor={key}>{item.label}</label>
</li>
);
}.bind(this))}
</ul>
);
}
});