The documentation search app

We're going to write an app that can search among many terms and show some simple detail for each of them. The search input field will have an autocomplete feature with a list of all the terms that match our search string.

Particularly, we'll use the documentation for PHP with 9,047 functions and write a fuzzy search algorithm that will search in it.

Fuzzy search is used in IDEs such as PHPStorm or PyCharm and also in the popular text editor Sublime Text. It doesn't search just for the strings that start with your search term but it checks whether the order of characters in your term and in the checked string is the same. For example, if you type docfrg, it will find DocumentFragment because the letters in DocumentFragment are in the same order as docfrg.

This is very handy because when there are a lot of functions with the same prefix, you can start typing with just the first character and then jump to the middle of the word and it's very likely that there won't be many functions with the same characters. This is quite common for PHP because there are a lot of functions that start with mysql or str_. If you're looking for a function called str_replace, you can type just splc.

We'll load the entire dictionary with Ajax as a JSON string and decode it to a Map object. Dart uses the Future-Based API for all asynchronous calls including Ajax, so we should talk about it first.

The Future-Based API

Dart, as well as JavaScript, uses asynchronous calls a lot. A common pitfall of this approach in JavaScript is that it tends to make many nested function calls with callbacks:

async1(function() {
  // do something
  async2(function() {
    // do something
    async3(function() {
      // do something
      callback();
    });
  }, callback);
});

The downsides of this approach are obvious:

  • It makes hard to read and debug code, so-called callback hell.
  • Each nested function can access variables from all parent scopes. This leads to variable shadowing and also prevents the JavaScript interpreter from deallocating unused variables. When working with a larger amount of data (for example, asynchronous calls when reading files), even simple script can use the entire available memory and cause the browser to crash.

A Future in Dart stands for an object that represents a value that will exist sometime in the future. Dart uses Future objects in nearly all their APIs, and we're going to use it in order to avoid passing callbacks around.

An example of using Future is HttpRequest.getString(), which returns a Future object immediately and makes an asynchronous Ajax call:

HttpRequest.getString('http://...').then(onDataReady);

To work with the data returned from a Future object, we use the then() method, which takes the callback function as an argument that can return another Future object as well.

If we want to create asynchronous behavior similar to that in the preceding example, we use the Completer class, which is a part of dart:async package. This class has a property called future, which represents our Future object and the complete() method, which resolves the Future object with some value. To keep the same order of function calls, we'll chain the then() methods of each Future object:

import 'dart:async';

Future async1() {
  var completer = new Completer();
  // Simulate long lasting async operation.
  new Future.delayed(const Duration(seconds: 2), () {
    // Resolve completer.future with this value. This will also
    // call callback passed to then() method for this Future.
    completer.complete('delayed call #1'),
  });
  // Call to [Completer.complete()] resolves the Future object.
  return completer.future;
}

Future async2(String val) {
  // Print result from the previous async call.
  print(val);
  // Then create a new Completer and schedule
  // it for later execution.
  var completer = new Completer();
  // Simulate long lasting async operation.
  new Future.delayed(const Duration(seconds: 3), () {
    completer.complete('delayed call #2'),
  });
  return completer.future;
}

Future async3(String val) {
  // Return another Future object.
}

void main() {
  // Chain async calls. Each function returns a Future object.
  async1()
    .then((String val) => async2(val))
    .then((String val) => async3(val))
    .then((String val) => print(val));
}

We got rid of nested calls and have quite straightforward, shallow code.

Note

APIs similar to Dart's Future are very common among most JavaScript frameworks. Maybe you've already seen $.Deferred() in jQuery or $q.defer() in AngularJS.

Future objects can also handle error states with catchError() that are emitted by a Completer object with completeError().

Another usage of Future is when we want a function to be called asynchronously, which is internally scheduled at the end of the event queue:

new Future(() {
  // function body
});

Sometimes, this is useful when you want to let the browser process all the events before executing more computationally intensive tasks that could make the browser unresponsive for a moment. For more in-depth information about Dart's event loop, see https://www.dartlang.org/articles/event-loop/.

Using async and await keywords

Dart 1.9 introduced two new keywords, async and await, that significantly simplify the usage of asynchronous calls with the Future-Based API.

Async

The async keyword is used to mark a function's body (which immediately returns a Future object) that is executed later and its return value is used to complete the Future object just like we saw previously when using the Completer class:

Future<String> hello() async {
  return 'Hello, World!';
}

In practice, you don't have to specify the Future<String> return type because even Dart Editor knows that the async function returns a Future object, so we'll omit it most of the time.

