Масонство с AngularJS
Я разрабатываю приложение "художественная галерея".
Не стесняйтесь вытаскивать источник на github и поиграть с ним.
Plunker с полным исходным кодом
Текущая работа для того, чтобы заставить масонство играть хорошо с 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');
});
});
}
};
});
Это не работает, потому что:
- По мере того, как контент растет, также накладные расходы на перезагрузку во всем контейнере.
Функция reload
:
- Не добавляет элементы "append", а перенастраивает каждый элемент в контейнере.
- Работает ли для запуска перезагрузки, когда элементы отфильтровываются из набора результатов.
В контексте приложения, которое я привел выше, эта проблема очень легко реплицируется.
Я ищу решение, которое будет использовать директивы для использования:
.masonry('appended', elem)
и .masonry('prepended', elem)
Вместо выполнения .masonry('reload')
каждый раз.
.masonry('reload')
, когда элементы удаляются из набора результатов.
ИЗМЕНИТЬ
Проект был обновлен для использования рабочего решения ниже.
Возьмите источник на GitHub
См. рабочую версию на Plunker
Ответы
Ответ 1
Я играл с этим немного больше, а ответ @ganaraj довольно опрятен. Если вы придерживаетесь $element.masonry('resize');
в своем методе appendBrick
контроллера и учетной записи
загрузка изображений тогда выглядит так, как будто она работает.
Здесь вилка плункера с ней: http://plnkr.co/edit/8t41rRnLYfhOF9oAfSUA
Причина, по которой это необходимо, состоит в том, что количество столбцов вычисляется только тогда, когда класификация инициализируется в элементе или размер контейнера изменяется, и в этот момент у нас нет кирпичей, поэтому по умолчанию используется один столбец.
Если вы не хотите использовать метод "resize" (я не думаю, что он задокументирован), вы можете просто вызвать $element.masonry(), но это приведет к повторной компоновке, поэтому вы хотите только назовите его, когда добавлен первый кирпич.
Изменить: Я обновил плункер выше, чтобы вызывать только resize
, когда список растет выше 0 длины и выполняет только одну "перезагрузку", когда несколько кирпичей удаляются в одном и том же $digest цикл.
Код директивы:
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);
});
}
};
});
Ответ 2
Это не то, что вы ищете (prepend
и append
), но должно быть именно то, что вы ищете:
http://plnkr.co/edit/dmuGHCNTCBBuYpjyKQ8E?p=preview
Ваша версия директивы запускает reload
для каждого brick
. Эта версия запускает только перезагрузку только один раз для изменения всего списка.
Подход очень прост:
- Зарегистрировать новый
bricks
в родительском masonry
controller
-
$watch
для изменений зарегистрированного bricks
и огня masonry('reload')
- Удалите
brick
из bricks
реестра при удалении элемента - $on('$destroy')
- Доход
Вы можете расширить этот подход, чтобы делать то, что вы хотели (используйте prepend
и append
), но я не вижу причин, почему вы захотите это сделать. Это также будет намного сложнее, так как вам придется вручную отслеживать порядок элементов. Я также не верю, что это будет быстрее - наоборот, это может быть медленнее, так как вам придется запускать несколько append/prepend
, если вы меняете много кирпичей.
Я не совсем уверен, но я думаю, вы могли бы использовать ng-animate
для этого (версия анимации JavaScript
)
Мы внедрили нечто подобное для событий tiling
в нашем приложении для календаря. Это решение оказалось самым быстрым. Если у кого-то есть лучшее решение, я бы с удовольствием это увидел.
Для тех, кто хочет выполнить код:
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:
есть одна вещь, о которой я забыл (загрузка изображений) - просто позвоните "перезагрузить", когда все изображения загружены. Я попытаюсь позже отредактировать код.
Ответ 3
Эй, я только что сделал дирекцию кладки для AngularJS, которая намного проще, чем большинство реализаций, которые я видел. Проверьте суть: https://gist.github.com/CMCDragonkai/6191419
Совместим с AMD. Требуется jQuery, imagesLoaded и lodash. Работает с динамическим количеством элементов, загруженными элементами AJAX (даже с начальными элементами), изменением размера окна и настраиваемыми параметрами. Преобладающие предметы, приложенные предметы, перезагруженные предметы... и т.д. 73 строки!
Здесь plunkr показывает, что он работает: http://plnkr.co/edit/ZuSrSh?p=preview (без AMD, но тот же код).
Ответ 4
Одна из наименее документированных функций Angular - это его контрольные контроллеры (хотя она находится на первой странице www.angularjs.org - вкладки).
Вот модифицированный плункер, который использует этот механизм.
http://plnkr.co/edit/NmV3m6DZFSpIkQOAjRRE
Люди используют Directive Controllers, но были использованы (и злоупотребляли) для вещей, которые, вероятно, не были предназначены.
В plunker выше я только изменил файл directives.js. Директивные контроллеры - это механизм для связи между директивами. Иногда, это недостаточно/легко сделать все в одной директиве. В этом случае вы уже создали две директивы, но правильный способ заставить их взаимодействовать через управляющий контроллер.
Мне не удалось выяснить, когда вы хотите добавить и когда хотите добавить. В настоящее время я реализовал "append".
Также на стороне примечания: если ресурсы уже не реализуют promises, вы можете реализовать их самостоятельно. Это не очень сложно сделать. Я заметил, что вы используете механизм обратного вызова (который я бы не рекомендовал). Вы уже ввели promises, но все же вы используете обратные вызовы, которые я не смог понять, почему.
Обеспечивает ли это правильное решение вашей проблемы?
Для документации см. http://docs.angularjs.org/guide/directive > Объект определения директивы > контроллер.
Ответ 5
Я считаю, что у меня была такая же проблема:
Много изображений в цикле ng-repeat и хотите применить к ним кладки/изотопы, когда они загружены и готовы.
Проблема заключается в том, что даже после обратного вызова imagesLoaded существует период времени, когда изображения не являются "завершенными" и поэтому не могут быть измерены и правильно отложены.
Я придумал следующее решение, которое работает для меня и требует только одного макета. Это происходит в три этапа
- Дождитесь загрузки изображений (когда последний добавлен из цикла - использует загруженный плагин jQuery).
- Дождитесь завершения всех изображений
- Разместите изображения.
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'
});
}
Это работает с макетом, подобным этому
<div id="imagesHolder">
<div class="imageHolder"
check-last
ng-repeat="image in images.image"
<img ng-src="{{image.url}}"/>
</div>
</div>
Надеюсь, это поможет.
Ответ 6
Вместо использования двух директив вы можете включить их в одну директиву. Что-то вроде:
.directive("masonry", function($timeout) {
return {
restrict: 'AC',
template: '<div class="masonry-brick" ng-repeat="image in pool | filter:{pool:true}">' +
'<span>{{image.albumTitle|truncate}}</span>' +
'<img ng-src="{{image.link|imageSize:t}}"/>' +
'</div>',
scope: {
pool: "="
},
link: function(scope, elem, attrs){
elem.masonry({itemSelector: '.masonry-brick'});
// When the pool changes put all your logic in for working out what needs to be prepended
// appended etc
function poolChanged(pool) {
//... Do some logic here working out what needs to be appended,
// prepended...
// Make sure the DOM has updated before continuing by doing a $timeout
$timeout(function(){
var bricks = elem.find('.masonry-brick');
brick.imagesLoaded(function() {
// ... Do the actual prepending/appending ...
});
});
}
// Watch for changes to the pool
scope.$watch('pool', poolChanged, true); // The final true compares for
// equality rather than reference
}
}
});
и html:
<div class="masonry" pool="pool"></div>