<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript" src="https://code.angularjs.org/1.4.8/angular.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.0/lodash.js"></script>
  <script type="text/javascript" src="https://code.jquery.com/jquery-1.9.1.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-route.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-sanitize.min.js"></script>
  <link type="text/css" rel="stylesheet" href="style.css" />
  <link type="text/css" rel="stylesheet" href="treeView.css" />
  <link type="text/css" rel="stylesheet" href="//d170x0azrpb2m0.cloudfront.net/2.5.0/stylesheets/progress.min.css" />

  <script src="treeView.js"></script>

  <script src="script.js"></script>
  <script src="example-data.js"></script>
</head>
  <body>
    <div ng-app="testApp" ng-controller="testCtrl">
      
      <div ng-hide="hideUsage">
        <button type="button" class="btn btn-blue btn-sm" ng-click="hideUsage = !hideUsage">Toggle Usage</button>

        <h1>Usage</h1>
        <div>
          <p>
          The directive is <strong>tree-view</strong> and has several configuration options available via attributes.
          </p>
          <p>The input data should be an array of objects, each with an id and parent id.</p>
        </div>
        
        <br>
        <h2>Input Objects $$tree Property</h2>
        <p>
          The objects used as input for the directive can have a <strong>$$tree</strong> property, which is an object used to override the default behavior for an item in the tree. This <strong>$$tree</strong> property can contain two flags, <strong>canSelect</strong> and <strong>expanded</strong>, which the tree view with respect.
        </p>
        
        <br/>
        <h2>Attributes</h2>
        <div class="table-wrapper">
          <table class="table">
            <thead class="thead">
              <tr>
                <th scope="column">Attribute</th>
                <th scope="column">Type</th>
                <th scope="column">Description</th>
              </tr>
            </thead>
            <tbody class="tbody">
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">ng-model</span>
                    <span class="item-sub-info">Required</span>
                  </div>
                </td>
                <td title="Should be an item from controller $scope">
                  <div class="item-info">
                    <span class="item-name">Reference</span>
                    <span class="item-sub-info">Two-Way binding</span>
                  </div>
                </td>
                <td>Output will be placed in this variable. For Multi-select, will be an array. For single select it will be a single item.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">tree-data</span>
                    <span class="item-sub-info">Required</span>
                  </div>
                </td>
                <td title="Should be an item from controller $scope">
                  <div class="item-info">
                    <span class="item-name">Reference</span>
                    <span class="item-sub-info">One-Way binding</span>
                  </div>
                </td>
                <td>Should be set to the primary tree data source. Should be array of objects.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">multiple</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">Flag</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>If present, allows for multiple items to be selected at once.</td>
              </tr>
              <tr>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">leaf-only</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name" >Flag</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>If present, only items without children can be selected.</td>
              </tr>
              <tr>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">dropdown</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">Flag</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>If present, will use dropdown UI template.</td>
              </tr>
              <tr>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">select-children-with-parent</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">Flag</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td><strong>Only works with multi-select mode.</strong> If present, will select all children when parent is selected.</td>
              </tr>
              <tr>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">deselect-children-with-parent</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Being present on the directive is enough to trigger">
                  <div class="item-info">
                    <span class="item-name">Flag</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td><strong>Only works with multi-select mode.</strong> If present, will deselect all children when parent is deselected.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">order-by</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be set a property name. If present, will use assigned property to order the tree data. The ordering is applied to each level of tree structure.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">filter-by</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td><strong>Only works with dropdown mode.</strong> Should be set a property name, or multiple property names delimited by spaces and/or commas. If present, will use assigned property to order the tree data. The ordering is applied to each level of tree structure.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">item-id-property</span>
                    <span class="item-sub-info">Optional, Default: "id"</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be set a property name. Used to determine the id for an object.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">parent-id-property</span>
                    <span class="item-sub-info">Optional, Default: "parentId"</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be set a property name. Used to determine the parent id for an object.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">item-output-property</span>
                    <span class="item-sub-info">Optional</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be set a property name. If present, ng-model will be assigned this property value rather than the full object.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">display-property-tree</span>
                    <span class="item-sub-info">Optional, Default: "name"</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be set a property name. Used to display value for object within tree structure.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">display-property-dropdown</span>
                    <span class="item-sub-info">Optional, Default: "name"</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be set a property name. Used to display value for object within selected section of dropdown.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">dropdown-placeholder</span>
                    <span class="item-sub-info">Optional, Default: "Select something..."</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be a string value. Shown in dropdown area when nothing has been selected.</td>
              </tr>
              <tr>
                <td>
                  <div class="item-info">
                    <span class="item-name">dropdown-search-placeholder</span>
                    <span class="item-sub-info">Optional, Default: "Search..."</span>
                  </div>
                </td>
                <td title="Should be a string value">
                  <div class="item-info">
                    <span class="item-name">Value</span>
                    <span class="item-sub-info">No binding</span>
                  </div>
                </td>
                <td>Should be a string value. Shown in filter textbox within the dropdown. <strong>Only used when filter-by is present.</strong></td>
              </tr>
            </tbody>
          </table>
        </div>
        
        <hr/>
      </div>
      
    <button type="button" class="btn btn-blue btn-sm" ng-click="hideUsage = !hideUsage">Toggle Usage</button>

      <div ng-show="currentInputIdx < 0">
        <h4>Select Data</h4>
        <button type="button" class="btn btn-green" ng-click="currentInputIdx = 0">Animal Data</button>
        <button type="button" class="btn btn-green" ng-click="currentInputIdx = 1">Political Data</button>
      </div>
      <div ng-show="currentInputIdx >= 0">
        <button class="btn btn-yellow btn-xs" ng-click="currentInputIdx = -1">Switch Data</button>
        <pre class="scrollable" ng-show="showData">{{activeData | jsonPrint}}</pre>
        <button ng-hide="showData" class="btn btn-green btn-xs" ng-click="showData = true">Show Data</button>
        <button ng-show="showData" class="btn btn-red btn-xs" ng-click="showData = false">Hide Data</button>
      </div>
      
      <div ng-show="currentInputIdx >= 0">
        <hr>
        
        <!-- Actual Example Usage Starts Here -->

        <h1>Core Trees</h1>

        <br>
        <h4>Multiple Select</h4>
        <tree-view ng-model="selected[0]" tree-data="activeData" multiple order-by="name"></tree-view>
        Output: {{selected[0]}}
        <br>
        <h4>Single Select</h4>
        <tree-view ng-model="selected[1]" tree-data="activeData" order-by="name"></tree-view>
        Output: {{selected[1]}}

        <hr>
        <h4>Multiple Select (Leaf Only)</h4>
        <tree-view ng-model="selected[2]" tree-data="activeData" multiple order-by="name" leaf-only></tree-view>
        Output: {{selected[2]}}
        <br>
        <h4>Single Select (Leaf Only)</h4>
        <tree-view ng-model="selected[3]" tree-data="activeData" order-by="name" leaf-only></tree-view>
        Output: {{selected[3]}}
        
        <hr>

        <h1>Dropdowns</h1>
        <br>
        <h4>Multiple Select</h4>
        <tree-view ng-model="selected[4]" tree-data="activeData" multiple dropdown></tree-view>
        Output: {{selected[4]}}

        <br>
        <h4>Single Select</h4>
        <tree-view ng-model="selected[5]" tree-data="activeData" dropdown></tree-view>
        Output: {{selected[5]}}

        <hr>
        <h1>Dropdowns with Filtering</h1>
        <br>
        <h4>Multiple Select</h4>
        <tree-view ng-model="selected[6]" tree-data="activeData" multiple dropdown filter-by="name"></tree-view>
        Output: {{selected[6]}}

        <br>
        <h4>Single Select</h4>
        <tree-view ng-model="selected[7]" tree-data="activeData" dropdown filter-by="name"></tree-view>
        Output: {{selected[7]}}

        <hr>

        <h1>Dropdowns with Child-Parent Selection Behavior</h1>
        <br>
        <h4>Multiple Select: Select Children with Parent</h4>
        <tree-view ng-model="selected[8]" tree-data="activeData" multiple dropdown select-children-with-parent></tree-view>
        Output: {{selected[8]}}

        <br>
        <h4>Multiple Select: Deselect Children with Parent</h4>
        <tree-view ng-model="selected[9]" tree-data="activeData" multiple dropdown deselect-children-with-parent></tree-view>
        Output: {{selected[9]}}
        
        <br>
        <h4>Multiple Select: Select & Deselect Children with Parent</h4>
        <tree-view ng-model="selected[10]" tree-data="activeData" multiple dropdown select-children-with-parent deselect-children-with-parent></tree-view>
        Output: {{selected[10]}}

        <hr>
        <h1>Dropdowns with Shifting Output</h1>
        <nav class="app-nav">
          <ul class="nav-tabs">
            <li ng-class="{'active': currentIdx == i}" ng-repeat="i in shifts">
              <a href="#" ng-click="setShift(i)">Output {{i+1}}</a>
          </ul>
        </nav>
        <br>
        <tree-view ng-model="output.shifting" tree-data="activeData" multiple dropdown></tree-view>
        Current Output: {{output.shifting}}
        <br>
        All Output: {{shifting}}
        <br>
        <tree-view ng-model="output.shiftingSingle" tree-data="activeData" dropdown></tree-view>
        Current Output: {{output.shiftingSingle}}
        <br>
        All Output: {{shiftingSingle}}

        <hr>
        <h4>With Alternate Output</h4>
        <tree-view ng-model="output.shiftingAlt" tree-data="activeData" multiple dropdown item-output-property="id"></tree-view>
        Current Output: {{output.shiftingAlt}}
        <br>
        All Output: {{shiftingAlt}}
        <br>
        <tree-view ng-model="output.shiftingSingleAlt" tree-data="activeData" dropdown item-output-property="id"></tree-view>
        Current Output: {{output.shiftingSingleAlt}}
        <button class="btn btn-blue btn-xs" ng-click="testValue('output.shiftingSingleAlt')">Check Value</button>
        <br>
        All Output: {{shiftingSingleAlt}}
      </div>
    </div>
  </body>