This saves some writing but its real power comes in combination with await.

Await

With Future objects, the only way to chain (or simulate synchronous) calls is to use the then()method multiple times, as we saw earlier. But there's a new keyword await that is able to pause the execution of the current VM's thread and wait until the Future object is completed:

String greetings = await hello();

The completed value of Future is then used as a value for the entire expression await hello().

In comparison to the preceding example of multiple asynchronous calls, we could use just:

print(await async3(await async2(await async1())));

The only limitation here is that await must be used inside an asynchronous function (for example, defined with async) in order not to block the main execution thread. If the expression with await raises an exception, it's propagated to its caller.

We're going to use async and await a lot here, but it's good to know how to use the "original" Future-Based API with the Future and Complete classes, because it will take some time for developers of third-party libraries to update their code with async and await.

Note

Dart 1.9 actually introduced even more keywords such as await-for, yield, async*, and a few more (also called generators), but these aren't very common and we're not going to discuss them here. If you want to know more about them, refer to https://www.dartlang.org/articles/beyond-async/.

Creating Ajax requests in Dart

Nearly every app these days uses Ajax. With libraries such as jQuery, it's very easy to make Ajax calls, and Dart is no different. Well, maybe the only difference is that Dart uses the Future-Based API.

Creating an Ajax call and getting the response is this easy:

String url = 'http://domain.com/foo/bar';
Future ajax = HttpRequest.getString(url);
ajax.then((String response) {
  print(response);
});

// Or even easier with await.
// Let's assume we're inside an asynchronous function.
String response = await HttpRequest.getString(url);

That's all. HttpRequest.getString() is a static method that returns a Future<String> object. When the response is ready, the callback function is called with the response as a string. You can handle an error state with catchError() method or just wrap the await expression with the try-catch block. By default, getString() uses the HTTP GET method.

There are also more general static methods such as HttpRequest.request(), which returns Future<HttpRequest>, where you can access return code, response type, and so on. Also, you can set a different HTTP method if you want.

To send form data via the POST method, the best way is to use HttpRequest.postFormData(), which takes a URL and a Map object with form fields as arguments.

In this chapter, we'll use Ajax to load a dictionary as JSON for our search algorithm, and we'll also see JSONP in action later.

Dart packages

Every Dart project that contains the pubspec.yaml file is also a package. Our search algorithm is a nice example of a component that can be used in multiple projects, so we'll stick to a few conventions that will make our code reusable.

Dart doesn't have namespaces like other languages, such as PHP, Java, or C++. Instead, it has libraries that are very similar in concept.

We'll start writing our app by creating a new project with the Uber Simple Web Application template and creating two directories. First, we create /lib in the project's root. Files in this directory are automatically made public for anyone using our package. The second directory is /lib/src, where we'll put the implementation of our library, which is going to be private. Let's create a new file in /lib/fuzzy.dart:

// lib/fuzzy.dart
library fuzzy;
part 'src/fuzzy_search.dart';

This creates a library called fuzzy. We could put all the code for this library right into fuzzy.dart, but that would be a mess. We'd rather split the implementation into multiple files and use the part keyword to tell Dart to make all the functions and classes defined in lib/src/fuzzy_search.dart public. One library can use the part keyword multiple times. Similarly to object properties, everything that starts with the _ underscore is private and not available from the outside.

Then, in lib/src/fuzzy_search.dart, we'll put just the basic skeleton code right now:

// lib/src/fuzzy_search.dart
part of fuzzy;
class FuzzySearch {
  /* ... */
}

The part of keyword tells Dart that this file belongs to the fuzzy library.

Then, in main.dart, we need to import our own library to be able to use the FuzzySearch class:

// web/main.dart
import 'package:Chapter_02_doc_search/fuzzy.dart';
// ... later in the code create an instance of FuzzySearch.
var obj = new FuzzySearch();

Note that the fuzzy.dart file is inside the lib directory, but we didn't have to specify it. The package importer is actually not working with directory names but package names, so Chapter_02_doc_search here is a package name from pubspec.yaml and not a directory, although these two have the same name. For more in-depth information about pubspec.yaml files, refer to https://www.dartlang.org/tools/pub/pubspec.html.

You should end up with a structure like this:

Dart packages

Note that the package has a reference to itself in the packages directory.

One package can be a library and a web app at the same time. If you think about it, it's not total nonsense, because you can create a library and ship it with a demo app that shows what the library does and how to use it.

Note

