web-dev-qa-db-fra.com

Maçonnerie avec AngularJS

Je développe une application "galerie d'art} _".

N'hésitez pas à baisser la source sur github et à jouer avec.

Plunker avec source complète.

Le travail en cours pour que la maçonnerie joue Nice avec Angular:

.directive("masonry", function($parse) {
  return {
    restrict: 'AC',
    link: function (scope, elem, attrs) {
      elem.masonry({ itemSelector: '.masonry-brick'});
    }
  };     
})
.directive('masonryBrick', function ($compile) {
  return {
    restrict: 'AC',
    link: function (scope, elem, attrs) {
      scope.$watch('$index',function(v){
        elem.imagesLoaded(function () {
          elem.parents('.masonry').masonry('reload');
        });
      });
    }
  };    
});

Cela ne fonctionne pas bien parce que:

  • Au fur et à mesure que le contenu grandit, les frais généraux liés à l'activation du chargement sur l'ensemble du conteneur augmentent également.

La fonction reload:

  • Ne pas "ajouter", plutôt réorganiser tous les éléments du conteneur.
  • Est-ce que fonctionne pour déclencher un rechargement lorsque les éléments sont filtrés d'un ensemble de résultats.

Dans le contexte de l'application à laquelle j'ai donné les liens ci-dessus, ce problème devient très facile à reproduire. 

Je recherche une solution qui utilisera des directives pour tirer parti de:

.masonry('appended', elem) et .masonry('prepended', elem) 

Plutôt que d'exécuter .masonry('reload') à chaque fois.

.masonry('reload') lorsque les éléments sont supprimés du jeu de résultats.


MODIFIER

Le projet a été mis à jour pour utiliser la solution de travail ci-dessous.

Prenez la source sur GitHub

Voir une version de travail sur Plunker

30
Dan Kanze

Je joue un peu plus avec cela et la réponse de @ ganaraj est plutôt soignée. Si vous collez une $element.masonry('resize'); dans la méthode appendBrick de son contrôleur et que vous tenez compte de Le chargement des images, il semble que cela fonctionne.

Voici un fork de plunker avec: http://plnkr.co/edit/8t41rRnLYfhOF9oAfSUA

Cela est nécessaire parce que le nombre de colonnes n'est calculé que lorsque la maçonnerie est initialisée sur l'élément ou que le conteneur est redimensionné. À ce stade, nous n'avons pas de briques, la valeur par défaut est donc une seule colonne. 

Si vous ne voulez pas utiliser la méthode 'redimensionner' (je ne pense pas que ce soit documenté), vous pouvez simplement appeler $ element.masonry (), mais cela provoque une nouvelle mise en page afin que vous souhaitiez l'appeler uniquement lorsque la première brique est ajoutée.

Edit: J'ai mis à jour le plunker ci-dessus pour n'appeler que resize lorsque la liste dépasse 0 et ne faire qu'un "rechargement" lorsque plusieurs briques sont supprimées dans le même cycle $ digest.

Le code de la directive est:

angular.module('myApp.directives', [])
  .directive("masonry", function($parse, $timeout) {
    return {
      restrict: 'AC',
      link: function (scope, elem, attrs) {
        elem.masonry({ itemSelector: '.masonry-brick'});
        // Opitonal Params, delimited in class name like:
        // class="masonry:70;"
        //elem.masonry({ itemSelector: '.masonry-item', columnWidth: 140, gutterWidth: $parse(attrs.masonry)(scope) });
      },
      controller : function($scope,$element){
          var bricks = [];
          this.appendBrick = function(child, brickId, waitForImage){
            function addBrick() {
              $element.masonry('appended', child, true);

              // If we don't have any bricks then we're going to want to 
              // resize when we add one.
              if (bricks.length === 0) {
                // Timeout here to allow for a potential
                // masonary timeout when appending (when animating
                // from the bottom)
                $timeout(function(){
                  $element.masonry('resize');  
                }, 2);  
              }

              // Store the brick id
              var index = bricks.indexOf(brickId);
              if (index === -1) {
                bricks.Push(brickId);
              }
            }

            if (waitForImage) {
              child.imagesLoaded(addBrick);      
            } else {
              addBrick();
            }
          };

          // Removed bricks - we only want to call masonry.reload() once
          // if a whole batch of bricks have been removed though so Push this
          // async.
          var willReload = false;
          function hasRemovedBrick() {
            if (!willReload) {
              willReload = true;
              $scope.$evalAsync(function(){
                willReload = false;
                $element.masonry("reload");
              });
            }
          }

          this.removeBrick = function(brickId){
              hasRemovedBrick();
              var index = bricks.indexOf(brickId);
              if (index != -1) {
                bricks.splice(index,1);
              }
          };
      }
    };     
  })
  .directive('masonryBrick', function ($compile) {
    return {
      restrict: 'AC',
      require : '^masonry',
      link: function (scope, elem, attrs, MasonryCtrl) {

      elem.imagesLoaded(function () {
        MasonryCtrl.appendBrick(elem, scope.$id, true);
      });

      scope.$on("$destroy",function(){
          MasonryCtrl.removeBrick(scope.$id);
      }); 
    }
  };
});
19
James Sharp

Ce n'est pas exactement ce que vous recherchez (prepend et append), mais devrait correspondre exactement à ce que vous recherchez:

http://plnkr.co/edit/dmuGHCNTCBBuYpjyKQ8E?p=preview

Votre version de la directive déclenche reload pour chaque brick. Cette version déclenche uniquement le rechargement de une fois pour toute la liste de modifications.

L'approche est très simple:

  1. Enregistrez new bricks dans parent masonrycontroller
  2. $watch pour les modifications de la bricks enregistrée et le feu masonry('reload')
  3. Supprimez brick du registre bricks lorsque vous supprimez l'élément - $on('$destroy')
  4. ?
  5. Profit

Vous pouvez étendre cette approche pour faire ce que vous vouliez (utilisez prepend et append) mais je ne vois aucune raison pour vouloir le faire. Cela serait également beaucoup plus compliqué, car vous auriez à suivre manuellement l’ordre des éléments. Je ne crois pas non plus que ce serait plus rapide. Au contraire, cela pourrait être plus lent, car vous auriez à déclencher plusieurs append/prepend si vous changez beaucoup de briques.

