RSS 2.0
Sign In
# Sunday, 18 January 2015

At first we have found that Typeahead (ui.bootstrap.typeahead) directive fits our needs, but later we run into its limitations.

These are tasks we required to solve:

  1. How to inform typeahead directive that it should update its list based on some event?
  2. How to implement an array of typeahead sources, where it's assumed that each next source delivers more data in cost of a longer working time?

The second task allows to show some data in popup almost immediately, while to provide more hints lately.

It took us a couple of days to answer both questions. The solution was either to write "typeahead" directive anew, or to write some additional "typeahead" directive to implement missing functionality. We have selected the later.

In the additional directive we:

  1. Handle a scople level event named "updateSource". Once the event is triggered the popup content is updated.
  2. Introduces scope.sources = function(value, sourcesFn) to build a source promise that knows how to update popup with more accurate data, when available.

Here is the code with small sample:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Typeahead</title>
  <link type="text/css" rel="stylesheet" href="bootstrap.css" />
  <style type="text/css">
    .ng-cloak { display: none; }
  </style>
  <script src="angular.js"></script>
  <script src="ui-bootstrap-tpls-0.12.0.js"></script>
  <script>
angular.module("app", ["ui.bootstrap"]).
directive(
  "typeahead",
  ["$q", function ($q)
  {
    return (
    {
        require: 'ngModel',
        link: function(scope, element, attrs, controller)
        {
          /**
           * @description 
           * Emits "updateSource" event on this scope.
           * @returns an event.
           */
          scope.updateSource = function() 
          { 
            return scope.$emit("updateSource"); 
          };

          scope.$on(
            "updateSource",
            function(event)
            {
              if (scope != event.targetScope)
              {
                return;
              }

              if (element.attr("aria-expanded") !== "true")
              {
                event.preventDefault();

                return;
              }

              var value = controller.$modelValue;

              for(var i = 0; i < controller.$parsers.length; i++)
              {
                if (value === undefined)
                {
                  break;
                }

                value = controller.$parsers[i](value);
              }
            });

          var typeaheadSources = null;
          var typeaheadValue = null;
          var typeaheadResult = null;

          /**
            * A wrapper of array of sources, where it's assumed that each next 
            * source delivers more data in cost of a longer working time.
            *
            * @param {object} value a typeahead hint.
            *
            * @param {function(object)} sourcesFn Function receiving typeahead
            *   hint, and returning an array of {Object|Promise}.
            *   If Object is passed rather than Promise then it should contain 
            *   following properties:
            *
            *    - `promise` - `{Promise}` - a result promise.
            *    - `cancel` - `{function()}` - optional function to cancel 
            *                 the promise.
            */
          scope.sources = function(value, sourcesFn)
          {
            var result = typeaheadResult;
            var prevValue = typeaheadValue;

            typeaheadValue = controller.$modelValue;
            typeaheadResult = null;

            if (result && (prevValue === typeaheadValue))
            {
              return result;
            }

            var sources = typeaheadSources;

            function cancel(count)
            {
              for (var i = 0; i < count; ++i)
              {
                var source = sources[i];

                source.cancel && source.cancel();
              }
            }

            sources && cancel(sources.length);
            typeaheadSources = sources = sourcesFn(value);

            return $q(function(resolve)
            {
              sources.forEach(function(source, index)
              {
                var promise = source.then ? source :
                  source.promise ? source.promise :
                  $q.when(source);

                promise.then(function(result)
                {
                  if (sources != typeaheadSources)
                  {
                    cancel(sources.length);

                    return;
                  }

                  if (element[0] != document.activeElement)
                  {
                    cancel(sources.length);
                    typeaheadSources = null;

                    return;
                  }

                  cancel(index);

                  if (!result || !result.length)
                  {
                    return;
                  }

                  if (resolve)
                  {
                    resolve(result);
                    resolve = null;
                  }
                  else
                  {
                    typeaheadResult = result;

                    if (scope.updateSource().defaultPrevented)
                    {
                      cancel(sources.length);
                      typeaheadSources = null;
                      typeaheadResult = null;
                      typeaheadValue = null;
                    }
                  }
                });
              });
            });
          }
        }
    });
  }]).
controller(
  "Test",
  ["$timeout",
  function ($timeout)
  {
    this.text = null;
    this.input = null;

    function source(timeout, count, value)
    {
      return (
      {
        promise: $timeout(
          function()
          {
            var result = [];

            for(var i = 0; i < count; ++i)
            {
              result.push({ text: value + " " + i, id: i });
            }

            return result;
          },
          timeout),

        cancel: function() { $timeout.cancel(this.promise); }
      });
    }

    // Gets an array of objects in format: 
    //   { promise: Promise, cancel: Function }
    this.suggest = function(value)
    {
      return [source(500, 5, value), source(2000, 10, value)];
    }
  }]);
  </script>
</head>
<body ng-app="app" ng-controller="Test as test">
  <input type="text"
    ng-model="test.text"
    typeahead="items.text for items in sources($viewValue, test.suggest)"
    typeahead-wait-ms="250"/><br />
  <input type="text" ng-model="test.input" />
</body>
</html>
    

Demo can be found at typeahead.html, and source at nesterovsky-bros/angularjs-api/bootstrap/typeahead.html.

Sunday, 18 January 2015 20:50:56 UTC  #    Comments [0] -
AngularJS | javascript
All comments require the approval of the site owner before being displayed.
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, b, blockquote@cite, em, i, strike, strong, sub, super, u) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

[Captcha]Enter the code shown (prevents robots):

Live Comment Preview
Archive
<2015 January>
SunMonTueWedThuFriSat
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567
Statistics
Total Posts: 360
This Year: 5
This Month: 1
This Week: 0
Comments: 223
Locations of visitors to this page
Disclaimer
The opinions expressed herein are our own personal opinions and do not represent our employer's view in anyway.

© 2018, Nesterovsky bros
All Content © 2018, Nesterovsky bros
DasBlog theme 'Business' created by Christoph De Baene (delarou)