</html>
(function(){

  angular.module('testApp',['puiTreeView', 'exampleData'])
    .controller('testCtrl',['$scope','flatData',
      function($scope, flatData){


        $scope.currentInputIdx = -1;

        $scope.selected = [];

        $scope.data = [];

        Object.defineProperty($scope,'activeData',{
          get: function(){
            return $scope.data[$scope.currentInputIdx];
          }
        });

        $scope.data[0] = flatData.animals;
        $scope.data[1] = flatData.political.all;

        $scope.shifting = [];
        $scope.shiftingSingle = [];

        $scope.shiftingAlt = [];
        $scope.shiftingSingleAlt = [];

        $scope.currentIdx = 0;

        $scope.shifts = [
          0,1,2,3,4
        ];

        $scope.setShift = function(i){
          $scope.currentIdx = i;
        };

        $scope.output = {};
        Object.defineProperty($scope.output,'shifting',{
          get: function(){
            return $scope.shifting[$scope.currentIdx];
          },
          set: function(val){
            $scope.shifting[$scope.currentIdx] = val;
          }
        });

        Object.defineProperty($scope.output,'shiftingSingle',{
          get: function(){
            return $scope.shiftingSingle[$scope.currentIdx];
          },
          set: function(val){
            $scope.shiftingSingle[$scope.currentIdx] = val;
          }
        });

        Object.defineProperty($scope.output,'shiftingAlt',{
          get: function(){
            return $scope.shiftingAlt[$scope.currentIdx];
          },
          set: function(val){
            $scope.shiftingAlt[$scope.currentIdx] = val;
          }
        });

        Object.defineProperty($scope.output,'shiftingSingleAlt',{
          get: function(){
            return $scope.shiftingSingleAlt[$scope.currentIdx];
          },
          set: function(val){
            $scope.shiftingSingleAlt[$scope.currentIdx] = val;
          }
        });


        $scope.testValue = function(prop){
          alert(_.get($scope,prop));
        };


      }]).filter('jsonPrint', [function(){
        
        return function(data){
          return angular.toJson(data,2);
        };
        
      }]);

})();
.scrollable {
  max-height: 500px;
  overflow: scroll;
}
Tree View with multiple UI presentations.

