AngularJS London, 30 June 2015
Sébastien Cevey
viewport
loaded cells
<!-- sets container height -->
<ul gu-lazy-table="ctrl.items"
gu-lazy-table-cell-height="280"
gu-lazy-table-cell-min-width="300"
gu-lazy-table-load-range="ctrl.loadRange($start, $end)">
<!-- absolutely positions cell within container -->
<li ng:repeat="item in ctrl.items"
gu-lazy-table-cell="image">
<!-- custom markup for each cell... -->
<img src="..." />
</li>
</ul>
/* in guLazyTable directive
link: function(scope, element, attrs) { */
// Config
const cellHeight = attrs.guLazyTableCellHeight; // 280px
const columns = 5; // hardcoded ;_;
// Dynamic
const itemCount = items.length;
const rows = Math.ceil(itemCount / columns);
const height = rows * cellHeight;
element.css('height', height + 'px');
Fixed column count: not responsive!
/* in guLazyTable directive
link: function(scope, element, attrs) { */
element.css('height', height + 'px');
/* in guLazyTable directive
link: function(scope, element, attrs) { */
// Dynamic
const itemCount = items.length;
const rows = Math.ceil(itemCount / columns);
const height = rows * cellHeight;
element.css('height', height + 'px');
<ul gu-lazy-table="ctrl.items"
gu-lazy-table-cell-height="280"
gu-lazy-table-cell-min-width="300"
... >
// Config
const cellHeight = attrs.guLazyTableCellHeight; // 280px
const cellMinWidth = attrs.guLazyTableCellMinWidth; // 300px
// Variable columns
const containerWidth = element[0].clientWidth;
const columns = Math.max(1, Math.floor(containerWidth / cellMinWidth));
// Dynamic
const itemCount = items.length;
const rows = Math.ceil(itemCount / columns);
const height = rows * cellHeight;
element.css('height', height + 'px');
Browser window resize?
<ul gu-lazy-table="ctrl.items"
gu-lazy-table-cell-height="280"
gu-lazy-table-cell-min-width="300"
... >
// Config
const cellHeight = attrs.guLazyTableCellHeight; // 280px
const cellMinWidth = attrs.guLazyTableCellMinWidth; // 300px
$window.addEventListener('resize', recomputeHeight);
recomputeHeight();
function recomputeHeight() {
// Variable columns
const containerWidth = element[0].clientWidth;
const columns = Math.max(1, Math.floor(containerWidth / cellMinWidth));
// Dynamic
const itemCount = items.length;
const rows = Math.ceil(itemCount / columns);
const height = rows * cellHeight;
element.css('height', height + 'px');
}
Why read from the DOM again though?
$window.addEventListener('resize', _.debounce(recomputeHeight, 100));
Debounce resize listener?
Attribute config changed?
let cellHeight;
$scope.$watch(() => attrs.guLazyTableCellHeight, (newHeight) => {
cellHeight = newHeight;
recomputeHeight();
});
// ditto cellMinWidth
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
const viewportResized$ = Rx.DOM.fromEvent($window, 'resize').
debounce(100).
startWith({/* init */});
const containerWidth$ = viewportResized$.map(
() => element[0].clientWidth
);
const columns$ = max$(1, floor$(div$(containerWidth$, cellMinWidth$)));
// ^ define stream combinators for common operations!
// previously:
// const columns = Math.max(1, Math.floor(containerWidth / cellMinWidth));
const items$ = observeCollection$(scope, attrs.guLazyTable);
const itemCount$ = items$.map(items => items.length);
const rows$ = ceil$(div$(itemCount$, columns$));
const height$ = mult$(rows$, cellHeight$);
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
const viewportResized$ = Rx.DOM.fromEvent($window, 'resize').
debounce(100).
startWith({/* init */});
const containerWidth$ = viewportResized$.map(
() => element[0].clientWidth
);
const columns$ = Rx.Observable.combineLatest$(
containerWidth$, cellMinWidth$,
(containerWidth, cellMinWidth) => {
return Math.max(1, Math.floor(containerWidth / cellMinWidth)
}
);
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
const viewportResized$ = Rx.DOM.fromEvent($window, 'resize').
debounce(100).
startWith({/* init */});
const containerWidth$ = viewportResized$.map(
() => element[0].clientWidth
);
const columns$ = max$(1, floor$(div$(containerWidth$, cellMinWidth$)));
// ^ define stream combinators for common operations!
// previously:
// const columns = Math.max(1, Math.floor(containerWidth / cellMinWidth));
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
const viewportResized$ = Rx.DOM.fromEvent($window, 'resize').
debounce(100).
startWith({/* init */});
const containerWidth$ = viewportResized$.map(
() => element[0].clientWidth
);
const columns$ = max$(1, floor$(div$(containerWidth$, cellMinWidth$)));
// ^ define stream combinators for common operations!
// previously:
// const columns = Math.max(1, Math.floor(containerWidth / cellMinWidth));
const items$ = observeCollection$(scope, attrs.guLazyTable);
const itemCount$ = items$.map(items => items.length);
const rows$ = ceil$(div$(itemCount$, columns$));
const height$ = mult$(rows$, cellHeight$);
height$.safeApply(scope, height => {
element.css('height', height + 'px');
});
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
const viewportResized$ = Rx.DOM.fromEvent($window, 'resize').
debounce(100).
startWith({/* init */});
const containerWidth$ = viewportResized$.map(
() => element[0].clientWidth
);
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
const viewportResized$ = Rx.DOM.fromEvent($window, 'resize').
debounce(100).
startWith({/* init */});
const cellHeight$ = observe$(scope, attrs.guLazyTableCellHeight);
const cellMinWidth$ = observe$(scope, attrs.guLazyTableCellMinWidth);
$start
$end
const viewportScrolled$ =
Rx.DOM.fromEvent($window, 'scroll').
debounce(100).
startWith({/* init */});
const offsetTop$ = viewportScrolled$.map(...);
// Stream of {$start: __, $end: __} range
// of indexes to load
const rangeToLoad$ = ...;
/*
<ul gu-lazy-table="ctrl.images"
gu-lazy-table-load-range="
ctrl.loadRange($start, $end)">
*/
rangeToLoad$.subscribe(range => {
scope.$eval(attrs.guLazyTableLoadRange, range);
});
return {
restrict: 'A',
require: '^guLazyTable',
link: function(scope, element, attrs, ctrl) {
const item = scope.$eval(attrs.guLazyTableCell);
const position$ = ctrl.getCellPosition$(item);
subscribe$(scope, position$,
({top, left, width, height, display}) => {
element.css({
position: 'absolute',
top: top + 'px',
left: left + 'px',
width: width + 'px',
height: height + 'px',
display: display
});
});
}
};
<!-- absolutely positions cell within container -->
<li ng:repeat="item in ctrl.items"
gu-lazy-table-cell="image">
<!-- custom markup for each cell... -->
<img src="..." />
</li>
top, left, width, height
const viewWidth$ = viewportResized$
.map(() => element[0].clientWidth);
// Accessed each time for each cell:
// const cellWidth = f(viewWidth$)
// ...
const viewWidth$ = viewportResized$
.map(() => element[0].clientWidth)
.shareReplay(1);
// Accessed once for all cells:
// const cellWidth = f(viewWidth$)
// ...
Only read from the DOM when needed
Not in a loop
Disabled ngAnimate
(failed to disable ngAnimate locally - Angular 1.3?)
/* in guLazyTableCell directive */
return {
restrict: 'A',
require: '^guLazyTable',
transclude: true,
template: '<ng-transclude ng:if="isCellVisible"></ng-transclude>',
link: function( // ...
Strip unnecessary nodes from DOM tree
/* in guLazyTableCell directive */
const position$ = ctrl.getCellPosition$(item);
subscribe$(scope, position$,
({top, left, width, height, display}) => {
element.css({
position: 'absolute',
top: top + 'px',
// ... etc ...
});
});
/* in guLazyTableCell directive */
const position$ = ctrl.getCellPosition$(item);
subscribe$(scope, position$,
({top, left, width, height, display}) => {
scope.$applyAsync(() => { // <- async!
element.css({
position: 'absolute',
top: top + 'px',
// ... etc ...
});
});
});
/* in guLazyTableCell directive */
const position$ = ctrl.getCellPosition$(item);
subscribe$(scope, position$,
({top, left, width, height, display}) => {
scope.$apply(() => {
element.css({
position: 'absolute',
top: top + 'px',
// ... etc ...
});
});
});
Batch DOM operations in next cycle
Asynchronous composition
Declarative syntax
(if you're careful)
Use to identify performance bottlenecks
Powerful combinators
Sébastien Cevey