AngularJS + Masonry (with zoom)

by Anov Siradj

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 () {
                               ...