Implemented in Angular and lodash. Some jQuery is used in a couple spots.

(function(){

  angular.module('exampleData',[])
    .factory('politicalData',[function(){

      var federal = {
        id: 'UNIVERSE_Federal',
        name: 'Federal',
        children: [
          {
            id: 'BRANCH_Executive_Federal',
            name: 'Executive',
            children: [
              {
                id:'CATEGORY_Whitehouse',
                name: 'White House',
                children: [
                  {
                    id: 'OFFICE_POTUS',
                    name: 'President'
                  },
                  {
                    id: 'OFFICE_VPOTUS',
                    name: 'Vice President'
                  },
                  {
                    id: 'OFFICE_FLOTUS',
                    name: 'First Lady'
                  }
                ]
              }
              ,
              {
                id: 'AGENCY_ALL',
                name: 'Federal Agencies',
                children: [
                  {
                    id:'AGENCY_DOD',
                    name: 'Department of Defense'
                  },
                  {
                    id: 'AGENCY_HLS',
                    name: 'Homeland Security'
                  }
                ]
              }
            ]
          },
          {
            id: 'BRANCH_Legislative_Federal',
            name: 'Legislative',
            children:[
              {
                id:'OFFICE_US_House',
                name: 'US House',
                children:[
                  {
                    id:'LEADERSHIP_House_Speaker',
                    name:'Speaker of the House'
                  }
                ]
              },
              {
                id: 'OFFICE_US_Senate',
                name: 'US Senate',
                children: [
                  {
                    id: 'LEADERSHIP_Senate_Leader',
                    name: 'Senate Majority Leader'
                  }
                ]
              }
            ]
          }

        ]
      };


      var state = {
        id: 'UNIVERSE_State',
        name: 'State',
        children: [
          {
            id:'BRANCH_Executive',
            name: 'Executive',
            children: [
              {
                id:'OFFICE_Governor',
                name: 'Governor'
              }
            ]
          },
          {
            id: 'BRANCH_Legislative',
            name:'Legislative',
            children: [
              {
                id: 'OFFICE_State_Upper',
                name: 'Upper'
              },
              {
                id: 'OFFICE_State_Lower',
                name: 'Lower'
              }
            ]
          }
        ]
      };


      var political = [
        federal,
        state
      ];


      return {
        all: political,
        state: state.children,
        federal: federal.children
      };

    }])
    .factory('flatData', [
      'politicalData',
      function(politicalData){
      var flatData = [
        {
          id:1,
          name: 'Mammals'
        },
        {
          id:2,
          name: 'Reptiles'
        },
        {
          id: 3,
          name: 'Amphibians'
        },
        {
          id:4,
          name: 'Frog',
          parentId: 3
        },
        {
          id:5,
          name: 'Poison Dart Frog',
          parentId:4
        },
        {
          id:6,
          name:'Toad',
          parentId:3
        },
        {
          id:7,
          name: 'Lion',
          parentId: 1
        },
        {
          id:8,
          name: 'Tiger',
          parentId: 1
        },
        {
          id:9,
          name: 'Bear',
          parentId:1
        },
        {
          id:10,
          name: 'Black Bear',
          parentId:9
        },
        {
          id: 11,
          name: 'Grizzly Bear',
          parentId: 9
        },
        {
          id: 12,
          name: 'Snake',
          parentId: 2
        },
        {
          id: 13,
          name: 'Lizard',
          parentId: 2
        }

      ];

      function flattenData(data, idProp,parentIdProp, childrenProperty, result){
        result = result || [];

        if(_.isArray(data)){
          _.forEach(data, function(d){
            flattenData(d, idProp, parentIdProp, childrenProperty, result);
          });
          return result;
        }

        var id = _.get(data,idProp);
        var children = _.get(data, childrenProperty);

        result.push(data);
        _.forEach(children, function(child){
          _.set(child,parentIdProp,id);
          flattenData(child,idProp,parentIdProp, childrenProperty,result);
        });
        _.set(data,childrenProperty,undefined);
        return result;
      }

      function rebuildWithNewTop(newTop,data,idProp, parentIdProp){
        data = _.cloneDeep(data);
        var topId = _.get(newTop,idProp);
        _.forEach(data, function(d){

          var parent = _.get(d,parentIdProp);
          if(!parent){
            _.set(d,parentIdProp,topId);
          }
        });
        return _.flatten([newTop,data]);
      }


        var flatState = flattenData(_.cloneDeep(politicalData.state),'id','parentId','children');
        var flatFederal = flattenData(_.cloneDeep(politicalData.federal),'id','parentId','children');

        var stateTop = {id:'UNIVERSE_State', name: 'State'};
        var federalTop = {id: 'UNIVERSE_Federal', name:'Federal'};


        var flatAll = _.flatten([
          rebuildWithNewTop(stateTop,flatState,'id','parentId'),
          rebuildWithNewTop(federalTop,flatFederal,'id','parentId')
        ]);

      return {
        animals: flatData,
        political:
        {
          all: flatAll,
          state: flatState,
          federal: flatFederal
        }
      };

    }]);


})();
(function () {

  angular.module('puiTreeView', ['ngSanitize'])
    .constant('treeViewConfig', {
      templates: {
        dropdown: {
          multiple: 'multi-select.tpl.html',
          single: 'single-select.tpl.html'
        },
        simple: 'core.tpl.html'
      }
    })
    .directive('treeView', ['treeViewConfig', 'attributeHelper', '$timeout',
      function (treeViewConfig, attributeHelper, $timeout) {

        return {

          restrict: 'E',
          controllerAs: '$tree',
          require: ['treeView', 'ngModel'],
          bindToController: {
            treeData: '='
          },
          scope: true,
          templateUrl: function (elem, attrs) {

            var multiple = attributeHelper.truthy(attrs.multiple);
            var dropdown = attributeHelper.truthy(attrs.dropdown);

            var tpl = treeViewConfig.templates.simple;
            if (dropdown) {
              tpl = multiple ?
                treeViewConfig.templates.dropdown.multiple :
                treeViewConfig.templates.dropdown.single;
            }

            return attrs.templateUrl || tpl;
          },
          controller: [
            '$scope', '$attrs', '$parse', 'attributeHelper', '$sce',
            function ($scope, $attrs, $parse, attributeHelper, $sce) {

              var ctrl = this;

              function parse(attr) {

                return function (val) {
                  var parsed = $parse(attr);

                  if (arguments.length) {
                    //set
                    parsed.assign($scope, val);
                  } else {
                    //get
                    return parsed($scope);
                  }

                };
              }


              function isInitiallySelected(item) {
                var modelData = ctrl.selected();
                if (!modelData || !modelData.length) {
                  return false;
                }
                return _.any(modelData, function(value){
                  if(ctrl.config.outputProperty){
                    return _.isEqual(value, _.get(item,ctrl.config.outputProperty));
                  }
                  return _.isEqual(value,item);
                });
              }

              function initWrapper(item, depth) {
                var data = {
                  children: [],
                  selected: isInitiallySelected(item),
                  canSelect: true,
                  canExpand: false,
                  expanded: false,
                  filterExpanded: false,
                  filterMatch: false,
                  item: item,
                  isChild: true,
                  depth: depth,
                  show: true,
                  init: _.extend({canSelect:true, expanded: false}, _.get(item,'$$tree') || {})
                };

                ctrl.allData.push(data);
                return data;
              }

              function updateWrapper(data) {

                if (!data.init.canSelect || (ctrl.config.leafOnly && data.children.length) ) {
                  data.canSelect = false;
                }
                if (data.children.length > 0) {
                  data.canExpand = true; //for css class
                  if(data.init.expanded){
                    data.expanded = true;
                  }
                }
              }

              function initItemFlat(item, allItems, depth) {
                depth = depth || 0;
                var data = initWrapper(item, depth);

                var children = getChildren(item, allItems);
                _.forEach(children, function (child) {
                  var childData = initItemFlat(child, allItems, depth + 1);
                  data.children.push(childData);
                });
                updateWrapper(data);
                return data;
              }


              function syncToModel(){
                ctrl.status.modifyingSelected = true;

                if(ctrl.config.multiple){
                  var selectedOutput = ctrl.config.outputProperty ?
                    _.pluck($scope.dropdown.selected,ctrl.config.outputProperty) :
                    $scope.dropdown.selected;
                  ctrl.selected(selectedOutput);
                } else {
                  var output = ctrl.config.outputProperty ?
                    _.get($scope.item,ctrl.config.outputProperty) :
                    $scope.item;
                  ctrl.selected(output);
                }
                $timeout(function () {
                  ctrl.status.modifyingSelected = false;
                });
              }

              function getDataForItem(item) {
                return _.find(ctrl.allData, function (data) {
                  return angular.equals(data.item, item);
                });
              }

              function getDataForOutputValue(val){
                if(!ctrl.config.outputProperty){
                  return getDataForItem(val);
                }
                return _.find(ctrl.allData, function(data){
                  var item = data.item;
                  var oVal = _.get(item, ctrl.config.outputProperty);
                  return angular.equals(oVal,val);
                });
              }

              function syncFromModel(){
                ctrl.status.modifyingSelected = true;

                var selected = ctrl.selected();
                deselectAll();
                if(ctrl.config.multiple){
                  if(!_.isArray(selected)){
                    selected = [selected];
                  }
                  $scope.dropdown.selected = _(selected).map(getDataForOutputValue).filter().forEach(function(data){
                    _.set(data,'selected',true);
                  }).pluck('item').value();
                } else {
                  var selectedData = getDataForOutputValue(selected);
                  _.set(selectedData,'selected',true);
                  $scope.item = _.get(selectedData,'item');
                }

                $timeout(function () {
                  ctrl.status.modifyingSelected = false;
                });
              }

              function addToData(data) {

                var item = data.item;

                if(ctrl.config.multiple){
                  $scope.dropdown.selected = $scope.dropdown.selected || [];
                  $scope.dropdown.selected.push(item);
                } else {
                  $scope.item = item;
                }
                syncToModel();
              }

              function removeFromData(data) {
                if (ctrl.config.multiple) {
                  //is array
                  var mData = $scope.dropdown.selected;
                  var idx = _.indexOf(mData, data.item);
                  _.pullAt(mData, idx);
                } else {
                  $scope.item = null;
                }
                syncToModel();
              }

              function deselectAll(){
                //deselect everything
                _.forEach(ctrl.allData, function (data) {
                  data.selected = false;
                });
              }

              function doSelect(data) {
                if (!ctrl.config.multiple) {
                  //deselect everything
                  deselectAll();
                }
                data.selected = true;
                addToData(data);
                $scope.dropdown.filter = '';
                
                if(ctrl.config.multiple && ctrl.config.behavior.selectChildrenWithParent){
                  _.forEach(data.children,function(child){
                    doSelect(child);
                  });
                }
              }

              function doDeselect(data) {
                //deselect
                data.selected = false;
                removeFromData(data);
                if(ctrl.config.multiple && ctrl.config.behavior.deselectChildrenWithParent){
                  _.forEach(data.children,function(child){
                    doDeselect(child);
                  });
                }
              }

              function getChildren(item, allItems) {
                var parentId = _.get(item, ctrl.config.idProperty);
                return _.filter(allItems, function (other) {
                  return _.isEqual(_.get(other, ctrl.config.parentIdProperty), parentId);
                });
              }

              function toggleSelect(data) {

                if (!data.selected) {
                  if(!data.canSelect){
                    return;
                  }
                  var selected = ctrl.selected();
                  var children = data.children;
                  if (ctrl.config.leafOnly && children && children.length > 0) {
                    return;
                  }
                  if (!ctrl.config.multiple && selected) {
                    //deselect current
                    var other = _.findWhere(ctrl.allData, { selected: true });
                    if (other) {
                      doDeselect(other);
                    }
                  }
                  doSelect(data);
                  $scope.dropdown.expanded = false;
                } else {
                  doDeselect(data);
                }
              }

              function toggleExpand(data) {
                if(data.filterExpanded){
                  data.filterExpanded = false;
                  data.expanded = false;
                } else {
                  data.expanded = !data.expanded;
                }
              }

              var matchesFilters = _.memoize(function (data, filterValue) {
                var ret = false;
                _.forEach(ctrl.filters, function (prop) {
                  var val = _.get(data.item, prop);
                  val = val ? val.toString() : val;
                  if (val && val.toLowerCase().indexOf(filterValue) >= 0) {
                    ret = true;
                    return false;//short-circuit
                  }
                });
                return ret;
              }, function (data, filterValue) {
                return JSON.stringify(data.item) + "::" + filterValue;
              });

              // Dropdown stuff
              function doNotClose(event) {
                if ($scope.dropdown.expanded) {
                  event.stopPropagation();
                }
              }
              
              function closeDropdown(){
                $scope.dropdown.expanded = false;
                $scope.dropdown.filter = '';
              }

              function blur() {
                if ($scope.dropdown.expanded) {
                  closeDropdown();
                }
              }
              function expandDropdown() {
                if (!$scope.dropdown.expanded) {
                  $scope.dropdown.expanded = true;
                } else {
                  closeDropdown();
                }
              }

              function deselectItem(item, event) {
                if (!arguments.length) {
                  item = $scope.item;
                }
                var data = getDataForItem(item);
                if (data && data.selected) {
                  toggleSelect(data);
                }
                if (event)
                  event.stopPropagation();
              }
              // ---

              function orderTree(data) {
                if (!ctrl.config.orderBy) {
                  return;
                }

                _.forEach(data.children, function (child) {
                  orderTree(child);
                });

                data.children = _.sortBy(data.children, function (child) {
                  return _.get(child.item, ctrl.config.orderBy);
                });
              }

              function initSelections(skipSyncToModel) {
                syncFromModel();
                if (!skipSyncToModel) {
                  syncToModel();
                }
              }

              function init() {
                ctrl.config = {
                  data: ctrl.treeData,
                  multiple: attributeHelper.truthy($attrs.multiple),
                  dropdown: attributeHelper.truthy($attrs.dropdown),
                  idProperty: $attrs.itemIdProperty || 'id',
                  parentIdProperty: $attrs.itemParentIdProperty || 'parentId',
                  outputProperty: $attrs.itemOutputProperty,
                  orderBy: $attrs.orderBy,
                  leafOnly: attributeHelper.truthy($attrs.leafOnly),
                  displayProperties: {
                    tree: $attrs.displayPropertyTree || 'name',
                    dropdown: $attrs.displayPropertyDropdown || 'name'
                  },
                  dropdown: {
                    placeholder: $attrs.dropdownPlaceholder || 'Select something...',
                    searchPlaceholder: $attrs.dropdownSearchPlaceholder || 'Search...'
                  },
                  behavior:{
                    selectChildrenWithParent: attributeHelper.truthy($attrs.selectChildrenWithParent),
                    deselectChildrenWithParent: attributeHelper.truthy($attrs.deselectChildrenWithParent)
                  }
                };

                ctrl.filters = _.filter(($attrs.filterBy || '').split(/\s+|,/));

                ctrl.status = {
                  modifyingSelected: false
                };
                ctrl.allData = [];
                ctrl.selected = parse($attrs.ngModel);

                var flat = ctrl.config.data;//();
                //get all top level items
                var root = {
                  children: []
                };

                var topItems = _.filter(flat, function (item) {
                  return !_.get(item, ctrl.config.parentIdProperty);
                });

                _.forEach(topItems, function (item) {
                  var topData = initItemFlat(item, flat);
                  topData.isChild = false;
                  root.children.push(topData);
                });
                orderTree(root);

                $scope.hasFilters = !!ctrl.filters.length;
                $scope.data = root;

                $scope.displayHtml = $sce.trustAsHtml('{{data.item.' + ctrl.config.displayProperties.tree + '}}');
                $scope.selectedHtml = $sce.trustAsHtml('{{item.' + ctrl.config.displayProperties.dropdown + '}}');
                $scope.dropdown = {
                  expanded: false,
                  filter: '',
                  placeholder: ctrl.config.dropdown.placeholder,
                  searchPlaceholder: ctrl.config.dropdown.searchPlaceholder,
                  doNotClose: doNotClose,
                  blur: blur,
                  expand: expandDropdown,
                  deselect: deselectItem
                };

                //init selections
                initSelections(true);
              }

              $scope.toggleExpand = toggleExpand;
              $scope.toggleSelect = toggleSelect;
              init();

              $scope.$watchCollection($attrs.treeData, function (newValue, oldValue) {
                if (!_.isEqual(oldValue,newValue)) {
                  init();
                }
              });

              if (ctrl.config.multiple) {
                $scope.$watchCollection($attrs.ngModel, function (newValue, oldValue) {
                  if (!_.isEqual(oldValue,newValue) && !ctrl.status.modifyingSelected) {
                    initSelections();
                  }
                });
              } else {
                $scope.$watch($attrs.ngModel, function (newValue, oldValue) {
                  if (!_.isEqual(oldValue,newValue) && !ctrl.status.modifyingSelected) {
                    initSelections();
                  }
                });
              }
              
              function handleFilter(data, filterValue, parentIsMatch){
                var isMatch = matchesFilters(data, filterValue);
                var childIsMatch;
                if(data.children && data.children.length){
                  _.forEach(data.children, function(child){
                    handleFilter(child, filterValue, isMatch || parentIsMatch);
                    if(child.filterMatch || child.filterExpanded){
                      childIsMatch = true;
                    }
                  });
                }
                
                data.filterMatch = isMatch;
                data.filterExpanded = childIsMatch;
                data.show = isMatch || parentIsMatch || childIsMatch;
                
              }
              
              function resetAll(){
                
                _.forEach(ctrl.allData, function(data){
                  data.filterMatch = false;
                  data.filterExpanded = false;
                  data.show = true;
                });
                
              }
              
              if(ctrl.config.dropdown && ctrl.filters.length){
                //filters and dropdown are enabled
                
                $scope.$watch(function(){
                  return $scope.dropdown.filter;
                }, function(newValue, oldValue){
                  if(!_.isEqual(oldValue,newValue)){
                    if(!newValue){
                      //show all
                      resetAll();
                    } else {
                      //figure out what should be visible
                      newValue = newValue.toLowerCase();
                      _.forEach($scope.data.children, function(topData){
                        handleFilter(topData,newValue);
                      });
                    }
                  }
                });
              }


            }]
        };



      }])
    .factory('attributeHelper', [
      function () {

        return {
          truthy: function (val) {
            return angular.isDefined(val) && (val === '' || val.toLowerCase() === 'true');
          }
        }

      }])
    .directive('compileTemplate', ["$compile", "$parse", function ($compile, $parse) {
      //src: http://stackoverflow.com/questions/25406461/angularjs-ng-bind-html-2way-data-binding
      return {
        restrict: 'A',
        link: function ($scope, element, attr) {
          var parse = $parse(attr.ngBindHtml);
          function value() { return (parse($scope) || '').toString(); }

          $scope.$watch(value, function () {
            $compile(element, null, -9999)($scope);
          });
        }
      }
    }]).directive("outsideClick", ['$document', '$parse', '$timeout', function ($document, $parse, $timeout) {
    return {
      link: function ($scope, $element, $attributes) {
        var scopeExpression = $attributes.outsideClick,
          enabled = $attributes.outsideClickEnabled,
          onDocumentClick = function (event) {
            //need jquery for this part
            var isChild = $($element).has(event.target).length > 0;
            var isSelf = $element[0] == event.target;
            var isInside = isChild || isSelf;
            if (!isChild && !isSelf && !isInside) {
              $scope.$apply(scopeExpression);
            }
          };

        $document.on("click", onDocumentClick);
        $element.on('$destroy', function () {
          $document.off("click", onDocumentClick);
        });
      }
    };
  }]).directive('focusMe', ['$timeout','$parse',function ($timeout, $parse) {
    //based on watching a flag, set focus to an element
    //src: http://stackoverflow.com/questions/14833326/how-to-set-focus-on-input-field
    return {
      link: function (scope, element, attrs) {
        var model = $parse(attrs.focusMe);
        scope.$watch(model, function (value) {
          if (value === true) {
            $timeout(function () {
              element[0].focus();
            });
          }
        });
      }
    };
  }]);


})();
<div class="treeview-container" outside-click="dropdown.blur()">
    <div class="selected-items-wrapper" ng-click="dropdown.expand()">
        <div class="no-selection-placeholder">
            <span ng-hide="dropdown.selected.length">{{dropdown.placeholder}}</span>
            <span class="glyphicons glyphicons-plus open"></span>
        </div>
        <div class="selected-item-container" ng-repeat="item in dropdown.selected">
            <span ng-click="$event.stopPropagation()" class="selected-item">
                <span ng-bind-html="selectedHtml" compile-template></span>&nbsp;<a class="selected-item-deselect" ng-click="dropdown.deselect(item,$event)">&times;</a>
            </span>
        </div>
    </div>
    <div class="dropdown-list" ng-show="dropdown.expanded">
        <div class="typeahead-container" ng-if="hasFilters">
            <input type="text" ng-model="dropdown.filter" placeholder="{{dropdown.searchPlaceholder}}" ng-click="dropdown.doNotClose($event)" class="input-element" focus-me="dropdown.expanded" ng-model-options="{debounce:1}"/>
        </div>
        <div ng-include="'core.tpl.html'"></div>
    </div>
