Dynamic search in a static world

While adding search to a new blog might not be the most important thing to do, it gave me perfect excuse to work a little with Jekyll liquid and AngularJS. There was no need to reinvent the wheel as Edward Hotchkiss already implemented something pretty similiar. Therefore here are just the major differences, I expect that you’re familiar with Edward’s post, if not please read it first. The implementation is based on AngularJS v1.0.1 and by using a small liquid condition scripts are only loaded on the search page.

Here’s the relevant excerpt from default.html.

1 {% if page.title == 'Search' %}
2     <script type="text/javascript" src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
3     <script type="text/javascript" src="/js/JekyllApp.js"></script>
4 {% endif %}

Edward Hotchkiss is making an XHR call to retrieve the RSS-feed feed.xml and then parses the xml. This is a pretty straight forward implementation, but I wanted to omit this extra XHR call and ‘bootstrap’ the information into the initial page. Therefore a liquid helper is used to create an JavaScript array with the relevant post information. This has the additional advantage that you can leverage liquid filter like strip_html | truncate : 120 (on the server side!) to clean up and cut down potentially large information.

One goal was to having a search box on every page. The Controller was enhanced so that the $scope .searchText = '' can now be passed in via querystring as well.

BTW: You can give it a try on all blog pages, to see it in action.

 1 /**
 2  * Setup Module with `highlight` filter
 3  */
 4 
 5 var JekyllApp = angular.module('JekyllApp', [], function ($routeProvider, $locationProvider) {
 6     $locationProvider.html5Mode(false);
 7 });
 8 
 9 JekyllApp.filter('highlight', function () {
10     return function (text, filter) {
11         if (filter === '') {
12             return text;
13         }
14         else {
15             return text.replace(new RegExp(filter, 'gi'), '<span class="match">$&</span>');
16         }
17     };
18 });
19 
20 /**
21  * Inject required $objects into our Controller
22  */
23 
24 JekyllSearchController.$inject = ['$scope', '$location', '$window'];
25 
26 function JekyllSearchController($scope, $location, $window) {
27     // Optionally passing in a search term via q=XXX
28     // $location.search won't work without html5Mode false using window.location.search instead
29     // $scope.searchText =  $location.search().q || "";
30     // Todo: Consider switching back to $location.search once it supports html5Mode false
31     $scope.searchText = '';
32     var search = window.location.search;
33     if (search.indexOf('?q=') > -1 || search.indexOf('&q=') > -1) {
34         var params = {};
35         angular.forEach(search.split('?')[1].split('&'), function (param) {
36             params[param.split('=')[0]] = param.split('=')[1];
37         });
38         $scope.searchText = params.q || '';
39     }
40 
41     $scope.externalLink = function () {
42         // Todo: Figure out the correct AngularJS way for a page reload on href click
43         // https://github.com/angular/angular.js/issues/1102
44         $window.location.href = this.post.url;
45     };
46     $scope.posts = JekyllApp.posts;
47 }

There are a couple of things that I didn’t grasp fully at the moment I’ve to admit, like

  • why $location.search() only works with $locationProvider.html5mode(true) or
  • why AngularJS doesn’t have an easy (to discover) method that allows a full page reload on href click

so I’d be happy if somebody could fill the gaps. In the meantime workarounds are used, so querystring q is determined in a good old fashioned way and there’s a thread on the angular groups that discusses page reload.

 1 <script type="text/javascript">
 2    // Using liquid to populate JeykllApp.posts array
 3    JekyllApp.posts = {% include Helper/JekyllAppPosts %}
 4 </script>
 5 
 6 <div id="search-container" class="entrance" ng-app="JekyllApp" ng-controller="JekyllSearchController">
 7   <div class="entrance-item">
 8     <p><input id="searchText" type="search" placeholder="Live Search Posts..." ng-model-instant ng-model="searchText" />
 9   </div>
10   <div class="entrance-item">
11     <ul>
12       <li ng-repeat="post in posts | filter:searchText">
13          <span ng-bind-html-unsafe="post.date | date:'MMM d, y' | highlight:searchText"></span> &raquo;
14         <a ng-href="{{ post.url }}" ng-click="externalLink()"
15            ng-bind-html-unsafe="post.title | highlight:searchText"></a>
16           <div class="preview" ng-bind-html-unsafe="post.content | highlight:searchText"></div>
17       </li>
18     </ul>
19   </div>
20 </div>

So where’s that magic include Helper/JekyllAppPosts coming from? This is where my very first liquid expression :) kicks in and returns an array with title, url, date and content information.

1 [{% for post in site.posts %}
2 {"title" : "{{ post.title }}",
3 "url" : "{{ post.url }}",
4 "date" : "{{ post.date | date_to_string }}",
5 "content" : "{{ post.content |  strip_html | truncate : 120 }}"}{% if forloop.rindex0 != 0 %},{% endif %}
6 {% endfor %}
7 ];

All in all the implementation was pretty straight forward, even when there were a couple of gotchas.

On very last gotcha I run into and that I’d like to share, is that on my local box jekyll is running with liquid 2.3 whatever, which allows you to use {% raw %}, while github is using liquid 2.2 whatever and therefore the older {% literal %} expression is used. After being hit by that one I finally understood Edward’s LeftCurley setting in _config.yml and I feel his pain.

Anyway as a rule of thumb, don’t use those tags for the time being and rewrite the code instead. There are some instructions from Khaja Minhajuddin that doesn’t require plug-ins.

Published: July 09 2012

blog comments powered by Disqus