diff --git a/sample/ImageSearch.js b/sample/ImageSearch.js
new file mode 100644
index 0000000..3a48630
--- /dev/null
+++ b/sample/ImageSearch.js
@@ -0,0 +1,96 @@
+define([
+ 'lodash',
+ 'jquery',
+ 'react',
+ 'ImageSearch.rt'
+], function (_, $, React, template) {
+ 'use strict';
+
+ var ImageSearch = React.createClass({
+
+ displayName: 'ImageSearch',
+ mixins: [React.addons.LinkedStateMixin],
+
+ seq: 0,
+ total: 0,
+ hasMore: true,
+ heights: [0, 0, 0],
+ realTerm: 'cats',
+
+ getInitialState: function () {
+ setTimeout(this.search, 0);
+ return {
+ searchTerm: this.realTerm,
+ items: [[], [], []]
+ };
+ },
+
+ search: function() {
+ this.state.items = [[], [], []];
+ this.total = 0;
+ this.heights = [0, 0, 0];
+ this.hasMore = true;
+ this.realTerm = this.state.searchTerm;
+ this.loadMore();
+ },
+
+ indexOfMin: function(array) {
+ var indexAndMin = _.reduce(array, function(accum, height, index) {
+ return (height < accum.min) ? { i: index, min: height } : accum;
+ }, {i: -1, min: Number.MAX_VALUE});
+ return indexAndMin.i;
+ },
+
+ loadMore: function(done) {
+ done = done || function() {};
+ if (!this.hasMore) {
+ done();
+ return;
+ }
+ var url = 'https://ajax.googleapis.com/ajax/services/search/images?v=1.0&rsz=8&start=' + this.total + '&q=' + this.realTerm + "&callback=?";
+
+ var self = this;
+ $.ajax({url: url, dataType: 'jsonp'})
+ .done(function(data){
+ if (!data.responseData) {
+ self.hasMore = false;
+ done();
+ return;
+ }
+ var results = data.responseData.results;
+
+ var items = _.cloneDeep(self.state.items);
+
+ for (var i = 0; i < results.length; i++) {
+ var result = data.responseData.results[i];
+ var minHeightIndex = self.indexOfMin(self.heights);
+
+ items[minHeightIndex].push({
+ id: self.seq + 1,
+ title: result.titleNoFormatting,
+ url: result.url,
+ ratio: result.width / result.height,
+ originalContext: result.originalContextUrl
+ });
+
+ var relativeHeight = result.height / result.width;
+ self.heights[minHeightIndex] = self.heights[minHeightIndex] + relativeHeight;
+ self.total++;
+ self.seq++;
+ }
+ self.setState({items: items});
+ done();
+ });
+ },
+
+ shouldComponentUpdate: function(nextProps, nextState) {
+ return !_.isEqual(this.state, nextState);
+ },
+
+ render: function () {
+ return template.apply(this);
+ }
+ });
+
+ return ImageSearch;
+});
\ No newline at end of file
diff --git a/sample/ImageSearch.rt b/sample/ImageSearch.rt
new file mode 100644
index 0000000..de43c9b
--- /dev/null
+++ b/sample/ImageSearch.rt
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/sample/ImageSearch.rt.js b/sample/ImageSearch.rt.js
new file mode 100644
index 0000000..9472194
--- /dev/null
+++ b/sample/ImageSearch.rt.js
@@ -0,0 +1,57 @@
+define([
+ 'react',
+ 'lodash',
+ 'InfiniteScroll'
+], function (React, _, InfiniteScroll) {
+ 'use strict';
+ function onKeyDown1(e) {
+ if (e.keyCode == 13) {
+ this.search();
+ return false;
+ }
+ }
+ function onClick2() {
+ this.search();
+ return false;
+ }
+ function repeatI3(row, rowIndex, i, iIndex) {
+ return React.DOM.a({
+ 'href': i.originalContext,
+ 'target': 'blank',
+ 'className': 'container fadeInDown',
+ 'key': i.id
+ }, React.DOM.div({
+ 'style': {
+ paddingTop: Math.floor(100 / i.ratio) + '%',
+ backgroundColor: 'grey'
+ }
+ }), React.DOM.div({ 'className': 'imgContainer' }, React.DOM.img({
+ 'width': '100%',
+ 'src': i.url
+ }), React.DOM.div({ 'className': 'title' }, i.title)));
+ }
+ function repeatRow4(row, rowIndex) {
+ return React.DOM.div.apply(this, _.flatten([
+ { 'key': row },
+ _.map(this.state.items[row], repeatI3.bind(this, row, rowIndex))
+ ]));
+ }
+ return function () {
+ return React.DOM.div({ 'className': 'innerContainer' }, React.DOM.div({ 'className': 'searchbox' }, React.DOM.input({
+ 'type': 'text',
+ 'valueLink': this.linkState('searchTerm'),
+ 'onKeyDown': onKeyDown1.bind(this)
+ }), React.DOM.button({ 'onClick': onClick2.bind(this) }, 'Search')), InfiniteScroll.apply(this, _.flatten([
+ {
+ 'className': 'fixed',
+ 'onLoadMore': this.loadMore,
+ 'threshold': 150
+ },
+ _.map([
+ 0,
+ 1,
+ 2
+ ], repeatRow4.bind(this))
+ ])));
+ };
+});
\ No newline at end of file
diff --git a/sample/InfiniteScroll.js b/sample/InfiniteScroll.js
new file mode 100644
index 0000000..8502e02
--- /dev/null
+++ b/sample/InfiniteScroll.js
@@ -0,0 +1,37 @@
+define([
+ 'lodash',
+ 'jquery',
+ 'react'
+], function (_, $, React) {
+ 'use strict';
+
+ var InfiniteScroll = React.createClass({
+
+ displayName: 'InfiniteScroll',
+ gettingMore: false,
+
+ onLoadMoreFinished: function() {
+ this.gettingMore = false;
+ },
+
+ onScroll: function(evt) {
+ if (!this.props.onLoadMore || this.gettingMore) {
+ return;
+ }
+ var threshold = this.props.threshold || 0;
+ var raw = evt.target;
+ if (raw.scrollTop + raw.offsetHeight + threshold >= raw.scrollHeight) {
+ this.gettingMore = true;
+ this.props.onLoadMore(this.onLoadMoreFinished);
+ }
+ },
+
+ render: function () {
+ var passedProps = _.omit(this.props, ['onLoadMore', 'threshold', 'children', 'onScroll']);
+ passedProps.onScroll = this.onScroll;
+ return React.DOM.div(passedProps, this.props.children);
+ }
+ });
+
+ return InfiniteScroll;
+});
diff --git a/sample/index.html b/sample/index.html
new file mode 100644
index 0000000..1a37c78
--- /dev/null
+++ b/sample/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ React templates - Image Search Sample
+
+
+
+
+
+
+
+
diff --git a/sample/main.js b/sample/main.js
new file mode 100644
index 0000000..b5c19c4
--- /dev/null
+++ b/sample/main.js
@@ -0,0 +1,20 @@
+'use strict';
+
+requirejs.config({
+// baseUrl: '/',
+ paths: {
+ lodash: 'http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash',
+ jquery: 'http://code.jquery.com/jquery-1.11.0.min',
+ react: 'http://fb.me/react-with-addons-0.12.0'
+ },
+ shim: {
+ lodash: { exports: '_' },
+ jquery: { exports: '$' },
+ react: { exports: 'React' }
+ }
+});
+
+requirejs(['jquery', 'react', 'ImageSearch'], function ($, React, ImageSearch) {
+ React.renderComponent(ImageSearch(), $('#main').get(0));
+});
+
diff --git a/sample/sample.css b/sample/sample.css
new file mode 100644
index 0000000..e45f98f
--- /dev/null
+++ b/sample/sample.css
@@ -0,0 +1,83 @@
+.imgContainer {
+ position:absolute;
+ top:0;
+ bottom:0;
+ left:0;
+ right:0;
+}
+
+.fixed {
+ height: calc(553px - 53px);
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.fixed > div {
+ width: 33%;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.searchbox {
+ text-align:center;
+ height: 53px;
+}
+
+.container {
+ display: block;
+ background-color: gray;
+ position: relative;
+ margin: 0 10px 10px 0;
+}
+
+@-webkit-keyframes fadeInDown{
+ 0%{
+ opacity:0;
+ -webkit-transform:translate3d(0,-50%,0);
+ transform:translate3d(0,-50%,0)
+ }
+
+ 100%{
+ opacity:1;
+ -webkit-transform:none;
+ transform:none
+ }
+
+}
+
+@keyframes fadeInDown{
+ 0%{
+ opacity:0;
+ -webkit-transform:translate3d(0,-50%,0);
+ -ms-transform:translate3d(0,-50%,0);
+ transform:translate3d(0,-50%,0)
+ }
+
+ 100%{
+ opacity:1;
+ -webkit-transform:none;
+ -ms-transform:none;
+ transform:none
+ }
+
+}
+
+.fadeInDown{
+ -webkit-animation:fadeInDown 1s;
+ animation:fadeInDown 1s;
+}
+
+.title {
+ color: white;
+ background-color: black;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 9pt;
+ opacity: 0;
+ position: absolute;
+ bottom:0;
+}
+
+.container:hover .title {
+ opacity: 1;
+ transition: opacity 0.75s;
+}