</div>
<div class="treeview-container" outside-click="dropdown.blur()">
    <div class="selected-items-wrapper" ng-click="dropdown.expand()">
        <div ng-show="item" class="selected-single-item-wrapper">
            <span class="selected-single-item">
                <span ng-bind-html="selectedHtml" compile-template></span>
                <a ng-click="dropdown.deselect()" style="float:right"><span class="glyphicons glyphicons-remove-2 single-item-remove"></span></a>
            </span>
        </div>
        <div ng-hide="item" class="no-selection-placeholder">
            <span>{{dropdown.placeholder}}</span>
        </div>
    </div>
    <div class="dropdown-list" ng-show="dropdown.expanded">
        <div class="typeahead-container" ng-if="hasFilters">
            <input type="text" ng-model="dropdown.filter" placeholder="{{dropdown.searchPlaceholder}}" ng-click="dropdown.doNotClose($event)" class="input-element" focus-me="dropdown.expanded" ng-model-options="{debounce:1}" />
        </div>
        <div ng-include="'core.tpl.html'"></div>
    </div>
</div>
<script type="text/ng-template" id="treeNode">
    <ul>
        <li ng-repeat="data in data.children"
            data-depth="{{data.depth}}"
            ng-show="data.show"
            class="tree-item-row"
            ng-class="{'tree-item-row-expanded':data.expanded, 'tree-item-row-collapsed': data.canExpand && !data.expanded, 'tree-item-row-selected':data.selected, 'tree-item-row-can-select':data.canSelect, 'tree-item-row-cannot-select':!data.canSelect, 'tree-item-row-cannot-expand': !data.children || !data.children.length}">
            <a ng-click="toggleExpand(data)"
               ng-show="data.children && data.children.length"
               class="tree-item-expand"
               ng-class="{'tree-expand-arrow-expanded': data.expanded, 'tree-expand-arrow-collapsed':!data.expanded}">
                <span class="glyphicons"
                      ng-class="{'glyphicons-chevron-down': data.expanded || data.filterExpanded, 'glyphicons-chevron-right': !(data.expanded || data.filterExpanded)}"></span>
            </a>
            <a
                    ng-click="toggleSelect(data)"
                    class="tree-item"
                    ng-class="{'selected':data.selected,'can-select':data.canSelect, 'cannot-select':!data.canSelect}"
                    compile-template
                    ng-bind-html="displayHtml"></a>
            <div class="child-tree"
                 ng-show="data.expanded || data.filterExpanded"
                 ng-include="'treeNode'"></div>
        </li>
    </ul>
