<!DOCTYPE html>
<html>

  <head>
    <link rel="stylesheet" href="styles.css">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
    <script src="app.js"></script>
    <script src="fsListView.js"></script>
  </head>

  <body ng-app="app">
    <app>
      Loading...
    </app>
  </body>

</html>
<div class="app">
  <div class="examples-control">
    <label>
      Delay: <input type="number" ng-model="$ctrl.delay" min="10" max="5000" placeholder="Delay" />
    </label>
    <label>
      Number of items
      <input type="number" ng-model="$ctrl.items" min="0" max="50" placeholder="Number of items" />
    </label>
    <button ng-click="$ctrl.rebuild()">Apply</button>
  </div>
  <div class="examples">
    <div class="examples__item">
      <fs-list-view source="$ctrl.dataSource"
                    on-select="$ctrl.selectedItem = selectedItem"
                    selected="$ctrl.selectedItem">
        <list-item>
          <p>Name: {{ $parent.listItem.name }}</p>
          <p>Description: {{ $parent.listItem.description }}</p>
        </list-item>
      </fs-list-view>
    </div>
    <div class="examples__item">
      <fs-list-view source="$ctrl.dataSource"
                    on-select="$ctrl.selectedItem = selectedItem"
                    selected="$ctrl.selectedItem">
        <list-item class="modified">
          <strong>{{ $parent.listItem.name }}</strong>
          {{ $parent.listItem.description }}
        </list-item>
        <list-empty></list-empty>
        <list-loading>
          <div id="floatingCirclesG">
            <div class="f_circleG" id="frotateG_01"></div>
            <div class="f_circleG" id="frotateG_02"></div>
            <div class="f_circleG" id="frotateG_03"></div>
            <div class="f_circleG" id="frotateG_04"></div>
            <div class="f_circleG" id="frotateG_05"></div>
            <div class="f_circleG" id="frotateG_06"></div>
            <div class="f_circleG" id="frotateG_07"></div>
            <div class="f_circleG" id="frotateG_08"></div>
          </div>
        </list-loading>
        <list-empty>Nothing to see :(</list-empty>
      </fs-list-view>
    </div>
    <div class="examples__item">

      <fs-list-view source="$ctrl.dataSource"
                    on-select="$ctrl.selectedItem = selectedItem"
                    selected="$ctrl.selectedItem">
        <list-item>
          <p>What? {{ $parent.listItem.name }}</p>
          <p>Why?: {{ $parent.listItem.description }}</p>
        </list-item>
        <list-loading>
          <div class="windows8">
            <div class="wBall" id="wBall_1">
              <div class="wInnerBall"></div>
            </div>
            <div class="wBall" id="wBall_2">
              <div class="wInnerBall"></div>
            </div>
            <div class="wBall" id="wBall_3">
              <div class="wInnerBall"></div>
            </div>
            <div class="wBall" id="wBall_4">
              <div class="wInnerBall"></div>
            </div>
            <div class="wBall" id="wBall_5">
              <div class="wInnerBall"></div>
            </div>
          </div>
        </list-loading>
        <list-empty>(╯°□°)╯︵ ┻━┻)</list-empty>
      </fs-list-view>
    </div>
  </div>
</div>
"use babel";
    
class AppComponent {
  constructor($timeout) {
    this.$timeout = $timeout;
    this.delay = 500;
    this.items = 6;
    this.rebuild();
  }
  
  rebuild() {
    this.dataSource = this.$timeout(this.delay).then(() => {

        return range(this.items).map((id) => {
          return {
            name: `Test ${id}`,
            description: `Description ${id}`
          }
        })
      });
  }
}

function range(n) {
  return Array.apply(null, Array(n)).map((_, i) => i + 1);
}

angular
    .module('app', [])
    .component('app', {
      templateUrl: 'app.html',
      controller: AppComponent
    })
"use babel";

class FsListViewComponent {
  constructor() {
    "ngInject";
    this.collection = [];
  }

  get source() {
    return this.$source;
  }

  set source(source) {
    this.$source = source;
    this.collection = [];
    if (source && angular.isFunction (source.then)) {
      this.$handleSource(source);  
    }
  }

  get isEmpty () {
    return !this.isLoading && !this.collection.length;
  }

  set selectedListItem(item) {
    this.$selectedItem = item;
  }

  selectItem(item) {
    // наш компонент не содержит состояния
    // таким образом решать "выделять" объект или нет
    // решать должен использующий код
    this.onSelect({
      selectedItem: item
    });
  }

