AngularJS + Masonry (with zoom)

by Avi Algaly

HTML

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/masonry/2.1.08/jquery.masonry.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.5/angular.min.js"></script>

<div ng-controller="MainCtrl">
  <input type="text" ng-model="nameFilter" />
  <a href="#" ng-click="order = 'id'">Order by id</a>
  <a href="#" ng-click="order = 'name'">Order by name</a>
  <a href="#" ng-click="order = 'age'">Order by age</a>

  <ul class="grid" masonry="true">
    <li ng-repeat="item in items | filter: { name: nameFilter } | orderBy: order">
      <span>{{item.name}}</span>
      <br /> <span>Age: {{item.age}}</span>
      <br /> <span>Company: {{item.company}}</span>
    </li>
  </ul>
</div>

CSS

.grid {
  border: solid 1px #ccc padding: 10px 10px 0 0;
}

.grid li {
  display: inline-block;
  width: 100px;
  height: 100px;
  border: solid 1px #ccc;
  margin: 0 0 10px 10px;
  background: #eee;
}

/**** Transitions ****/

.masonry,
.masonry li {
  /* apply before masonry add class */
  position: absolute;
  /* important to add it before enter animation start */
  -webkit-transition-duration: .7s;
  transition-duration: .7s;
  -webkit-transition-property: all;
  transition-property: all;
}

.masonry .masonry-enter {
  /* limit animated properties for entering elements */
  -webkit-transition-property: -webkit-transform, opacity;
  transition-property: transform, opacity;
}

.masonry-enter,
.masonry-leave.masonry-leave-active {
  opacity: 0;
  -webkit-transform: scale(0.1);
  transform: scale(0.1);
}

.masonry-leave,
.masonry-enter.masonry-enter-active {
  opacity: 1;
  -webkit-transform: scale(1);
  transform: scale(1);
}

JavaScript

angular.module('app', [])
  .directive("masonry", function() {
    var NGREPEAT_SOURCE_RE = '<!-- ngRepeat: ((.*) in ((.*?)( track by (.*))?)) -->';
    return {
      compile: function(element, attrs) {
        // auto add animation to brick element
        var animation = attrs.ngAnimate || "'masonry'";
        var $brick = element.children();
        $brick.attr("ng-animate", animation);

        // generate item selector (exclude leaving items)
        var type = $brick.prop('tagName');
        var itemSelector = type + ":not([class$='-leave-active'])";

        return function(scope, element, attrs) {
          var options = angular.extend({
            itemSelector: itemSelector
          }, scope.$eval(attrs.masonry));

          // try to infer model from ngRepeat
          if (!options.model) {
            var ngRepeatMatch = element.html().match(NGREPEAT_SOURCE_RE);
            if (ngRepeatMatch) {
              options.model = ngRepeatMatch[4];
            }
          }

          // initial animation
          element.addClass('masonry');

          // Wait inside directives to render
          setTimeout(function() {
            element.masonry(options);

            element.on("$destroy", function() {
              element.masonry('destroy')
            });

            if (options.model) {
              scope.$apply(function() {
                scope.$watchCollection(options.model, function(_new, _old) {
                  if (_new == _old) return;

                  // Wait inside directives to render
                  setTimeout(function() {
                    element.masonry("reload");
                  });
                });
              });
            }
          });
        };
      }
    };
  })
  .controller('MainCtrl', function($scope, $http) {
    $scope.items = [{
        "id": 0,
        "picture": "http://placehold.it/32x32",
        "age": 31,
        "name": "Mathews Goff"
      },
      {
        "id": 1,
        "picture":...