</script>
<div ng-include="'treeNode'" class="root-tree"></div>
.dropdown-list {
    -webkit-box-shadow: 1px 1px 3px #dde4ed;
    -ms-box-shadow: 1px 1px 3px #dde4ed;
    box-shadow: 1px 1px 3px #dde4ed;
    z-index: 9999;
    -ms-border-bottom-right-radius: 2px;
    border-bottom-right-radius: 2px;
    -ms-border-bottom-left-radius: 2px;
    border-bottom-left-radius: 2px;
    background-color: white;
    border: 1px solid #B2C1DB;
    margin-top: -1px;
    position: absolute;
    width: 100%;
}

.tree-item-row .selected {
    color: #98a4b8;
}

.dropdown-list .root-tree {
    max-height: 250px;
    overflow: auto;
}

.dropdown-list .typeahead {
    border: 1px solid #B2C1DB !important;
    -ms-border-radius: 2px;
    border-radius: 2px;
    display: block;
    padding: 10px 14px;
    width: 100%;
}

.dropdown-list .typeahead:focus {
    border: 1px solid #B2C1DB !important;
}

.dropdown-list .selected {
    color: #98a4b8;
}

.dropdown-list .selected:hover {
    color: #98a4b8;
}

.typeahead-container {
    position: relative;
    padding: 14px 10px 0 14px;
    margin-bottom: 5px;
}