  $handleSource() {
    this.isLoading = true;

    this.$source
      .then(
        (data) => this.collection = data,
        (reason) => this.collection = []
      )
      .then(() => this.isLoading = false);
  }
}

angular.module('app').component('fsListView', {
  bindings: {
    source: '<',
    selectedListItem: '<selected',
    onSelect: '&'
  },
  transclude: {
    listItem: 'listItem',
    listEmpty: '?listEmpty',
    listLoading: '?listLoading'
  },
  template: `
    <div class="list-view">
       <div class="list-view__loading" ng-show="$ctrl.isLoading" ng-transclude="listLoading">Loading...</div>
       <div class="list-view__empty" ng-show="$ctrl.isEmpty" ng-transclude="listEmpty">No Data</div>
       <div class="list-view__scroll-view" ng-if="$ctrl.collection.length">
          <div  class="list-view__item"
                ng-class="{'list-view__item--selected': listItem === $ctrl.$selectedItem}"
                ng-click="$ctrl.selectItem(listItem)"
                ng-repeat="listItem in $ctrl.collection"
                ng-transclude="listItem"
          ></div>
       </div>
    </div>
  `,
  controller: FsListViewComponent
});

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: verdana, arial;
}

app {
  display: block;
  height: 100%;
}

app, .app {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

.examples-control {
  height: 50px;
  padding: 10px;
  font-size: 16px;
}
.examples-control input[type=number] {
  margin: 10px;
}
.examples {
  background: #efefef;
  flex: 1;
  display: flex;
  flex-direction: row;
  padding: 0 5px;
}
.examples__item {
  margin: 10px 5px;
  flex: 1;
  display: flex;
}
fs-list-view {
  display: flex;
  flex: 1;
  width: 100%;
}
.list-view {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
}
.list-view__loading {
  font-size: 24px;
  text-align: center;
}
.list-view__empty {
  font-size: 24px;
  text-align: center;
}
.list-view__scroll-view {
  flex: 1;
  padding: 0 10px;
  overflow: scroll;
}
.list-view__item {
  font-size: 16px;
  padding: 10px;
  margin: 10px 0;
  background: #fefefe;
  border: solid 1px #efefef;
  border-radius: 3px;
  box-shadow: 3px 3px 7px rgba(0,0,0,0.20);
}
.list-view__item--selected {
  background-color: #426daa;
  border-color: #334faa;
  color: #fff;
}
.windows8 {
  position: relative;
  width: 44px;
  height: 44px;
  margin: auto;
}
.windows8 .wBall {
  position: absolute;
  width: 42px;
  height: 42px;
  opacity: 0;
  transform: rotate(225deg);
  -o-transform: rotate(225deg);
  -ms-transform: rotate(225deg);
  -webkit-transform: rotate(225deg);
  -moz-transform: rotate(225deg);
  animation: orbit 5.7425s infinite;
  -o-animation: orbit 5.7425s infinite;
  -ms-animation: orbit 5.7425s infinite;
  -webkit-animation: orbit 5.7425s infinite;
  -moz-animation: orbit 5.7425s infinite;
}
.windows8 .wBall .wInnerBall {
  position: absolute;
  width: 5px;
  height: 5px;
  background: #000;
  left: 0px;
  top: 0px;
  border-radius: 5px;
}
.windows8 #wBall_1 {
  animation-delay: 1.256s;
  -o-animation-delay: 1.256s;
  -ms-animation-delay: 1.256s;
  -webkit-animation-delay: 1.256s;
  -moz-animation-delay: 1.256s;
}
.windows8 #wBall_2 {
  animation-delay: 0.243s;
  -o-animation-delay: 0.243s;
  -ms-animation-delay: 0.243s;
  -webkit-animation-delay: 0.243s;
  -moz-animation-delay: 0.243s;
}
.windows8 #wBall_3 {
  animation-delay: 0.5065s;
  -o-animation-delay: 0.5065s;
  -ms-animation-delay: 0.5065s;
  -webkit-animation-delay: 0.5065s;
  -moz-animation-delay: 0.5065s;
}
.windows8 #wBall_4 {
  animation-delay: 0.7495s;
  -o-animation-delay: 0.7495s;
  -ms-animation-delay: 0.7495s;
  -webkit-animation-delay: 0.7495s;
  -moz-animation-delay: 0.7495s;
}
.windows8 #wBall_5 {
  animation-delay: 1.003s;
  -o-animation-delay: 1.003s;
  -ms-animation-delay: 1.003s;
  -webkit-animation-delay: 1.003s;
  -moz-animation-delay: 1.003s;
}
@-o-keyframes orbit {
  0% {
    opacity: 1;
    z-index: 99;
    -o-transform: rotate(180deg);
    -o-animation-timing-function: ease-out;
  }

  7% {
    opacity: 1;
    -o-transform: rotate(300deg);
    -o-animation-timing-function: linear;
    -o-origin: 0%;
  }

  30% {
    opacity: 1;
    -o-transform: rotate(410deg);
    -o-animation-timing-function: ease-in-out;
    -o-origin: 7%;
  }

  39% {
    opacity: 1;
    -o-transform: rotate(645deg);
    -o-animation-timing-function: linear;
    -o-origin: 30%;
  }

  70% {
    opacity: 1;
    -o-transform: rotate(770deg);
    -o-animation-timing-function: ease-out;
    -o-origin: 39%;
  }

  75% {
    opacity: 1;
    -o-transform: rotate(900deg);
    -o-animation-timing-function: ease-out;
    -o-origin: 70%;
  }

  76% {
    opacity: 0;
    -o-transform: rotate(900deg);
  }

  100% {
    opacity: 0;
    -o-transform: rotate(900deg);
  }
}
@-ms-keyframes orbit {
  0% {
    opacity: 1;
    z-index: 99;
    -ms-transform: rotate(180deg);
    -ms-animation-timing-function: ease-out;
  }

  7% {
    opacity: 1;
    -ms-transform: rotate(300deg);
    -ms-animation-timing-function: linear;
    -ms-origin: 0%;
  }

  30% {
    opacity: 1;
    -ms-transform: rotate(410deg);
    -ms-animation-timing-function: ease-in-out;
    -ms-origin: 7%;
  }

  39% {
    opacity: 1;
    -ms-transform: rotate(645deg);
    -ms-animation-timing-function: linear;
    -ms-origin: 30%;
  }

  70% {
    opacity: 1;
    -ms-transform: rotate(770deg);
    -ms-animation-timing-function: ease-out;
    -ms-origin: 39%;
  }

  75% {
    opacity: 1;
    -ms-transform: rotate(900deg);
    -ms-animation-timing-function: ease-out;
    -ms-origin: 70%;
  }

  76% {
    opacity: 0;
    -ms-transform: rotate(900deg);
  }

  100% {
    opacity: 0;
    -ms-transform: rotate(900deg);
  }
}
@-webkit-keyframes orbit {
  0% {
    opacity: 1;
    z-index: 99;
    -webkit-transform: rotate(180deg);
    -webkit-animation-timing-function: ease-out;
  }

  7% {
    opacity: 1;
    -webkit-transform: rotate(300deg);
    -webkit-animation-timing-function: linear;
    -webkit-origin: 0%;
  }

  30% {
    opacity: 1;
    -webkit-transform: rotate(410deg);
    -webkit-animation-timing-function: ease-in-out;
    -webkit-origin: 7%;
  }

  39% {
    opacity: 1;
    -webkit-transform: rotate(645deg);
    -webkit-animation-timing-function: linear;
    -webkit-origin: 30%;
  }

  70% {
    opacity: 1;
    -webkit-transform: rotate(770deg);
    -webkit-animation-timing-function: ease-out;
    -webkit-origin: 39%;
  }

  75% {
    opacity: 1;
    -webkit-transform: rotate(900deg);
    -webkit-animation-timing-function: ease-out;
    -webkit-origin: 70%;
  }

  76% {
    opacity: 0;
    -webkit-transform: rotate(900deg);
  }

  100% {
    opacity: 0;
    -webkit-transform: rotate(900deg);
  }
}
@-moz-keyframes orbit {
  0% {
    opacity: 1;
    z-index: 99;
    -moz-transform: rotate(180deg);
    -moz-animation-timing-function: ease-out;
  }

  7% {
    opacity: 1;
    -moz-transform: rotate(300deg);
    -moz-animation-timing-function: linear;
    -moz-origin: 0%;
  }

  30% {
    opacity: 1;
    -moz-transform: rotate(410deg);
    -moz-animation-timing-function: ease-in-out;
    -moz-origin: 7%;
  }

  39% {
    opacity: 1;
    -moz-transform: rotate(645deg);
    -moz-animation-timing-function: linear;
    -moz-origin: 30%;
  }

  70% {
    opacity: 1;
    -moz-transform: rotate(770deg);
    -moz-animation-timing-function: ease-out;
    -moz-origin: 39%;
  }

  75% {
    opacity: 1;
    -moz-transform: rotate(900deg);
    -moz-animation-timing-function: ease-out;
    -moz-origin: 70%;
  }

  76% {
    opacity: 0;
    -moz-transform: rotate(900deg);
  }

  100% {
    opacity: 0;
    -moz-transform: rotate(900deg);
  }
}
#floatingCirclesG {
  position: relative;
  width: 125px;
  height: 125px;
  margin: auto;
  transform: scale(0.6);
  -o-transform: scale(0.6);
  -ms-transform: scale(0.6);
  -webkit-transform: scale(0.6);
  -moz-transform: scale(0.6);
}
.f_circleG {
  position: absolute;
  background-color: #efefef;
  height: 22px;
  width: 22px;
  border-radius: 12px;
  -o-border-radius: 12px;
  -ms-border-radius: 12px;
  -webkit-border-radius: 12px;
  -moz-border-radius: 12px;
  animation-name: f_fadeG;
  -o-animation-name: f_fadeG;
  -ms-animation-name: f_fadeG;
  -webkit-animation-name: f_fadeG;
  -moz-animation-name: f_fadeG;
  animation-duration: 1.2s;
  -o-animation-duration: 1.2s;
  -ms-animation-duration: 1.2s;
  -webkit-animation-duration: 1.2s;
  -moz-animation-duration: 1.2s;
  animation-iteration-count: infinite;
  -o-animation-iteration-count: infinite;
  -ms-animation-iteration-count: infinite;
  -webkit-animation-iteration-count: infinite;
  -moz-animation-iteration-count: infinite;
  animation-direction: normal;
  -o-animation-direction: normal;
  -ms-animation-direction: normal;
  -webkit-animation-direction: normal;
  -moz-animation-direction: normal;
}
#frotateG_01 {
  left: 0;
  top: 51px;
  animation-delay: 0.45s;
  -o-animation-delay: 0.45s;
  -ms-animation-delay: 0.45s;
  -webkit-animation-delay: 0.45s;
  -moz-animation-delay: 0.45s;
}
#frotateG_02 {
  left: 15px;
  top: 15px;
  animation-delay: 0.6s;
  -o-animation-delay: 0.6s;
  -ms-animation-delay: 0.6s;
  -webkit-animation-delay: 0.6s;
  -moz-animation-delay: 0.6s;
}
#frotateG_03 {
  left: 51px;
  top: 0;
  animation-delay: 0.75s;
  -o-animation-delay: 0.75s;
  -ms-animation-delay: 0.75s;
  -webkit-animation-delay: 0.75s;
  -moz-animation-delay: 0.75s;
}
#frotateG_04 {
  right: 15px;
  top: 15px;
  animation-delay: 0.9s;
  -o-animation-delay: 0.9s;
  -ms-animation-delay: 0.9s;
  -webkit-animation-delay: 0.9s;
  -moz-animation-delay: 0.9s;
}
#frotateG_05 {
  right: 0;
  top: 51px;
  animation-delay: 1.05s;
  -o-animation-delay: 1.05s;
  -ms-animation-delay: 1.05s;
  -webkit-animation-delay: 1.05s;
  -moz-animation-delay: 1.05s;
}
#frotateG_06 {
  right: 15px;
  bottom: 15px;
  animation-delay: 1.2s;
  -o-animation-delay: 1.2s;
  -ms-animation-delay: 1.2s;
  -webkit-animation-delay: 1.2s;
  -moz-animation-delay: 1.2s;
}
#frotateG_07 {
  left: 51px;
  bottom: 0;
  animation-delay: 1.35s;
  -o-animation-delay: 1.35s;
  -ms-animation-delay: 1.35s;
  -webkit-animation-delay: 1.35s;
  -moz-animation-delay: 1.35s;
}
#frotateG_08 {
  left: 15px;
  bottom: 15px;
  animation-delay: 1.5s;
  -o-animation-delay: 1.5s;
  -ms-animation-delay: 1.5s;
  -webkit-animation-delay: 1.5s;
  -moz-animation-delay: 1.5s;
}
@-o-keyframes f_fadeG {
  0% {
    background-color: #000;
  }

  100% {
    background-color: #efefef;
  }
}
@-ms-keyframes f_fadeG {
  0% {
    background-color: #000;
  }

  100% {
    background-color: #efefef;
  }
}
@-webkit-keyframes f_fadeG {
  0% {
    background-color: #000;
  }

  100% {
    background-color: #efefef;
  }
}
@-moz-keyframes f_fadeG {
  0% {
    background-color: #000;
  }

  100% {
    background-color: #efefef;
  }
}

p {
  margin: 10px 0;
}

list-item {
  display: block;
}

list-item strong {
  display: block;
  margin-bottom: 5px;
}

list-item.modified, list-item.modified p {
  text-align: right;
}