Je ne suis pas tout à fait sûr, mais je suppose que vous pourriez utiliser ng-animate pour cela (la version d'animation JavaScript)

Nous avons implémenté quelque chose de similaire pour les événements tiling dans notre application de calendrier. Cette solution s'est avérée être la plus rapide. Si quelqu'un a une meilleure solution, j'aimerais voir ça.

Pour ceux qui veulent voir le code:

angular.module('myApp.directives', [])
  .directive("masonry", function($parse) {
    return {
      restrict: 'AC',
      controller:function($scope,$element){
        // register and unregister bricks
        var bricks = [];
        this.addBrick = function(brick){
          bricks.Push(brick)
        }
        this.removeBrick = function(brick){
          var index = bricks.indexOf(brick);
          if(index!=-1)bricks.splice(index,1);
        }
        $scope.$watch(function(){
          return bricks
        },function(){
          // triggers only once per list change (not for each brick)
          console.log('reload');
          $element.masonry('reload');
        },true);
      },
      link: function (scope, elem, attrs) {
        elem.masonry({ itemSelector: '.masonry-brick'});
      }
    };     
  })
  .directive('masonryBrick', function ($compile) {
    return {
      restrict: 'AC',
      require:'^masonry',
      link: function (scope, elem, attrs,ctrl) {
        ctrl.addBrick(scope.$id);

        scope.$on('$destroy',function(){
          ctrl.removeBrick(scope.$id);
        });
      }
    };
  });

Edit: Il y a une chose que j'ai oubliée (charger des images) - il suffit d'appeler 'reload' quand toutes les images ont été chargées. Je vais essayer de modifier le code plus tard.

7
g00fy

Hey, je viens de faire une directive de maçonnerie pour AngularJS qui est bien plus simple que la plupart des implémentations que j'ai vues. Consultez le résumé ici: https://Gist.github.com/CMCDragonkai/6191419

C'est compatible avec AMD. Nécessite jQuery, imagesLoaded et lodash. Fonctionne avec la quantité dynamique d'éléments, les éléments chargés AJAX (même avec les éléments initiaux), le redimensionnement de la fenêtre et les options personnalisées. Articles pré-ajoutés, ajoutés, rechargés, etc. 73 lignes!

Voici un exemple montrant que cela fonctionne: http://plnkr.co/edit/ZuSrSh?p=preview (sans AMD, mais avec le même code).

3
CMCDragonkai

Une des caractéristiques les moins documentées de Angular est ses contrôleurs de direction (bien qu’elle se trouve sur la page de couverture de www.angularjs.org - Onglets).

Voici un pistolet modifié qui utilise ce mécanisme. 

http://plnkr.co/edit/NmV3m6DZFSpIkQOAjRRE

Les gens utilisent des contrôleurs de directive, mais ils ont été utilisés (et maltraités) pour des choses pour lesquelles ils n'étaient probablement pas destinés. 

Dans le dossier ci-dessus, je n'ai modifié que le fichier directives.js. Les contrôleurs de directives constituent un mécanisme de communication entre les directives. Parfois, il n’est pas suffisant/facile de tout faire dans une même directive. Dans ce cas, vous avez déjà créé deux directives mais la bonne façon de les faire interagir consiste à utiliser un contrôleur de directives. 

Je n'ai pas été en mesure de déterminer quand vous vouliez ajouter des éléments et quand. J'ai seulement implémenté "append" pour le moment. 

Remarque supplémentaire: si les ressources n'implémentent pas déjà les promesses, vous pouvez les implémenter vous-même. Ce n'est pas très difficile à faire. J'ai remarqué que vous utilisez un mécanisme de rappel (que je ne recommanderais pas). Vous avez déjà fait des promesses là-bas, mais vous utilisez toujours des rappels pour lesquels je n’étais pas capable de comprendre pourquoi. 

Est-ce que cela fournit une solution appropriée à votre problème? 

Pour la documentation, voir http://docs.angularjs.org/guide/directive > Objet de définition de directive> contrôleur.

1
ganaraj

Je crois avoir eu exactement le même problème:

Beaucoup d'images dans une boucle ng-repeat et veulent leur appliquer maçonnerie/isotope quand elles sont chargées et prêtes.

Le problème est que même après le rappel des imagesLoaded, il y a une période de temps pendant laquelle les images ne sont pas "complètes" et ne peuvent donc pas être mesurées et affichées correctement.

J'ai mis au point la solution suivante qui fonctionne pour moi et qui ne nécessite qu'un seul laissez-passer de mise en page. Il se déroule en trois étapes

  1. Attendez que les images se chargent (lorsque la dernière est ajoutée à partir de la boucle - utilise le plugin jQuery chargé d’images).
  2. Attendez que toutes les images soient "terminées"
  3. Disposez les images.

angularApp.directive('checkLast', function () {
    return {
        restrict: 'A',
        compile: function (element, attributes) {
            return function postLink(scope, element) {
                if (scope.$last === true) {
                    $('#imagesHolder').imagesLoaded(function () {
                        waitForRender();
                    });
                }
            }
        }
    }
});

function waitForRender() {
    //
    // We have to wait for every image to be ready before we can lay them out
    //
    var ready = true;
    var images = $('#imagesHolder').find('img');
    $.each(images,function(index,img) {
        if ( !img.complete ) {
            setTimeout(waitForRender);
            ready = false;
            return false;
        }
    });
    if (ready) {
        layoutImages();
    }
}

function layoutImages() {
    $('#imagesHolder').isotope({
        itemSelector: '.imageHolder',
        layoutMode: 'fitRows'
    });
}

Cela fonctionne avec la disposition comme ceci

<div id="imagesHolder">
    <div class="imageHolder"
         check-last
         ng-repeat="image in images.image"
        <img ng-src="{{image.url}}"/>
    </div>
</div>

J'espère que ça aide.

0
Conrad