.selected {
    color: #394357;
}

.tree-item-row-can-select > .tree-item:hover {
    color: #0d98e6;
}

.tree-item-row-cannot-select > .tree-item:hover {
    cursor: default;
}

.selected-items-wrapper {
    background-color: white;
}

.tree-item-expand:hover {
    color: #394357;
}

.selected-item-deselect {
    color: gray;
    margin-left: 6px;
}

.selected-item-deselect:hover {
    color: #394357;
    text-decoration: none;
}

.selected-single-item {
    display: block;
}

.no-selection-placeholder {
    display: block;
    color: #98a4b8;
    padding-top: 2px;
}

.tree-item-row-cannot-expand .tree-item {
    margin-left: 15px;
}

.selected-item-container + .typeahead {
    min-height: 34px;
    padding: 6px 8px;
}

.single-item-remove {
    height: 18px;
    cursor: pointer;
}

.treeview-container {
    position: relative;
}

.tree-item-expand {
    display: inline-block;
    line-height: 22px;
    vertical-align: middle;
    cursor: pointer;
}

.tree-item-expand span {
    font-size: 12px;
    margin-right: 0;
}

.typeahead {
    border: 0px !important;
    font-size: 14px;
    -moz-min-width: 50px;
    -ms-min-width: 50px;
    -o-min-width: 50px;
    -webkit-min-width: 50px;
    min-width: 50px;
    outline: none;
    padding: 3px;
}

