I am using a Masonry JavaScript grid layout library https://masonry.desandro.com/. I have an issue when appending new items to the grid directly from the DOM using Angular *ngfor which iterate through an a array as follows:
<div class="grid">
<div class="grid-item grid-item--height{{i}}" *ngFor="let i of array">
{{i}}
</div>
</div>
Because I am not appending them using the "appended masonry method", masonry does not layout them out. The array is getting new elements every time a user scroll down. So a JavaScript method is called:
onScrollDown() {
// add another 20 items
this.sum += 20;
for (let i = start; i < this.sum; ++i) {
this.array.push(i); }
}
When the array get more elements, it automatically creates new html items. So i need to find the way masonry layout them out again after the new items are being created. I tried calling $grid.masonry('layout') after the elements have being added to the array in the OnScrollMethod but it did not work.
I am trying $('.grid').masonry('reloadItems') as well, but calling this method the new items are overlapping the previous ones.
I would appreciate any help.
Update:
I am initializing masonry using my angular initialize component method as follows:
ngOnInit() {
this.$grid = jQuery('.grid').masonry({
// options
itemSelector: '.grid-item',
columnWidth: 384,
gutter: 24
});
UPDATED
You might try reloadItems. If your loading images, use imagesLoaded.js to avoid overlaps:
ngOnInit() {
this.$grid = jQuery('.grid').imagesLoaded( function() {
jQuery('.grid').masonry({
// options
itemSelector: '.grid-item',
columnWidth: 384,
gutter: 24
});
});
onScrollDown() {
// add another 20 items
this.sum += 20;
for (let i = start; i < this.sum; ++i) {
this.array.push(i); }
jQuery('.grid').imagesLoaded( function() {
jQuery('.grid').masonry('reloadItems');
});
}
It's been a lot of years since I had to do this, but basically:
Masonry runs a lot of JS, in order to figure out optimal bin-packing for the elements you are creating.
That means that it hard-codes a lot of style information on each "card" inside of your area, and after it calculates it, it sets it.
If you use Masonry to add new "cards" to that view, then it will continue to update the view, and change the layout and the sizes of those cards.
If you don't use the Masonry instance you had before (you use the element, not the Masonry object), then Masonry doesn't have a way to track the changes. At that point, you either have to add them to the DOM and then add them to the object, through addItems, and THEN call layout() (or masonry())... or you have to rewrap the DOM element in Masonry again, now that the element has updated.
This might have changed, but I doubt it.
There are things you could do by writing angular directives, I guess, but the cheap and cheerful way of accomplishing this would be to save the instance of your component's element (or the element that controller is bound to... whatever), and also save the reference to the Masonry view you made, and on update, add all of the items, and call layout again...
...or just, on each render, make a new Masonry view, using your root element.
PS: make sure to load a lot of new elements at once, because if you're just loading 3 at a time it'll be messy...
...also, make sure you pre-load the images in the elements, because if the images load afterwards your Masonry layout is going to get very, very messy.
I used masonry-layout in Vue.js and had the same problem. The way that solved my issue is:
onScrollDown() {
// ...
// after add another 20 items
this.msnry.reloadItems();
this.msnry.layout();
}
That this.msnry is my masonry-layout instance.
Related
I am working with a video carousel using video.js and vueslickcarousel, these two library. The current code is as below:
<template>
<div>
<VueSlickCarousel
ref="carousel"
v-bind="settings"
#beforeChange="onSlideBeforeChange"
>
<carousel-video
v-for="video in visibleVideos"
....
></carousel-video>
</div>
</template>
....
methods: {
onSlideBeforeChange: function(currentIndex, nextIndex) {
...calculate lowerBound, upperBound for visibleVideos.
},
},
computed: {
visibleVideos: function() {
slice from a videoArray using lowerBound and upperBound
}
}
Here visibleVideos is a computed property. I want to increase/decrease the number of <carousel-video> rendered dynamically because the list is huge and can't render all of them at once.
However when the upperBound and lowerBound update is triggerd based on some condition, the internal state of VueSlickCarousel doesn't update.
The items are rendered in dom but these two class is missing in slick-list items - slick-current slick-active. That's why nothing is shown in the view. After that, If I click the next or previous button provided by VueSlickCarousel, it removes the whole component from dom.
Before updating upperBound and lowerBound
After update of upperBound/lowerBound
How can I fix this? Or is there any other library or way to efficiently render video.js videos in a carousel? Any similar implementation or opensource example will also be equally helpful.
Thanks in advance for any kind of help or suggestion.
I need to select the element by class 'gm-style-iw' to fix some styles in Infowindow. The selection is happening inside of the angularjs directive.
<div ui-view="full-map" id="full-map" class="mainMap col-xs-6"></div>
ui-view - loading a directive with IW content. map is initialized inside that directive.
on the directive's controller initialization i have to edit element with class 'gm-style-iw'.
var iwElem = $document[0].getElementsByClassName("gm-style-iw")
does return the correct element.
console.log(iwElem) result is:
[]
length: 1
0: div.gm-style-iw
__proto__: HTMLCollection
However i'm stuck after it.
it as an HTMLCollection, which is the array of HTML elements, as i understand. => i must be able to get this 0 element by iwElem[0], the strange thing is that iwElem[0] returns undefined.
Also tried with jquery selectors:
$('.gm-style-iw') => length:0
$('div.gm-style-iw') => length:0
If you are sure that your html is something like
<div ui-view="full-map" id="full-map" class="mainMap col-xs-6">
...some codes
<div class="gm-style-iw"></div>
... other codes
</div>
And if your are using JQuery, how about trying the following selector?
$('.mainMap .gm-style-iw')
which means getting the child node of class .gm-style-iw under the parent node of class .mainMap
The problem is partially solved.
$timeout(function() {
removeIwStandardStyles()
},300);
function removeIwStandardStyles () {
var iwOuter = $(iwOuterTemp);
while (iwOuterTemp.length < 1){
iwOuterTemp = document.getElementsByClassName("gm-style-iw");
}
var iwBackground = iwOuter.prev();
iwBackground.children(':nth-child(2)').css({'display' : 'none'});
iwBackground.children(':nth-child(4)').css({'display' : 'none'});
}
$document[0].getElementsByClassName("gm-style-iw") was catching a corrent elements, but seems like they was not applied to DOM yet, or some other load issues.
However, the solution is not complete (see a while) - i suppose i can do that cause that element is definitely going to load. $timeout itself isn't helping completely - sometimes it does wait till complete IW load, sometimes not.
Let's say I have a lot (3000+) of items I want to render (in a ng-repeat) in a div with a fixed height and overflow: auto, so I'd get N visible items and a scrollbar for the rest of them.
I'm guessing doing a simple ng-repeat with so many items will probably take a lot of time. Is there a way I can make AngularJS render only those visible N items?
Edit:
An infinite scroll is not what I want. I want the user to be able to scroll to any point of the list, so I literally want a text editor-like behavior. Said with other words: I'd like the scroll to contain the "height" of all the items, but place in the DOM just a few ones.
This answer provides an approach for lazy-rendering only items currently in-view, as defined by the edit to the original question. I want the user to be able to scroll to any point of the list, so I literally want a text editor-like behavior. Said with other words: I'd like the scroll to contain the "height" of all the items, but place in the DOM just a few ones.
Install the angular-inview plugin before trying this.
In order to get your scrollheight you'd need something holding the space for your array items. So I'd start with an array of 3000 simple items (or combine with infinite scroll to whatever extent you want.)
var $app = angular.module('app', ['infinite-scroll']);
$app.controller('listingController', function($scope, $window) {
$scope.listOfItems = new Array($window._bootstrappedData.length);
$scope.loadItem = function($index,$inview) {
if($inview) {
$scope.listOfItems[$index] = $window._bootstrappedData[$index];
}
}
});
Since we're talking about flexible heights, I would create a placeholder for what your content looks like pre-render.
<div ng-controller="listingController">
<ul>
<li ng-repeat="item in listOfItems track by $index" in-view="loadItem($index,$inview)" style="min-height:100px"><div ng-if="item">{{item.name}}</div></li>
</ul>
</div>
Using ng-if will prevent rendering logic from being run unnecessarily. When you scroll an item into view, it'll automatically display. If you want to wait a second to see if the user is still scrolling you could set a timeout in the loadItem method that cancels if the same index gets pushed out of view within a reasonable time period.
Note: If you truly wanted to avoid putting anything in the DOM, you could set your scrollable area to a specific multiple of your "placeholder" height. Then you could create a directive that uses that height to determine the indexes of the items that should be displayed. As soon as you display new items, you'd need to add their heights to the total and make sure you position them at the right spot and make sure your directive knows how to interpret those heights into evaluating the next set of displayed elements. But I think that's way too radical and unnecessary.
Expanding on Grundy's point of using .slice().
I use ngInfiniteScroll when I need to lazy-render/lazy-load data.
I would keep those 3000 records out of your scope to prevent weighing down your digest performance unnecessarily and then append them to your scope data as you need them. Here's an example.
var $app = angular.module('app', ['infinite-scroll']);
$app.controller('listingController', function($scope, $window) {
/*
* Outside of this controller you should bootstrap your data to a non-digested array.
* If you're loading the data via Ajax, save your data similarly.
* For example:
* <script>
* window._bootstrappedData = [{id:1,name:'foo'},{id:2,name:'bar'},...];
* </script>
*/
var currentPage, pageLength;
$scope.listOfItems = [];
currentPage = 0;
pageLength = 100;
$scope.nextPage = function() {
// make sure we don't keep trying to slice data that doesn't exist.
if (currentPage * pageLength >= $window._bootstrappedData.length) {
return false;
}
// append the next data set to your array
$scope.listOfItems.push($window._bootstrappedData.slice(currentPage * pageLength, (currentPage + 1) * pageLength));
currentPage++;
};
/*
* Kickstart this data with our first page.
*/
return $scope.nextPage();
});
And your template:
<div ng-controller="listingController" infinite-scroll="nextPage()" infinite-scroll-distance="3">
<ul>
<li ng-repeat="item in listOfItems">{{item.name}}</li>
</ul>
</div>
Hello dear SO users and contributers. I'm unsure on how to manipulate compositeViews using the JS framework MarionetteJS based on BackboneJS. Hopefully someone will be able to give me some advice on how to continue.
The application context: The application I'm building is a GRID that can accept draggable items. This grid is being build using two CompositeView's to build the rows within the page section and an ItemView for each field that's actually represented on the website.
The application views: I posses a gridView that has a collection of gridModels which get split over multiple rows in the initialize method. This generates a section in the HTML that will hold the GRID. The gridRowView will generate a div with the class 'day' for each row within the gridView build from all of the gridItems.
It has been mentioned that in a previous question relating my GRID that a treeView is probably more suitable for the purposes. But I feel this does not concern the scope of my question.
planboard.gridItemView = Backbone.Marionette.ItemView.extend({
model: planboard.gridModel,
collection: planboard.gridCollection,
template: "#grid-item-template",
onShow: function() {
//code to plan dropped orders
}
});
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
planboard.gridView = Backbone.Marionette.CompositeView.extend({
template: "#grid-template",
itemView: planboard.gridRowView,
tagName: "section",
initialize: function() {
var grid = this.collection.groupBy(function(list, iterator) {
return Math.floor(iterator / collums); // 4 == number of columns
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
});
The application templates:
<!-- Grid templates -->
<script type="text/template" id="grid-item-template">
<div class="gridItem droppable" id="{{cid}}"><b>{{data}}</b></div>
</script>
<script type="text/template" id="grid-row-template">
<div class="day"></div>
</script>
<script type="text/template" id="grid-template">
<section></section>
</script>
The problem: The code below (minus the event wrapper) resembles code as structured in the DOM. As you can see the views generate the expected result. I am however in a spot where I need to use positioning to place items on the GRID. To do this I've taken the start of div container around the section (the start of the GRID).
Although this works, this becomes a problem once scroll-bars are introduced. Since the start position of the GRID does not actualy change you can scroll down but the orders will always remain on the same position off the screen.
Possible solution: Watching the Google Calendar I've realized that they use an additional event-wrapper inside of their rows for positioning. This seems logical to me since then the offset to determine the position from would no longer be the start of your GRID, but rather the start of row (class 'day'). This should solve the problem that scroll-bars could pose.
In my mind this would look something like the code below:
<section>
<div> <!-- wrapper generated by framework -->
<div class="day">
<div class="event-x"> <!-- used as offset for positioning -->
<div id="c39" class="gridItem droppable"> <!-- grid column --></div>
</div>
</div>
<div>
</section>
My question: Since my row (the div with class 'day') is automatically generated by the MarionetteJS CompsositeView I am unable to give this some unique value to identify it with that does not change. Having an additional event wrapper with a unique identifier would most likely solve my problem, since then I could use JQuerySelectors to get the positioning from that element.
However... Marionette practically generates the wrapper for me and I'm unsure how to add a wrapper inside of this row with a unique on top of this. Do any of you (who have experience with MarionetteJS or Backbone) have any idea how to go about this?
Am I even looking in the right direction? Any help would be much appreciated.
If you only need to assign unique id / class to wrapper, generated by View, following answer might help. Note, that it actually doesn't any additional wrapper, it is just way to add attributes to automatically generated wrappers.
You can define attributes for View (or anything, that extends it e.g. CompositeView). These attributes will be automatically assigned to wrapper element. You can define them in extend or as a function or hash
If you need just static identifier, use first approach:
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
id: "some-id",
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
If you need to generate id based on model, use second approach:
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
attributes: function() {
return {
id: this.model.get("id") // or something like this
};
},
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
I've a small webapplication that works with some drag / drop functionality. Just imagine a small order system in which you can say order 1 (draggable) will be done by employee 2 (droppable). That works fine. BUT:
Every 20sec. I ask the database via AJAX for new orders. These new orders will also be draggable. In case that another college has given an order to en employee the list of orders for every employee is also no loaded. To enable drag / drop after the ajax request I had to run:
$('.order').draggable({
..
});
and
$('.employee').dropable({
..
});
So the jquery function walks every 20 secunds through the hole DOM. After 10-15 minitues the app becomes very slow. Do you have an idea how to ingrease that process? Is it possible to give an absolute statement that every .order class is draggable even if this element will be create after the first registration?
I think the problem is that the elements that are already on the page become draggable over and over again.
I think a solution would be to assign a class to those that already have it:
$('.order').each(function() {
var element = $(this);
if ( !element.hasClass('event-already-attached')) {
element.addClass('event-already-attached').draggable({
})
}
});
Thanks # Jonas your great idea did it!
You can use the classes ui-droppable and ui-draggable for that job
$('.order').each(function() {
var element = $(this);
if ( !element.hasClass('ui-droppable')) {
element.droppable({
...
}
});