You can read more about Dart packages at https://www.dartlang.org/tools/pub/package-layout.html.

Writing the fuzzy search algorithm

We can move on with writing the fuzzy search algorithm. A proper name for this algorithm would be probably approximate string matching, because our implementation is simpler than the canonical and we don't handle typos. Try to read the code; we've already seen every language concept used here in Chapter 1, Getting Started with Dart:

// lib/src/fuzzy_search.dart
part of fuzzy;

class FuzzySearch {
  List<String> list;
  
  FuzzySearch(this.list);
  
  List<String> search(String term) {
    List<String> results = [];
    
    if (term.isEmpty) {
      return [];
    }
    
    // Iterate entire list.
    List<String> result = list.where((String key) {
      int ti = 0; // term index
      int si = 0; // key index
      // Check order of characters in the search
      // term and in the string key.
      for (int si = 0; si < key.length; si++) {
        if (term[ti] == key[si]) {
          ti++;
          if (ti == term.length) {
            return true;
          }
        }
      }
      return false;
    }).toList(growable: false);
    
    // Custom sort function.
    // We want the shorter terms to be first because it's more
    // likely that what you're looking for is there.
    result.sort((String a, String b) {
      if (a.length > b.length) {
        return 1;
      } else if (a.length == b.length) {
        return 0;
      }
      return -1;
    });
    
    return result;
  }
}