.typeahead:focus {
    border: 0px !important;
    outline: none;
}

.root-tree ul {
    color: #394357;
    list-style-type: none;
    padding: 0 14px;
}

.root-tree ul ul {
    padding: 0 24px;
}

.selected-item {
    -ms-border-radius: 2px;
    border-radius: 2px;
    border: 1px solid #B2C1DB;
    float: left;
    line-height: 16px;
    padding: 6px 8px;
    margin: 2px;
}

.selected-item-deselect {
    font-size: 16px;
    cursor: pointer;
}

.no-selection-placeholder .open {
    float: right;
    color: white;
    font-size: 1px;
}

.selected-items-wrapper {
    cursor: pointer;
    -ms-border-radius: 2px;
    border-radius: 2px;
    border: 1px solid #B2C1DB;
    min-height: 38px;
    overflow: auto;
    font-size: 14px;
    padding: 8px 10px;

}

a.tree-item {
    color: black;
    text-decoration: none;
}

a.tree-item.cannot-select {
    cursor: default;
}

a.tree-item.cannot-select:hover {
    color:black;
}

a.tree-item.can-select {
    cursor: pointer;
}

.tree-item-row {
    line-height: 22px;
    min-height: 25px;
}
- Ability to collapse trees when filtering
  - Would need to rearrange things. ng-show would be off a property on wrapper data
  - property would be populated via a $watch on $scope.dropdown.filter
- (configurable) When filtering, show all children if parent matches filter
  - only expand parent if a child matches