Monday, July 1, 2013

AngularJS: Filtering Data

I'm currently learning how to use ng-repeat to display lists of items, and how to use filters to limit the data displayed in those lists.

Based on the AngularJS tutorials (docs.angularjs.org/tutorial/), I have a collection of phone JSON objects in the following format:

{
  "age": 0,
  "id": "motorola-xoom-with-wi-fi",
  "imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg",
  "name": "Motorola XOOM\u2122 with Wi-Fi",
  "snippet": "The Next, Next Generation\r\n\r\nEx[eroemce the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)."
}

There are a bunch of those; some are tablets, others are phones.

We then have some HTML with Angular directives to display a list of phones (showing only the pertinent parts -- a controller is referenced elsewhere in this HTML, and that controller knows where to find the 'phones' data):

Search: <input ng-model="query" />

<ul>
  <li ng-repeat="phone in phones | filter: query">
    {{phone.name}}
    <p>{{phone.snippet}}</p>
  </li>
</ul>

This is all grand.  When I type search text in the "query" input, the data is restricted to only that data that contains that text.  However, what happens when I want to limit my data to only phones, and not tablets?  When I type "phone" into the search bar, it still shows me everything, because all of my images are under a directory called "phones".  My options are to either a) rename my directory structure, or b) eliminate the imageUrl field from my filter.  While my directory structure probably shouldn't reference "phones" if it contains more than just phones, renaming a structure can often affect your entire project, so let's modify the filter instead.

One option is to specify what portion of data you'd like to filter on.

I've learned two ways of doing this.  The first is to specify the intended data on the input itself:

Search: <input ng-model="query.snippet" />
...
<li ng-repeat="phone in phones | filter: query">
...

This modifies the filter so that you'll only be searching in the 'snippet' field in the data.  Now this technique is effective, but seems non-intuitive to me.  Why should you be tweaking the input instead of tweaking the filter itself?  To me, it looks like we've just changed the name of the input's model reference to "query.snippet".  The fact that we're only querying the "snippet" field in the data is not obvious to me.  I'm no expert, however, so if you like this method, then don't let me stop you -- in fact, I encourage you to add a comment to this post explaining why you like this method.

The second method I've learned, and the one I like more, allows you to tweak the actual filter:

Search: <input ng-model="query" />
...
<li ng-repeat="phone in phones | filter: {snippet:query}">
...

This code behaves in the exact same way as the code above, but, to me, is much more readable.  It says "show me a list of all objects in 'phones', but filter the data so that the 'snippet' field contains whatever is in the 'query' model reference."

Note:  If you run into an issue where the solution above results in ALL data being filtered out on app startup (and then working properly when you edit the input query), consider upgrading to a newer version of Angular.  This was a problem for me with version 1.0.6, but is no longer a problem in version 1.1.5

But what if we want to filter on multiple fields?  What if we want to filter on both 'snippet' and 'id'?

For that, we'd write a custom filter:

In the controller:

$scope.queryFilter = function(movie) {
  if($scope.query != null) {
    return movie.snippet.toLowerCase().indexOf($scope.query.toLowerCase()) != -1
   || movie.id.toLowerCase().indexOf($scope.query.toLowerCase()) != -1;
  }
  return true;
};

In the HTML:

<li ng-repeat="phone in phones | filter: queryFilter">

That will filter only on the fields we specified, namely 'id' and 'snippet'

Ideally, there'd be a way to filter against all fields *except* a particular field.  For example, we'd like to filter against all fields except for 'imageUrl', without needing to specify 'snippet', 'id', 'name', etc.  I imagine we could write a custom filter to iterate all fields of a particular object, ignoring 'imageUrl', but I'm not creative enough with JavaScript to do that.  If you'd like to give it a shot, please add a comment.

I hope this helped someone.  Please leave questions and comments below.

Monday, June 17, 2013

Problem 1: Can't install Git, try running Update; Can't run Update, please install Git

My CoffeeScript PeepCode screencast started off by telling me to install HomeBrew.

The HomeBrew installation is easy enough on a Mac, I literally just ran:

ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"

and HomeBrew was installed properly on my machine.

However, the instructions then said that I should run "brew doctor" before doing anything else, as a way of ensuring that everything was up and running properly.  So, I run that command:

brew doctor

And it says:

You must: brew install git

So I do that:

brew install git

And it replies:

Failed to execute: make
Error: Homebrew doesn't know what compiler versions ship with your version
of Xcode (dunno). Please `brew update` and if that doesn't help, file
an issue with the output of `brew --config`:
  https://github.com/mxcl/homebrew/issues

Ok, so I run "brew update," and I get:

You must: brew install git
Error: Failure while executing: git init 

Hopefully, by now, you're picking up on my frustration.  I can't install Git until I run "brew update", and I can't run Update until I install Git.

So I go to the Git HomePage and download the GUI installer.  I run it, it says "Installation Successful!", but, when I try to run 'brew update' again, it still complains that I need to install Git.  I try running "which git" to see where Git was installed, but my Mac can't seem to find it.

Asking around the office led me to the solution:  the Git GUI installer installs Git to /usr/local/git/bin, instead of /usr/local/bin.  So, I added /usr/local/git/bin to my PATH, and everything worked wonderfully.

-----------------------

Update

Turns out you can bypass all of this mess by simply installing Xcode, which is free from the Apple AppStore.

When my HomeBrew told me:

Error: Homebrew doesn't know what compiler versions ship with your version
of Xcode (dunno). Please `brew update` and if that doesn't help, file

I assumed it meant "you have Xcode installed, but there's something wonky with the compiler version."  Turns out it could also mean "you don't have Xcode installed."

I installed XCode onto my machine, after which "brew install git" worked just fine.

I'll probably delete the "regular" version of Git so that it doesn't conflict with the Homebrew version.

The King is Dead; Long Live the King

So my foray into Android was pretty short-lived.  Turns out that it's really difficult to find time to learn a new language when your place of employment has no use for that language.  Sure, I could've spent more time learning at home, but, as I don't *need* to learn Android, it was never high enough on my priority list to actually sit down and do it.

So, it's on to a "new" language:  JavaScript.

Now, I put "new" in quotes because I already "know" JavaScript -- at least I thought I did.  After all, JavaScript was a huge part of my job description at Shutterfly when I was hired here 5+ years ago.  However, there were so many advances in JavaScript and its use during the past 3 years that I've spent writing Flex code, that I feel like I don't know JavaScript anymore.  To put it into perspective, I've never used JQuery, and that puts me into the "elementary" level of JavaScript coding by today's standards.

When I told some colleagues that I want to learn JavaScript, it was recommended that I learn CoffeeScript instead.  While I initially rebuked, because why learn B when you're trying to learn A, some good points were made:  most significantly the point that CoffeeScript translates *directly* to JavaScript, and can help you learn the "correct" ways to write JavaScript, without having to suffer through all of the "wrong" ways.

So I'm off to learn CoffeeScript using a screencast from PeepCode at https://peepcode.com/products/coffeescript/.  PeepCode, unfortunately, is not free.

I'll also be referencing the O'Reilly book, JavaScript: The Good Parts, which can be downloaded for free at http://it-ebooks.info/book/274/.