Aaaaaaand we’re back! Here’s another round! So we here at Black Mutt Media really love movies. I mean we REALLY love movies. And we love learning new things. Like ReactJS, a really impressive JavaScript library from Facebook. So what are we going to do today? We’re going to learn about a great combination of these two things!
So first off, I didn’t learn this the right way. I’ve got a site that I’ve been working on for some self-directed Ruby on Rails and Foundation Framework learning. I’ve had a couple ideas for sites kicking around in my head for a while. So the way this worked? I started off with a standard jQuery UI Autocomplete for searching movies using The Movie Database API. The Movie Database vs IMDb vs Letterboxd … that’s a whole different story. The first two actually have APIs for this exact purpose, Letterboxd uses The Movie Database, which is what a lot of people use for synchronizing library data for Plex or Boxee on HTPC systems … something that seems to be quickly losing relevance in the world of Roku, FireTV, Chromecast, and countless alternatives. But this isn’t about that, is it? This is about ReactJS and The Movie Database … well … so … the user can search for a film in an autocomplete and it will show a dropdown with posters and names … oh, hell, like this …
Because, as anyone who knows me already knows, with me it’s always about the Coen Brothers. So yeah, I had this set up with jQuery. But that wasn’t good enough. No. This needed done with ReactJS. Why? Well, because REACTJS IS AWESOME! Well, maybe I didn’t know that at the time, but I did know that I wanted to learn it, so that’s a good enough reason for me. So how did I do this? Good question!
Well, first things first, I was using Ruby on Rails so I needed to add the following to my Gemfile in order to use the react-rails gem.
1 |
gem 'react-rails' |
This should come as a shock to anyone who’s ever used Rails … ever … so … that’s set up! Next up … setting up a search box!
Before we continue, I feel I should repeat this: I went about this the wrong way. Facebook’s React documentation has a great section called Thinking in React that documents the entire process of breaking up your application into parts, which should then reflect the structure of your React classes. They’re dead on. I didn’t follow it. Sometimes, when I’m exploring, I just take something and replace it little by little. For one, this helps me to run into (and fix) problems, which means I’ll be better prepared for that next time. Also … it’s just how I like to learn. Goof around a bit at first just to figure it out. If I were really doing something from scratch again, I’d follow that post. Properly. But this time …
Like I said, I already had the Autocomplete set up. It was functional, but needed cleaned up. For now, though, I just created a React class that basically spit out a textbox that would use the existing code.
1 2 3 4 5 6 |
var SearchBox = React.createClass({ render: function() { return ( <input type="text" className="titleSearch" placeholder="Search For Movies" /> ); }); |
Complicated, huh? So how did I make this work in my view? Well, initially …
1 |
<%= react_component('SearchBox') %> |
1 |
<div class="large-12 columns" data-react-class="SearchBox" /> |
This wouldn’t actually properly get the Autocomplete code to work, as it wasn’t rendered prior to the autocomplete call. So what did I do? I used componentDidMount. Although, it still didn’t work at first … so … what I had to do was actually use the getDOMNode() method in order to properly implement the autocomplete.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
componentDidMount: function() { $(this.getDOMNode()).autocomplete({ source: function( request, response ) { theMovieDb.search.getMovie({"query":escape(request.term), "search_type":"ngram", "include_adult":"false"}, function(data) { var results = _.first(JSON.parse(data).results, 8); response(results); }, function(data) { response(null); }); }, minLength: 3, select: function( event, ui ) { if(ui.item) { var newUrl = getFilmUrl(ui.item); window.location.href = newUrl; } }, open: function() { $( this ).removeClass( "ui-corner-all" ).addClass( "ui-corner-top" ); }, close: function() { $( this ).removeClass( "ui-corner-top" ).addClass( "ui-corner-all" ); }, focus: function(event, ui) { $(this).val(getFormattedTitle(ui.item)); event.preventDefault(); }, delay: 0 }) .autocomplete("instance") ._renderItem = function(ul, item) { $(ul).addClass('search-dropdown small').data('dropdown-content'); var imageUrl = ''; var listItem = $("<li>").addClass('row'); listItem.data("ui-autocomplete-item", item); $(ul).append(listItem); var anchor = $('<div>'); // { class: 'row'}); listItem.append(anchor); var div =$('<div>').addClass('small-3').addClass('medium-3').addClass('large-3').addClass('column'); //'<div>', { class: 'small-3 medium-3 large-3 column'}); listItem.append(div); if(!(typeof item.poster_path === 'undefined' || item.poster_path == null || item.poster_path.replace(/\s/g, '').length < 1)) { imageUrl = "http://image.tmdb.org/t/p/w45/" + item.poster_path ; div.append($('<img></img>').attr('src', imageUrl)); // '<img>', { src: imageUrl }); } var titleDiv = $('<div>').addClass('small-9').addClass('right'); titleDiv.append(getFormattedTitle(item)); listItem.append(titleDiv); return ul; } |
1 2 3 4 5 6 7 |
isNullOrWhitespace: function(stringValue) { return (typeof stringValue === 'undefined' || stringValue == null || stringValue.replace(/\s/g, '').length < 1); }, getTmdbThumbnailUrl: function(poster_path) { return this.isNullOrWhitespace(poster_path) ? null : "http://image.tmdb.org/t/p/w45/" + poster_path; } |
Arguably, this isn’t the place to check for null or whitespace, but hey … what does a double check hurt? A lot if you program it wrong. Anyhow … I then created a function for getting the image tag.
1 2 3 4 5 6 7 8 9 |
getTmdbThumbnailTag: function(poster_path) { if(!this.isNullOrEmpty(poster_path)) { imageTag = <img src=this.getTmdbThumbnailUrl(poster_path) />; } return imageTag; } |
XJS value should be either an expression or a quoted XJS text
1 2 3 4 5 6 7 8 9 |
getTmdbThumbnailTag: function(poster_path) { if(!this.isNullOrEmpty(poster_path)) { imageTag = <img src={this.getTmdbThumbnailUrl(poster_path)} />; } return imageTag; } |
So that did the trick. Alright, so everything’s working, we’re good! We’re all done! Wait, but this isn’t how React is supposed to work, is it? There shouldn’t be a function returning a tag. There should be another react class or two. So let’s work on that! First, what are they? Well, here are a couple …
1 2 3 4 |
var SearchBoxItem = React.createClass({ }); var SearchBoxItemImage = React.createClass({ }); |
We’ve got that item and the image, then, right? Good. That’s a start. Well, let’s start working on that SearchBoxItem because, now that we want to do one thing right, we should go from the top down. (Let’s be clear, though, this should all have been planned out ahead of time). First we need the render:
1 2 3 4 5 6 7 8 9 10 11 12 |
var SearchBoxItem = React.createClass({ render: function() { return( <li className="row"> <div className="small-3 medium-3 large-3 column"> {this.getTmdbThumbnailTag(item.poster_path)} <div className="small-9 right">{getFormattedTitle(item)}</div> </div> </li> ); } }); |
Perfect, then! It should work and we can make a ton of money! But wait, what is item in this context? And do we really need the full item or just the thumbnail image and the formatted title? Well … I think you know the answer … and we’ll set up the properties for those.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var SearchBoxItem = React.createClass({ render: function() { var searchBoxImage; if(!isNullOrWhitespace(this.props.posterPath)) { searchBoxImage = <SearchBoxItemImage baseImageUrl={this.props.baseImageUrl} posterPath={this.props.posterPath} />; } return( <li className="row"> <div className="small-3 medium-3 large-3 column"> {searchBoxImage} </div> <div className="small-9 medium-9 large-9 right">{this.props.filmTitle}</div> </li> ); } }); var SearchBoxItemImage = React.createClass({ render: function() { return(<img src={this.props.baseImageUrl + this.props.posterPath} />); } }); |
Oh, man … those are looking good … “But wait, Scott,” you ask, “does it work??” Well, almost. I had an issue when trying to add the item to the UL element. You remember this line of code?
1 |
$(ul).append(listItem); |
Alright, we’re getting closer, but that needs to be rendered first, right? I can’t just append it directly since it’s an instance of a React class? Correct. So I looked for a while. And I asked for help. I needed to render it to a string first.
1 2 |
var listItem = <SearchBoxItem dataItem={item} baseImageUrl="http://image.tmdb.org/t/p/w45/" posterPath={item.poster_path} filmTitle={getFormattedTitle(item)} />; var renderedListItem = React.renderToString(listItem); |
and on the receiving end …
1 |
<li className="row" data-ui-autocomplete-item={this.props.dataItem}> |
But you know what happens there? I could be wrong about this, and please let me know if I am, but it seems as though React renders the object as the string representation. You know the syntax – [object object]. So I had to actually create the element, then add it to the ul (like above), but then I had to grab it after the append when it was more than just a string and add on that data attribute.
1 |
$(ul[0].lastChild).data("ui-autocomplete-item", item); |
There we go. It’s all set up and working just like before! I’ve made no progress! Really, I’ve learned quite a few things along the way.
So help me. Take a look at everything. Let me know what works for you, what doesn’t, what I should change, what you like, what you don’t, and which movies to search for. Until then …
Be the first to leave a comment. Don’t be shy.