<div ng-controller="app as $ctrl">
  <h1>Custom selector</h1>

  <div>
    I like
    <my-select items="['apple', 'banana', 'carrot']"
               ng-model="$ctrl.value"
               ng-change="$ctrl.onSelected()">
    </my-select>
    .
  </div>

  <div>
    Value: {{$ctrl.value}}
  </div>

  <h3>History</h3>
  <div ng-repeat="s in $ctrl.history track by $index"
       ng-bind="s"></div>
</div>
my-select {
  display: inline-block;

  .select {
    position: relative;
    display: inline-block;
    padding: 8px;
    border: 1px solid gray;
    border-radius: 8px;
    background-color: white;
    box-sizing: content-box;
    white-space: nowrap;
    text-align: center;
    cursor: pointer;
    transition-duration:0.25s;

    .text {
      display: inline-block;
    }

    .popup {
      position: absolute;
      border: 1px solid gray;
      border-radius: 8px;
      overflow: hidden;
      box-shadow: 0 0 40px rgba(0,0,0,1.0);
      opacity: 0;
      transition-duration:0.25s;

      .item {
        padding:8px;
        color: black;
        transition-duration:0.25s;
      }

      .item:hover {
        background: cyan;
        color: black;
      }

      .item.selected {
        background: blue;
        color: white;
      }
    }
  }

  .select.empty {
    background: red;
  }

  .select:hover {
    background: blue;
    color: white;
  }
}
View Compiled
class MySelect {
  constructor($element, $timeout) {
    this.$element = $element
    this.$timeout = $timeout

    this.ngModel = $element.controller('ngModel')  // Must be exist, since it is specified in `require`
    //this.ngModel.$render = () => {
    //  console.log(`$render: ${this.ngModel.$viewValue}`)
    //}

    this.popupStyle = {
      display: 'none',
      backgroundColor: 'white',
      left: '-1px',  // -1 for border
      opacity: 0,
    }
  }

  $onInit() {
    this.$timeout(() => {  // I don't know why this is needed...
      const widths = this.items.map(item => MySelect.calcTextWidth(this.$element[0], item))
      this.width = widths.reduce((acc, x) => Math.max(acc, x))
      const style = window.getComputedStyle(this.$element[0])
      const border = 2
      this.height = parseInt(style.height, 10) - border
    })
  }

  getText() {
    return this.items[this.ngModel.$viewValue]
  }

  onClick($event) {
    $event.stopPropagation()
    $event.preventDefault()
    if (this.popupStyle.display !== 'none')
      return this.closePopup()

    delete this.popupStyle.display
    const selectedIndex = this.ngModel.$viewValue
    this.popupStyle.top = `${-(selectedIndex * this.height) - 1}px`  // -1 for border

    // Fade in
    this.popupStyle.opacity = 0
    this.$timeout(() => {
      this.popupStyle.opacity = 1
    })

    const docClicked = () => {
      document.removeEventListener('click', docClicked)
      this.$timeout(() => {
        this.closePopup()
      })
    }
    document.addEventListener('click', docClicked)
  }

  onClickItem($event, index) {
    $event.stopPropagation()
    $event.preventDefault()
    this.closePopup()

    this.ngModel.$setViewValue(index)
  }

  closePopup() {
    this.popupStyle.opacity = 0
    this.$timeout(() => {
      this.popupStyle.display = 'none'
    }, 500)
  }

  static calcTextWidth(parent, text) {
    const span = document.createElement('span')
    parent.appendChild(span)
    span.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap'
    span.innerText = text
    const width = span.offsetWidth
    parent.removeChild(span)
    return width
  }
}

angular.module('mySelect', [])
  .component('mySelect', {
    restrict: 'AE',
    controller: ['$element', '$timeout', MySelect],
    require: 'ngModel',
    bindings: {
      items: '=',
    },
    template: `
<span class="select"
      ng-class="{empty:!$ctrl.getText()}"
      ng-style="{width:$ctrl.width+'px'}"
      ng-click="$ctrl.onClick($event)">
  <!-- select box -->
  <span class="text"
        ng-bind="$ctrl.getText()"></span>

  <!-- popup -->
  <div class="popup"
       ng-style="$ctrl.popupStyle">
    <div ng-repeat="item in $ctrl.items"
         class="item"
         ng-class="{selected:$index===$ctrl.ngModel.$viewValue}"
         ng-bind="item"
         ng-click="$ctrl.onClickItem($event, $index)"></div>
  </div>
</span>
`,
  })

angular.module('app', ['mySelect'])
  .controller('app', [class AppController {
    constructor() {
      this.value = -1
      this.history = []
    }

    onSelected() {
      this.history.push(this.value)
    }
  }])

angular.element(document).ready(() => {
  angular.bootstrap(document, ['app'])
})
View Compiled
Rerun