Skip to content

Commit a8f88bd

Browse files
committed
feat(viewport-ruler): cache document client rect
Now the `ViewportRuler` service, which is used in many components, caches the documents `BoundingClientRect` to avoid frequent recalculation of styles in the browser (basically each click for ripples etc.) > The `ScrollDispatcher` is used as the service which triggers an update of the cached rectangles because it listens to the `resize` and `scroll` events. It's important that the `ViewportRuler` also updates the rectangle for scroll events, because the ViewportRuler is also responsible for the scroll position of the viewport.
1 parent b49bfce commit a8f88bd

File tree

4 files changed

+64
-19
lines changed

4 files changed

+64
-19
lines changed

src/lib/core/overlay/position/connected-position-strategy.spec.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {ElementRef} from '@angular/core';
22
import {ConnectedPositionStrategy} from './connected-position-strategy';
3-
import {ViewportRuler} from './viewport-ruler';
3+
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from './viewport-ruler';
44
import {OverlayPositionBuilder} from './overlay-position-builder';
55
import {ConnectedOverlayPositionChange} from './connected-position';
66
import {Scrollable} from '../scroll/scrollable';
77
import {Subscription} from 'rxjs';
8+
import {TestBed, inject} from '@angular/core/testing';
89
import Spy = jasmine.Spy;
910

1011

@@ -18,6 +19,16 @@ const DEFAULT_WIDTH = 60;
1819