The app itself will require a simple HTML code (we're omitting obvious surrounding code, such as <html> or <head>):

<body>
  <input type="search" id="search" disabled>
  <ul id="autocomplete-results"></ul>

  <div id="detail">
    <h1></h1>
    <div></div>
  </div>

  <script type="application/dart" src="main.dart"></script>
  <script data-pub-inline src="packages/browser/dart.js"></script>
</body>

We don't want to hardcode the dictionary, so we'll load it using Ajax. JSON file with all search terms is part of this chapter's source code, and it looks like this:

{ ... 
  "strpos": {
    "desc": "Find the numeric position of the first occurrence  of 'needle' in the 'haystack' string.", 
    "name": "strpos"
  },
    ...
  "pdo::commit": {
    "desc": "...", 
    "name": "PDO::commit"
  }, ...
}

The key for each item is its lowercased name. In Dart, this JSON will be represented as:

Map<String, Map<String, String>>

Now, we'll write a static method that creates an instance of our app and the main() function:

import 'dart:html';
import 'dart:convert';
import 'dart:async';
import 'package:Chapter_02_doc_search/fuzzy.dart';

class DocSearch {
  static fromJson(Element root, String url) async {
    String json = await HttpRequest.getString(url);
    Map decoded = JSON.decode(json);
    return new DocSearch(root, decoded);
  }

  DocSearch(Element root, [Map<String, dynamic> inputDict]) {
    // Rest of the constructor. 
  }
  // The rest of the class goes here. 
}


main() async {
  try {
    await DocSearch.fromJson(querySelector('body'), 'dict.json'),
  } catch(e) {
    print("It's broken.");
  }
}

Note how we're creating an instance of DocSearch and are declaring main() as asynchronous. We call a DocSearch.fromJson()static method, which returns a Future object (the async keyword does this for us automatically), which is completed with an instance of DocSearch when the Ajax call is finished and when we decoded JSON into a Map object.

Note

The source code for this example contains both Dart 1.9 implementation with async and await and pre 1.9 version with the raw Future and Completer classes.

Handling HTML elements

You can see that if we hardcoded our dictionary, we could call the constructor of DocSearch like with any other class. We can now look at the constructor particularly:

// web/main.dart
class DocSearch {
  Element _root;
  InputElement _input;
  UListElement _ul;
  FuzzySearch _fuzzy;
  Map<String, dynamic> _dict;
  
  static Future fromJson(Element root, String url) async {
    /* The same as above. */
  }
  
  DocSearch(Element root, [Map<String, dynamic> inputDict]) {
    _root = root;
    dict = inputDict;
    _input = _root.querySelector('input'),
    _ul = _root.querySelector('ul'),
    
    // Usage of ".." notation.
    _input
      ..attributes.remove('disabled')
      ..onKeyUp.listen((_) => search(_input.value))
      ..onFocus.listen((_) => showAutocomplete());
    
    _ul.onClick.listen((Event e) {
      Element target = e.target;
      showDetail(target.dataset['key']);
    });
    
    // Pass only clicks that are not into <ul> or <input>.
    Stream customOnClick = document.onClick.where((Event e) {
      Element target = e.target;
      return target != _input && target.parent != _ul;
    });
    customOnClick.listen((Event e) => hideAutocomplete());
  }

  /* The rest of the class goes here. */
}

To set multiple properties to the same object, we can use the double dot operator. This lets you avoid copying and pasting the same object name over and over again. This notation is equal to:

_input.attributes.remove('disabled')
_input.onKeyUp.listen((_) => search(_input.value))
_input.onFocus.listen((_) => showAutocomplete());

Of course, we can use it for more nested properties as well:

elm.attributes
 ..remove('whatever')
 ..putIfAbsent('value', 'key')

In the constructor, we're creating a custom Stream object, as we talked about earlier in this chapter. This stream passes only clicks outside our <ul> and <input> elements, which represent autocomplete container and a search input filed, respectively. We need to do this because we want to be able to hide the autocomplete when the user clicks outside of the search field. Using just onBlur in the input field (the lost focus event) wouldn't work as we wanted, because any click in the autocomplete would hide it immediately without emitting onClick inside the autocomplete.

This is a nice place for custom streams. We could also make our stream a public property and let other developers bind listeners to it. In vanilla JavaScript, you would probably do this as an event that checks both conditions and emits a second event and then listen only to the second event.

The rest of the code is mostly what we've already seen, but it's probably good idea to recap it in context. From now on, we'll skip obvious things such as DOM manipulation unless there's something important. We're also omitting CSS files because they aren't important to us:

// web/main.dart
class DocSearch {
  /* Properties are the same as above. */
  static fromJson(Element root, String url) async { /* ... */ }
  
  DocSearch(Element root, [Map<String, dynamic> inputDict]) { /* ... */
  }

  // Custom setter for dict property. When we change
  // the dictionary that this app uses, it will also change
  // the search list for the FuzzySearch instance.
  void set dict(Map<String, dynamic> dict) {
    _dict = dict;
    if (_fuzzy == null) {
      _fuzzy = new FuzzySearch(_dict.keys.toList());
    } else {
      _fuzzy.list = _dict.keys.toList();
    }
  }
  
  void search(String term) {
    if (term.length > 1) {
      int start = new DateTime.now().millisecondsSinceEpoch;
      List<String> results = 
         _fuzzy.search(_input.value.toLowerCase());
      int end = new DateTime.now().millisecondsSinceEpoch;
      // Debug performance. Note the usage of interpolation.
      print('$term: ${(end - start).toString()} ms'),

      renderAutocomplete(results);
    } else {
      hideAutocomplete();
    }
  }
  
  void renderAutocomplete(List<String> list) {
    if (list.length == 0) hideAutocomplete();
    // We'll use DocumentFragment as we talked about earlier.
    // http://jsperf.com/document-fragment-test-peluchetti
    DocumentFragment frag = new DocumentFragment();
    
    list.forEach((String key) {
      LIElement li = new LIElement();
      li.text = _dict[key]['name'];
      // Same as creating 'data-key' attribute or using data()
      // method in jQuery.
      li.dataset['key'] = key;
      frag.append(li);
    });
    
    _ul.children.clear();
    _ul.append(frag.clone(true));
    showAutocomplete();
  }
  
  void showDetail(String key) {
    Map<String, String> info = _dict[key];
    _root.querySelector('#detail > h1').text = info['name'];
    
    String desc = info['desc']
      ..replaceAll('\n\n', '</p><p>')
      ..replaceAll('\_', '_'),
    _root.querySelector('#detail > div').innerHtml =
        '<p>' + desc + '</p>';
    
    hideAutocomplete();
  }
  
  void showAutocomplete() { _ul.style.display = 'block'; }
  void hideAutocomplete() { _ul.style.display = 'none'; }
}

Note that we defined a custom setter for the dict property, so when we change it from anywhere in the code, it also changes the list property in the instance of the FuzzySearch class. Dart allows writing both custom getters and setters:

void set property(<T> newValue) {
  // Custom logic here.
}
<T> get property {
  // Custom logic here.
  // return an instance of <T>
}

Finally, we can test it in the browser:

Handling HTML elements

When you type at least two characters in the search field, it opens an autocomplete with suggested function names. You can click on one of them; it closes the autocomplete and shows a simple detail window with its name and description.

You can open Developer Tools and see how much time it took for Dart to traverse the entire 9,047 string list (it's about 25 ms on Intel Core Duo 2.5 GHz).

As we're already creating the FuzzySearch class as a reusable library, it would be nice if we could use it not just in Dart but also in JavaScript.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.240.196