├── .gitignore ├── README.md ├── api └── data.json ├── index.html └── search.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Typeahead Search 2 | A basic search app that shows a bootstrap modal search form and, on typing, pulls in search results via ajax and React. 3 | 4 | See a demo here http://tonyspiro.com/dev/react-typeahead-search/ 5 | 6 | ![Screencast](http://g.recordit.co/YVqFDpH4jM.gif) 7 | 8 | -------------------------------------------------------------------------------- /api/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "link": "http://github.com", 5 | "title": "Github", 6 | "icon": "fa-github", 7 | "content": "GitHub is the best place to build software together. Over 4 million people use GitHub to share code." 8 | }, 9 | { 10 | "link": "http://stackoverflow.com", 11 | "title": "Stack Overflow", 12 | "icon": "fa-stack-overflow", 13 | "content": "Q & A for professional and enthusiast programmers." 14 | }, 15 | { 16 | "link": "http://piedpiper.com", 17 | "title": "Pied Piper", 18 | "icon": "fa-pied-piper", 19 | "content": "Silicon Valley on HBO." 20 | }, 21 | { 22 | "link": "http://slack.com", 23 | "title": "Slack", 24 | "icon": "fa-slack", 25 | "content": "Slack brings all your communication together in one place. It’s real-time messaging, archiving and search for modern teams." 26 | }, 27 | { 28 | "link": "https://medium.com", 29 | "title": "Medium", 30 | "icon": "fa-medium", 31 | "content": "A magazine for a generation who grew up not caring about magazines." 32 | }, 33 | { 34 | "link": "https://stripe.com", 35 | "icon": "fa-cc-stripe", 36 | "title": "Stripe", 37 | "content": "Stripe is a suite of APIs that powers commerce for businesses of all sizes." 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Typeahead Search 6 | 7 | 8 | 9 | 10 | 65 | 66 | 67 |
68 | 69 | 70 | 73 | 74 |
75 | 76 | 77 |
78 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /search.jsx: -------------------------------------------------------------------------------- 1 | /* Search 2 | ==================================== */ 3 | $('#searchModal').on('shown.bs.modal', function(){ 4 | 5 | // Search Main 6 | var SearchArea = React.createClass({ 7 | 8 | // Gets initial state 9 | getInitialState: function(){ 10 | return { 11 | data: { 12 | items: [] 13 | } 14 | }; 15 | }, 16 | 17 | componentDidMount: function(){ 18 | 19 | $('#search').focus(); 20 | 21 | }, 22 | 23 | getResults: function(e){ 24 | 25 | var _this = this; 26 | 27 | var date = new Date(); 28 | var q = $('#search').val(); 29 | var api_url = this.props.api_url; 30 | 31 | switch(e.which) { 32 | 33 | case 38: // up 34 | var currentId = $('#search-results .result.active').attr('data-id'); 35 | var prevId = parseInt(currentId, 10) - 1; 36 | $('#search-results .result').removeClass('active'); 37 | if(!$('#search-results #result-' + prevId).length) return false; 38 | $('#search-results #result-' + prevId).addClass('active'); 39 | break; 40 | 41 | case 40: // down 42 | if(!$('#search-results .result.active').length){ 43 | 44 | $('#search-results .result').first().addClass('active'); 45 | 46 | } else { 47 | 48 | var currentId = $('#search-results .result.active').attr('data-id'); 49 | var nextId = parseInt(currentId, 10) + 1; 50 | if(!$('#search-results #result-' + nextId).length) return false; 51 | $('#search-results .result').removeClass('active'); 52 | $('#search-results #result-' + nextId).addClass('active'); 53 | 54 | } 55 | break; 56 | 57 | case 13: // enter 58 | var link = $('#search-results .result.active').attr('href'); 59 | window.open(link); 60 | break; 61 | 62 | default: 63 | 64 | var q = $('#search').val(); 65 | var url = api_url + '?q=' + q + '&time=' + date.getTime(); // for yo cache 66 | 67 | $.getJSON(url, function(data){ 68 | 69 | var items = data.items; 70 | var newItems = []; 71 | var q = $('#search').val(); 72 | 73 | // Do your searching here 74 | items.forEach(function(item, i){ 75 | 76 | var qLower = q.toLowerCase(); 77 | var titleLower = item.title.toLowerCase(); 78 | var contentLower = item.content.toLowerCase(); 79 | var formattedTitle = highlight(item.title, q); 80 | var formattedContent = highlight(item.content, q); 81 | 82 | item.formattedContent = formattedContent; 83 | item.formattedTitle = formattedTitle; 84 | 85 | // Add custom search criteria here 86 | if(titleLower.indexOf(qLower)!==-1 || 87 | contentLower.indexOf(qLower)!==-1){ 88 | newItems.push(item); 89 | } 90 | 91 | }); 92 | 93 | if(!q) newItems = []; 94 | 95 | if(!newItems.length){ 96 | 97 | $('#search-results').removeClass('open'); 98 | 99 | } else { 100 | 101 | $('#search-results').addClass('open'); 102 | 103 | } 104 | 105 | if (_this.isMounted()){ 106 | _this.setState({ 107 | data: { 108 | items: newItems 109 | } 110 | }); 111 | } 112 | 113 | }); 114 | 115 | return; // exit this handler for other keys 116 | } 117 | }, 118 | 119 | render: function(){ 120 | 121 | return ( 122 |
123 | 124 | 125 |
126 | ); 127 | } 128 | }); 129 | 130 | // List Component 131 | var SearchList = React.createClass({ 132 | 133 | render: function() { 134 | 135 | var rows; 136 | var items = this.props.items; 137 | 138 | if(items){ 139 | 140 | rows = items.map(function(item, i) { 141 | 142 | return( 143 | 144 | ); 145 | 146 | }.bind(this)); 147 | 148 | } 149 | 150 | return ( 151 | 154 | ); 155 | 156 | } 157 | 158 | }); 159 | 160 | // List Item 161 | var ListItem = React.createClass({ 162 | 163 | render: function(){ 164 | 165 | var item = this.props.item; 166 | var id = this.props.id; 167 | 168 | return ( 169 |
  • 170 | 171 | 172 |     173 | 174 |
    175 | 176 |
    177 |
  • 178 | ); 179 | } 180 | }); 181 | 182 | React.render(, document.getElementById('search-area')); 183 | 184 | 185 | }); // modal opened 186 | 187 | function preg_quote( str ) { 188 | return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); 189 | } 190 | 191 | function highlight(data, search){ 192 | return data.replace( new RegExp( "(" + preg_quote( search ) + ")" , 'gi' ), "$1" ); 193 | } 194 | --------------------------------------------------------------------------------