1920
describe('ConnectedPositionStrategy', () => {
2021

22+
let viewportRuler: ViewportRuler;
23+
24+
beforeEach(() => TestBed.configureTestingModule({
25+
providers: [VIEWPORT_RULER_PROVIDER]
26+
}));
27+
28+
beforeEach(inject([ViewportRuler], (_ruler: ViewportRuler) => {
29+
viewportRuler = _ruler;
30+
}));
31+
2132
describe('with origin on document body', () => {
2233
const ORIGIN_HEIGHT = DEFAULT_HEIGHT;
2334
const ORIGIN_WIDTH = DEFAULT_WIDTH;
@@ -48,7 +59,7 @@ describe('ConnectedPositionStrategy', () => {
4859
overlayContainerElement.appendChild(overlayElement);
4960

5061
fakeElementRef = new FakeElementRef(originElement);
51-
positionBuilder = new OverlayPositionBuilder(new ViewportRuler());
62+
positionBuilder = new OverlayPositionBuilder(viewportRuler);
5263
});
5364

5465
afterEach(() => {
@@ -457,7 +468,7 @@ describe('ConnectedPositionStrategy', () => {
457468
scrollable.appendChild(originElement);
458469

459470
// Create a strategy with knowledge of the scrollable container
460-
let positionBuilder = new OverlayPositionBuilder(new ViewportRuler());
471+
let positionBuilder = new OverlayPositionBuilder(viewportRuler);
461472
let fakeElementRef = new FakeElementRef(originElement);
462473
strategy = positionBuilder.connectedTo(
463474
fakeElementRef,

src/lib/core/overlay/position/viewport-ruler.spec.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {ViewportRuler} from './viewport-ruler';
2-
1+
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from './viewport-ruler';
2+
import {TestBed, inject} from '@angular/core/testing';
33

44
// For all tests, we assume the browser window is 1024x786 (outerWidth x outerHeight).
55
// The karma config has been set to this for local tests, and it is the default size
@@ -20,10 +20,14 @@ describe('ViewportRuler', () => {
2020
veryLargeElement.style.width = '6000px';
2121
veryLargeElement.style.height = '6000px';
2222

23-
beforeEach(() => {
24-
ruler = new ViewportRuler();
23+
beforeEach(() => TestBed.configureTestingModule({
24+
providers: [VIEWPORT_RULER_PROVIDER]
25+
}));
26+
27+
beforeEach(inject([ViewportRuler], (viewportRuler: ViewportRuler) => {
28+
ruler = viewportRuler;
2529
scrollTo(0, 0);
26-
});
30+
}));
2731

2832
it('should get the viewport bounds when the page is not scrolled', () => {
2933
let bounds = ruler.getViewportRect();
@@ -35,7 +39,10 @@ describe('ViewportRuler', () => {
3539

3640
it('should get the viewport bounds when the page is scrolled', () => {
3741
document.body.appendChild(veryLargeElement);
42+
3843
scrollTo(1500, 2000);
44+
// Force an update of the cached viewport geometries because IE11 emits the scroll event later.
45+
ruler._cacheViewportGeometry();
3946

4047
let bounds = ruler.getViewportRect();
4148

@@ -63,14 +70,17 @@ describe('ViewportRuler', () => {
6370
});
6471

6572
it('should get the scroll position when the page is not scrolled', () => {
66-
var scrollPos = ruler.getViewportScrollPosition();
73+
let scrollPos = ruler.getViewportScrollPosition();
6774
expect(scrollPos.top).toBe(0);
6875
expect(scrollPos.left).toBe(0);
6976
});
7077

7178
it('should get the scroll position when the page is scrolled', () => {
7279
document.body.appendChild(veryLargeElement);
80+
7381
scrollTo(1500, 2000);
82+
// Force an update of the cached viewport geometries because IE11 emits the scroll event later.
83+
ruler._cacheViewportGeometry();
7484

7585
// In the iOS simulator (BrowserStack & SauceLabs), adding the content to the
7686
// body causes karma's iframe for the test to stretch to fit that content once we attempt to
@@ -82,7 +92,7 @@ describe('ViewportRuler', () => {
8292
return;
8393
}
8494

85-
var scrollPos = ruler.getViewportScrollPosition();
95+
let scrollPos = ruler.getViewportScrollPosition();
8696
expect(scrollPos.top).toBe(2000);
8797
expect(scrollPos.left).toBe(1500);
8898

src/lib/core/overlay/position/viewport-ruler.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Injectable, Optional, SkipSelf} from '@angular/core';
2+
import {ScrollDispatcher} from '../scroll/scroll-dispatcher';
23

34

45
/**
@@ -7,12 +8,20 @@ import {Injectable, Optional, SkipSelf} from '@angular/core';
78
*/
89
@Injectable()
910
export class ViewportRuler {
10-
// TODO(jelbourn): cache the document's bounding rect and only update it when the window
11-
// is resized (debounced).
1211

12+
/** Cached document client rectangle. */
13+
private _documentRect?: ClientRect;
14+
15+
constructor(scrollDispatcher: ScrollDispatcher) {
16+
// Initially cache the document rectangle.
17+
this._cacheViewportGeometry();
18+
19+
// Subscribe to scroll and resize events and update the document rectangle on changes.
20+
scrollDispatcher.scrolled().subscribe(() => this._cacheViewportGeometry());
21+
}
1322

1423
/** Gets a ClientRect for the viewport's bounds. */
15-
getViewportRect(): ClientRect {
24+
getViewportRect(documentRect = this._documentRect): ClientRect {
1625
// Use the document element's bounding rect rather than the window scroll properties
1726
// (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE where window scroll
1827
// properties and client coordinates (boundingClientRect, clientX/Y, etc.) are in different
@@ -22,7 +31,6 @@ export class ViewportRuler {
2231
// We use the documentElement instead of the body because, by default (without a css reset)
2332
// browsers typically give the document body an 8px margin, which is not included in
2433
// getBoundingClientRect().
25-
const documentRect = document.documentElement.getBoundingClientRect();
2634
const scrollPosition = this.getViewportScrollPosition(documentRect);
2735
const height = window.innerHeight;
2836
const width = window.innerWidth;
@@ -42,7 +50,7 @@ export class ViewportRuler {
4250
* Gets the (top, left) scroll position of the viewport.
4351
* @param documentRect
4452
*/
45-
getViewportScrollPosition(documentRect = document.documentElement.getBoundingClientRect()) {
53+
getViewportScrollPosition(documentRect = this._documentRect) {
4654
// The top-left-corner of the viewport is determined by the scroll position of the document
4755
// body, normally just (scrollLeft, scrollTop). However, Chrome and Firefox disagree about
4856
// whether `document.body` or `document.documentElement` is the scrolled element, so reading
@@ -54,10 +62,16 @@ export class ViewportRuler {
5462

5563
return {top, left};
5664
}
65+
66+
/** Caches the latest client rectangle of the document element. */
67+
_cacheViewportGeometry?() {
68+
this._documentRect = document.documentElement.getBoundingClientRect();
69+
}
70+
5771
}
5872

5973
export function VIEWPORT_RULER_PROVIDER_FACTORY(parentDispatcher: ViewportRuler) {
60-
return parentDispatcher || new ViewportRuler();
74+
return parentDispatcher || new ViewportRuler(new ScrollDispatcher());
6175
};
6276

6377
export const VIEWPORT_RULER_PROVIDER = {

src/lib/core/ripple/ripple.spec.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing';
1+
import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/testing';
22
import {Component, ViewChild} from '@angular/core';
33
import {MdRipple, MdRippleModule} from './ripple';
4+
import {ViewportRuler} from '../overlay/position/viewport-ruler';
45

56

67
/** Creates a DOM event to indicate that a CSS transition for the given property ended. */
@@ -60,6 +61,7 @@ describe('MdRipple', () => {
6061
let rippleElement: HTMLElement;
6162
let rippleBackground: Element;
6263
let originalBodyMargin: string;
64+
let viewportRuler: ViewportRuler;
6365

6466
beforeEach(() => {
6567
TestBed.configureTestingModule({
@@ -72,11 +74,13 @@ describe('MdRipple', () => {
7274
});
7375
});
7476

75-
beforeEach(() => {
77+
beforeEach(inject([ViewportRuler], (ruler: ViewportRuler) => {
78+
viewportRuler = ruler;
79+
7680
// Set body margin to 0 during tests so it doesn't mess up position calculations.
7781
originalBodyMargin = document.body.style.margin;
7882
document.body.style.margin = '0';
79-
});
83+
}));
8084

8185
afterEach(() => {
8286
document.body.style.margin = originalBodyMargin;
@@ -228,6 +232,9 @@ describe('MdRipple', () => {
228232
document.documentElement.scrollTop = pageScrollTop;
229233
// Mobile safari
230234
window.scrollTo(pageScrollLeft, pageScrollTop);
235+
// Force an update of the cached viewport geometries because IE11 emits the
236+
// scroll event later.
237+
viewportRuler._cacheViewportGeometry();
231238
});
232239

233240
afterEach(() => {
@@ -239,6 +246,9 @@ describe('MdRipple', () => {
239246
document.documentElement.scrollTop = 0;
240247
// Mobile safari
241248
window.scrollTo(0, 0);
249+
// Force an update of the cached viewport geometries because IE11 emits the
250+
// scroll event later.
251+
viewportRuler._cacheViewportGeometry();
242252
});
243253

244254
it('create ripple with correct position', () => {

0 commit comments

Comments
 (0)